16068315408c301d4a712d8de072b20f383f4dad
[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.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;
20
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;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.Optional;
37 import java.util.Set;
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;
67
68 public abstract class BaseYangOpenApiGenerator {
69
70     private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
71
72     private static final String API_VERSION = "1.0.0";
73     private static final String OPEN_API_VERSION = "3.0.3";
74
75     private final DefinitionGenerator jsonConverter = new DefinitionGenerator();
76     private final DOMSchemaService schemaService;
77
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()
81             .put("type", "http")
82             .put("scheme", "basic");
83     private static final ArrayNode SECURITY = JsonNodeFactory.instance.arrayNode()
84             .add(JsonNodeFactory.instance.objectNode().set("basicAuth", JsonNodeFactory.instance.arrayNode()));
85
86     protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
87         this.schemaService = requireNonNull(schemaService);
88     }
89
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)
94             .build();
95     }
96
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();
105         }
106
107         final String title = name + " modules of RESTCONF";
108         final OpenApiObject.Builder docBuilder = createOpenApiObjectBuilder(schema, host, BASE_PATH, title);
109         docBuilder.paths(new HashMap<>());
110
111         fillDoc(docBuilder, range, schemaContext, context, deviceName, definitionNames);
112
113         // FIXME rework callers logic to make possible to return OpenApiObject from here
114         return docBuilder;
115     }
116
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());
124         } else {
125             filteredModules = modules;
126         }
127
128         for (final Module module : filteredModules) {
129             final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
130
131             LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
132
133             getOpenApiSpec(module, context, deviceName, schemaContext, definitionNames, docBuilder, false);
134         }
135     }
136
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();
140
141         Module firstModule = null;
142
143         final Iterator<Module> iterator = modules.iterator();
144         int counter = 0;
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;
150                 }
151                 counter++;
152             }
153         }
154
155         if (iterator.hasNext()) {
156             return modules.subSet(firstModule, iterator.next());
157         } else {
158             return modules.tailSet(firstModule);
159         }
160     }
161
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, "");
166     }
167
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;
171
172         try {
173             rev = Revision.ofNullable(revision);
174         } catch (final DateTimeParseException e) {
175             throw new IllegalArgumentException(e);
176         }
177
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);
181
182         return getApiDeclaration(module, uriInfo, context, schemaContext);
183     }
184
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);
189
190         return getOpenApiSpec(module, schema, host, BASE_PATH, context, schemaContext);
191     }
192
193     public String createHostFromUriInfo(final UriInfo uriInfo) {
194         String portPart = "";
195         final int port = uriInfo.getBaseUri().getPort();
196         if (port != -1) {
197             portPart = ":" + port;
198         }
199         return uriInfo.getBaseUri().getHost() + portPart;
200     }
201
202     public String createSchemaFromUriInfo(final UriInfo uriInfo) {
203         return uriInfo.getBaseUri().getScheme();
204     }
205
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);
211     }
212
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) {
216         try {
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);
222         }
223         final Map<String, Path> paths = new HashMap<>();
224         final String moduleName = module.getName();
225
226         boolean hasAddRootPostLink = false;
227
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());
234
235                 final String localName = moduleName + ":" + node.getQName().getLocalName();
236                 final String resourcePath  = getResourcePath("data", context);
237
238                 final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
239                 /*
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.
243                  */
244                 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
245                     LOG.debug("Has added root post link for module {}", moduleName);
246                     addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
247
248                     hasAddRootPostLink = true;
249                 }
250                 final String resourcePathPart = createPath(node, pathParams, localName);
251                 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, isConfig,
252                     moduleName, definitionNames, resourcePathPart, context);
253             }
254         }
255
256         final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
257         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
258             final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
259                     + rpcDefinition.getQName().getLocalName();
260             addOperations(rpcDefinition, moduleName, deviceName, paths, moduleName, definitionNames,
261                 resolvedPath, pathParams);
262         }
263
264         LOG.debug("Number of Paths found [{}]", paths.size());
265
266         if (isForSingleModule) {
267             docBuilder.paths(paths);
268         } else {
269             docBuilder.getPaths().putAll(paths);
270         }
271
272         return docBuilder.build();
273     }
274
275     private static void addRootPostLink(final Module module, final Optional<String> deviceName,
276             final ArrayNode pathParams, final String resourcePath, final Map<String, Path> paths) {
277         if (containsListOrContainer(module.getChildNodes())) {
278             final String moduleName = module.getName();
279             final String name = moduleName + MODULE_NAME_SUFFIX;
280             final var postBuilder = new Path.Builder();
281             postBuilder.post(buildPost("", name, "", moduleName, deviceName,
282                     module.getDescription().orElse(""), pathParams));
283             paths.put(resourcePath, postBuilder.build());
284         }
285     }
286
287     public OpenApiObject.Builder createOpenApiObjectBuilder(final String schema, final String host,
288             final String basePath, final String title) {
289         final OpenApiObject.Builder docBuilder = new OpenApiObject.Builder();
290         docBuilder.openapi(OPEN_API_VERSION);
291         docBuilder.info(new Info.Builder().title(title).version(API_VERSION).build())
292             .servers(List.of(new Server(schema + "://" + host + basePath)))
293             .components(new Components(new HashMap<>(), new SecuritySchemes(OPEN_API_BASIC_AUTH)))
294             .security(SECURITY);
295         return docBuilder;
296     }
297
298     public abstract String getResourcePath(String resourceType, String context);
299
300     private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
301             final Map<String, Path> paths, final ArrayNode parentPathParams, final EffectiveModelContext schemaContext,
302             final boolean isConfig, final String parentName, final DefinitionNames definitionNames,
303             final String resourcePathPart, final String context) {
304         final String dataPath = getResourcePath("data", context) + "/" + resourcePathPart;
305         LOG.debug("Adding path: [{}]", dataPath);
306         final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode().addAll(parentPathParams);
307         Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
308         if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
309             final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
310             childSchemaNodes = dataNodeContainer.getChildNodes();
311         }
312         paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName,
313                 definitionNames));
314
315         if (node instanceof ActionNodeContainer) {
316             ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
317                 final String operationsPath = getResourcePath("operations", context)
318                     + "/" + resourcePathPart
319                     + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
320                 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, operationsPath,
321                     pathParams);
322             });
323         }
324
325         for (final DataSchemaNode childNode : childSchemaNodes) {
326             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
327                 final String newParent = parentName + "_" + node.getQName().getLocalName();
328                 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
329                 final String newPathPart = resourcePathPart + "/" + createPath(childNode, pathParams, localName);
330                 final boolean newIsConfig = isConfig && childNode.isConfiguration();
331                 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
332                     newIsConfig, newParent, definitionNames, newPathPart, context);
333                 pathParams.removeAll();
334                 pathParams.addAll(parentPathParams);
335             }
336         }
337     }
338
339     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
340         for (final DataSchemaNode child : nodes) {
341             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
342                 return true;
343             }
344         }
345         return false;
346     }
347
348     private static Path operations(final DataSchemaNode node, final String moduleName,
349             final Optional<String> deviceName, final ArrayNode pathParams, final boolean isConfig,
350             final String parentName, final DefinitionNames definitionNames) {
351         final Path.Builder operationsBuilder = new Path.Builder();
352
353         final String discriminator = definitionNames.getDiscriminator(node);
354         final String nodeName = node.getQName().getLocalName();
355
356         final String defName = parentName + "_" + nodeName + discriminator;
357         final String defNameTop = parentName + "_" + nodeName + TOP + discriminator;
358         final Operation get = buildGet(node, moduleName, deviceName, pathParams, defName, defNameTop, isConfig);
359         operationsBuilder.get(get);
360
361         if (isConfig) {
362             final Operation put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
363                     node.getDescription().orElse(""), pathParams);
364             operationsBuilder.put(put);
365
366             final Operation patch = buildPatch(parentName, nodeName, moduleName, deviceName,
367                     node.getDescription().orElse(""), pathParams);
368             operationsBuilder.patch(patch);
369
370             final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
371             operationsBuilder.delete(delete);
372
373             final Operation post = buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
374                     node.getDescription().orElse(""), pathParams);
375             operationsBuilder.post(post);
376         }
377         return operationsBuilder.build();
378     }
379
380     private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams, final String localName) {
381         final StringBuilder path = new StringBuilder();
382         path.append(localName);
383
384         if (schemaNode instanceof ListSchemaNode) {
385             String prefix = "=";
386             int discriminator = 1;
387             for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
388                 final String keyName = listKey.getLocalName();
389                 String paramName = keyName;
390                 for (final JsonNode pathParam : pathParams) {
391                     if (paramName.equals(pathParam.get("name").asText())) {
392                         paramName = keyName + discriminator;
393                         discriminator++;
394                         for (final JsonNode pathParameter : pathParams) {
395                             if (paramName.equals(pathParameter.get("name").asText())) {
396                                 paramName = keyName + discriminator;
397                                 discriminator++;
398                             }
399                         }
400                     }
401                 }
402
403                 final String pathParamIdentifier = prefix + "{" + paramName + "}";
404                 prefix = ",";
405
406                 path.append(pathParamIdentifier);
407
408                 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
409                 pathParam.put("name", paramName);
410
411                 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
412                         .ifPresent(desc -> pathParam.put("description", desc));
413
414                 final ObjectNode typeParent = getTypeParentNode(pathParam);
415
416                 typeParent.put("type", "string");
417                 pathParam.put("in", "path");
418                 pathParam.put("required", true);
419
420                 pathParams.add(pathParam);
421             }
422         }
423         return path.toString();
424     }
425
426     public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
427         if (schemaContext == null) {
428             return Collections.emptySortedSet();
429         }
430
431         final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
432             int result = module1.getName().compareTo(module2.getName());
433             if (result == 0) {
434                 result = Revision.compare(module1.getRevision(), module2.getRevision());
435             }
436             if (result == 0) {
437                 result = module1.getNamespace().compareTo(module2.getNamespace());
438             }
439             return result;
440         });
441         for (final Module m : schemaContext.getModules()) {
442             if (m != null) {
443                 sortedModules.add(m);
444             }
445         }
446         return sortedModules;
447     }
448
449     private static void addOperations(final OperationDefinition operDef, final String moduleName,
450             final Optional<String> deviceName, final Map<String, Path> paths, final String parentName,
451             final DefinitionNames definitionNames, final String resourcePath, final ArrayNode parentPathParams) {
452         final var pathBuilder = new Path.Builder();
453         pathBuilder.post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
454             parentPathParams));
455         paths.put(resourcePath, pathBuilder.build());
456     }
457
458     protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
459
460     public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
461         final StringBuilder builder = new StringBuilder();
462         builder.append("/");
463         if (moduleName != null) {
464             builder.append(moduleName).append(':');
465         }
466         for (final PathArgument arg : key.getPathArguments()) {
467             final String name = arg.getNodeType().getLocalName();
468             if (arg instanceof NodeIdentifierWithPredicates nodeId) {
469                 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
470                     appendPathKeyValue(builder, entry.getValue());
471                 }
472             } else {
473                 builder.append(name).append('/');
474             }
475         }
476         return builder.toString();
477     }
478 }