16e0d3254863029ff5d6eb0d1690b887d4b5fc88
[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
251                 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, localName);
252                 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, isConfig,
253                     moduleName, definitionNames, resolvedPath);
254             }
255         }
256
257         final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
258         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
259             final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
260                     + rpcDefinition.getQName().getLocalName();
261             addOperations(rpcDefinition, moduleName, deviceName, paths, moduleName, definitionNames,
262                 resolvedPath, pathParams);
263         }
264
265         LOG.debug("Number of Paths found [{}]", paths.size());
266
267         if (isForSingleModule) {
268             docBuilder.paths(paths);
269         } else {
270             docBuilder.getPaths().putAll(paths);
271         }
272
273         return docBuilder.build();
274     }
275
276     private static void addRootPostLink(final Module module, final Optional<String> deviceName,
277             final ArrayNode pathParams, final String resourcePath, final Map<String, Path> paths) {
278         if (containsListOrContainer(module.getChildNodes())) {
279             final String moduleName = module.getName();
280             final String name = moduleName + MODULE_NAME_SUFFIX;
281             final var postBuilder = new Path.Builder();
282             postBuilder.post(buildPost("", name, "", moduleName, deviceName,
283                     module.getDescription().orElse(""), pathParams));
284             paths.put(resourcePath, postBuilder.build());
285         }
286     }
287
288     public OpenApiObject.Builder createOpenApiObjectBuilder(final String schema, final String host,
289             final String basePath, final String title) {
290         final OpenApiObject.Builder docBuilder = new OpenApiObject.Builder();
291         docBuilder.openapi(OPEN_API_VERSION);
292         docBuilder.info(new Info.Builder().title(title).version(API_VERSION).build())
293             .servers(List.of(new Server(schema + "://" + host + basePath)))
294             .components(new Components(new HashMap<>(), new SecuritySchemes(OPEN_API_BASIC_AUTH)))
295             .security(SECURITY);
296         return docBuilder;
297     }
298
299     public abstract String getResourcePath(String resourceType, String context);
300
301     private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
302             final Map<String, Path> paths, final ArrayNode parentPathParams, final EffectiveModelContext schemaContext,
303             final boolean isConfig, final String parentName, final DefinitionNames definitionNames,
304             final String resourcePath) {
305         LOG.debug("Adding path: [{}]", resourcePath);
306
307         final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode().addAll(parentPathParams);
308         Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
309         if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
310             final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
311             childSchemaNodes = dataNodeContainer.getChildNodes();
312         }
313
314         paths.put(resourcePath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName,
315                 definitionNames));
316
317         if (node instanceof ActionNodeContainer) {
318             ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
319                 final String resolvedPath = "/rests/operations" + resourcePath.substring(11)
320                         + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
321                 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, resolvedPath,
322                     pathParams);
323             });
324         }
325
326         for (final DataSchemaNode childNode : childSchemaNodes) {
327             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
328                 final String newParent = parentName + "_" + node.getQName().getLocalName();
329                 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
330                 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, localName);
331                 final boolean newIsConfig = isConfig && childNode.isConfiguration();
332                 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
333                     newIsConfig, newParent, definitionNames, newResourcePath);
334                 pathParams.removeAll();
335                 pathParams.addAll(parentPathParams);
336             }
337         }
338     }
339
340     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
341         for (final DataSchemaNode child : nodes) {
342             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
343                 return true;
344             }
345         }
346         return false;
347     }
348
349     private static Path operations(final DataSchemaNode node, final String moduleName,
350             final Optional<String> deviceName, final ArrayNode pathParams, final boolean isConfig,
351             final String parentName, final DefinitionNames definitionNames) {
352         final Path.Builder operationsBuilder = new Path.Builder();
353
354         final String discriminator = definitionNames.getDiscriminator(node);
355         final String nodeName = node.getQName().getLocalName();
356
357         final String defName = parentName + "_" + nodeName + discriminator;
358         final String defNameTop = parentName + "_" + nodeName + TOP + discriminator;
359         final Operation get = buildGet(node, moduleName, deviceName, pathParams, defName, defNameTop, isConfig);
360         operationsBuilder.get(get);
361
362         if (isConfig) {
363             final Operation put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
364                     node.getDescription().orElse(""), pathParams);
365             operationsBuilder.put(put);
366
367             final Operation patch = buildPatch(parentName, nodeName, moduleName, deviceName,
368                     node.getDescription().orElse(""), pathParams);
369             operationsBuilder.patch(patch);
370
371             final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
372             operationsBuilder.delete(delete);
373
374             final Operation post = buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
375                     node.getDescription().orElse(""), pathParams);
376             operationsBuilder.post(post);
377         }
378         return operationsBuilder.build();
379     }
380
381     private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams, final String localName) {
382         final StringBuilder path = new StringBuilder();
383         path.append(localName);
384
385         if (schemaNode instanceof ListSchemaNode) {
386             String prefix = "=";
387             int discriminator = 1;
388             for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
389                 final String keyName = listKey.getLocalName();
390                 String paramName = keyName;
391                 for (final JsonNode pathParam : pathParams) {
392                     if (paramName.equals(pathParam.get("name").asText())) {
393                         paramName = keyName + discriminator;
394                         discriminator++;
395                     }
396                 }
397
398                 final String pathParamIdentifier = prefix + "{" + paramName + "}";
399                 prefix = ",";
400
401                 path.append(pathParamIdentifier);
402
403                 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
404                 pathParam.put("name", paramName);
405
406                 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
407                         .ifPresent(desc -> pathParam.put("description", desc));
408
409                 final ObjectNode typeParent = getTypeParentNode(pathParam);
410
411                 typeParent.put("type", "string");
412                 pathParam.put("in", "path");
413                 pathParam.put("required", true);
414
415                 pathParams.add(pathParam);
416             }
417         }
418         return path.toString();
419     }
420
421     public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
422         if (schemaContext == null) {
423             return Collections.emptySortedSet();
424         }
425
426         final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
427             int result = module1.getName().compareTo(module2.getName());
428             if (result == 0) {
429                 result = Revision.compare(module1.getRevision(), module2.getRevision());
430             }
431             if (result == 0) {
432                 result = module1.getNamespace().compareTo(module2.getNamespace());
433             }
434             return result;
435         });
436         for (final Module m : schemaContext.getModules()) {
437             if (m != null) {
438                 sortedModules.add(m);
439             }
440         }
441         return sortedModules;
442     }
443
444     private static void addOperations(final OperationDefinition operDef, final String moduleName,
445             final Optional<String> deviceName, final Map<String, Path> paths, final String parentName,
446             final DefinitionNames definitionNames, final String resourcePath, final ArrayNode parentPathParams) {
447         final var pathBuilder = new Path.Builder();
448         pathBuilder.post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
449             parentPathParams));
450         paths.put(resourcePath, pathBuilder.build());
451     }
452
453     protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
454
455     public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
456         final StringBuilder builder = new StringBuilder();
457         builder.append("/");
458         if (moduleName != null) {
459             builder.append(moduleName).append(':');
460         }
461         for (final PathArgument arg : key.getPathArguments()) {
462             final String name = arg.getNodeType().getLocalName();
463             if (arg instanceof NodeIdentifierWithPredicates nodeId) {
464                 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
465                     appendPathKeyValue(builder, entry.getValue());
466                 }
467             } else {
468                 builder.append(name).append('/');
469             }
470         }
471         return builder.toString();
472     }
473 }