a81e9c0769c7fd3d482f391d83bda5e2d7c3651e
[netconf.git] / restconf / sal-rest-docgen / src / main / java / org / opendaylight / netconf / sal / rest / doc / 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.netconf.sal.rest.doc.impl;
9
10 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildDelete;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildGet;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPatch;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPost;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPostOperation;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPut;
17 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getTypeParentNode;
18 import static org.opendaylight.netconf.sal.rest.doc.util.JsonUtil.addFields;
19 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolvePathArgumentsName;
20
21 import com.fasterxml.jackson.databind.JsonNode;
22 import com.fasterxml.jackson.databind.ObjectMapper;
23 import com.fasterxml.jackson.databind.SerializationFeature;
24 import com.fasterxml.jackson.databind.node.ArrayNode;
25 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
26 import com.fasterxml.jackson.databind.node.ObjectNode;
27 import com.google.common.base.Preconditions;
28 import com.google.common.collect.Range;
29 import java.io.IOException;
30 import java.time.format.DateTimeParseException;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Map.Entry;
38 import java.util.Optional;
39 import java.util.Set;
40 import java.util.SortedSet;
41 import java.util.TreeSet;
42 import javax.ws.rs.core.UriInfo;
43 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
44 import org.opendaylight.netconf.sal.rest.doc.openapi.Components;
45 import org.opendaylight.netconf.sal.rest.doc.openapi.Info;
46 import org.opendaylight.netconf.sal.rest.doc.openapi.OpenApiObject;
47 import org.opendaylight.netconf.sal.rest.doc.openapi.SecuritySchemes;
48 import org.opendaylight.netconf.sal.rest.doc.openapi.Server;
49 import org.opendaylight.netconf.sal.rest.doc.util.JsonUtil;
50 import org.opendaylight.yangtools.yang.common.QName;
51 import org.opendaylight.yangtools.yang.common.Revision;
52 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
53 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
55 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
56 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
57 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
58 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
59 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
60 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.Module;
62 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
63 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67 public abstract class BaseYangOpenApiGenerator {
68
69     private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
70
71     private static final String API_VERSION = "1.0.0";
72     private static final String OPEN_API_VERSION = "3.0.3";
73     private static final ObjectMapper MAPPER = new ObjectMapper();
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     static {
87         MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
88     }
89
90     protected BaseYangOpenApiGenerator(final Optional<DOMSchemaService> schemaService) {
91         this.schemaService = schemaService.orElse(null);
92     }
93
94     public OpenApiObject getAllModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames) {
95         final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
96         Preconditions.checkState(schemaContext != null);
97         return getAllModulesDoc(uriInfo, Optional.empty(), schemaContext, Optional.empty(), "", definitionNames);
98     }
99
100     public OpenApiObject getAllModulesDoc(final UriInfo uriInfo, final Optional<Range<Integer>> range,
101             final EffectiveModelContext schemaContext, final Optional<String> deviceName, final String context,
102             final DefinitionNames definitionNames) {
103         final String schema = createSchemaFromUriInfo(uriInfo);
104         final String host = createHostFromUriInfo(uriInfo);
105         String name = "Controller";
106         if (deviceName.isPresent()) {
107             name = deviceName.orElseThrow();
108         }
109
110         final String title = name + " modules of RESTCONF";
111         final OpenApiObject doc = createOpenApiObject(schema, host, BASE_PATH, title);
112         doc.setPaths(JsonNodeFactory.instance.objectNode());
113
114         fillDoc(doc, range, schemaContext, context, deviceName, definitionNames);
115
116         return doc;
117     }
118
119     public void fillDoc(final OpenApiObject doc, final Optional<Range<Integer>> range,
120             final EffectiveModelContext schemaContext, final String context, final Optional<String> deviceName,
121             final DefinitionNames definitionNames) {
122         final SortedSet<Module> modules = getSortedModules(schemaContext);
123         final Set<Module> filteredModules;
124         if (range.isPresent()) {
125             filteredModules = filterByRange(modules, range.orElseThrow());
126         } else {
127             filteredModules = modules;
128         }
129
130         for (final Module module : filteredModules) {
131             final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
132
133             LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
134
135             getOpenApiDocSpec(module, context, deviceName, schemaContext, definitionNames, doc, false);
136         }
137     }
138
139     private static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
140         final int begin = range.lowerEndpoint();
141         final int end = range.upperEndpoint();
142
143         Module firstModule = null;
144
145         final Iterator<Module> iterator = modules.iterator();
146         int counter = 0;
147         while (iterator.hasNext() && counter < end) {
148             final Module module = iterator.next();
149             if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
150                 if (counter == begin) {
151                     firstModule = module;
152                 }
153                 counter++;
154             }
155         }
156
157         if (iterator.hasNext()) {
158             return modules.subSet(firstModule, iterator.next());
159         } else {
160             return modules.tailSet(firstModule);
161         }
162     }
163
164     public OpenApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo) {
165         final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
166         Preconditions.checkState(schemaContext != null);
167         final OpenApiObject doc = getApiDeclaration(module, revision, uriInfo, schemaContext, "");
168         return doc;
169     }
170
171     public OpenApiObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
172             final EffectiveModelContext schemaContext, final String context) {
173         final Optional<Revision> rev;
174
175         try {
176             rev = Revision.ofNullable(revision);
177         } catch (final DateTimeParseException e) {
178             throw new IllegalArgumentException(e);
179         }
180
181         final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
182         Preconditions.checkArgument(module != null,
183                 "Could not find module by name,revision: " + moduleName + "," + revision);
184
185         return getApiDeclaration(module, uriInfo, context, schemaContext);
186     }
187
188     public OpenApiObject getApiDeclaration(final Module module, final UriInfo uriInfo, final String context,
189             final EffectiveModelContext schemaContext) {
190         final String schema = createSchemaFromUriInfo(uriInfo);
191         final String host = createHostFromUriInfo(uriInfo);
192
193         return getOpenApiDocSpec(module, schema, host, BASE_PATH, context, schemaContext);
194     }
195
196     public String createHostFromUriInfo(final UriInfo uriInfo) {
197         String portPart = "";
198         final int port = uriInfo.getBaseUri().getPort();
199         if (port != -1) {
200             portPart = ":" + port;
201         }
202         return uriInfo.getBaseUri().getHost() + portPart;
203     }
204
205     public String createSchemaFromUriInfo(final UriInfo uriInfo) {
206         return uriInfo.getBaseUri().getScheme();
207     }
208
209     public OpenApiObject getOpenApiDocSpec(final Module module, final String schema, final String host,
210             final String basePath, final String context, final EffectiveModelContext schemaContext) {
211         final OpenApiObject doc = createOpenApiObject(schema, host, basePath, module.getName());
212         final DefinitionNames definitionNames = new DefinitionNames();
213         return getOpenApiDocSpec(module, context, Optional.empty(), schemaContext, definitionNames, doc, true);
214     }
215
216     public OpenApiObject getOpenApiDocSpec(final Module module, final String context, final Optional<String> deviceName,
217             final EffectiveModelContext schemaContext, final DefinitionNames definitionNames, final OpenApiObject doc,
218             final boolean isForSingleModule) {
219         try {
220             final ObjectNode schema;
221             if (isForSingleModule) {
222                 schema = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, true);
223             } else {
224                 schema = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, false);
225             }
226             addFields(doc.getComponents().getSchemas(), schema.fields());
227             if (LOG.isDebugEnabled()) {
228                 LOG.debug("Document: {}", MAPPER.writeValueAsString(doc));
229             }
230         } catch (final IOException e) {
231             LOG.error("Exception occurred in DefinitionGenerator", e);
232         }
233
234         final ObjectNode paths = JsonNodeFactory.instance.objectNode();
235         final String moduleName = module.getName();
236
237         boolean hasAddRootPostLink = false;
238
239         final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
240         LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
241         for (final DataSchemaNode node : dataSchemaNodes) {
242             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
243                 LOG.debug("Is Configuration node [{}] [{}]", node.isConfiguration(), node.getQName().getLocalName());
244
245                 final String localName = module.getName() + ":" + node.getQName().getLocalName();
246                 final String resourcePath  = getResourcePath("data", context);
247
248                 if (node.isConfiguration()) {
249                     final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
250                     /*
251                      * When there are two or more top container or list nodes
252                      * whose config statement is true in module, make sure that
253                      * only one root post link is added for this module.
254                      */
255                     if (isForSingleModule && !hasAddRootPostLink) {
256                         LOG.debug("Has added root post link for module {}", module.getName());
257                         addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
258
259                         hasAddRootPostLink = true;
260                     }
261
262                     final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, localName);
263                     addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, true, module.getName(),
264                         definitionNames, resolvedPath);
265                 } else  {
266                     final ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
267                     final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, localName);
268                     addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, false, moduleName,
269                         definitionNames, resolvedPath);
270                 }
271             }
272         }
273
274         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
275             final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
276                     + rpcDefinition.getQName().getLocalName();
277             addOperations(rpcDefinition, moduleName, deviceName, paths, module.getName(), definitionNames,
278                 resolvedPath);
279         }
280
281         LOG.debug("Number of Paths found [{}]", paths.size());
282
283         if (isForSingleModule) {
284             doc.setPaths(paths);
285         } else {
286             addFields(doc.getPaths(), paths.fields());
287         }
288
289         return doc;
290     }
291
292     private static void addRootPostLink(final Module module, final Optional<String> deviceName,
293             final ArrayNode pathParams, final String resourcePath, final ObjectNode paths) {
294         if (containsListOrContainer(module.getChildNodes())) {
295             final ObjectNode post = JsonNodeFactory.instance.objectNode();
296             final String moduleName = module.getName();
297             final String name = moduleName + MODULE_NAME_SUFFIX;
298             post.set("post", buildPost("", name, "", moduleName, deviceName,
299                     module.getDescription().orElse(""), pathParams));
300             paths.set(resourcePath, post);
301         }
302     }
303
304     public OpenApiObject createOpenApiObject(final String schema, final String host, final String basePath,
305                 final String title) {
306         final OpenApiObject doc = new OpenApiObject();
307         doc.setOpenapi(OPEN_API_VERSION);
308         final Info info = new Info();
309         info.setTitle(title);
310         info.setVersion(API_VERSION);
311         doc.setInfo(info);
312         doc.setServers(convertToServers(List.of(schema), host, basePath));
313         doc.setComponents(new Components(JsonNodeFactory.instance.objectNode(),
314                 new SecuritySchemes(OPEN_API_BASIC_AUTH)));
315         doc.setSecurity(SECURITY);
316         return doc;
317     }
318
319     private static List<Server> convertToServers(final List<String> schemes, final String host, final String basePath) {
320         return List.of(new Server(schemes.get(0) + "://" + host + basePath));
321     }
322
323     public abstract String getResourcePath(String resourceType, String context);
324
325     private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
326             final ObjectNode paths, final ArrayNode parentPathParams, final EffectiveModelContext schemaContext,
327             final boolean isConfig, final String parentName, final DefinitionNames definitionNames,
328             final String resourcePath) {
329         LOG.debug("Adding path: [{}]", resourcePath);
330
331         final ArrayNode pathParams = JsonUtil.copy(parentPathParams);
332         Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
333         if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
334             final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
335             childSchemaNodes = dataNodeContainer.getChildNodes();
336         }
337
338         final ObjectNode path = JsonNodeFactory.instance.objectNode();
339         path.setAll(operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames));
340         paths.set(resourcePath, path);
341
342         if (node instanceof ActionNodeContainer) {
343             ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
344                 final String resolvedPath = "rests/operations" + resourcePath.substring(11)
345                         + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
346                 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, resolvedPath);
347             });
348         }
349
350         for (final DataSchemaNode childNode : childSchemaNodes) {
351             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
352                 final String newParent = parentName + "_" + node.getQName().getLocalName();
353                 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
354                 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, localName);
355                 final boolean newIsConfig = isConfig && childNode.isConfiguration();
356                 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
357                     newIsConfig, newParent, definitionNames, newResourcePath);
358             }
359         }
360     }
361
362     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
363         for (final DataSchemaNode child : nodes) {
364             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
365                 return true;
366             }
367         }
368         return false;
369     }
370
371     private static Map<String, ObjectNode> operations(final DataSchemaNode node, final String moduleName,
372             final Optional<String> deviceName, final ArrayNode pathParams, final boolean isConfig,
373             final String parentName, final DefinitionNames definitionNames) {
374         final Map<String, ObjectNode> operations = new HashMap<>();
375         final String discriminator = definitionNames.getDiscriminator(node);
376
377         final String nodeName = node.getQName().getLocalName();
378
379         final String defName = parentName + "_" + nodeName + TOP + discriminator;
380         final ObjectNode get = buildGet(node, moduleName, deviceName, pathParams, defName, isConfig);
381         operations.put("get", get);
382
383
384         if (isConfig) {
385             final ObjectNode put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
386                     node.getDescription().orElse(""), pathParams);
387             operations.put("put", put);
388
389             final ObjectNode patch = buildPatch(parentName, nodeName, moduleName, deviceName,
390                     node.getDescription().orElse(""), pathParams);
391             operations.put("patch", patch);
392
393             final ObjectNode delete = buildDelete(node, moduleName, deviceName, pathParams);
394             operations.put("delete", delete);
395
396             operations.put("post", buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
397                     node.getDescription().orElse(""), pathParams));
398         }
399         return operations;
400     }
401
402     protected abstract ListPathBuilder newListPathBuilder();
403
404     private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams, final String localName) {
405         final StringBuilder path = new StringBuilder();
406         path.append(localName);
407
408         if (schemaNode instanceof ListSchemaNode) {
409             final ListPathBuilder keyBuilder = newListPathBuilder();
410             for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
411                 final String paramName = createUniquePathParamName(listKey.getLocalName(), pathParams);
412                 final String pathParamIdentifier = keyBuilder.nextParamIdentifier(paramName);
413
414                 path.append(pathParamIdentifier);
415
416                 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
417                 pathParam.put("name", paramName);
418
419                 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
420                         .ifPresent(desc -> pathParam.put("description", desc));
421
422                 final ObjectNode typeParent = getTypeParentNode(pathParam);
423
424                 typeParent.put("type", "string");
425                 pathParam.put("in", "path");
426                 pathParam.put("required", true);
427
428                 pathParams.add(pathParam);
429             }
430         }
431         return path.toString();
432     }
433
434     private String createUniquePathParamName(final String clearName, final ArrayNode pathParams) {
435         for (final JsonNode pathParam : pathParams) {
436             if (isNamePicked(clearName, pathParam)) {
437                 return createUniquePathParamName(clearName, pathParams, 1);
438             }
439         }
440         return clearName;
441     }
442
443     private String createUniquePathParamName(final String clearName, final ArrayNode pathParams,
444             final int discriminator) {
445         final String newName = clearName + discriminator;
446         for (final JsonNode pathParam : pathParams) {
447             if (isNamePicked(newName, pathParam)) {
448                 return createUniquePathParamName(clearName, pathParams, discriminator + 1);
449             }
450         }
451         return newName;
452     }
453
454     private static boolean isNamePicked(final String name, final JsonNode pathParam) {
455         return name.equals(pathParam.get("name").asText());
456     }
457
458     public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
459         if (schemaContext == null) {
460             return Collections.emptySortedSet();
461         }
462
463         final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
464             int result = module1.getName().compareTo(module2.getName());
465             if (result == 0) {
466                 result = Revision.compare(module1.getRevision(), module2.getRevision());
467             }
468             if (result == 0) {
469                 result = module1.getNamespace().compareTo(module2.getNamespace());
470             }
471             return result;
472         });
473         for (final Module m : schemaContext.getModules()) {
474             if (m != null) {
475                 sortedModules.add(m);
476             }
477         }
478         return sortedModules;
479     }
480
481     private static void addOperations(final OperationDefinition operDef, final String moduleName,
482             final Optional<String> deviceName, final ObjectNode paths, final String parentName,
483             final DefinitionNames definitionNames, final String resourcePath) {
484         final ObjectNode operations = JsonNodeFactory.instance.objectNode();
485         operations.set("post", buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames));
486         paths.set(resourcePath, operations);
487     }
488
489     protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
490
491     public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
492         final StringBuilder builder = new StringBuilder();
493         builder.append("/");
494         if (moduleName != null) {
495             builder.append(moduleName).append(':');
496         }
497         for (final PathArgument arg : key.getPathArguments()) {
498             final String name = arg.getNodeType().getLocalName();
499             if (arg instanceof NodeIdentifierWithPredicates nodeId) {
500                 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
501                     appendPathKeyValue(builder, entry.getValue());
502                 }
503             } else {
504                 builder.append(name).append('/');
505             }
506         }
507         return builder.toString();
508     }
509
510     protected interface ListPathBuilder {
511         String nextParamIdentifier(String key);
512     }
513 }