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.buildDelete;
12 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildGet;
13 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPatch;
14 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPost;
15 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPostOperation;
16 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPut;
17 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
18 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
20 import com.google.common.base.Preconditions;
21 import com.google.common.collect.Range;
22 import java.io.IOException;
23 import java.time.format.DateTimeParseException;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.List;
31 import java.util.Optional;
33 import java.util.SortedSet;
34 import java.util.TreeSet;
35 import java.util.stream.Collectors;
36 import javax.ws.rs.core.UriInfo;
37 import org.eclipse.jdt.annotation.NonNull;
38 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
39 import org.opendaylight.restconf.openapi.model.Components;
40 import org.opendaylight.restconf.openapi.model.Info;
41 import org.opendaylight.restconf.openapi.model.OpenApiObject;
42 import org.opendaylight.restconf.openapi.model.Operation;
43 import org.opendaylight.restconf.openapi.model.Parameter;
44 import org.opendaylight.restconf.openapi.model.Path;
45 import org.opendaylight.restconf.openapi.model.Schema;
46 import org.opendaylight.restconf.openapi.model.Server;
47 import org.opendaylight.restconf.openapi.model.security.Http;
48 import org.opendaylight.yangtools.yang.common.QName;
49 import org.opendaylight.yangtools.yang.common.Revision;
50 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
51 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
53 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
55 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.Module;
57 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
58 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
62 public abstract class BaseYangOpenApiGenerator {
64 private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
65 private static final String CONTROLLER_RESOURCE_NAME = "Controller";
67 public static final String API_VERSION = "1.0.0";
68 public static final String OPEN_API_VERSION = "3.0.3";
69 public static final String BASE_PATH = "/";
70 public static final String MODULE_NAME_SUFFIX = "_module";
71 public static final String BASIC_AUTH_NAME = "basicAuth";
72 public static final Http OPEN_API_BASIC_AUTH = new Http("basic", null, null);
73 public static final List<Map<String, List<String>>> SECURITY = List.of(Map.of(BASIC_AUTH_NAME, List.of()));
74 public static final String DESCRIPTION = """
75 We are providing full API for configurational data which can be edited (by POST, PUT, PATCH and DELETE).
76 For operational data we only provide GET API.\n
77 For majority of request you can see only config data in examples. That’s because we can show only one example
78 per request. The exception when you can see operational data in example is when data are representing
79 operational (config false) container with no config data in it.""";
81 private final DOMSchemaService schemaService;
83 protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
84 this.schemaService = requireNonNull(schemaService);
87 public OpenApiObject getControllerModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames) {
88 final var context = requireNonNull(schemaService.getGlobalContext());
89 final var schema = createSchemaFromUriInfo(uriInfo);
90 final var host = createHostFromUriInfo(uriInfo);
91 final var title = "Controller modules of RESTCONF";
92 final var info = new Info(API_VERSION, title, DESCRIPTION);
93 final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
95 final var paths = new HashMap<String, Path>();
96 final var schemas = new HashMap<String, Schema>();
97 for (final var module : getSortedModules(context)) {
98 LOG.debug("Working on [{},{}]...", module.getName(), module.getQNameModule().getRevision().orElse(null));
99 schemas.putAll(getSchemas(module, context, definitionNames, false));
100 paths.putAll(getPaths(module, "", CONTROLLER_RESOURCE_NAME, context, definitionNames, false));
103 final var components = new Components(schemas, Map.of(BASIC_AUTH_NAME, OPEN_API_BASIC_AUTH));
104 return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
107 public static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
108 if (range.equals(Range.all())) {
111 final int begin = range.lowerEndpoint();
112 final int end = range.upperEndpoint();
114 Module firstModule = null;
116 final Iterator<Module> iterator = modules.iterator();
118 while (iterator.hasNext() && counter < end) {
119 final Module module = iterator.next();
120 if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
121 if (counter == begin) {
122 firstModule = module;
128 if (iterator.hasNext()) {
129 return modules.subSet(firstModule, iterator.next());
131 return modules.tailSet(firstModule);
135 public OpenApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo) {
136 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
137 Preconditions.checkState(schemaContext != null);
138 return getApiDeclaration(module, revision, uriInfo, schemaContext, "", CONTROLLER_RESOURCE_NAME);
141 public OpenApiObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
142 final EffectiveModelContext schemaContext, final String context, final @NonNull String deviceName) {
143 final Optional<Revision> rev;
146 rev = Revision.ofNullable(revision);
147 } catch (final DateTimeParseException e) {
148 throw new IllegalArgumentException(e);
151 final var module = schemaContext.findModule(moduleName, rev).orElse(null);
152 Preconditions.checkArgument(module != null,
153 "Could not find module by name,revision: " + moduleName + "," + revision);
155 final var schema = createSchemaFromUriInfo(uriInfo);
156 final var host = createHostFromUriInfo(uriInfo);
157 final var info = new Info(API_VERSION, module.getName(), DESCRIPTION);
158 final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
159 final var definitionNames = new DefinitionNames();
160 final var schemas = getSchemas(module, schemaContext, definitionNames, true);
161 final var components = new Components(schemas, Map.of(BASIC_AUTH_NAME, OPEN_API_BASIC_AUTH));
162 final var paths = getPaths(module, context, deviceName, schemaContext, definitionNames, true);
163 return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
166 public String createHostFromUriInfo(final UriInfo uriInfo) {
167 String portPart = "";
168 final int port = uriInfo.getBaseUri().getPort();
170 portPart = ":" + port;
172 return uriInfo.getBaseUri().getHost() + portPart;
175 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
176 return uriInfo.getBaseUri().getScheme();
179 public Map<String, Path> getPaths(final Module module, final String context, final String deviceName,
180 final EffectiveModelContext schemaContext, final DefinitionNames definitionNames,
181 final boolean isForSingleModule) {
182 final Map<String, Path> paths = new HashMap<>();
183 final String moduleName = module.getName();
185 boolean hasAddRootPostLink = false;
187 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
188 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
189 for (final DataSchemaNode node : dataSchemaNodes) {
190 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
191 final boolean isConfig = node.isConfiguration();
192 LOG.debug("Is Configuration node [{}] [{}]", isConfig, node.getQName().getLocalName());
194 final String localName = moduleName + ":" + node.getQName().getLocalName();
195 final String resourcePath = getResourcePath("data", context);
197 final List<Parameter> pathParams = new ArrayList<>();
199 * When there are two or more top container or list nodes
200 * whose config statement is true in module, make sure that
201 * only one root post link is added for this module.
203 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
204 LOG.debug("Has added root post link for module {}", moduleName);
205 addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
207 hasAddRootPostLink = true;
209 final String resourcePathPart = createPath(node, pathParams, localName);
210 addPaths(node, deviceName, moduleName, paths, pathParams, isConfig, schemaContext,
211 moduleName, definitionNames, resourcePathPart, context);
215 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
216 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
217 + rpcDefinition.getQName().getLocalName();
218 paths.put(resolvedPath, buildPostPath(rpcDefinition, moduleName, deviceName, moduleName, definitionNames,
222 LOG.debug("Number of Paths found [{}]", paths.size());
227 public Map<String, Schema> getSchemas(final Module module, final EffectiveModelContext schemaContext,
228 final DefinitionNames definitionNames, final boolean isForSingleModule) {
229 Map<String, Schema> schemas = new HashMap<>();
231 schemas = DefinitionGenerator.convertToSchemas(module, schemaContext, definitionNames, isForSingleModule);
232 } catch (final IOException e) {
233 LOG.error("Exception occurred in DefinitionGenerator", e); // FIXME propagate exception
239 private static void addRootPostLink(final Module module, final String deviceName,
240 final List<Parameter> pathParams, final String resourcePath, final Map<String, Path> paths) {
241 if (containsListOrContainer(module.getChildNodes())) {
242 final String moduleName = module.getName();
243 final String name = moduleName + MODULE_NAME_SUFFIX;
244 paths.put(resourcePath, new Path.Builder()
245 .post(buildPost(null, null, name, "", moduleName, deviceName,
246 module.getDescription().orElse(""), pathParams))
251 public abstract String getResourcePath(String resourceType, String context);
253 private void addPaths(final DataSchemaNode node, final String deviceName, final String moduleName,
254 final Map<String, Path> paths, final List<Parameter> parentPathParams,
255 final boolean isConfig, final EffectiveModelContext schemaContext, final String parentName,
256 final DefinitionNames definitionNames, final String resourcePathPart, final String context) {
257 final String dataPath = getResourcePath("data", context) + "/" + resourcePathPart;
258 LOG.debug("Adding path: [{}]", dataPath);
259 final List<Parameter> pathParams = new ArrayList<>(parentPathParams);
260 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
261 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
262 childSchemaNodes = ((DataNodeContainer) node).getChildNodes();
264 final String fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
265 paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
268 if (node instanceof ActionNodeContainer actionContainer) {
269 actionContainer.getActions().forEach(actionDef -> {
270 final String operationsPath = getResourcePath("operations", context)
271 + "/" + resourcePathPart
272 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
273 paths.put(operationsPath, buildPostPath(actionDef, moduleName, deviceName, parentName,
274 definitionNames, pathParams));
278 for (final DataSchemaNode childNode : childSchemaNodes) {
279 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
280 final String newParent = parentName + "_" + node.getQName().getLocalName();
281 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
282 final String newPathPart = resourcePathPart + "/" + createPath(childNode, pathParams, localName);
283 final boolean newIsConfig = isConfig && childNode.isConfiguration();
284 addPaths(childNode, deviceName, moduleName, paths, pathParams, newIsConfig, schemaContext,
285 newParent, definitionNames, newPathPart, context);
287 pathParams.addAll(parentPathParams);
292 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
293 for (final DataSchemaNode child : nodes) {
294 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
301 private static Path operations(final DataSchemaNode node, final String moduleName,
302 final String deviceName, final List<Parameter> pathParams, final boolean isConfig, final String parentName,
303 final DefinitionNames definitionNames, final String fullName) {
304 final Path.Builder operationsBuilder = new Path.Builder();
306 final String discriminator = definitionNames.getDiscriminator(node);
307 final String nodeName = node.getQName().getLocalName();
309 final Operation get = buildGet(node, parentName, moduleName, deviceName, pathParams, isConfig);
310 operationsBuilder.get(get);
313 final Operation put = buildPut(node, parentName, moduleName, deviceName, pathParams, fullName);
314 operationsBuilder.put(put);
316 final Operation patch = buildPatch(node, parentName, moduleName, deviceName, pathParams, fullName);
317 operationsBuilder.patch(patch);
319 final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
320 operationsBuilder.delete(delete);
322 if (node instanceof ContainerSchemaNode container) {
323 final var childNode = getListOrContainerChildNode(container);
324 // we have to ensure that we are able to create POST payload containing the first container/list child
325 if (childNode != null) {
326 final Operation post = buildPost(childNode, parentName, nodeName, discriminator, moduleName,
327 deviceName, node.getDescription().orElse(""), pathParams);
328 operationsBuilder.post(post);
332 return operationsBuilder.build();
335 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
336 return node.getChildNodes().stream()
337 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
338 .findFirst().orElse(null);
341 private static String createPath(final DataSchemaNode schemaNode, final List<Parameter> pathParams,
342 final String localName) {
343 final StringBuilder path = new StringBuilder();
344 path.append(localName);
345 final Set<String> parameters = pathParams.stream()
346 .map(Parameter::name)
347 .collect(Collectors.toSet());
349 if (schemaNode instanceof ListSchemaNode listSchemaNode) {
351 int discriminator = 1;
352 for (final QName listKey : listSchemaNode.getKeyDefinition()) {
353 final String keyName = listKey.getLocalName();
354 String paramName = keyName;
355 while (!parameters.add(paramName)) {
356 paramName = keyName + discriminator;
360 final String pathParamIdentifier = prefix + "{" + paramName + "}";
362 path.append(pathParamIdentifier);
364 final String description = listSchemaNode.findDataChildByName(listKey)
365 .flatMap(DataSchemaNode::getDescription).orElse(null);
366 pathParams.add(new Parameter.Builder()
368 .schema(new Schema.Builder().type("string").build())
371 .description(description)
375 return path.toString();
378 public static SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
379 if (schemaContext == null) {
380 return Collections.emptySortedSet();
383 final var sortedModules = new TreeSet<Module>((module1, module2) -> {
384 int result = module1.getName().compareTo(module2.getName());
386 result = Revision.compare(module1.getRevision(), module2.getRevision());
389 result = module1.getNamespace().compareTo(module2.getNamespace());
393 sortedModules.addAll(schemaContext.getModules());
394 return sortedModules;
397 private static Path buildPostPath(final OperationDefinition operDef, final String moduleName,
398 final String deviceName, final String parentName, final DefinitionNames definitionNames,
399 final List<Parameter> parentPathParams) {
400 return new Path.Builder()
401 .post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames, parentPathParams))