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