2 * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.sal.rest.doc.impl;
10 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildDelete;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildGet;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPatch;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPost;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPostOperation;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPut;
17 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getTypeParentNode;
18 import static org.opendaylight.netconf.sal.rest.doc.util.JsonUtil.addFields;
19 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolvePathArgumentsName;
21 import com.fasterxml.jackson.databind.JsonNode;
22 import com.fasterxml.jackson.databind.ObjectMapper;
23 import com.fasterxml.jackson.databind.SerializationFeature;
24 import com.fasterxml.jackson.databind.node.ArrayNode;
25 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
26 import com.fasterxml.jackson.databind.node.ObjectNode;
27 import com.google.common.base.Preconditions;
28 import com.google.common.collect.Range;
29 import java.io.IOException;
30 import java.time.format.DateTimeParseException;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.List;
37 import java.util.Map.Entry;
38 import java.util.Optional;
40 import java.util.SortedSet;
41 import java.util.TreeSet;
42 import javax.ws.rs.core.UriInfo;
43 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
44 import org.opendaylight.netconf.sal.rest.doc.openapi.Components;
45 import org.opendaylight.netconf.sal.rest.doc.openapi.Info;
46 import org.opendaylight.netconf.sal.rest.doc.openapi.OpenApiObject;
47 import org.opendaylight.netconf.sal.rest.doc.openapi.Path;
48 import org.opendaylight.netconf.sal.rest.doc.openapi.SecuritySchemes;
49 import org.opendaylight.netconf.sal.rest.doc.openapi.Server;
50 import org.opendaylight.netconf.sal.rest.doc.util.JsonUtil;
51 import org.opendaylight.yangtools.yang.common.QName;
52 import org.opendaylight.yangtools.yang.common.Revision;
53 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
56 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
57 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
58 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
59 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
61 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.Module;
63 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
64 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
68 public abstract class BaseYangOpenApiGenerator {
70 private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
72 private static final String API_VERSION = "1.0.0";
73 private static final String OPEN_API_VERSION = "3.0.3";
74 private static final ObjectMapper MAPPER = new ObjectMapper();
76 private final DefinitionGenerator jsonConverter = new DefinitionGenerator();
77 private final DOMSchemaService schemaService;
79 public static final String BASE_PATH = "/";
80 public static final String MODULE_NAME_SUFFIX = "_module";
81 private static final ObjectNode OPEN_API_BASIC_AUTH = JsonNodeFactory.instance.objectNode()
83 .put("scheme", "basic");
84 private static final ArrayNode SECURITY = JsonNodeFactory.instance.arrayNode()
85 .add(JsonNodeFactory.instance.objectNode().set("basicAuth", JsonNodeFactory.instance.arrayNode()));
88 MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
91 protected BaseYangOpenApiGenerator(final Optional<DOMSchemaService> schemaService) {
92 this.schemaService = schemaService.orElse(null);
95 public OpenApiObject getAllModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames) {
96 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
97 Preconditions.checkState(schemaContext != null);
98 return getAllModulesDoc(uriInfo, Optional.empty(), schemaContext, Optional.empty(), "", definitionNames);
101 public OpenApiObject getAllModulesDoc(final UriInfo uriInfo, final Optional<Range<Integer>> range,
102 final EffectiveModelContext schemaContext, final Optional<String> deviceName, final String context,
103 final DefinitionNames definitionNames) {
104 final String schema = createSchemaFromUriInfo(uriInfo);
105 final String host = createHostFromUriInfo(uriInfo);
106 String name = "Controller";
107 if (deviceName.isPresent()) {
108 name = deviceName.orElseThrow();
111 final String title = name + " modules of RESTCONF";
112 final OpenApiObject doc = createOpenApiObject(schema, host, BASE_PATH, title);
113 doc.setPaths(new HashMap<>());
115 fillDoc(doc, range, schemaContext, context, deviceName, definitionNames);
120 public void fillDoc(final OpenApiObject doc, final Optional<Range<Integer>> range,
121 final EffectiveModelContext schemaContext, final String context, final Optional<String> deviceName,
122 final DefinitionNames definitionNames) {
123 final SortedSet<Module> modules = getSortedModules(schemaContext);
124 final Set<Module> filteredModules;
125 if (range.isPresent()) {
126 filteredModules = filterByRange(modules, range.orElseThrow());
128 filteredModules = modules;
131 for (final Module module : filteredModules) {
132 final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
134 LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
136 getOpenApiDocSpec(module, context, deviceName, schemaContext, definitionNames, doc, false);
140 private static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
141 final int begin = range.lowerEndpoint();
142 final int end = range.upperEndpoint();
144 Module firstModule = null;
146 final Iterator<Module> iterator = modules.iterator();
148 while (iterator.hasNext() && counter < end) {
149 final Module module = iterator.next();
150 if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
151 if (counter == begin) {
152 firstModule = module;
158 if (iterator.hasNext()) {
159 return modules.subSet(firstModule, iterator.next());
161 return modules.tailSet(firstModule);
165 public OpenApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo) {
166 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
167 Preconditions.checkState(schemaContext != null);
168 final OpenApiObject doc = getApiDeclaration(module, revision, uriInfo, schemaContext, "");
172 public OpenApiObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
173 final EffectiveModelContext schemaContext, final String context) {
174 final Optional<Revision> rev;
177 rev = Revision.ofNullable(revision);
178 } catch (final DateTimeParseException e) {
179 throw new IllegalArgumentException(e);
182 final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
183 Preconditions.checkArgument(module != null,
184 "Could not find module by name,revision: " + moduleName + "," + revision);
186 return getApiDeclaration(module, uriInfo, context, schemaContext);
189 public OpenApiObject getApiDeclaration(final Module module, final UriInfo uriInfo, final String context,
190 final EffectiveModelContext schemaContext) {
191 final String schema = createSchemaFromUriInfo(uriInfo);
192 final String host = createHostFromUriInfo(uriInfo);
194 return getOpenApiDocSpec(module, schema, host, BASE_PATH, context, schemaContext);
197 public String createHostFromUriInfo(final UriInfo uriInfo) {
198 String portPart = "";
199 final int port = uriInfo.getBaseUri().getPort();
201 portPart = ":" + port;
203 return uriInfo.getBaseUri().getHost() + portPart;
206 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
207 return uriInfo.getBaseUri().getScheme();
210 public OpenApiObject getOpenApiDocSpec(final Module module, final String schema, final String host,
211 final String basePath, final String context, final EffectiveModelContext schemaContext) {
212 final OpenApiObject doc = createOpenApiObject(schema, host, basePath, module.getName());
213 final DefinitionNames definitionNames = new DefinitionNames();
214 return getOpenApiDocSpec(module, context, Optional.empty(), schemaContext, definitionNames, doc, true);
217 public OpenApiObject getOpenApiDocSpec(final Module module, final String context, final Optional<String> deviceName,
218 final EffectiveModelContext schemaContext, final DefinitionNames definitionNames, final OpenApiObject doc,
219 final boolean isForSingleModule) {
221 final ObjectNode schema;
222 if (isForSingleModule) {
223 schema = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, true);
225 schema = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, false);
227 addFields(doc.getComponents().getSchemas(), schema.fields());
228 if (LOG.isDebugEnabled()) {
229 LOG.debug("Document: {}", MAPPER.writeValueAsString(doc));
231 } catch (final IOException e) {
232 LOG.error("Exception occurred in DefinitionGenerator", e);
234 final Map<String, Path> paths = new HashMap<>();
235 final String moduleName = module.getName();
237 boolean hasAddRootPostLink = false;
239 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
240 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
241 for (final DataSchemaNode node : dataSchemaNodes) {
242 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
243 LOG.debug("Is Configuration node [{}] [{}]", node.isConfiguration(), node.getQName().getLocalName());
245 final String localName = module.getName() + ":" + node.getQName().getLocalName();
246 final String resourcePath = getResourcePath("data", context);
248 if (node.isConfiguration()) {
249 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
251 * When there are two or more top container or list nodes
252 * whose config statement is true in module, make sure that
253 * only one root post link is added for this module.
255 if (isForSingleModule && !hasAddRootPostLink) {
256 LOG.debug("Has added root post link for module {}", module.getName());
257 addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
259 hasAddRootPostLink = true;
262 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, localName);
263 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, true, module.getName(),
264 definitionNames, resolvedPath);
266 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
267 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, localName);
268 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, false, moduleName,
269 definitionNames, resolvedPath);
274 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
275 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
276 + rpcDefinition.getQName().getLocalName();
277 addOperations(rpcDefinition, moduleName, deviceName, paths, module.getName(), definitionNames,
281 LOG.debug("Number of Paths found [{}]", paths.size());
283 if (isForSingleModule) {
286 doc.getPaths().putAll(paths);
292 private static void addRootPostLink(final Module module, final Optional<String> deviceName,
293 final ArrayNode pathParams, final String resourcePath, final Map<String, Path> paths) {
294 if (containsListOrContainer(module.getChildNodes())) {
295 final String moduleName = module.getName();
296 final String name = moduleName + MODULE_NAME_SUFFIX;
297 final var post = new Path();
298 post.setPost(buildPost("", name, "", moduleName, deviceName,
299 module.getDescription().orElse(""), pathParams));
300 paths.put(resourcePath, post);
304 public OpenApiObject createOpenApiObject(final String schema, final String host, final String basePath,
305 final String title) {
306 final OpenApiObject doc = new OpenApiObject();
307 doc.setOpenapi(OPEN_API_VERSION);
308 final Info info = new Info();
309 info.setTitle(title);
310 info.setVersion(API_VERSION);
312 doc.setServers(List.of(new Server(schema + "://" + host + basePath)));
313 doc.setComponents(new Components(JsonNodeFactory.instance.objectNode(),
314 new SecuritySchemes(OPEN_API_BASIC_AUTH)));
315 doc.setSecurity(SECURITY);
319 public abstract String getResourcePath(String resourceType, String context);
321 private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
322 final Map<String, Path> paths, final ArrayNode parentPathParams, final EffectiveModelContext schemaContext,
323 final boolean isConfig, final String parentName, final DefinitionNames definitionNames,
324 final String resourcePath) {
325 LOG.debug("Adding path: [{}]", resourcePath);
327 final ArrayNode pathParams = JsonUtil.copy(parentPathParams);
328 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
329 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
330 final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
331 childSchemaNodes = dataNodeContainer.getChildNodes();
334 paths.put(resourcePath, operations(resourcePath, node, moduleName, deviceName, pathParams, isConfig, parentName,
337 if (node instanceof ActionNodeContainer) {
338 ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
339 final String resolvedPath = "rests/operations" + resourcePath.substring(11)
340 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
341 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, resolvedPath);
345 for (final DataSchemaNode childNode : childSchemaNodes) {
346 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
347 final String newParent = parentName + "_" + node.getQName().getLocalName();
348 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
349 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, localName);
350 final boolean newIsConfig = isConfig && childNode.isConfiguration();
351 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
352 newIsConfig, newParent, definitionNames, newResourcePath);
357 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
358 for (final DataSchemaNode child : nodes) {
359 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
366 private static Path operations(final String resourcePath, final DataSchemaNode node, final String moduleName,
367 final Optional<String> deviceName, final ArrayNode pathParams, final boolean isConfig,
368 final String parentName, final DefinitionNames definitionNames) {
369 final Path operations = new Path();
371 final String discriminator = definitionNames.getDiscriminator(node);
372 final String nodeName = node.getQName().getLocalName();
374 final String defName = parentName + "_" + nodeName + TOP + discriminator;
375 final ObjectNode get = buildGet(node, moduleName, deviceName, pathParams, defName, isConfig);
376 operations.setGet(get);
379 final ObjectNode put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
380 node.getDescription().orElse(""), pathParams);
381 operations.setPut(put);
383 final ObjectNode patch = buildPatch(parentName, nodeName, moduleName, deviceName,
384 node.getDescription().orElse(""), pathParams);
385 operations.setPatch(patch);
387 final ObjectNode delete = buildDelete(node, moduleName, deviceName, pathParams);
388 operations.setDelete(delete);
390 final ObjectNode post = buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
391 node.getDescription().orElse(""), pathParams);
392 operations.setPost(post);
397 protected abstract ListPathBuilder newListPathBuilder();
399 private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams, final String localName) {
400 final StringBuilder path = new StringBuilder();
401 path.append(localName);
403 if (schemaNode instanceof ListSchemaNode) {
404 final ListPathBuilder keyBuilder = newListPathBuilder();
405 for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
406 final String paramName = createUniquePathParamName(listKey.getLocalName(), pathParams);
407 final String pathParamIdentifier = keyBuilder.nextParamIdentifier(paramName);
409 path.append(pathParamIdentifier);
411 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
412 pathParam.put("name", paramName);
414 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
415 .ifPresent(desc -> pathParam.put("description", desc));
417 final ObjectNode typeParent = getTypeParentNode(pathParam);
419 typeParent.put("type", "string");
420 pathParam.put("in", "path");
421 pathParam.put("required", true);
423 pathParams.add(pathParam);
426 return path.toString();
429 private String createUniquePathParamName(final String clearName, final ArrayNode pathParams) {
430 for (final JsonNode pathParam : pathParams) {
431 if (isNamePicked(clearName, pathParam)) {
432 return createUniquePathParamName(clearName, pathParams, 1);
438 private String createUniquePathParamName(final String clearName, final ArrayNode pathParams,
439 final int discriminator) {
440 final String newName = clearName + discriminator;
441 for (final JsonNode pathParam : pathParams) {
442 if (isNamePicked(newName, pathParam)) {
443 return createUniquePathParamName(clearName, pathParams, discriminator + 1);
449 private static boolean isNamePicked(final String name, final JsonNode pathParam) {
450 return name.equals(pathParam.get("name").asText());
453 public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
454 if (schemaContext == null) {
455 return Collections.emptySortedSet();
458 final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
459 int result = module1.getName().compareTo(module2.getName());
461 result = Revision.compare(module1.getRevision(), module2.getRevision());
464 result = module1.getNamespace().compareTo(module2.getNamespace());
468 for (final Module m : schemaContext.getModules()) {
470 sortedModules.add(m);
473 return sortedModules;
476 private static void addOperations(final OperationDefinition operDef, final String moduleName,
477 final Optional<String> deviceName, final Map<String, Path> paths, final String parentName,
478 final DefinitionNames definitionNames, final String resourcePath) {
479 final var path = new Path();
480 path.setPost(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames));
481 paths.put(resourcePath, path);
484 protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
486 public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
487 final StringBuilder builder = new StringBuilder();
489 if (moduleName != null) {
490 builder.append(moduleName).append(':');
492 for (final PathArgument arg : key.getPathArguments()) {
493 final String name = arg.getNodeType().getLocalName();
494 if (arg instanceof NodeIdentifierWithPredicates nodeId) {
495 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
496 appendPathKeyValue(builder, entry.getValue());
499 builder.append(name).append('/');
502 return builder.toString();
505 protected interface ListPathBuilder {
506 String nextParamIdentifier(String key);