Skip POST for nodes without container/list child
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / impl / BaseYangOpenApiGenerator.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.openapi.impl;
9
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildDelete;
12 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildGet;
13 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPatch;
14 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPost;
15 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPostOperation;
16 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.buildPut;
17 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
18 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
19
20 import com.google.common.base.Preconditions;
21 import com.google.common.collect.Range;
22 import java.io.IOException;
23 import java.time.format.DateTimeParseException;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Optional;
32 import java.util.Set;
33 import java.util.SortedSet;
34 import java.util.TreeSet;
35 import java.util.stream.Collectors;
36 import javax.ws.rs.core.UriInfo;
37 import org.eclipse.jdt.annotation.NonNull;
38 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
39 import org.opendaylight.restconf.openapi.model.Components;
40 import org.opendaylight.restconf.openapi.model.Info;
41 import org.opendaylight.restconf.openapi.model.OpenApiObject;
42 import org.opendaylight.restconf.openapi.model.Operation;
43 import org.opendaylight.restconf.openapi.model.Parameter;
44 import org.opendaylight.restconf.openapi.model.Path;
45 import org.opendaylight.restconf.openapi.model.Schema;
46 import org.opendaylight.restconf.openapi.model.Server;
47 import org.opendaylight.restconf.openapi.model.security.Http;
48 import org.opendaylight.yangtools.yang.common.QName;
49 import org.opendaylight.yangtools.yang.common.Revision;
50 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
51 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
53 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
55 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.Module;
57 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
58 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 public abstract class BaseYangOpenApiGenerator {
63
64     private static final Logger LOG = LoggerFactory.getLogger(BaseYangOpenApiGenerator.class);
65     private static final String CONTROLLER_RESOURCE_NAME = "Controller";
66
67     public static final String API_VERSION = "1.0.0";
68     public static final String OPEN_API_VERSION = "3.0.3";
69     public static final String BASE_PATH = "/";
70     public static final String MODULE_NAME_SUFFIX = "_module";
71     public static final String BASIC_AUTH_NAME = "basicAuth";
72     public static final Http OPEN_API_BASIC_AUTH = new Http("basic", null, null);
73     public static final List<Map<String, List<String>>> SECURITY = List.of(Map.of(BASIC_AUTH_NAME, List.of()));
74     public static final String DESCRIPTION = """
75         We are providing full API for configurational data which can be edited (by POST, PUT, PATCH and DELETE).
76         For operational data we only provide GET API.\n
77         For majority of request you can see only config data in examples. That’s because we can show only one example
78         per request. The exception when you can see operational data in example is when data are representing
79         operational (config false) container with no config data in it.""";
80
81     private final DOMSchemaService schemaService;
82
83     protected BaseYangOpenApiGenerator(final @NonNull DOMSchemaService schemaService) {
84         this.schemaService = requireNonNull(schemaService);
85     }
86
87     public OpenApiObject getControllerModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames) {
88         final var context = requireNonNull(schemaService.getGlobalContext());
89         final var schema = createSchemaFromUriInfo(uriInfo);
90         final var host = createHostFromUriInfo(uriInfo);
91         final var title = "Controller modules of RESTCONF";
92         final var info = new Info(API_VERSION, title, DESCRIPTION);
93         final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
94
95         final var paths = new HashMap<String, Path>();
96         final var schemas = new HashMap<String, Schema>();
97         for (final var module : getSortedModules(context)) {
98             LOG.debug("Working on [{},{}]...", module.getName(), module.getQNameModule().getRevision().orElse(null));
99             schemas.putAll(getSchemas(module, context, definitionNames, false));
100             paths.putAll(getPaths(module, "", CONTROLLER_RESOURCE_NAME, context, definitionNames, false));
101         }
102
103         final var components = new Components(schemas, Map.of(BASIC_AUTH_NAME, OPEN_API_BASIC_AUTH));
104         return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
105     }
106
107     public static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
108         if (range.equals(Range.all())) {
109             return modules;
110         }
111         final int begin = range.lowerEndpoint();
112         final int end = range.upperEndpoint();
113
114         Module firstModule = null;
115
116         final Iterator<Module> iterator = modules.iterator();
117         int counter = 0;
118         while (iterator.hasNext() && counter < end) {
119             final Module module = iterator.next();
120             if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
121                 if (counter == begin) {
122                     firstModule = module;
123                 }
124                 counter++;
125             }
126         }
127
128         if (iterator.hasNext()) {
129             return modules.subSet(firstModule, iterator.next());
130         } else {
131             return modules.tailSet(firstModule);
132         }
133     }
134
135     public OpenApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo) {
136         final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
137         Preconditions.checkState(schemaContext != null);
138         return getApiDeclaration(module, revision, uriInfo, schemaContext, "", CONTROLLER_RESOURCE_NAME);
139     }
140
141     public OpenApiObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
142             final EffectiveModelContext schemaContext, final String context, final @NonNull String deviceName) {
143         final Optional<Revision> rev;
144
145         try {
146             rev = Revision.ofNullable(revision);
147         } catch (final DateTimeParseException e) {
148             throw new IllegalArgumentException(e);
149         }
150
151         final var module = schemaContext.findModule(moduleName, rev).orElse(null);
152         Preconditions.checkArgument(module != null,
153                 "Could not find module by name,revision: " + moduleName + "," + revision);
154
155         final var schema = createSchemaFromUriInfo(uriInfo);
156         final var host = createHostFromUriInfo(uriInfo);
157         final var info = new Info(API_VERSION, module.getName(), DESCRIPTION);
158         final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
159         final var definitionNames = new DefinitionNames();
160         final var schemas = getSchemas(module, schemaContext, definitionNames, true);
161         final var components = new Components(schemas, Map.of(BASIC_AUTH_NAME, OPEN_API_BASIC_AUTH));
162         final var paths = getPaths(module, context, deviceName, schemaContext, definitionNames, true);
163         return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
164     }
165
166     public String createHostFromUriInfo(final UriInfo uriInfo) {
167         String portPart = "";
168         final int port = uriInfo.getBaseUri().getPort();
169         if (port != -1) {
170             portPart = ":" + port;
171         }
172         return uriInfo.getBaseUri().getHost() + portPart;
173     }
174
175     public String createSchemaFromUriInfo(final UriInfo uriInfo) {
176         return uriInfo.getBaseUri().getScheme();
177     }
178
179     public Map<String, Path> getPaths(final Module module, final String context, final String deviceName,
180             final EffectiveModelContext schemaContext, final DefinitionNames definitionNames,
181             final boolean isForSingleModule) {
182         final Map<String, Path> paths = new HashMap<>();
183         final String moduleName = module.getName();
184
185         boolean hasAddRootPostLink = false;
186
187         final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
188         LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
189         for (final DataSchemaNode node : dataSchemaNodes) {
190             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
191                 final boolean isConfig = node.isConfiguration();
192                 LOG.debug("Is Configuration node [{}] [{}]", isConfig, node.getQName().getLocalName());
193
194                 final String localName = moduleName + ":" + node.getQName().getLocalName();
195                 final String resourcePath  = getResourcePath("data", context);
196
197                 final List<Parameter> pathParams = new ArrayList<>();
198                 /*
199                  * When there are two or more top container or list nodes
200                  * whose config statement is true in module, make sure that
201                  * only one root post link is added for this module.
202                  */
203                 if (isConfig && isForSingleModule && !hasAddRootPostLink) {
204                     LOG.debug("Has added root post link for module {}", moduleName);
205                     addRootPostLink(module, deviceName, pathParams, resourcePath, paths);
206
207                     hasAddRootPostLink = true;
208                 }
209                 final String resourcePathPart = createPath(node, pathParams, localName);
210                 addPaths(node, deviceName, moduleName, paths, pathParams, isConfig, schemaContext,
211                     moduleName, definitionNames, resourcePathPart, context);
212             }
213         }
214
215         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
216             final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
217                     + rpcDefinition.getQName().getLocalName();
218             paths.put(resolvedPath, buildPostPath(rpcDefinition, moduleName, deviceName, moduleName, definitionNames,
219                 List.of()));
220         }
221
222         LOG.debug("Number of Paths found [{}]", paths.size());
223
224         return paths;
225     }
226
227     public Map<String, Schema> getSchemas(final Module module, final EffectiveModelContext schemaContext,
228             final DefinitionNames definitionNames, final boolean isForSingleModule) {
229         Map<String, Schema> schemas = new HashMap<>();
230         try {
231             schemas = DefinitionGenerator.convertToSchemas(module, schemaContext, definitionNames, isForSingleModule);
232         } catch (final IOException e) {
233             LOG.error("Exception occurred in DefinitionGenerator", e); // FIXME propagate exception
234         }
235
236         return schemas;
237     }
238
239     private static void addRootPostLink(final Module module, final String deviceName,
240             final List<Parameter> pathParams, final String resourcePath, final Map<String, Path> paths) {
241         if (containsListOrContainer(module.getChildNodes())) {
242             final String moduleName = module.getName();
243             final String name = moduleName + MODULE_NAME_SUFFIX;
244             paths.put(resourcePath, new Path.Builder()
245                 .post(buildPost(null, null, name, "", moduleName, deviceName,
246                     module.getDescription().orElse(""), pathParams))
247                 .build());
248         }
249     }
250
251     public abstract String getResourcePath(String resourceType, String context);
252
253     private void addPaths(final DataSchemaNode node, final String deviceName, final String moduleName,
254             final Map<String, Path> paths, final List<Parameter> parentPathParams,
255             final boolean isConfig, final EffectiveModelContext schemaContext, final String parentName,
256             final DefinitionNames definitionNames, final String resourcePathPart, final String context) {
257         final String dataPath = getResourcePath("data", context) + "/" + resourcePathPart;
258         LOG.debug("Adding path: [{}]", dataPath);
259         final List<Parameter> pathParams = new ArrayList<>(parentPathParams);
260         Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
261         if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
262             childSchemaNodes = ((DataNodeContainer) node).getChildNodes();
263         }
264         final String fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
265         paths.put(dataPath, operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
266             fullName));
267
268         if (node instanceof ActionNodeContainer actionContainer) {
269             actionContainer.getActions().forEach(actionDef -> {
270                 final String operationsPath = getResourcePath("operations", context)
271                     + "/" + resourcePathPart
272                     + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
273                 paths.put(operationsPath, buildPostPath(actionDef, moduleName, deviceName, parentName,
274                     definitionNames, pathParams));
275             });
276         }
277
278         for (final DataSchemaNode childNode : childSchemaNodes) {
279             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
280                 final String newParent = parentName + "_" + node.getQName().getLocalName();
281                 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
282                 final String newPathPart = resourcePathPart + "/" + createPath(childNode, pathParams, localName);
283                 final boolean newIsConfig = isConfig && childNode.isConfiguration();
284                 addPaths(childNode, deviceName, moduleName, paths, pathParams, newIsConfig, schemaContext,
285                     newParent, definitionNames, newPathPart, context);
286                 pathParams.clear();
287                 pathParams.addAll(parentPathParams);
288             }
289         }
290     }
291
292     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
293         for (final DataSchemaNode child : nodes) {
294             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
295                 return true;
296             }
297         }
298         return false;
299     }
300
301     private static Path operations(final DataSchemaNode node, final String moduleName,
302             final String deviceName, final List<Parameter> pathParams, final boolean isConfig, final String parentName,
303             final DefinitionNames definitionNames, final String fullName) {
304         final Path.Builder operationsBuilder = new Path.Builder();
305
306         final String discriminator = definitionNames.getDiscriminator(node);
307         final String nodeName = node.getQName().getLocalName();
308
309         final Operation get = buildGet(node, parentName, moduleName, deviceName, pathParams, isConfig);
310         operationsBuilder.get(get);
311
312         if (isConfig) {
313             final Operation put = buildPut(node, parentName, moduleName, deviceName, pathParams, fullName);
314             operationsBuilder.put(put);
315
316             final Operation patch = buildPatch(node, parentName, moduleName, deviceName, pathParams, fullName);
317             operationsBuilder.patch(patch);
318
319             final Operation delete = buildDelete(node, moduleName, deviceName, pathParams);
320             operationsBuilder.delete(delete);
321
322             if (node instanceof ContainerSchemaNode container) {
323                 final var childNode = getListOrContainerChildNode(container);
324                 // we have to ensure that we are able to create POST payload containing the first container/list child
325                 if (childNode != null) {
326                     final Operation post = buildPost(childNode, parentName, nodeName, discriminator, moduleName,
327                         deviceName, node.getDescription().orElse(""), pathParams);
328                     operationsBuilder.post(post);
329                 }
330             }
331         }
332         return operationsBuilder.build();
333     }
334
335     private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
336         return node.getChildNodes().stream()
337             .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
338             .findFirst().orElse(null);
339     }
340
341     private static String createPath(final DataSchemaNode schemaNode, final List<Parameter> pathParams,
342             final String localName) {
343         final StringBuilder path = new StringBuilder();
344         path.append(localName);
345         final Set<String> parameters = pathParams.stream()
346             .map(Parameter::name)
347             .collect(Collectors.toSet());
348
349         if (schemaNode instanceof ListSchemaNode listSchemaNode) {
350             String prefix = "=";
351             int discriminator = 1;
352             for (final QName listKey : listSchemaNode.getKeyDefinition()) {
353                 final String keyName = listKey.getLocalName();
354                 String paramName = keyName;
355                 while (!parameters.add(paramName)) {
356                     paramName = keyName + discriminator;
357                     discriminator++;
358                 }
359
360                 final String pathParamIdentifier = prefix + "{" + paramName + "}";
361                 prefix = ",";
362                 path.append(pathParamIdentifier);
363
364                 final String description = listSchemaNode.findDataChildByName(listKey)
365                     .flatMap(DataSchemaNode::getDescription).orElse(null);
366                 pathParams.add(new Parameter.Builder()
367                     .name(paramName)
368                     .schema(new Schema.Builder().type("string").build())
369                     .in("path")
370                     .required(true)
371                     .description(description)
372                     .build());
373             }
374         }
375         return path.toString();
376     }
377
378     public static SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
379         if (schemaContext == null) {
380             return Collections.emptySortedSet();
381         }
382
383         final var sortedModules = new TreeSet<Module>((module1, module2) -> {
384             int result = module1.getName().compareTo(module2.getName());
385             if (result == 0) {
386                 result = Revision.compare(module1.getRevision(), module2.getRevision());
387             }
388             if (result == 0) {
389                 result = module1.getNamespace().compareTo(module2.getNamespace());
390             }
391             return result;
392         });
393         sortedModules.addAll(schemaContext.getModules());
394         return sortedModules;
395     }
396
397     private static Path buildPostPath(final OperationDefinition operDef, final String moduleName,
398             final String deviceName, final String parentName, final DefinitionNames definitionNames,
399             final List<Parameter> parentPathParams) {
400         return new Path.Builder()
401             .post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames, parentPathParams))
402             .build();
403     }
404 }