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