Fix incorrect schema reference in root POST link
[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 ListSchemaNode)) {
323                 final Operation post = buildPost(node, parentName, nodeName, discriminator, moduleName, deviceName,
324                     node.getDescription().orElse(""), pathParams);
325                 operationsBuilder.post(post);
326             }
327         }
328         return operationsBuilder.build();
329     }
330
331     private static String createPath(final DataSchemaNode schemaNode, final List<Parameter> pathParams,
332             final String localName) {
333         final StringBuilder path = new StringBuilder();
334         path.append(localName);
335         final Set<String> parameters = pathParams.stream()
336             .map(Parameter::name)
337             .collect(Collectors.toSet());
338
339         if (schemaNode instanceof ListSchemaNode listSchemaNode) {
340             String prefix = "=";
341             int discriminator = 1;
342             for (final QName listKey : listSchemaNode.getKeyDefinition()) {
343                 final String keyName = listKey.getLocalName();
344                 String paramName = keyName;
345                 while (!parameters.add(paramName)) {
346                     paramName = keyName + discriminator;
347                     discriminator++;
348                 }
349
350                 final String pathParamIdentifier = prefix + "{" + paramName + "}";
351                 prefix = ",";
352                 path.append(pathParamIdentifier);
353
354                 final String description = listSchemaNode.findDataChildByName(listKey)
355                     .flatMap(DataSchemaNode::getDescription).orElse(null);
356                 pathParams.add(new Parameter.Builder()
357                     .name(paramName)
358                     .schema(new Schema.Builder().type("string").build())
359                     .in("path")
360                     .required(true)
361                     .description(description)
362                     .build());
363             }
364         }
365         return path.toString();
366     }
367
368     public static SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
369         if (schemaContext == null) {
370             return Collections.emptySortedSet();
371         }
372
373         final var sortedModules = new TreeSet<Module>((module1, module2) -> {
374             int result = module1.getName().compareTo(module2.getName());
375             if (result == 0) {
376                 result = Revision.compare(module1.getRevision(), module2.getRevision());
377             }
378             if (result == 0) {
379                 result = module1.getNamespace().compareTo(module2.getNamespace());
380             }
381             return result;
382         });
383         sortedModules.addAll(schemaContext.getModules());
384         return sortedModules;
385     }
386
387     private static Path buildPostPath(final OperationDefinition operDef, final String moduleName,
388             final String deviceName, final String parentName, final DefinitionNames definitionNames,
389             final List<Parameter> parentPathParams) {
390         return new Path.Builder()
391             .post(buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames, parentPathParams))
392             .build();
393     }
394 }