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