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;
251 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, localName);
252 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, isConfig,
253 moduleName, definitionNames, resolvedPath);
257 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
258 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
259 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
260 + rpcDefinition.getQName().getLocalName();
261 addOperations(rpcDefinition, moduleName, deviceName, paths, moduleName, definitionNames,
262 resolvedPath, pathParams);
265 LOG.debug("Number of Paths found [{}]", paths.size());
267 if (isForSingleModule) {
268 docBuilder.paths(paths);
270 docBuilder.getPaths().putAll(paths);
273 return docBuilder.build();
276 private static void addRootPostLink(final Module module, final Optional<String> deviceName,
277 final ArrayNode pathParams, final String resourcePath, final Map<String, Path> paths) {
278 if (containsListOrContainer(module.getChildNodes())) {
279 final String moduleName = module.getName();
280 final String name = moduleName + MODULE_NAME_SUFFIX;
281 final var postBuilder = new Path.Builder();
282 postBuilder.post(buildPost("", name, "", moduleName, deviceName,
283 module.getDescription().orElse(""), pathParams));
284 paths.put(resourcePath, postBuilder.build());
288 public OpenApiObject.Builder createOpenApiObjectBuilder(final String schema, final String host,
289 final String basePath, final String title) {
290 final OpenApiObject.Builder docBuilder = new OpenApiObject.Builder();
291 docBuilder.openapi(OPEN_API_VERSION);
292 docBuilder.info(new Info.Builder().title(title).version(API_VERSION).build())
293 .servers(List.of(new Server(schema + "://" + host + basePath)))
294 .components(new Components(new HashMap<>(), new SecuritySchemes(OPEN_API_BASIC_AUTH)))
299 public abstract String getResourcePath(String resourceType, String context);
301 private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
302 final Map<String, Path> paths, final ArrayNode parentPathParams, final EffectiveModelContext schemaContext,
303 final boolean isConfig, final String parentName, final DefinitionNames definitionNames,
304 final String resourcePath) {
305 LOG.debug("Adding path: [{}]", resourcePath);
307 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode().addAll(parentPathParams);
308 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
309 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
310 final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
311 childSchemaNodes = dataNodeContainer.getChildNodes();
314 paths.put(resourcePath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName,
317 if (node instanceof ActionNodeContainer) {
318 ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
319 final String resolvedPath = "/rests/operations" + resourcePath.substring(11)
320 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
321 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, resolvedPath,
326 for (final DataSchemaNode childNode : childSchemaNodes) {
327 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
328 final String newParent = parentName + "_" + node.getQName().getLocalName();
329 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
330 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, localName);
331 final boolean newIsConfig = isConfig && childNode.isConfiguration();
332 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
333 newIsConfig, newParent, definitionNames, newResourcePath);
334 pathParams.removeAll();
335 pathParams.addAll(parentPathParams);
340 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
341 for (final DataSchemaNode child : nodes) {
342 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
349 private static Path operations(final DataSchemaNode node, final String moduleName,
350 final Optional<String> deviceName, final ArrayNode pathParams, final boolean isConfig,
351 final String parentName, final DefinitionNames definitionNames) {
352 final Path.Builder operationsBuilder = new Path.Builder();
354 final String discriminator = definitionNames.getDiscriminator(node);
355 final String nodeName = node.getQName().getLocalName();
357 final String defName = parentName + "_" + nodeName + discriminator;
358 final String defNameTop = parentName + "_" + nodeName + TOP + discriminator;
359 final Operation get = buildGet(node, moduleName, deviceName, pathParams, defName, defNameTop, isConfig);
360 operationsBuilder.get(get);
363 final Operation put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
364 node.getDescription().orElse(""), pathParams);
365 operationsBuilder.put(put);
367 final Operation patch = buildPatch(parentName, nodeName, moduleName, deviceName,
368 node.getDescription().orElse(""), pathParams);
369 operationsBuilder.patch(patch);
371 final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
372 operationsBuilder.delete(delete);
374 final Operation post = buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
375 node.getDescription().orElse(""), pathParams);
376 operationsBuilder.post(post);
378 return operationsBuilder.build();
381 private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams, final String localName) {
382 final StringBuilder path = new StringBuilder();
383 path.append(localName);
385 if (schemaNode instanceof ListSchemaNode) {
387 int discriminator = 1;
388 for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
389 final String keyName = listKey.getLocalName();
390 String paramName = keyName;
391 for (final JsonNode pathParam : pathParams) {
392 if (paramName.equals(pathParam.get("name").asText())) {
393 paramName = keyName + discriminator;
398 final String pathParamIdentifier = prefix + "{" + paramName + "}";
401 path.append(pathParamIdentifier);
403 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
404 pathParam.put("name", paramName);
406 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
407 .ifPresent(desc -> pathParam.put("description", desc));
409 final ObjectNode typeParent = getTypeParentNode(pathParam);
411 typeParent.put("type", "string");
412 pathParam.put("in", "path");
413 pathParam.put("required", true);
415 pathParams.add(pathParam);
418 return path.toString();
421 public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
422 if (schemaContext == null) {
423 return Collections.emptySortedSet();
426 final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
427 int result = module1.getName().compareTo(module2.getName());
429 result = Revision.compare(module1.getRevision(), module2.getRevision());
432 result = module1.getNamespace().compareTo(module2.getNamespace());
436 for (final Module m : schemaContext.getModules()) {
438 sortedModules.add(m);
441 return sortedModules;
444 private static void addOperations(final OperationDefinition operDef, final String moduleName,
445 final Optional<String> deviceName, final Map<String, Path> paths, final String parentName,
446 final DefinitionNames definitionNames, final String resourcePath, final ArrayNode parentPathParams) {
447 final var pathBuilder = new Path.Builder();
448 pathBuilder.post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
450 paths.put(resourcePath, pathBuilder.build());
453 protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
455 public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
456 final StringBuilder builder = new StringBuilder();
458 if (moduleName != null) {
459 builder.append(moduleName).append(':');
461 for (final PathArgument arg : key.getPathArguments()) {
462 final String name = arg.getNodeType().getLocalName();
463 if (arg instanceof NodeIdentifierWithPredicates nodeId) {
464 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
465 appendPathKeyValue(builder, entry.getValue());
468 builder.append(name).append('/');
471 return builder.toString();