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.restconf.openapi.impl;
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.TOP;
12 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildDelete;
13 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildGet;
14 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPatch;
15 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPost;
16 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPostOperation;
17 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPut;
18 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.getTypeParentNode;
19 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
21 import com.fasterxml.jackson.databind.JsonNode;
22 import com.fasterxml.jackson.databind.node.ArrayNode;
23 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
24 import com.fasterxml.jackson.databind.node.ObjectNode;
25 import com.google.common.base.Preconditions;
26 import com.google.common.collect.Range;
27 import java.io.IOException;
28 import java.time.format.DateTimeParseException;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.List;
35 import java.util.Map.Entry;
36 import java.util.Optional;
38 import java.util.SortedSet;
39 import java.util.TreeSet;
40 import javax.ws.rs.core.UriInfo;
41 import org.eclipse.jdt.annotation.NonNull;
42 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
43 import org.opendaylight.restconf.openapi.model.Components;
44 import org.opendaylight.restconf.openapi.model.Info;
45 import org.opendaylight.restconf.openapi.model.OpenApiObject;
46 import org.opendaylight.restconf.openapi.model.Operation;
47 import org.opendaylight.restconf.openapi.model.Path;
48 import org.opendaylight.restconf.openapi.model.Schema;
49 import org.opendaylight.restconf.openapi.model.SecuritySchemes;
50 import org.opendaylight.restconf.openapi.model.Server;
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";
75 private final DefinitionGenerator jsonConverter = new DefinitionGenerator();
76 private final DOMSchemaService schemaService;
78 public static final String BASE_PATH = "/";
79 public static final String MODULE_NAME_SUFFIX = "_module";
80 private static final ObjectNode OPEN_API_BASIC_AUTH = JsonNodeFactory.instance.objectNode()
82 .put("scheme", "basic");
83 private static final ArrayNode SECURITY = JsonNodeFactory.instance.arrayNode()
84 .add(JsonNodeFactory.instance.objectNode().set("basicAuth", JsonNodeFactory.instance.arrayNode()));
86 protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
87 this.schemaService = requireNonNull(schemaService);
90 public OpenApiObject getAllModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames) {
91 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
92 Preconditions.checkState(schemaContext != null);
93 return getAllModulesDoc(uriInfo, Optional.empty(), schemaContext, Optional.empty(), "", definitionNames)
97 public OpenApiObject.Builder getAllModulesDoc(final UriInfo uriInfo, final Optional<Range<Integer>> range,
98 final EffectiveModelContext schemaContext, final Optional<String> deviceName, final String context,
99 final DefinitionNames definitionNames) {
100 final String schema = createSchemaFromUriInfo(uriInfo);
101 final String host = createHostFromUriInfo(uriInfo);
102 String name = "Controller";
103 if (deviceName.isPresent()) {
104 name = deviceName.orElseThrow();
107 final String title = name + " modules of RESTCONF";
108 final OpenApiObject.Builder docBuilder = createOpenApiObjectBuilder(schema, host, BASE_PATH, title);
109 docBuilder.paths(new HashMap<>());
111 fillDoc(docBuilder, range, schemaContext, context, deviceName, definitionNames);
113 // FIXME rework callers logic to make possible to return OpenApiObject from here
117 public void fillDoc(final OpenApiObject.Builder docBuilder, final Optional<Range<Integer>> range,
118 final EffectiveModelContext schemaContext, final String context, final Optional<String> deviceName,
119 final DefinitionNames definitionNames) {
120 final SortedSet<Module> modules = getSortedModules(schemaContext);
121 final Set<Module> filteredModules;
122 if (range.isPresent()) {
123 filteredModules = filterByRange(modules, range.orElseThrow());
125 filteredModules = modules;
128 for (final Module module : filteredModules) {
129 final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
131 LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
133 getOpenApiSpec(module, context, deviceName, schemaContext, definitionNames, docBuilder, false);
137 private static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
138 final int begin = range.lowerEndpoint();
139 final int end = range.upperEndpoint();
141 Module firstModule = null;
143 final Iterator<Module> iterator = modules.iterator();
145 while (iterator.hasNext() && counter < end) {
146 final Module module = iterator.next();
147 if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
148 if (counter == begin) {
149 firstModule = module;
155 if (iterator.hasNext()) {
156 return modules.subSet(firstModule, iterator.next());
158 return modules.tailSet(firstModule);
162 public OpenApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo) {
163 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
164 Preconditions.checkState(schemaContext != null);
165 return getApiDeclaration(module, revision, uriInfo, schemaContext, "");
168 public OpenApiObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
169 final EffectiveModelContext schemaContext, final String context) {
170 final Optional<Revision> rev;
173 rev = Revision.ofNullable(revision);
174 } catch (final DateTimeParseException e) {
175 throw new IllegalArgumentException(e);
178 final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
179 Preconditions.checkArgument(module != null,
180 "Could not find module by name,revision: " + moduleName + "," + revision);
182 return getApiDeclaration(module, uriInfo, context, schemaContext);
185 public OpenApiObject getApiDeclaration(final Module module, final UriInfo uriInfo, final String context,
186 final EffectiveModelContext schemaContext) {
187 final String schema = createSchemaFromUriInfo(uriInfo);
188 final String host = createHostFromUriInfo(uriInfo);
190 return getOpenApiSpec(module, schema, host, BASE_PATH, context, schemaContext);
193 public String createHostFromUriInfo(final UriInfo uriInfo) {
194 String portPart = "";
195 final int port = uriInfo.getBaseUri().getPort();
197 portPart = ":" + port;
199 return uriInfo.getBaseUri().getHost() + portPart;
202 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
203 return uriInfo.getBaseUri().getScheme();
206 public OpenApiObject getOpenApiSpec(final Module module, final String schema, final String host,
207 final String basePath, final String context, final EffectiveModelContext schemaContext) {
208 final OpenApiObject.Builder docBuilder = createOpenApiObjectBuilder(schema, host, basePath, module.getName());
209 final DefinitionNames definitionNames = new DefinitionNames();
210 return getOpenApiSpec(module, context, Optional.empty(), schemaContext, definitionNames, docBuilder, true);
213 public OpenApiObject getOpenApiSpec(final Module module, final String context, final Optional<String> deviceName,
214 final EffectiveModelContext schemaContext, final DefinitionNames definitionNames,
215 final OpenApiObject.Builder docBuilder, final boolean isForSingleModule) {
217 final Map<String, Schema> schemas = jsonConverter.convertToSchemas(module, schemaContext,
218 definitionNames, isForSingleModule);
219 docBuilder.getComponents().schemas().putAll(schemas);
220 } catch (final IOException e) {
221 LOG.error("Exception occurred in DefinitionGenerator", e);
223 final Map<String, Path> paths = new HashMap<>();
224 final String moduleName = module.getName();
226 boolean hasAddRootPostLink = false;
228 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
229 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
230 for (final DataSchemaNode node : dataSchemaNodes) {
231 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
232 final boolean isConfig = node.isConfiguration();
233 LOG.debug("Is Configuration node [{}] [{}]", isConfig, node.getQName().getLocalName());
235 final String localName = moduleName + ":" + node.getQName().getLocalName();
236 final String resourcePath = getResourcePath("data", context);
238 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
240 * When there are two or more top container or list nodes
241 * whose config statement is true in module, make sure that
242 * only one root post link is added for this module.
244 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
245 LOG.debug("Has added root post link for module {}", moduleName);
246 addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
248 hasAddRootPostLink = true;
250 final String resourcePathPart = createPath(node, pathParams, localName);
251 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, isConfig,
252 moduleName, definitionNames, resourcePathPart, context);
256 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
257 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
258 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
259 + rpcDefinition.getQName().getLocalName();
260 addOperations(rpcDefinition, moduleName, deviceName, paths, moduleName, definitionNames,
261 resolvedPath, pathParams);
264 LOG.debug("Number of Paths found [{}]", paths.size());
266 if (isForSingleModule) {
267 docBuilder.paths(paths);
269 docBuilder.getPaths().putAll(paths);
272 return docBuilder.build();
275 private static void addRootPostLink(final Module module, final Optional<String> deviceName,
276 final ArrayNode pathParams, final String resourcePath, final Map<String, Path> paths) {
277 if (containsListOrContainer(module.getChildNodes())) {
278 final String moduleName = module.getName();
279 final String name = moduleName + MODULE_NAME_SUFFIX;
280 final var postBuilder = new Path.Builder();
281 postBuilder.post(buildPost("", name, "", moduleName, deviceName,
282 module.getDescription().orElse(""), pathParams));
283 paths.put(resourcePath, postBuilder.build());
287 public OpenApiObject.Builder createOpenApiObjectBuilder(final String schema, final String host,
288 final String basePath, final String title) {
289 final OpenApiObject.Builder docBuilder = new OpenApiObject.Builder();
290 docBuilder.openapi(OPEN_API_VERSION);
291 docBuilder.info(new Info.Builder().title(title).version(API_VERSION).build())
292 .servers(List.of(new Server(schema + "://" + host + basePath)))
293 .components(new Components(new HashMap<>(), new SecuritySchemes(OPEN_API_BASIC_AUTH)))
298 public abstract String getResourcePath(String resourceType, String context);
300 private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
301 final Map<String, Path> paths, final ArrayNode parentPathParams, final EffectiveModelContext schemaContext,
302 final boolean isConfig, final String parentName, final DefinitionNames definitionNames,
303 final String resourcePathPart, final String context) {
304 final String dataPath = getResourcePath("data", context) + "/" + resourcePathPart;
305 LOG.debug("Adding path: [{}]", dataPath);
306 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode().addAll(parentPathParams);
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 paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName,
315 if (node instanceof ActionNodeContainer) {
316 ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
317 final String operationsPath = getResourcePath("operations", context)
318 + "/" + resourcePathPart
319 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
320 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, operationsPath,
325 for (final DataSchemaNode childNode : childSchemaNodes) {
326 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
327 final String newParent = parentName + "_" + node.getQName().getLocalName();
328 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
329 final String newPathPart = resourcePathPart + "/" + createPath(childNode, pathParams, localName);
330 final boolean newIsConfig = isConfig && childNode.isConfiguration();
331 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
332 newIsConfig, newParent, definitionNames, newPathPart, context);
333 pathParams.removeAll();
334 pathParams.addAll(parentPathParams);
339 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
340 for (final DataSchemaNode child : nodes) {
341 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
348 private static Path operations(final DataSchemaNode node, final String moduleName,
349 final Optional<String> deviceName, final ArrayNode pathParams, final boolean isConfig,
350 final String parentName, final DefinitionNames definitionNames) {
351 final Path.Builder operationsBuilder = new Path.Builder();
353 final String discriminator = definitionNames.getDiscriminator(node);
354 final String nodeName = node.getQName().getLocalName();
356 final String defName = parentName + "_" + nodeName + discriminator;
357 final String defNameTop = parentName + "_" + nodeName + TOP + discriminator;
358 final Operation get = buildGet(node, moduleName, deviceName, pathParams, defName, defNameTop, isConfig);
359 operationsBuilder.get(get);
362 final Operation put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
363 node.getDescription().orElse(""), pathParams);
364 operationsBuilder.put(put);
366 final Operation patch = buildPatch(parentName, nodeName, moduleName, deviceName,
367 node.getDescription().orElse(""), pathParams);
368 operationsBuilder.patch(patch);
370 final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
371 operationsBuilder.delete(delete);
373 final Operation post = buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
374 node.getDescription().orElse(""), pathParams);
375 operationsBuilder.post(post);
377 return operationsBuilder.build();
380 private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams, final String localName) {
381 final StringBuilder path = new StringBuilder();
382 path.append(localName);
384 if (schemaNode instanceof ListSchemaNode) {
386 int discriminator = 1;
387 for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
388 final String keyName = listKey.getLocalName();
389 String paramName = keyName;
390 for (final JsonNode pathParam : pathParams) {
391 if (paramName.equals(pathParam.get("name").asText())) {
392 paramName = keyName + discriminator;
394 for (final JsonNode pathParameter : pathParams) {
395 if (paramName.equals(pathParameter.get("name").asText())) {
396 paramName = keyName + discriminator;
403 final String pathParamIdentifier = prefix + "{" + paramName + "}";
406 path.append(pathParamIdentifier);
408 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
409 pathParam.put("name", paramName);
411 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
412 .ifPresent(desc -> pathParam.put("description", desc));
414 final ObjectNode typeParent = getTypeParentNode(pathParam);
416 typeParent.put("type", "string");
417 pathParam.put("in", "path");
418 pathParam.put("required", true);
420 pathParams.add(pathParam);
423 return path.toString();
426 public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
427 if (schemaContext == null) {
428 return Collections.emptySortedSet();
431 final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
432 int result = module1.getName().compareTo(module2.getName());
434 result = Revision.compare(module1.getRevision(), module2.getRevision());
437 result = module1.getNamespace().compareTo(module2.getNamespace());
441 for (final Module m : schemaContext.getModules()) {
443 sortedModules.add(m);
446 return sortedModules;
449 private static void addOperations(final OperationDefinition operDef, final String moduleName,
450 final Optional<String> deviceName, final Map<String, Path> paths, final String parentName,
451 final DefinitionNames definitionNames, final String resourcePath, final ArrayNode parentPathParams) {
452 final var pathBuilder = new Path.Builder();
453 pathBuilder.post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
455 paths.put(resourcePath, pathBuilder.build());
458 protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
460 public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
461 final StringBuilder builder = new StringBuilder();
463 if (moduleName != null) {
464 builder.append(moduleName).append(':');
466 for (final PathArgument arg : key.getPathArguments()) {
467 final String name = arg.getNodeType().getLocalName();
468 if (arg instanceof NodeIdentifierWithPredicates nodeId) {
469 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
470 appendPathKeyValue(builder, entry.getValue());
473 builder.append(name).append('/');
476 return builder.toString();