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