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.LeafSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
57 import org.opendaylight.yangtools.yang.model.api.Module;
58 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
59 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
73 public abstract class BaseYangOpenApiGenerator {
75 private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
76 private static final String CONTROLLER_RESOURCE_NAME = "Controller";
78 public static final String API_VERSION = "1.0.0";
79 public static final String OPEN_API_VERSION = "3.0.3";
80 public static final String BASE_PATH = "/";
81 public static final String MODULE_NAME_SUFFIX = "_module";
82 public static final String BASIC_AUTH_NAME = "basicAuth";
83 public static final Http OPEN_API_BASIC_AUTH = new Http("basic", null, null);
84 public static final List<Map<String, List<String>>> SECURITY = List.of(Map.of(BASIC_AUTH_NAME, List.of()));
85 public static final String DESCRIPTION = """
86 We are providing full API for configurational data which can be edited (by POST, PUT, PATCH and DELETE).
87 For operational data we only provide GET API.\n
88 For majority of request you can see only config data in examples. That’s because we can show only one example
89 per request. The exception when you can see operational data in example is when data are representing
90 operational (config false) container with no config data in it.""";
92 private final DOMSchemaService schemaService;
94 protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
95 this.schemaService = requireNonNull(schemaService);
98 public OpenApiObject getControllerModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames) {
99 final var context = requireNonNull(schemaService.getGlobalContext());
100 final var schema = createSchemaFromUriInfo(uriInfo);
101 final var host = createHostFromUriInfo(uriInfo);
102 final var title = "Controller modules of RESTCONF";
103 final var info = new Info(API_VERSION, title, DESCRIPTION);
104 final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
106 final var paths = new HashMap<String, Path>();
107 final var schemas = new HashMap<String, Schema>();
108 for (final var module : getSortedModules(context)) {
109 LOG.debug("Working on [{},{}]...", module.getName(), module.getQNameModule().getRevision().orElse(null));
110 schemas.putAll(getSchemas(module, context, definitionNames, false));
111 paths.putAll(getPaths(module, "", CONTROLLER_RESOURCE_NAME, context, definitionNames, false));
114 final var components = new Components(schemas, Map.of(BASIC_AUTH_NAME, OPEN_API_BASIC_AUTH));
115 return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
118 public static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
119 if (range.equals(Range.all())) {
122 final int begin = range.lowerEndpoint();
123 final int end = range.upperEndpoint();
125 Module firstModule = null;
127 final Iterator<Module> iterator = modules.iterator();
129 while (iterator.hasNext() && counter < end) {
130 final Module module = iterator.next();
131 if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
132 if (counter == begin) {
133 firstModule = module;
139 if (iterator.hasNext()) {
140 return modules.subSet(firstModule, iterator.next());
142 return modules.tailSet(firstModule);
146 public OpenApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo) {
147 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
148 Preconditions.checkState(schemaContext != null);
149 return getApiDeclaration(module, revision, uriInfo, schemaContext, "", CONTROLLER_RESOURCE_NAME);
152 public OpenApiObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
153 final EffectiveModelContext schemaContext, final String context, final @NonNull String deviceName) {
154 final Optional<Revision> rev;
157 rev = Revision.ofNullable(revision);
158 } catch (final DateTimeParseException e) {
159 throw new IllegalArgumentException(e);
162 final var module = schemaContext.findModule(moduleName, rev).orElse(null);
163 Preconditions.checkArgument(module != null,
164 "Could not find module by name,revision: " + moduleName + "," + revision);
166 final var schema = createSchemaFromUriInfo(uriInfo);
167 final var host = createHostFromUriInfo(uriInfo);
168 final var info = new Info(API_VERSION, module.getName(), DESCRIPTION);
169 final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
170 final var definitionNames = new DefinitionNames();
171 final var schemas = getSchemas(module, schemaContext, definitionNames, true);
172 final var components = new Components(schemas, Map.of(BASIC_AUTH_NAME, OPEN_API_BASIC_AUTH));
173 final var paths = getPaths(module, context, deviceName, schemaContext, definitionNames, true);
174 return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
177 public String createHostFromUriInfo(final UriInfo uriInfo) {
178 String portPart = "";
179 final int port = uriInfo.getBaseUri().getPort();
181 portPart = ":" + port;
183 return uriInfo.getBaseUri().getHost() + portPart;
186 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
187 return uriInfo.getBaseUri().getScheme();
190 public Map<String, Path> getPaths(final Module module, final String context, final String deviceName,
191 final EffectiveModelContext schemaContext, final DefinitionNames definitionNames,
192 final boolean isForSingleModule) {
193 final Map<String, Path> paths = new HashMap<>();
194 final String moduleName = module.getName();
196 boolean hasAddRootPostLink = false;
198 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
199 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
200 for (final DataSchemaNode node : dataSchemaNodes) {
201 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
202 final boolean isConfig = node.isConfiguration();
203 LOG.debug("Is Configuration node [{}] [{}]", isConfig, node.getQName().getLocalName());
205 final String localName = moduleName + ":" + node.getQName().getLocalName();
206 final String resourcePath = getResourcePath("data", context);
208 final List<Parameter> pathParams = new ArrayList<>();
210 * When there are two or more top container or list nodes
211 * whose config statement is true in module, make sure that
212 * only one root post link is added for this module.
214 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
215 LOG.debug("Has added root post link for module {}", moduleName);
216 addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
218 hasAddRootPostLink = true;
220 final String resourcePathPart = createPath(node, pathParams, localName);
221 addPaths(node, deviceName, moduleName, paths, pathParams, isConfig, schemaContext,
222 moduleName, definitionNames, resourcePathPart, context);
226 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
227 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
228 + rpcDefinition.getQName().getLocalName();
229 paths.put(resolvedPath, buildPostPath(rpcDefinition, moduleName, deviceName, moduleName, definitionNames,
233 LOG.debug("Number of Paths found [{}]", paths.size());
238 public Map<String, Schema> getSchemas(final Module module, final EffectiveModelContext schemaContext,
239 final DefinitionNames definitionNames, final boolean isForSingleModule) {
240 Map<String, Schema> schemas = new HashMap<>();
242 schemas = DefinitionGenerator.convertToSchemas(module, schemaContext, definitionNames, isForSingleModule);
243 } catch (final IOException e) {
244 LOG.error("Exception occurred in DefinitionGenerator", e); // FIXME propagate exception
250 private static void addRootPostLink(final Module module, final String deviceName,
251 final List<Parameter> pathParams, final String resourcePath, final Map<String, Path> paths) {
252 final var childNode = getListOrContainerChildNode(module);
253 if (childNode != null) {
254 final String moduleName = module.getName();
255 paths.put(resourcePath, new Path.Builder()
256 .post(buildPost(childNode, null, moduleName, "", moduleName, deviceName,
257 module.getDescription().orElse(""), pathParams))
262 public abstract String getResourcePath(String resourceType, String context);
264 private void addPaths(final DataSchemaNode node, final String deviceName, final String moduleName,
265 final Map<String, Path> paths, final List<Parameter> parentPathParams,
266 final boolean isConfig, final EffectiveModelContext schemaContext, final String parentName,
267 final DefinitionNames definitionNames, final String resourcePathPart, final String context) {
268 final String dataPath = getResourcePath("data", context) + "/" + resourcePathPart;
269 LOG.debug("Adding path: [{}]", dataPath);
270 final List<Parameter> pathParams = new ArrayList<>(parentPathParams);
271 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
272 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
273 childSchemaNodes = ((DataNodeContainer) node).getChildNodes();
275 final String fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
276 paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
279 if (node instanceof ActionNodeContainer actionContainer) {
280 actionContainer.getActions().forEach(actionDef -> {
281 final String operationsPath = getResourcePath("operations", context)
282 + "/" + resourcePathPart
283 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
284 paths.put(operationsPath, buildPostPath(actionDef, moduleName, deviceName, parentName,
285 definitionNames, pathParams));
289 for (final DataSchemaNode childNode : childSchemaNodes) {
290 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
291 final String newParent = parentName + "_" + node.getQName().getLocalName();
292 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
293 final String newPathPart = resourcePathPart + "/" + createPath(childNode, pathParams, localName);
294 final boolean newIsConfig = isConfig && childNode.isConfiguration();
295 addPaths(childNode, deviceName, moduleName, paths, pathParams, newIsConfig, schemaContext,
296 newParent, definitionNames, newPathPart, context);
298 pathParams.addAll(parentPathParams);
303 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
304 for (final DataSchemaNode child : nodes) {
305 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
312 private static Path operations(final DataSchemaNode node, final String moduleName,
313 final String deviceName, final List<Parameter> pathParams, final boolean isConfig, final String parentName,
314 final DefinitionNames definitionNames, final String fullName) {
315 final Path.Builder operationsBuilder = new Path.Builder();
317 final String discriminator = definitionNames.getDiscriminator(node);
318 final String nodeName = node.getQName().getLocalName();
320 final Operation get = buildGet(node, parentName, moduleName, deviceName, pathParams, isConfig);
321 operationsBuilder.get(get);
324 final Operation put = buildPut(node, parentName, moduleName, deviceName, pathParams, fullName);
325 operationsBuilder.put(put);
327 final Operation patch = buildPatch(node, parentName, moduleName, deviceName, pathParams, fullName);
328 operationsBuilder.patch(patch);
330 final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
331 operationsBuilder.delete(delete);
333 if (node instanceof ContainerSchemaNode container) {
334 final var childNode = getListOrContainerChildNode(container);
335 // we have to ensure that we are able to create POST payload containing the first container/list child
336 if (childNode != null) {
337 final Operation post = buildPost(childNode, parentName, nodeName, discriminator, moduleName,
338 deviceName, node.getDescription().orElse(""), pathParams);
339 operationsBuilder.post(post);
343 return operationsBuilder.build();
346 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
347 return node.getChildNodes().stream()
348 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
349 .findFirst().orElse(null);
352 private static String createPath(final DataSchemaNode schemaNode, final List<Parameter> pathParams,
353 final String localName) {
354 final StringBuilder path = new StringBuilder();
355 path.append(localName);
356 final Set<String> parameters = pathParams.stream()
357 .map(Parameter::name)
358 .collect(Collectors.toSet());
360 if (schemaNode instanceof ListSchemaNode listSchemaNode) {
362 int discriminator = 1;
363 for (final QName listKey : listSchemaNode.getKeyDefinition()) {
364 final String keyName = listKey.getLocalName();
365 String paramName = keyName;
366 while (!parameters.add(paramName)) {
367 paramName = keyName + discriminator;
371 final String pathParamIdentifier = prefix + "{" + paramName + "}";
373 path.append(pathParamIdentifier);
375 final String description = listSchemaNode.findDataChildByName(listKey)
376 .flatMap(DataSchemaNode::getDescription).orElse(null);
377 pathParams.add(new Parameter.Builder()
379 .schema(new Schema.Builder().type(getAllowedType(listSchemaNode, listKey)).build())
382 .description(description)
386 return path.toString();
389 private static String getAllowedType(final ListSchemaNode list, final QName key) {
390 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
392 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
393 // see: https://swagger.io/docs/specification/data-models/data-types/
394 // TODO: Java 21 use pattern matching for switch
395 if (keyType instanceof Int8TypeDefinition) {
398 if (keyType instanceof Int16TypeDefinition) {
401 if (keyType instanceof Int32TypeDefinition) {
404 if (keyType instanceof Int64TypeDefinition) {
407 if (keyType instanceof Uint8TypeDefinition) {
410 if (keyType instanceof Uint16TypeDefinition) {
413 if (keyType instanceof Uint32TypeDefinition) {
416 if (keyType instanceof Uint64TypeDefinition) {
420 if (keyType instanceof DecimalTypeDefinition) {
424 if (keyType instanceof BooleanTypeDefinition) {
431 public static SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
432 if (schemaContext == null) {
433 return Collections.emptySortedSet();
436 final var sortedModules = new TreeSet<Module>((module1, module2) -> {
437 int result = module1.getName().compareTo(module2.getName());
439 result = Revision.compare(module1.getRevision(), module2.getRevision());
442 result = module1.getNamespace().compareTo(module2.getNamespace());
446 sortedModules.addAll(schemaContext.getModules());
447 return sortedModules;
450 private static Path buildPostPath(final OperationDefinition operDef, final String moduleName,
451 final String deviceName, final String parentName, final DefinitionNames definitionNames,
452 final List<Parameter> parentPathParams) {
453 return new Path.Builder()
454 .post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames, parentPathParams))