Separate logic of get root path
[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(), new ArrayList<>(), localName, null);
173             final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
174             final var entity = new PathEntity(resolvedPath, post);
175             result.add(entity);
176         }
177         for (final var node : module.getChildNodes()) {
178             final var moduleName = module.getName();
179             final boolean isConfig = node.isConfiguration();
180             final var nodeLocalName = node.getQName().getLocalName();
181
182             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
183                 if (isConfig && !hasRootPostLink && isForSingleModule) {
184                     final var resolvedPath = basePath + DATA + urlPrefix;
185                     result.add(new PathEntity(resolvedPath,
186                         new PostEntity(node, deviceName, moduleName, new ArrayList<>(), nodeLocalName, module)));
187                     hasRootPostLink = true;
188                 }
189                 //process first node
190                 final var pathParams = new ArrayList<ParameterEntity>();
191                 final var localName = moduleName + ":" + nodeLocalName;
192                 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
193                 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
194                     deviceName, basePath, null);
195             }
196         }
197         return result;
198     }
199
200     private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
201             final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
202             final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
203             final String basePath, final SchemaNode parentNode) {
204         final var resourcePath = basePath + DATA + path;
205         final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
206         final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
207         if (firstChild != null && node instanceof ContainerSchemaNode) {
208             result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
209                 fullName, firstChild, deviceName));
210         } else {
211             result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
212                 isConfig, fullName, deviceName));
213         }
214         final var childNodes = ((DataNodeContainer) node).getChildNodes();
215         if (node instanceof ActionNodeContainer actionContainer) {
216             final var actionParams = new ArrayList<>(pathParams);
217             actionContainer.getActions().forEach(actionDef -> {
218                 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
219                     node.getQName(), schemaContext);
220                 final var childPath = basePath + OPERATIONS + resourceActionPath;
221                 result.add(processActionPathEntity(actionDef, childPath, actionParams, moduleName,
222                     refPath, deviceName, parentNode));
223             });
224         }
225         for (final var childNode : childNodes) {
226             final var childParams = new ArrayList<>(pathParams);
227             final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
228             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
229                 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
230                 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
231                 final var newConfig = isConfig && childNode.isConfiguration();
232                 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
233                     schemaContext, deviceName, basePath, node);
234             }
235         }
236     }
237
238     private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
239         return node.getChildNodes().stream()
240             .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
241             .findFirst().orElse(null);
242     }
243
244     private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
245             final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
246             final boolean isConfig, final String fullName, final String deviceName) {
247         if (isConfig) {
248             return new PathEntity(resourcePath,
249                 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
250                 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
251                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
252                 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
253         } else {
254             return new PathEntity(resourcePath,
255                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
256         }
257     }
258
259     private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
260             final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
261             final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
262         if (isConfig) {
263             final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
264             var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
265             if (!((DataSchemaNode) childNode).isConfiguration()) {
266                 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
267             }
268             return new PathEntity(resourcePath, post,
269                 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
270                 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
271                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
272                 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
273         } else {
274             return new PathEntity(resourcePath,
275                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
276         }
277     }
278
279     private static PathEntity processActionPathEntity(final SchemaNode node, final String resourcePath,
280             final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
281             final String deviceName, final SchemaNode parentNode) {
282         return new PathEntity(resourcePath,
283             new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode));
284     }
285
286     private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
287             final String localName) {
288         final var path = new StringBuilder();
289         path.append(localName);
290         final var parameters = pathParams.stream()
291             .map(ParameterEntity::name)
292             .collect(Collectors.toSet());
293
294         if (node instanceof ListSchemaNode listSchemaNode) {
295             var prefix = "=";
296             var discriminator = 1;
297             for (final var listKey : listSchemaNode.getKeyDefinition()) {
298                 final var keyName = listKey.getLocalName();
299                 var paramName = keyName;
300                 while (!parameters.add(paramName)) {
301                     paramName = keyName + discriminator;
302                     discriminator++;
303                 }
304
305                 final var pathParamIdentifier = prefix + "{" + paramName + "}";
306                 prefix = ",";
307                 path.append(pathParamIdentifier);
308
309                 final var description = listSchemaNode.findDataChildByName(listKey)
310                     .flatMap(DataSchemaNode::getDescription).orElse(null);
311
312                 pathParams.add(new ParameterEntity(paramName, "path", true,
313                     new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
314             }
315         }
316         return path.toString();
317     }
318
319     private static String getAllowedType(final ListSchemaNode list, final QName key) {
320         final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
321
322         // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
323         // see: https://swagger.io/docs/specification/data-models/data-types/
324         // TODO: Java 21 use pattern matching for switch
325         if (keyType instanceof Int8TypeDefinition) {
326             return "integer";
327         }
328         if (keyType instanceof Int16TypeDefinition) {
329             return "integer";
330         }
331         if (keyType instanceof Int32TypeDefinition) {
332             return "integer";
333         }
334         if (keyType instanceof Int64TypeDefinition) {
335             return "integer";
336         }
337         if (keyType instanceof Uint8TypeDefinition) {
338             return "integer";
339         }
340         if (keyType instanceof Uint16TypeDefinition) {
341             return "integer";
342         }
343         if (keyType instanceof Uint32TypeDefinition) {
344             return "integer";
345         }
346         if (keyType instanceof Uint64TypeDefinition) {
347             return "integer";
348         }
349         if (keyType instanceof DecimalTypeDefinition) {
350             return "number";
351         }
352         if (keyType instanceof BooleanTypeDefinition) {
353             return "boolean";
354         }
355
356         return "string";
357     }
358 }