OpenAPI: netopeer2 wrong schemas for actions
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / impl / PathsStream.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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 org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
11 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
12
13 import com.fasterxml.jackson.core.JsonGenerator;
14 import java.io.BufferedReader;
15 import java.io.ByteArrayInputStream;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InputStreamReader;
20 import java.io.Reader;
21 import java.nio.ByteBuffer;
22 import java.nio.channels.Channels;
23 import java.nio.channels.ReadableByteChannel;
24 import java.nio.charset.StandardCharsets;
25 import java.util.ArrayDeque;
26 import java.util.ArrayList;
27 import java.util.Deque;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.stream.Collectors;
31 import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
32 import org.opendaylight.restconf.openapi.model.DeleteEntity;
33 import org.opendaylight.restconf.openapi.model.GetEntity;
34 import org.opendaylight.restconf.openapi.model.GetRootEntity;
35 import org.opendaylight.restconf.openapi.model.ParameterEntity;
36 import org.opendaylight.restconf.openapi.model.ParameterSchemaEntity;
37 import org.opendaylight.restconf.openapi.model.PatchEntity;
38 import org.opendaylight.restconf.openapi.model.PathEntity;
39 import org.opendaylight.restconf.openapi.model.PostEntity;
40 import org.opendaylight.restconf.openapi.model.PutEntity;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
43 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
45 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
47 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.Module;
50 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
52 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
53 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
54 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
55 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
56 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
58 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
61
62 public final class PathsStream extends InputStream {
63     private static final String OPERATIONS = "operations";
64     private static final String DATA = "data";
65
66     private final Iterator<? extends Module> iterator;
67     private final OpenApiBodyWriter writer;
68     private final EffectiveModelContext schemaContext;
69     private final String deviceName;
70     private final String urlPrefix;
71     private final String basePath;
72     private final boolean isForSingleModule;
73     private final boolean includeDataStore;
74     private final ByteArrayOutputStream stream;
75     private final JsonGenerator generator;
76
77     private boolean hasRootPostLink;
78     private boolean hasAddedDataStore;
79     private Reader reader;
80     private ReadableByteChannel channel;
81     private boolean eof;
82
83     public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
84             final String deviceName, final String urlPrefix, final boolean isForSingleModule,
85             final boolean includeDataStore, final Iterator<? extends Module> iterator, final String basePath,
86             final ByteArrayOutputStream stream, final JsonGenerator generator) {
87         this.iterator = iterator;
88         this.writer = writer;
89         this.schemaContext = schemaContext;
90         this.isForSingleModule = isForSingleModule;
91         this.deviceName = deviceName;
92         this.urlPrefix = urlPrefix;
93         this.includeDataStore = includeDataStore;
94         this.basePath = basePath;
95         this.stream = stream;
96         this.generator = generator;
97         hasRootPostLink = false;
98         hasAddedDataStore = false;
99     }
100
101     @Override
102     public int read() throws IOException {
103         if (eof) {
104             return -1;
105         }
106         if (reader == null) {
107             generator.writeObjectFieldStart("paths");
108             generator.flush();
109             reader = new BufferedReader(
110                 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
111             stream.reset();
112         }
113         var read = reader.read();
114         while (read == -1) {
115             if (iterator.hasNext()) {
116                 reader = new BufferedReader(
117                     new InputStreamReader(new PathStream(toPaths(iterator.next()), writer), StandardCharsets.UTF_8));
118                 read = reader.read();
119                 continue;
120             }
121             generator.writeEndObject();
122             generator.flush();
123             reader = new BufferedReader(
124                 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
125             stream.reset();
126             eof = true;
127             return reader.read();
128         }
129         return read;
130     }
131
132     @Override
133     public int read(final byte[] array, final int off, final int len) throws IOException {
134         if (eof) {
135             return -1;
136         }
137         if (channel == null) {
138             generator.writeObjectFieldStart("paths");
139             generator.flush();
140             channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
141             stream.reset();
142         }
143         var read = channel.read(ByteBuffer.wrap(array, off, len));
144         while (read == -1) {
145             if (iterator.hasNext()) {
146                 channel = Channels.newChannel(new PathStream(toPaths(iterator.next()), writer));
147                 read = channel.read(ByteBuffer.wrap(array, off, len));
148                 continue;
149             }
150             generator.writeEndObject();
151             generator.flush();
152             channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
153             stream.reset();
154             eof = true;
155             return channel.read(ByteBuffer.wrap(array, off, len));
156         }
157         return read;
158     }
159
160     private Deque<PathEntity> toPaths(final Module module) {
161         final var result = new ArrayDeque<PathEntity>();
162         if (includeDataStore && !hasAddedDataStore) {
163             final var dataPath = basePath + DATA + urlPrefix;
164             result.add(new PathEntity(dataPath, new GetRootEntity(deviceName, "data")));
165             final var operationsPath = basePath + OPERATIONS + urlPrefix;
166             result.add(new PathEntity(operationsPath, new GetRootEntity(deviceName, "operations")));
167             hasAddedDataStore = true;
168         }
169         // RPC operations (via post) - RPCs have their own path
170         for (final var rpc : module.getRpcs()) {
171             final var localName = rpc.getQName().getLocalName();
172             final var post = new PostEntity(rpc, deviceName, module.getName(), List.of(), localName, null,
173                 List.of());
174             final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
175             final var entity = new PathEntity(resolvedPath, post);
176             result.add(entity);
177         }
178         for (final var node : module.getChildNodes()) {
179             final var moduleName = module.getName();
180             final boolean isConfig = node.isConfiguration();
181             final var nodeLocalName = node.getQName().getLocalName();
182
183             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
184                 if (isConfig && !hasRootPostLink && isForSingleModule) {
185                     final var resolvedPath = basePath + DATA + urlPrefix;
186                     result.add(new PathEntity(resolvedPath,
187                         new PostEntity(node, deviceName, moduleName, List.of(), nodeLocalName, module, List.of())));
188                     hasRootPostLink = true;
189                 }
190                 //process first node
191                 final var pathParams = new ArrayList<ParameterEntity>();
192                 final var localName = moduleName + ":" + nodeLocalName;
193                 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
194                 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
195                     deviceName, basePath, null, List.of());
196             }
197         }
198         return result;
199     }
200
201     private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
202             final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
203             final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
204             final String basePath, final SchemaNode parentNode, final List<SchemaNode> parentNodes) {
205         final var resourcePath = basePath + DATA + path;
206         final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
207         final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
208         if (firstChild != null && node instanceof ContainerSchemaNode) {
209             result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
210                 fullName, firstChild, deviceName));
211         } else {
212             result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
213                 isConfig, fullName, deviceName));
214         }
215         final var childNodes = ((DataNodeContainer) node).getChildNodes();
216         final var listOfParents = new ArrayList<>(parentNodes);
217         if (parentNode != null) {
218             listOfParents.add(parentNode);
219         }
220         if (node instanceof ActionNodeContainer actionContainer) {
221             final var listOfParentsForActions = new ArrayList<>(listOfParents);
222             listOfParentsForActions.add(node);
223             final var actionParams = new ArrayList<>(pathParams);
224             actionContainer.getActions().forEach(actionDef -> {
225                 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
226                     node.getQName(), schemaContext);
227                 final var childPath = basePath + OPERATIONS + resourceActionPath;
228                 result.add(processActionPathEntity(actionDef, childPath, actionParams, moduleName,
229                     refPath, deviceName, parentNode, listOfParentsForActions));
230             });
231         }
232         for (final var childNode : childNodes) {
233             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
234                 final var childParams = new ArrayList<>(pathParams);
235                 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
236                 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
237                 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
238                 final var newConfig = isConfig && childNode.isConfiguration();
239                 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
240                     schemaContext, deviceName, basePath, node, listOfParents);
241             }
242         }
243     }
244
245     private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
246         return node.getChildNodes().stream()
247             .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
248             .findFirst().orElse(null);
249     }
250
251     private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
252             final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
253             final boolean isConfig, final String fullName, final String deviceName) {
254         if (isConfig) {
255             return new PathEntity(resourcePath,
256                 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
257                 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
258                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
259                 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
260         } else {
261             return new PathEntity(resourcePath,
262                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
263         }
264     }
265
266     private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
267             final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
268             final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
269         if (isConfig) {
270             final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
271             var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node, List.of());
272             if (!((DataSchemaNode) childNode).isConfiguration()) {
273                 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null, List.of());
274             }
275             return new PathEntity(resourcePath, post,
276                 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
277                 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
278                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
279                 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
280         } else {
281             return new PathEntity(resourcePath,
282                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
283         }
284     }
285
286     private static PathEntity processActionPathEntity(final SchemaNode node, final String resourcePath,
287             final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
288             final String deviceName, final SchemaNode parentNode, final List<SchemaNode> parentNodes) {
289         return new PathEntity(resourcePath,
290             new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode, parentNodes));
291     }
292
293     private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
294             final String localName) {
295         final var path = new StringBuilder();
296         path.append(localName);
297         final var parameters = pathParams.stream()
298             .map(ParameterEntity::name)
299             .collect(Collectors.toSet());
300
301         if (node instanceof ListSchemaNode listSchemaNode) {
302             var prefix = "=";
303             var discriminator = 1;
304             for (final var listKey : listSchemaNode.getKeyDefinition()) {
305                 final var keyName = listKey.getLocalName();
306                 var paramName = keyName;
307                 while (!parameters.add(paramName)) {
308                     paramName = keyName + discriminator;
309                     discriminator++;
310                 }
311
312                 final var pathParamIdentifier = prefix + "{" + paramName + "}";
313                 prefix = ",";
314                 path.append(pathParamIdentifier);
315
316                 final var description = listSchemaNode.findDataChildByName(listKey)
317                     .flatMap(DataSchemaNode::getDescription).orElse(null);
318
319                 pathParams.add(new ParameterEntity(paramName, "path", true,
320                     new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
321             }
322         }
323         return path.toString();
324     }
325
326     private static String getAllowedType(final ListSchemaNode list, final QName key) {
327         final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
328
329         // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
330         // see: https://swagger.io/docs/specification/data-models/data-types/
331         // TODO: Java 21 use pattern matching for switch
332         if (keyType instanceof Int8TypeDefinition) {
333             return "integer";
334         }
335         if (keyType instanceof Int16TypeDefinition) {
336             return "integer";
337         }
338         if (keyType instanceof Int32TypeDefinition) {
339             return "integer";
340         }
341         if (keyType instanceof Int64TypeDefinition) {
342             return "integer";
343         }
344         if (keyType instanceof Uint8TypeDefinition) {
345             return "integer";
346         }
347         if (keyType instanceof Uint16TypeDefinition) {
348             return "integer";
349         }
350         if (keyType instanceof Uint32TypeDefinition) {
351             return "integer";
352         }
353         if (keyType instanceof Uint64TypeDefinition) {
354             return "integer";
355         }
356         if (keyType instanceof DecimalTypeDefinition) {
357             return "number";
358         }
359         if (keyType instanceof BooleanTypeDefinition) {
360             return "boolean";
361         }
362
363         return "string";
364     }
365 }