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