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 java.io.IOException;
22 import java.time.format.DateTimeParseException;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
29 import java.util.Optional;
31 import java.util.SortedSet;
32 import java.util.TreeSet;
33 import java.util.stream.Collectors;
34 import javax.ws.rs.core.UriInfo;
35 import org.eclipse.jdt.annotation.NonNull;
36 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
37 import org.opendaylight.restconf.openapi.model.Operation;
38 import org.opendaylight.restconf.openapi.model.Parameter;
39 import org.opendaylight.restconf.openapi.model.Path;
40 import org.opendaylight.restconf.openapi.model.Schema;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.common.Revision;
43 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
44 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
46 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
48 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.Module;
51 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
52 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
53 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
54 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
55 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
56 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
58 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
66 public abstract class BaseYangOpenApiGenerator {
68 private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
69 private static final String CONTROLLER_RESOURCE_NAME = "Controller";
70 public static final String BASE_PATH = "/";
71 public static final String MODULE_NAME_SUFFIX = "_module";
72 public static final List<Map<String, List<String>>> SECURITY = List.of(Map.of("basicAuth", List.of()));
74 private final DOMSchemaService schemaService;
76 protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
77 this.schemaService = requireNonNull(schemaService);
80 public OpenApiInputStream getControllerModulesDoc(final UriInfo uriInfo) throws IOException {
81 final var context = requireNonNull(schemaService.getGlobalContext());
82 final var schema = createSchemaFromUriInfo(uriInfo);
83 final var host = createHostFromUriInfo(uriInfo);
84 final var title = "Controller modules of RESTCONF";
85 final var url = schema + "://" + host + BASE_PATH;
86 final var modules = context.getModules();
87 return new OpenApiInputStream(context, title, url, SECURITY, CONTROLLER_RESOURCE_NAME, "",false, false,
91 public OpenApiInputStream getApiDeclaration(final String module, final String revision, final UriInfo uriInfo)
93 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
94 Preconditions.checkState(schemaContext != null);
95 return getApiDeclaration(module, revision, uriInfo, schemaContext, "", CONTROLLER_RESOURCE_NAME);
98 public OpenApiInputStream getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
99 final EffectiveModelContext schemaContext, final String urlPrefix, final @NonNull String deviceName)
101 final Optional<Revision> rev;
104 rev = Revision.ofNullable(revision);
105 } catch (final DateTimeParseException e) {
106 throw new IllegalArgumentException(e);
109 final var module = schemaContext.findModule(moduleName, rev).orElse(null);
110 Preconditions.checkArgument(module != null,
111 "Could not find module by name,revision: " + moduleName + "," + revision);
113 final var schema = createSchemaFromUriInfo(uriInfo);
114 final var host = createHostFromUriInfo(uriInfo);
115 final var title = module.getName();
116 final var url = schema + "://" + host + BASE_PATH;
117 final var modules = List.of(module);
118 return new OpenApiInputStream(schemaContext, title, url, SECURITY, deviceName, urlPrefix, true, false,
122 public String createHostFromUriInfo(final UriInfo uriInfo) {
123 String portPart = "";
124 final int port = uriInfo.getBaseUri().getPort();
126 portPart = ":" + port;
128 return uriInfo.getBaseUri().getHost() + portPart;
131 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
132 return uriInfo.getBaseUri().getScheme();
135 public Map<String, Path> getPaths(final Module module, final String context, final String deviceName,
136 final EffectiveModelContext schemaContext, final DefinitionNames definitionNames,
137 final boolean isForSingleModule) {
138 final Map<String, Path> paths = new HashMap<>();
139 final String moduleName = module.getName();
141 boolean hasAddRootPostLink = false;
143 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
144 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
145 for (final DataSchemaNode node : dataSchemaNodes) {
146 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
147 final boolean isConfig = node.isConfiguration();
148 LOG.debug("Is Configuration node [{}] [{}]", isConfig, node.getQName().getLocalName());
150 final String localName = moduleName + ":" + node.getQName().getLocalName();
151 final String resourcePath = getResourcePath("data", context);
153 final List<Parameter> pathParams = new ArrayList<>();
155 * When there are two or more top container or list nodes
156 * whose config statement is true in module, make sure that
157 * only one root post link is added for this module.
159 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
160 LOG.debug("Has added root post link for module {}", moduleName);
161 addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
163 hasAddRootPostLink = true;
165 final String resourcePathPart = createPath(node, pathParams, localName);
166 addPaths(node, deviceName, moduleName, paths, pathParams, isConfig, schemaContext,
167 moduleName, definitionNames, resourcePathPart, context);
171 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
172 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
173 + rpcDefinition.getQName().getLocalName();
174 paths.put(resolvedPath, buildPostPath(rpcDefinition, moduleName, deviceName, moduleName, definitionNames,
178 LOG.debug("Number of Paths found [{}]", paths.size());
183 public Map<String, Schema> getSchemas(final Module module, final EffectiveModelContext schemaContext,
184 final DefinitionNames definitionNames, final boolean isForSingleModule) {
185 Map<String, Schema> schemas = new HashMap<>();
187 schemas = DefinitionGenerator.convertToSchemas(module, schemaContext, definitionNames, isForSingleModule);
188 } catch (final IOException e) {
189 LOG.error("Exception occurred in DefinitionGenerator", e); // FIXME propagate exception
195 private static void addRootPostLink(final Module module, final String deviceName,
196 final List<Parameter> pathParams, final String resourcePath, final Map<String, Path> paths) {
197 final var childNode = getListOrContainerChildNode(module);
198 if (childNode != null) {
199 final String moduleName = module.getName();
200 paths.put(resourcePath, new Path.Builder()
201 .post(buildPost(childNode, null, moduleName, "", moduleName, deviceName,
202 module.getDescription().orElse(""), pathParams))
207 public abstract String getResourcePath(String resourceType, String context);
209 private void addPaths(final DataSchemaNode node, final String deviceName, final String moduleName,
210 final Map<String, Path> paths, final List<Parameter> parentPathParams,
211 final boolean isConfig, final EffectiveModelContext schemaContext, final String parentName,
212 final DefinitionNames definitionNames, final String resourcePathPart, final String context) {
213 final String dataPath = getResourcePath("data", context) + "/" + resourcePathPart;
214 LOG.debug("Adding path: [{}]", dataPath);
215 final List<Parameter> pathParams = new ArrayList<>(parentPathParams);
216 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
217 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
218 childSchemaNodes = ((DataNodeContainer) node).getChildNodes();
220 final String fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
221 paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
224 if (node instanceof ActionNodeContainer actionContainer) {
225 actionContainer.getActions().forEach(actionDef -> {
226 final String operationsPath = getResourcePath("operations", context)
227 + "/" + resourcePathPart
228 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
229 paths.put(operationsPath, buildPostPath(actionDef, moduleName, deviceName, parentName,
230 definitionNames, pathParams));
234 for (final DataSchemaNode childNode : childSchemaNodes) {
235 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
236 final String newParent = parentName + "_" + node.getQName().getLocalName();
237 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
238 final String newPathPart = resourcePathPart + "/" + createPath(childNode, pathParams, localName);
239 final boolean newIsConfig = isConfig && childNode.isConfiguration();
240 addPaths(childNode, deviceName, moduleName, paths, pathParams, newIsConfig, schemaContext,
241 newParent, definitionNames, newPathPart, context);
243 pathParams.addAll(parentPathParams);
248 private static Path operations(final DataSchemaNode node, final String moduleName,
249 final String deviceName, final List<Parameter> pathParams, final boolean isConfig, final String parentName,
250 final DefinitionNames definitionNames, final String fullName) {
251 final Path.Builder operationsBuilder = new Path.Builder();
253 final String discriminator = definitionNames.getDiscriminator(node);
254 final String nodeName = node.getQName().getLocalName();
256 final Operation get = buildGet(node, parentName, moduleName, deviceName, pathParams, isConfig);
257 operationsBuilder.get(get);
260 final Operation put = buildPut(node, parentName, moduleName, deviceName, pathParams, fullName);
261 operationsBuilder.put(put);
263 final Operation patch = buildPatch(node, parentName, moduleName, deviceName, pathParams, fullName);
264 operationsBuilder.patch(patch);
266 final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
267 operationsBuilder.delete(delete);
269 if (node instanceof ContainerSchemaNode container) {
270 final var childNode = getListOrContainerChildNode(container);
271 // we have to ensure that we are able to create POST payload containing the first container/list child
272 if (childNode != null) {
273 final Operation post = buildPost(childNode, parentName, nodeName, discriminator, moduleName,
274 deviceName, node.getDescription().orElse(""), pathParams);
275 operationsBuilder.post(post);
279 return operationsBuilder.build();
282 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
283 return node.getChildNodes().stream()
284 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
285 .findFirst().orElse(null);
288 private static String createPath(final DataSchemaNode schemaNode, final List<Parameter> pathParams,
289 final String localName) {
290 final StringBuilder path = new StringBuilder();
291 path.append(localName);
292 final Set<String> parameters = pathParams.stream()
293 .map(Parameter::name)
294 .collect(Collectors.toSet());
296 if (schemaNode instanceof ListSchemaNode listSchemaNode) {
298 int discriminator = 1;
299 for (final QName listKey : listSchemaNode.getKeyDefinition()) {
300 final String keyName = listKey.getLocalName();
301 String paramName = keyName;
302 while (!parameters.add(paramName)) {
303 paramName = keyName + discriminator;
307 final String pathParamIdentifier = prefix + "{" + paramName + "}";
309 path.append(pathParamIdentifier);
311 final String description = listSchemaNode.findDataChildByName(listKey)
312 .flatMap(DataSchemaNode::getDescription).orElse(null);
313 pathParams.add(new Parameter.Builder()
315 .schema(new Schema.Builder().type(getAllowedType(listSchemaNode, listKey)).build())
318 .description(description)
322 return path.toString();
325 private static String getAllowedType(final ListSchemaNode list, final QName key) {
326 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
328 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
329 // see: https://swagger.io/docs/specification/data-models/data-types/
330 // TODO: Java 21 use pattern matching for switch
331 if (keyType instanceof Int8TypeDefinition) {
334 if (keyType instanceof Int16TypeDefinition) {
337 if (keyType instanceof Int32TypeDefinition) {
340 if (keyType instanceof Int64TypeDefinition) {
343 if (keyType instanceof Uint8TypeDefinition) {
346 if (keyType instanceof Uint16TypeDefinition) {
349 if (keyType instanceof Uint32TypeDefinition) {
352 if (keyType instanceof Uint64TypeDefinition) {
356 if (keyType instanceof DecimalTypeDefinition) {
360 if (keyType instanceof BooleanTypeDefinition) {
367 public static SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
368 if (schemaContext == null) {
369 return Collections.emptySortedSet();
372 final var sortedModules = new TreeSet<Module>((module1, module2) -> {
373 int result = module1.getName().compareTo(module2.getName());
375 result = Revision.compare(module1.getRevision(), module2.getRevision());
378 result = module1.getNamespace().compareTo(module2.getNamespace());
382 sortedModules.addAll(schemaContext.getModules());
383 return sortedModules;
386 private static Path buildPostPath(final OperationDefinition operDef, final String moduleName,
387 final String deviceName, final String parentName, final DefinitionNames definitionNames,
388 final List<Parameter> parentPathParams) {
389 return new Path.Builder()
390 .post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames, parentPathParams))