f9ab42c6acfaa3cce6ac6eece7a77cec407f9f56
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / impl / BaseYangOpenApiGenerator.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.openapi.impl;
9
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;
19
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;
30 import java.util.Map;
31 import java.util.Optional;
32 import java.util.Set;
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;
72
73 public abstract class BaseYangOpenApiGenerator {
74
75     private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
76     private static final String CONTROLLER_RESOURCE_NAME = "Controller";
77
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.""";
91
92     private final DOMSchemaService schemaService;
93
94     protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
95         this.schemaService = requireNonNull(schemaService);
96     }
97
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));
105
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));
112         }
113
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);
116     }
117
118     public static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
119         if (range.equals(Range.all())) {
120             return modules;
121         }
122         final int begin = range.lowerEndpoint();
123         final int end = range.upperEndpoint();
124
125         Module firstModule = null;
126
127         final Iterator<Module> iterator = modules.iterator();
128         int counter = 0;
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;
134                 }
135                 counter++;
136             }
137         }
138
139         if (iterator.hasNext()) {
140             return modules.subSet(firstModule, iterator.next());
141         } else {
142             return modules.tailSet(firstModule);
143         }
144     }
145
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);
150     }
151
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;
155
156         try {
157             rev = Revision.ofNullable(revision);
158         } catch (final DateTimeParseException e) {
159             throw new IllegalArgumentException(e);
160         }
161
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);
165
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);
175     }
176
177     public String createHostFromUriInfo(final UriInfo uriInfo) {
178         String portPart = "";
179         final int port = uriInfo.getBaseUri().getPort();
180         if (port != -1) {
181             portPart = ":" + port;
182         }
183         return uriInfo.getBaseUri().getHost() + portPart;
184     }
185
186     public String createSchemaFromUriInfo(final UriInfo uriInfo) {
187         return uriInfo.getBaseUri().getScheme();
188     }
189
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();
195
196         boolean hasAddRootPostLink = false;
197
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());
204
205                 final String localName = moduleName + ":" + node.getQName().getLocalName();
206                 final String resourcePath  = getResourcePath("data", context);
207
208                 final List<Parameter> pathParams = new ArrayList<>();
209                 /*
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.
213                  */
214                 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
215                     LOG.debug("Has added root post link for module {}", moduleName);
216                     addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
217
218                     hasAddRootPostLink = true;
219                 }
220                 final String resourcePathPart = createPath(node, pathParams, localName);
221                 addPaths(node, deviceName, moduleName, paths, pathParams, isConfig, schemaContext,
222                     moduleName, definitionNames, resourcePathPart, context);
223             }
224         }
225
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,
230                 List.of()));
231         }
232
233         LOG.debug("Number of Paths found [{}]", paths.size());
234
235         return paths;
236     }
237
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<>();
241         try {
242             schemas = DefinitionGenerator.convertToSchemas(module, schemaContext, definitionNames, isForSingleModule);
243         } catch (final IOException e) {
244             LOG.error("Exception occurred in DefinitionGenerator", e); // FIXME propagate exception
245         }
246
247         return schemas;
248     }
249
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))
258                 .build());
259         }
260     }
261
262     public abstract String getResourcePath(String resourceType, String context);
263
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();
274         }
275         final String fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
276         paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
277             fullName));
278
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));
286             });
287         }
288
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);
297                 pathParams.clear();
298                 pathParams.addAll(parentPathParams);
299             }
300         }
301     }
302
303     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
304         for (final DataSchemaNode child : nodes) {
305             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
306                 return true;
307             }
308         }
309         return false;
310     }
311
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();
316
317         final String discriminator = definitionNames.getDiscriminator(node);
318         final String nodeName = node.getQName().getLocalName();
319
320         final Operation get = buildGet(node, parentName, moduleName, deviceName, pathParams, isConfig);
321         operationsBuilder.get(get);
322
323         if (isConfig) {
324             final Operation put = buildPut(node, parentName, moduleName, deviceName, pathParams, fullName);
325             operationsBuilder.put(put);
326
327             final Operation patch = buildPatch(node, parentName, moduleName, deviceName, pathParams, fullName);
328             operationsBuilder.patch(patch);
329
330             final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
331             operationsBuilder.delete(delete);
332
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);
340                 }
341             }
342         }
343         return operationsBuilder.build();
344     }
345
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);
350     }
351
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());
359
360         if (schemaNode instanceof ListSchemaNode listSchemaNode) {
361             String prefix = "=";
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;
368                     discriminator++;
369                 }
370
371                 final String pathParamIdentifier = prefix + "{" + paramName + "}";
372                 prefix = ",";
373                 path.append(pathParamIdentifier);
374
375                 final String description = listSchemaNode.findDataChildByName(listKey)
376                     .flatMap(DataSchemaNode::getDescription).orElse(null);
377                 pathParams.add(new Parameter.Builder()
378                     .name(paramName)
379                     .schema(new Schema.Builder().type(getAllowedType(listSchemaNode, listKey)).build())
380                     .in("path")
381                     .required(true)
382                     .description(description)
383                     .build());
384             }
385         }
386         return path.toString();
387     }
388
389     private static String getAllowedType(final ListSchemaNode list, final QName key) {
390         final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
391
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) {
396             return "integer";
397         }
398         if (keyType instanceof Int16TypeDefinition) {
399             return "integer";
400         }
401         if (keyType instanceof Int32TypeDefinition) {
402             return "integer";
403         }
404         if (keyType instanceof Int64TypeDefinition) {
405             return "integer";
406         }
407         if (keyType instanceof Uint8TypeDefinition) {
408             return "integer";
409         }
410         if (keyType instanceof Uint16TypeDefinition) {
411             return "integer";
412         }
413         if (keyType instanceof Uint32TypeDefinition) {
414             return "integer";
415         }
416         if (keyType instanceof Uint64TypeDefinition) {
417             return "integer";
418         }
419
420         if (keyType instanceof DecimalTypeDefinition) {
421             return "number";
422         }
423
424         if (keyType instanceof BooleanTypeDefinition) {
425             return "boolean";
426         }
427
428         return "string";
429     }
430
431     public static SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
432         if (schemaContext == null) {
433             return Collections.emptySortedSet();
434         }
435
436         final var sortedModules = new TreeSet<Module>((module1, module2) -> {
437             int result = module1.getName().compareTo(module2.getName());
438             if (result == 0) {
439                 result = Revision.compare(module1.getRevision(), module2.getRevision());
440             }
441             if (result == 0) {
442                 result = module1.getNamespace().compareTo(module2.getNamespace());
443             }
444             return result;
445         });
446         sortedModules.addAll(schemaContext.getModules());
447         return sortedModules;
448     }
449
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))
455             .build();
456     }
457 }