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