2 * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.restconf.openapi.impl;
10 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
11 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
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;
62 public final class PathsStream extends InputStream {
63 private static final String OPERATIONS = "operations";
64 private static final String DATA = "data";
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;
77 private boolean hasRootPostLink;
78 private boolean hasAddedDataStore;
79 private Reader reader;
80 private ReadableByteChannel channel;
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;
89 this.schemaContext = schemaContext;
90 this.isForSingleModule = isForSingleModule;
91 this.deviceName = deviceName;
92 this.urlPrefix = urlPrefix;
93 this.includeDataStore = includeDataStore;
94 this.basePath = basePath;
96 this.generator = generator;
97 hasRootPostLink = false;
98 hasAddedDataStore = false;
102 public int read() throws IOException {
106 if (reader == null) {
107 generator.writeObjectFieldStart("paths");
109 reader = new BufferedReader(
110 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
113 var read = reader.read();
115 if (iterator.hasNext()) {
116 reader = new BufferedReader(
117 new InputStreamReader(new PathStream(toPaths(iterator.next()), writer), StandardCharsets.UTF_8));
118 read = reader.read();
121 generator.writeEndObject();
123 reader = new BufferedReader(
124 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
127 return reader.read();
133 public int read(final byte[] array, final int off, final int len) throws IOException {
137 if (channel == null) {
138 generator.writeObjectFieldStart("paths");
140 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
143 var read = channel.read(ByteBuffer.wrap(array, off, len));
145 if (iterator.hasNext()) {
146 channel = Channels.newChannel(new PathStream(toPaths(iterator.next()), writer));
147 read = channel.read(ByteBuffer.wrap(array, off, len));
150 generator.writeEndObject();
152 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
155 return channel.read(ByteBuffer.wrap(array, off, len));
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;
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(), List.of(), localName, null,
174 final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
175 final var entity = new PathEntity(resolvedPath, post);
178 for (final var node : module.getChildNodes()) {
179 final var moduleName = module.getName();
180 final boolean isConfig = node.isConfiguration();
181 final var nodeLocalName = node.getQName().getLocalName();
183 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
184 if (isConfig && !hasRootPostLink && isForSingleModule) {
185 final var resolvedPath = basePath + DATA + urlPrefix;
186 result.add(new PathEntity(resolvedPath,
187 new PostEntity(node, deviceName, moduleName, List.of(), nodeLocalName, module, List.of())));
188 hasRootPostLink = true;
191 final var pathParams = new ArrayList<ParameterEntity>();
192 final var localName = moduleName + ":" + nodeLocalName;
193 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
194 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
195 deviceName, basePath, null, List.of());
201 private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
202 final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
203 final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
204 final String basePath, final SchemaNode parentNode, final List<SchemaNode> parentNodes) {
205 final var resourcePath = basePath + DATA + path;
206 final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
207 final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
208 if (firstChild != null && node instanceof ContainerSchemaNode) {
209 result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
210 fullName, firstChild, deviceName));
212 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
213 isConfig, fullName, deviceName));
215 final var childNodes = ((DataNodeContainer) node).getChildNodes();
216 final var listOfParents = new ArrayList<>(parentNodes);
217 if (parentNode != null) {
218 listOfParents.add(parentNode);
220 if (node instanceof ActionNodeContainer actionContainer) {
221 final var listOfParentsForActions = new ArrayList<>(listOfParents);
222 listOfParentsForActions.add(node);
223 final var actionParams = new ArrayList<>(pathParams);
224 actionContainer.getActions().forEach(actionDef -> {
225 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
226 node.getQName(), schemaContext);
227 final var childPath = basePath + OPERATIONS + resourceActionPath;
228 result.add(processActionPathEntity(actionDef, childPath, actionParams, moduleName,
229 refPath, deviceName, parentNode, listOfParentsForActions));
232 for (final var childNode : childNodes) {
233 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
234 final var childParams = new ArrayList<>(pathParams);
235 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
236 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
237 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
238 final var newConfig = isConfig && childNode.isConfiguration();
239 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
240 schemaContext, deviceName, basePath, node, listOfParents);
245 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
246 return node.getChildNodes().stream()
247 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
248 .findFirst().orElse(null);
251 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
252 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
253 final boolean isConfig, final String fullName, final String deviceName) {
255 return new PathEntity(resourcePath,
256 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
257 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
258 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
259 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
261 return new PathEntity(resourcePath,
262 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
266 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
267 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
268 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
270 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
271 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node, List.of());
272 if (!((DataSchemaNode) childNode).isConfiguration()) {
273 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null, List.of());
275 return new PathEntity(resourcePath, post,
276 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
277 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
278 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
279 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
281 return new PathEntity(resourcePath,
282 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
286 private static PathEntity processActionPathEntity(final SchemaNode node, final String resourcePath,
287 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
288 final String deviceName, final SchemaNode parentNode, final List<SchemaNode> parentNodes) {
289 return new PathEntity(resourcePath,
290 new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode, parentNodes));
293 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
294 final String localName) {
295 final var path = new StringBuilder();
296 path.append(localName);
297 final var parameters = pathParams.stream()
298 .map(ParameterEntity::name)
299 .collect(Collectors.toSet());
301 if (node instanceof ListSchemaNode listSchemaNode) {
303 var discriminator = 1;
304 for (final var listKey : listSchemaNode.getKeyDefinition()) {
305 final var keyName = listKey.getLocalName();
306 var paramName = keyName;
307 while (!parameters.add(paramName)) {
308 paramName = keyName + discriminator;
312 final var pathParamIdentifier = prefix + "{" + paramName + "}";
314 path.append(pathParamIdentifier);
316 final var description = listSchemaNode.findDataChildByName(listKey)
317 .flatMap(DataSchemaNode::getDescription).orElse(null);
319 pathParams.add(new ParameterEntity(paramName, "path", true,
320 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
323 return path.toString();
326 private static String getAllowedType(final ListSchemaNode list, final QName key) {
327 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
329 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
330 // see: https://swagger.io/docs/specification/data-models/data-types/
331 // TODO: Java 21 use pattern matching for switch
332 if (keyType instanceof Int8TypeDefinition) {
335 if (keyType instanceof Int16TypeDefinition) {
338 if (keyType instanceof Int32TypeDefinition) {
341 if (keyType instanceof Int64TypeDefinition) {
344 if (keyType instanceof Uint8TypeDefinition) {
347 if (keyType instanceof Uint16TypeDefinition) {
350 if (keyType instanceof Uint32TypeDefinition) {
353 if (keyType instanceof Uint64TypeDefinition) {
356 if (keyType instanceof DecimalTypeDefinition) {
359 if (keyType instanceof BooleanTypeDefinition) {