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.buildPost;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPostOperation;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPut;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getTypeParentNode;
17 import static org.opendaylight.netconf.sal.rest.doc.util.JsonUtil.addFields;
18 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolvePathArgumentsName;
20 import com.fasterxml.jackson.databind.JsonNode;
21 import com.fasterxml.jackson.databind.ObjectMapper;
22 import com.fasterxml.jackson.databind.SerializationFeature;
23 import com.fasterxml.jackson.databind.node.ArrayNode;
24 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import com.google.common.base.Preconditions;
27 import com.google.common.collect.ImmutableList;
28 import com.google.common.collect.Range;
29 import java.io.IOException;
30 import java.time.format.DateTimeParseException;
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.Iterator;
36 import java.util.List;
38 import java.util.Map.Entry;
39 import java.util.Optional;
41 import java.util.SortedSet;
42 import java.util.TreeSet;
43 import javax.ws.rs.core.UriInfo;
44 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
45 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
46 import org.opendaylight.netconf.sal.rest.doc.swagger.CommonApiObject;
47 import org.opendaylight.netconf.sal.rest.doc.swagger.Components;
48 import org.opendaylight.netconf.sal.rest.doc.swagger.Info;
49 import org.opendaylight.netconf.sal.rest.doc.swagger.OpenApiObject;
50 import org.opendaylight.netconf.sal.rest.doc.swagger.Server;
51 import org.opendaylight.netconf.sal.rest.doc.swagger.SwaggerObject;
52 import org.opendaylight.netconf.sal.rest.doc.util.JsonUtil;
53 import org.opendaylight.yangtools.yang.common.QName;
54 import org.opendaylight.yangtools.yang.common.Revision;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
57 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
58 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
59 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
61 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
63 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.Module;
65 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
66 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
70 public abstract class BaseYangSwaggerGenerator {
72 private static final Logger LOG = LoggerFactory.getLogger(BaseYangSwaggerGenerator.class);
74 private static final String API_VERSION = "1.0.0";
75 private static final String SWAGGER_VERSION = "2.0";
76 private static final String OPEN_API_VERSION = "3.0.3";
77 private static final ObjectMapper MAPPER = new ObjectMapper();
79 private final DefinitionGenerator jsonConverter = new DefinitionGenerator();
80 private final DOMSchemaService schemaService;
82 public static final String BASE_PATH = "/";
83 public static final String MODULE_NAME_SUFFIX = "_module";
86 MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
89 protected BaseYangSwaggerGenerator(final Optional<DOMSchemaService> schemaService) {
90 this.schemaService = schemaService.orElse(null);
93 public DOMSchemaService getSchemaService() {
97 public SwaggerObject getAllModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames,
98 final OAversion oaversion) {
99 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
100 Preconditions.checkState(schemaContext != null);
101 return getAllModulesDoc(uriInfo, Optional.empty(), schemaContext, Optional.empty(), "", definitionNames,
105 public SwaggerObject getAllModulesDoc(final UriInfo uriInfo, final Optional<Range<Integer>> range,
106 final EffectiveModelContext schemaContext, final Optional<String> deviceName,
107 final String context, final DefinitionNames definitionNames,
108 final OAversion oaversion) {
109 final String schema = createSchemaFromUriInfo(uriInfo);
110 final String host = createHostFromUriInfo(uriInfo);
111 String name = "Controller";
112 if (deviceName.isPresent()) {
113 name = deviceName.get();
116 final String title = name + " modules of RESTCONF";
117 final SwaggerObject doc = createSwaggerObject(schema, host, BASE_PATH, title);
118 doc.setDefinitions(JsonNodeFactory.instance.objectNode());
119 doc.setPaths(JsonNodeFactory.instance.objectNode());
121 fillDoc(doc, range, schemaContext, context, deviceName, oaversion, definitionNames);
126 public void fillDoc(final SwaggerObject doc, final Optional<Range<Integer>> range,
127 final EffectiveModelContext schemaContext, final String context,
128 final Optional<String> deviceName, final OAversion oaversion,
129 final DefinitionNames definitionNames) {
130 final SortedSet<Module> modules = getSortedModules(schemaContext);
131 final Set<Module> filteredModules;
132 if (range.isPresent()) {
133 filteredModules = filterByRange(modules, range.get());
135 filteredModules = modules;
138 for (final Module module : filteredModules) {
139 final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
141 LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
143 getSwaggerDocSpec(module, context, deviceName, schemaContext, oaversion, definitionNames, doc, false);
147 private static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
148 final int begin = range.lowerEndpoint();
149 final int end = range.upperEndpoint();
151 Module firstModule = null;
153 final Iterator<Module> iterator = modules.iterator();
155 while (iterator.hasNext() && counter < end) {
156 final Module module = iterator.next();
157 if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
158 if (counter == begin) {
159 firstModule = module;
165 if (iterator.hasNext()) {
166 return modules.subSet(firstModule, iterator.next());
168 return modules.tailSet(firstModule);
172 public CommonApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo,
173 final OAversion oaversion) {
174 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
175 Preconditions.checkState(schemaContext != null);
176 final SwaggerObject doc = getApiDeclaration(module, revision, uriInfo, schemaContext, "", oaversion);
177 return getAppropriateDoc(doc, oaversion);
180 public SwaggerObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
181 final EffectiveModelContext schemaContext, final String context,
182 final OAversion oaversion) {
183 final Optional<Revision> rev;
186 rev = Revision.ofNullable(revision);
187 } catch (final DateTimeParseException e) {
188 throw new IllegalArgumentException(e);
191 final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
192 Preconditions.checkArgument(module != null,
193 "Could not find module by name,revision: " + moduleName + "," + revision);
195 return getApiDeclaration(module, uriInfo, context, schemaContext, oaversion);
198 public SwaggerObject getApiDeclaration(final Module module, final UriInfo uriInfo, final String context,
199 final EffectiveModelContext schemaContext, final OAversion oaversion) {
200 final String schema = createSchemaFromUriInfo(uriInfo);
201 final String host = createHostFromUriInfo(uriInfo);
203 return getSwaggerDocSpec(module, schema, host, BASE_PATH, context, schemaContext, oaversion);
206 public String createHostFromUriInfo(final UriInfo uriInfo) {
207 String portPart = "";
208 final int port = uriInfo.getBaseUri().getPort();
210 portPart = ":" + port;
212 return uriInfo.getBaseUri().getHost() + portPart;
215 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
216 return uriInfo.getBaseUri().getScheme();
219 public SwaggerObject getSwaggerDocSpec(final Module module, final String schema, final String host,
220 final String basePath, final String context,
221 final EffectiveModelContext schemaContext, final OAversion oaversion) {
222 final SwaggerObject doc = createSwaggerObject(schema, host, basePath, module.getName());
223 final DefinitionNames definitionNames = new DefinitionNames();
224 return getSwaggerDocSpec(module, context, Optional.empty(), schemaContext, oaversion, definitionNames, doc,
228 public SwaggerObject getSwaggerDocSpec(final Module module, final String context, final Optional<String> deviceName,
229 final EffectiveModelContext schemaContext, final OAversion oaversion,
230 final DefinitionNames definitionNames, final SwaggerObject doc,
231 final boolean isForSingleModule) {
232 final ObjectNode definitions;
235 if (isForSingleModule) {
236 definitions = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, oaversion,
238 doc.setDefinitions(definitions);
240 definitions = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, oaversion,
242 addFields(doc.getDefinitions(), definitions.fields());
244 if (LOG.isDebugEnabled()) {
245 LOG.debug("Document: {}", MAPPER.writeValueAsString(doc));
247 } catch (final IOException e) {
248 LOG.error("Exception occured in DefinitionGenerator", e);
251 final ObjectNode paths = JsonNodeFactory.instance.objectNode();
252 final String moduleName = module.getName();
254 boolean hasAddRootPostLink = false;
256 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
257 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
258 for (final DataSchemaNode node : dataSchemaNodes) {
259 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
260 LOG.debug("Is Configuration node [{}] [{}]", node.isConfiguration(), node.getQName().getLocalName());
262 final String localName = module.getName() + ":" + node.getQName().getLocalName();
263 ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
266 if (node.isConfiguration()) { // This node's config statement is
268 resourcePath = getResourcePath("config", context);
271 * When there are two or more top container or list nodes
272 * whose config statement is true in module, make sure that
273 * only one root post link is added for this module.
275 if (isForSingleModule && !hasAddRootPostLink) {
276 LOG.debug("Has added root post link for module {}", module.getName());
277 addRootPostLink(module, deviceName, pathParams, resourcePath, paths, oaversion);
279 hasAddRootPostLink = true;
282 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, oaversion, localName);
283 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, true, module.getName(),
284 definitionNames, oaversion, resolvedPath);
286 pathParams = JsonNodeFactory.instance.arrayNode();
287 resourcePath = getResourcePath("operational", context);
289 if (!node.isConfiguration()) {
290 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, oaversion, localName);
291 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, false, moduleName,
292 definitionNames, oaversion, resolvedPath);
297 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
298 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
299 + rpcDefinition.getQName().getLocalName();
300 addOperations(rpcDefinition, moduleName, deviceName, paths, module.getName(), definitionNames, oaversion,
304 LOG.debug("Number of Paths found [{}]", paths.size());
306 if (isForSingleModule) {
309 addFields(doc.getPaths(), paths.fields());
315 private static void addRootPostLink(final Module module, final Optional<String> deviceName,
316 final ArrayNode pathParams, final String resourcePath, final ObjectNode paths, final OAversion oaversion) {
317 if (containsListOrContainer(module.getChildNodes())) {
318 final ObjectNode post = JsonNodeFactory.instance.objectNode();
319 final String moduleName = module.getName();
320 final String name = moduleName + MODULE_NAME_SUFFIX;
321 post.set("post", buildPost("", name, "", moduleName, deviceName,
322 module.getDescription().orElse(""), pathParams, oaversion));
323 paths.set(resourcePath, post);
327 public SwaggerObject createSwaggerObject(final String schema, final String host, final String basePath,
328 final String title) {
329 final SwaggerObject doc = new SwaggerObject();
330 doc.setSwagger(SWAGGER_VERSION);
331 final Info info = new Info();
332 info.setTitle(title);
333 info.setVersion(API_VERSION);
335 doc.setSchemes(ImmutableList.of(schema));
337 doc.setBasePath(basePath);
338 doc.setProduces(Arrays.asList("application/xml", "application/json"));
342 public static CommonApiObject getAppropriateDoc(final SwaggerObject swaggerObject, final OAversion oaversion) {
343 if (oaversion.equals(OAversion.V3_0)) {
344 return convertToOpenApi(swaggerObject);
346 return swaggerObject;
349 private static OpenApiObject convertToOpenApi(final SwaggerObject swaggerObject) {
350 final OpenApiObject doc = new OpenApiObject();
351 doc.setOpenapi(OPEN_API_VERSION);
352 doc.setInfo(swaggerObject.getInfo());
353 doc.setServers(convertToServers(swaggerObject.getSchemes(), swaggerObject.getHost(),
354 swaggerObject.getBasePath()));
355 doc.setPaths(swaggerObject.getPaths());
356 doc.setComponents(new Components(swaggerObject.getDefinitions()));
361 private static List<Server> convertToServers(final List<String> schemes, final String host, final String basePath) {
362 return ImmutableList.of(new Server(schemes.get(0) + "://" + host + basePath));
365 protected abstract String getPathVersion();
367 public abstract String getResourcePath(String resourceType, String context);
369 public abstract String getResourcePathPart(String resourceType);
371 private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
372 final ObjectNode paths, final ArrayNode parentPathParams,
373 final EffectiveModelContext schemaContext, final boolean isConfig, final String parentName,
374 final DefinitionNames definitionNames, final OAversion oaversion, final String resourcePath) {
375 LOG.debug("Adding path: [{}]", resourcePath);
377 final ArrayNode pathParams = JsonUtil.copy(parentPathParams);
378 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
379 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
380 final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
381 childSchemaNodes = dataNodeContainer.getChildNodes();
384 final ObjectNode path = JsonNodeFactory.instance.objectNode();
385 path.setAll(operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
387 paths.set(resourcePath, path);
389 if (node instanceof ActionNodeContainer) {
390 ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
391 final String resolvedPath = "rests/operations" + resourcePath.substring(11)
392 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
393 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, oaversion,
398 for (final DataSchemaNode childNode : childSchemaNodes) {
399 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
400 final String newParent = parentName + "_" + node.getQName().getLocalName();
401 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
402 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, oaversion,
404 final boolean newIsConfig = isConfig && childNode.isConfiguration();
405 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
406 newIsConfig, newParent, definitionNames, oaversion, newResourcePath);
411 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
412 for (final DataSchemaNode child : nodes) {
413 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
420 private static Map<String, ObjectNode> operations(final DataSchemaNode node, final String moduleName,
421 final Optional<String> deviceName, final ArrayNode pathParams,
422 final boolean isConfig, final String parentName,
423 final DefinitionNames definitionNames,
424 final OAversion oaversion) {
425 final Map<String, ObjectNode> operations = new HashMap<>();
426 final String discriminator = definitionNames.getDiscriminator(node);
428 final String nodeName = node.getQName().getLocalName();
430 final String defName = parentName + "_" + nodeName + TOP + discriminator;
431 final ObjectNode get = buildGet(node, moduleName, deviceName, pathParams, defName, isConfig, oaversion);
432 operations.put("get", get);
436 final ObjectNode put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
437 node.getDescription().orElse(""), pathParams, oaversion);
438 operations.put("put", put);
440 final ObjectNode delete = buildDelete(node, moduleName, deviceName, pathParams, oaversion);
441 operations.put("delete", delete);
443 operations.put("post", buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
444 node.getDescription().orElse(""), pathParams, oaversion));
449 protected abstract ListPathBuilder newListPathBuilder();
451 private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams,
452 final OAversion oaversion, final String localName) {
453 final StringBuilder path = new StringBuilder();
454 path.append(localName);
456 if (schemaNode instanceof ListSchemaNode) {
457 final ListPathBuilder keyBuilder = newListPathBuilder();
458 for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
459 final String paramName = createUniquePathParamName(listKey.getLocalName(), pathParams);
460 final String pathParamIdentifier = keyBuilder.nextParamIdentifier(paramName);
462 path.append(pathParamIdentifier);
464 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
465 pathParam.put("name", paramName);
467 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
468 .ifPresent(desc -> pathParam.put("description", desc));
470 final ObjectNode typeParent = getTypeParentNode(pathParam, oaversion);
472 typeParent.put("type", "string");
473 pathParam.put("in", "path");
474 pathParam.put("required", true);
476 pathParams.add(pathParam);
479 return path.toString();
482 private String createUniquePathParamName(final String clearName, final ArrayNode pathParams) {
483 for (final JsonNode pathParam : pathParams) {
484 if (isNamePicked(clearName, pathParam)) {
485 return createUniquePathParamName(clearName, pathParams, 1);
491 private String createUniquePathParamName(final String clearName, final ArrayNode pathParams,
492 final int discriminator) {
493 final String newName = clearName + discriminator;
494 for (final JsonNode pathParam : pathParams) {
495 if (isNamePicked(newName, pathParam)) {
496 return createUniquePathParamName(clearName, pathParams, discriminator + 1);
502 private static boolean isNamePicked(final String name, final JsonNode pathParam) {
503 return name.equals(pathParam.get("name").asText());
506 public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
507 if (schemaContext == null) {
508 return Collections.emptySortedSet();
511 final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
512 int result = module1.getName().compareTo(module2.getName());
514 result = Revision.compare(module1.getRevision(), module2.getRevision());
517 result = module1.getNamespace().compareTo(module2.getNamespace());
521 for (final Module m : schemaContext.getModules()) {
523 sortedModules.add(m);
526 return sortedModules;
529 private static void addOperations(final OperationDefinition operDef, final String moduleName,
530 final Optional<String> deviceName, final ObjectNode paths, final String parentName,
531 final DefinitionNames definitionNames, final OAversion oaversion, final String resourcePath) {
532 final ObjectNode operations = JsonNodeFactory.instance.objectNode();
533 operations.set("post", buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
535 paths.set(resourcePath, operations);
538 protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
540 public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
541 final StringBuilder builder = new StringBuilder();
543 if (moduleName != null) {
544 builder.append(moduleName).append(':');
546 for (final PathArgument arg : key.getPathArguments()) {
547 final String name = arg.getNodeType().getLocalName();
548 if (arg instanceof NodeIdentifierWithPredicates) {
549 final NodeIdentifierWithPredicates nodeId = (NodeIdentifierWithPredicates) arg;
550 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
551 appendPathKeyValue(builder, entry.getValue());
554 builder.append(name).append('/');
557 return builder.toString();
560 protected interface ListPathBuilder {
561 String nextParamIdentifier(String key);