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.impl.ApiDocServiceImpl.DEFAULT_PAGESIZE;
11 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolvePathArgumentsName;
13 import com.fasterxml.jackson.databind.ObjectMapper;
14 import com.fasterxml.jackson.databind.SerializationFeature;
15 import com.fasterxml.jackson.databind.node.ObjectNode;
16 import com.google.common.base.Preconditions;
17 import java.io.IOException;
19 import java.time.format.DateTimeParseException;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Map.Entry;
26 import java.util.Optional;
28 import java.util.SortedSet;
29 import java.util.TreeSet;
30 import javax.ws.rs.core.UriInfo;
31 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
32 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.URIType;
33 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder;
34 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.Delete;
35 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.Get;
36 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.Post;
37 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.Put;
38 import org.opendaylight.netconf.sal.rest.doc.swagger.Api;
39 import org.opendaylight.netconf.sal.rest.doc.swagger.ApiDeclaration;
40 import org.opendaylight.netconf.sal.rest.doc.swagger.Operation;
41 import org.opendaylight.netconf.sal.rest.doc.swagger.Parameter;
42 import org.opendaylight.netconf.sal.rest.doc.swagger.Resource;
43 import org.opendaylight.netconf.sal.rest.doc.swagger.ResourceList;
44 import org.opendaylight.yangtools.yang.common.QName;
45 import org.opendaylight.yangtools.yang.common.Revision;
46 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
47 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
48 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
49 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
50 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
52 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.Module;
55 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
56 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
57 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
61 public abstract class BaseYangSwaggerGenerator {
63 private static final Logger LOG = LoggerFactory.getLogger(BaseYangSwaggerGenerator.class);
65 protected static final String API_VERSION = "1.0.0";
66 protected static final String SWAGGER_VERSION = "1.2";
68 static final String MODULE_NAME_SUFFIX = "_module";
69 private final ModelGenerator jsonConverter = new ModelGenerator();
71 // private Map<String, ApiDeclaration> MODULE_DOC_CACHE = new HashMap<>()
72 private final ObjectMapper mapper = new ObjectMapper();
73 private final DOMSchemaService schemaService;
75 protected BaseYangSwaggerGenerator(final Optional<DOMSchemaService> schemaService) {
76 this.schemaService = schemaService.orElse(null);
77 this.mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
80 public DOMSchemaService getSchemaService() {
84 public ResourceList getResourceListing(final UriInfo uriInfo, final URIType uriType) {
85 final SchemaContext schemaContext = schemaService.getGlobalContext();
86 Preconditions.checkState(schemaContext != null);
87 return getResourceListing(uriInfo, schemaContext, "", 0, true, uriType);
90 public ResourceList getResourceListing(final UriInfo uriInfo, final SchemaContext schemaContext,
91 final String context, final URIType uriType) {
92 return getResourceListing(uriInfo, schemaContext, context, 0, true, uriType);
96 * Return list of modules converted to swagger compliant resource list.
98 public ResourceList getResourceListing(final UriInfo uriInfo, final SchemaContext schemaContext,
99 final String context, final int pageNum, final boolean all, final URIType uriType) {
101 final ResourceList resourceList = createResourceList();
103 final Set<Module> modules = getSortedModules(schemaContext);
105 final List<Resource> resources = new ArrayList<>(DEFAULT_PAGESIZE);
107 LOG.info("Modules found [{}]", modules.size());
108 final int start = DEFAULT_PAGESIZE * pageNum;
109 final int end = start + DEFAULT_PAGESIZE;
111 for (final Module module : modules) {
112 final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
114 LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
115 final ApiDeclaration doc =
116 getApiDeclaration(module.getName(), revisionString, uriInfo, schemaContext, context, uriType);
119 if (count >= start && count < end || all) {
120 final Resource resource = new Resource();
121 resource.setPath(generatePath(uriInfo, module.getName(), revisionString));
122 resources.add(resource);
125 if (count >= end && !all) {
129 LOG.warn("Could not generate doc for {},{}", module.getName(), revisionString);
133 resourceList.setApis(resources);
138 public ResourceList createResourceList() {
139 final ResourceList resourceList = new ResourceList();
140 resourceList.setApiVersion(API_VERSION);
141 resourceList.setSwaggerVersion(SWAGGER_VERSION);
145 public String generatePath(final UriInfo uriInfo, final String name, final String revision) {
146 final URI uri = uriInfo.getRequestUriBuilder().replaceQuery("").path(generateCacheKey(name, revision)).build();
147 return uri.toASCIIString();
150 public ApiDeclaration getApiDeclaration(final String module, final String revision, final UriInfo uriInfo,
151 final URIType uriType) {
152 final SchemaContext schemaContext = schemaService.getGlobalContext();
153 Preconditions.checkState(schemaContext != null);
154 return getApiDeclaration(module, revision, uriInfo, schemaContext, "", uriType);
157 public ApiDeclaration getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
158 final SchemaContext schemaContext, final String context, final URIType uriType) {
159 final Optional<Revision> rev;
162 rev = Revision.ofNullable(revision);
163 } catch (final DateTimeParseException e) {
164 throw new IllegalArgumentException(e);
167 final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
168 Preconditions.checkArgument(module != null,
169 "Could not find module by name,revision: " + moduleName + "," + revision);
171 return getApiDeclaration(module, uriInfo, context, schemaContext, uriType);
174 public ApiDeclaration getApiDeclaration(final Module module, final UriInfo uriInfo,
175 final String context, final SchemaContext schemaContext, final URIType uriType) {
176 final String basePath = createBasePathFromUriInfo(uriInfo);
178 final ApiDeclaration doc = getSwaggerDocSpec(module, basePath, context, schemaContext, uriType);
185 public String createBasePathFromUriInfo(final UriInfo uriInfo) {
186 String portPart = "";
187 final int port = uriInfo.getBaseUri().getPort();
189 portPart = ":" + port;
191 final String basePath =
192 new StringBuilder(uriInfo.getBaseUri().getScheme()).append("://").append(uriInfo.getBaseUri().getHost())
193 .append(portPart).toString();
197 public ApiDeclaration getSwaggerDocSpec(final Module module, final String basePath, final String context,
198 final SchemaContext schemaContext, final URIType uriType) {
199 final ApiDeclaration doc = createApiDeclaration(basePath);
201 final List<Api> apis = new ArrayList<>();
202 boolean hasAddRootPostLink = false;
204 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
205 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
206 for (final DataSchemaNode node : dataSchemaNodes) {
207 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
208 LOG.debug("Is Configuration node [{}] [{}]", node.isConfiguration(), node.getQName().getLocalName());
210 List<Parameter> pathParams = new ArrayList<>();
214 * Only when the node's config statement is true, such apis as
215 * GET/PUT/POST/DELETE config are added for this node.
217 if (node.isConfiguration()) { // This node's config statement is
219 resourcePath = getDataStorePath("config", context);
222 * When there are two or more top container or list nodes
223 * whose config statement is true in module, make sure that
224 * only one root post link is added for this module.
226 if (!hasAddRootPostLink) {
227 LOG.debug("Has added root post link for module {}", module.getName());
228 addRootPostLink(module, (DataNodeContainer) node, pathParams, resourcePath, "config", apis);
230 hasAddRootPostLink = true;
233 addApis(node, apis, resourcePath, pathParams, schemaContext, true, module.getName(), "config",
236 pathParams = new ArrayList<>();
237 resourcePath = getDataStorePath("operational", context);
239 addApis(node, apis, resourcePath, pathParams, schemaContext, false, module.getName(), "operational",
244 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
245 final String resourcePath;
246 resourcePath = getDataStorePath("operations", context);
247 addOperations(rpcDefinition, apis, resourcePath, schemaContext);
250 LOG.debug("Number of APIs found [{}]", apis.size());
252 if (!apis.isEmpty()) {
254 ObjectNode models = null;
257 models = this.jsonConverter.convertToJsonSchema(module, schemaContext);
258 doc.setModels(models);
259 if (LOG.isDebugEnabled()) {
260 LOG.debug("Document: {}", this.mapper.writeValueAsString(doc));
262 } catch (IOException e) {
263 LOG.error("Exception occured in ModelGenerator", e);
271 private void addRootPostLink(final Module module, final DataNodeContainer node,
272 final List<Parameter> pathParams, final String resourcePath, final String dataStore, final List<Api> apis) {
273 if (containsListOrContainer(module.getChildNodes())) {
274 final Api apiForRootPostUri = new Api();
275 apiForRootPostUri.setPath(resourcePath.concat(getContent(dataStore)));
276 apiForRootPostUri.setOperations(operationPost(module.getName() + MODULE_NAME_SUFFIX,
277 module.getDescription().orElse(null), module, pathParams, true, ""));
278 apis.add(apiForRootPostUri);
282 public ApiDeclaration createApiDeclaration(final String basePath) {
283 final ApiDeclaration doc = new ApiDeclaration();
284 doc.setApiVersion(API_VERSION);
285 doc.setSwaggerVersion(SWAGGER_VERSION);
286 doc.setBasePath(basePath);
287 doc.setProduces(Arrays.asList("application/json", "application/xml"));
291 public abstract String getDataStorePath(String dataStore, String context);
293 private static String generateCacheKey(final String module, final String revision) {
294 return module + "(" + revision + ")";
297 private void addApis(final DataSchemaNode node, final List<Api> apis, final String parentPath,
298 final List<Parameter> parentPathParams, final SchemaContext schemaContext, final boolean addConfigApi,
299 final String parentName, final String dataStore, final URIType uriType) {
300 final Api api = new Api();
301 final List<Parameter> pathParams = new ArrayList<>(parentPathParams);
303 final String resourcePath = parentPath + "/" + createPath(node, pathParams, schemaContext);
304 LOG.debug("Adding path: [{}]", resourcePath);
305 api.setPath(resourcePath.concat(getContent(dataStore)));
307 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
308 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
309 final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
310 childSchemaNodes = dataNodeContainer.getChildNodes();
312 api.setOperations(operation(node, pathParams, addConfigApi, childSchemaNodes, parentName));
315 if (uriType.equals(URIType.RFC8040)) {
316 ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
317 addOperations(actionDef, apis, resourcePath, schemaContext);
321 for (final DataSchemaNode childNode : childSchemaNodes) {
322 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
323 // keep config and operation attributes separate.
324 if (childNode.isConfiguration() == addConfigApi) {
325 final String newParent = parentName + "/" + node.getQName().getLocalName();
326 addApis(childNode, apis, resourcePath, pathParams, schemaContext, addConfigApi, newParent,
333 public abstract String getContent(String dataStore);
335 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
336 for (final DataSchemaNode child : nodes) {
337 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
344 private static List<Operation> operation(final DataSchemaNode node, final List<Parameter> pathParams,
345 final boolean isConfig, final Iterable<? extends DataSchemaNode> childSchemaNodes,
346 final String parentName) {
347 final List<Operation> operations = new ArrayList<>();
349 final Get getBuilder = new Get(node, isConfig);
350 operations.add(getBuilder.pathParams(pathParams).build());
353 final Put putBuilder = new Put(node.getQName().getLocalName(), node.getDescription().orElse(null),
355 operations.add(putBuilder.pathParams(pathParams).build());
357 final Delete deleteBuilder = new Delete(node);
358 operations.add(deleteBuilder.pathParams(pathParams).build());
360 if (containsListOrContainer(childSchemaNodes)) {
361 operations.addAll(operationPost(node.getQName().getLocalName(), node.getDescription().orElse(null),
362 (DataNodeContainer) node, pathParams, isConfig, parentName + "/"));
368 private static List<Operation> operationPost(final String name, final String description,
369 final DataNodeContainer dataNodeContainer, final List<Parameter> pathParams, final boolean isConfig,
370 final String parentName) {
371 final List<Operation> operations = new ArrayList<>();
373 final Post postBuilder = new Post(name, parentName + name, description, dataNodeContainer);
374 operations.add(postBuilder.pathParams(pathParams).build());
379 protected abstract ListPathBuilder newListPathBuilder();
381 private String createPath(final DataSchemaNode schemaNode, final List<Parameter> pathParams,
382 final SchemaContext schemaContext) {
383 final StringBuilder path = new StringBuilder();
384 final String localName = resolvePathArgumentsName(schemaNode, schemaContext);
385 path.append(localName);
387 if (schemaNode instanceof ListSchemaNode) {
388 final List<QName> listKeys = ((ListSchemaNode) schemaNode).getKeyDefinition();
389 for (final QName listKey : listKeys) {
390 final ListPathBuilder keyBuilder = newListPathBuilder();
391 final String pathParamIdentifier = keyBuilder.nextParamIdentifier(listKey.getLocalName());
393 path.append(pathParamIdentifier);
395 final Parameter pathParam = new Parameter();
396 pathParam.setName(listKey.getLocalName());
398 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
399 .ifPresent(pathParam::setDescription);
401 pathParam.setType("string");
402 pathParam.setParamType("path");
404 pathParams.add(pathParam);
407 return path.toString();
410 protected void addOperations(final OperationDefinition operDef, final List<Api> apis, final String parentPath,
411 final SchemaContext schemaContext) {
412 final Api operationApi = new Api();
413 final String resourcePath = parentPath + "/" + resolvePathArgumentsName(operDef, schemaContext);
414 operationApi.setPath(resourcePath);
416 final Operation operationSpec = new Operation();
417 operationSpec.setMethod("POST");
418 operationSpec.setNotes(operDef.getDescription().orElse(null));
419 operationSpec.setNickname(operDef.getQName().getLocalName());
420 if (!operDef.getOutput().getChildNodes().isEmpty()) {
421 operationSpec.setType("(" + operDef.getQName().getLocalName() + ")output" + OperationBuilder.TOP);
423 if (!operDef.getInput().getChildNodes().isEmpty()) {
424 final Parameter payload = new Parameter();
425 payload.setParamType("body");
426 payload.setType("(" + operDef.getQName().getLocalName() + ")input" + OperationBuilder.TOP);
427 operationSpec.setParameters(Collections.singletonList(payload));
428 operationSpec.setConsumes(OperationBuilder.CONSUMES_PUT_POST);
430 operationApi.setOperations(Arrays.asList(operationSpec));
431 apis.add(operationApi);
434 protected SortedSet<Module> getSortedModules(final SchemaContext schemaContext) {
435 if (schemaContext == null) {
436 return new TreeSet<>();
439 final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
440 int result = module1.getName().compareTo(module2.getName());
442 result = Revision.compare(module1.getRevision(), module2.getRevision());
445 result = module1.getNamespace().compareTo(module2.getNamespace());
449 for (final Module m : schemaContext.getModules()) {
451 sortedModules.add(m);
454 return sortedModules;
457 protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
459 public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
460 final StringBuilder builder = new StringBuilder();
462 if (moduleName != null) {
463 builder.append(moduleName).append(':');
465 for (final PathArgument arg : key.getPathArguments()) {
466 final String name = arg.getNodeType().getLocalName();
467 if (arg instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
468 final NodeIdentifierWithPredicates nodeId = (NodeIdentifierWithPredicates) arg;
469 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
470 appendPathKeyValue(builder, entry.getValue());
473 builder.append(name).append('/');
476 return builder.toString();
479 protected interface ListPathBuilder {
480 String nextParamIdentifier(String key);