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(), new ArrayList<>(), localName, null);
173 final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
174 final var entity = new PathEntity(resolvedPath, post);
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();
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;
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);
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));
211 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
212 isConfig, fullName, deviceName));
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));
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);
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);
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) {
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));
254 return new PathEntity(resourcePath,
255 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
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) {
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);
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));
274 return new PathEntity(resourcePath,
275 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
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));
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());
294 if (node instanceof ListSchemaNode listSchemaNode) {
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;
305 final var pathParamIdentifier = prefix + "{" + paramName + "}";
307 path.append(pathParamIdentifier);
309 final var description = listSchemaNode.findDataChildByName(listKey)
310 .flatMap(DataSchemaNode::getDescription).orElse(null);
312 pathParams.add(new ParameterEntity(paramName, "path", true,
313 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
316 return path.toString();
319 private static String getAllowedType(final ListSchemaNode list, final QName key) {
320 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
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) {
328 if (keyType instanceof Int16TypeDefinition) {
331 if (keyType instanceof Int32TypeDefinition) {
334 if (keyType instanceof Int64TypeDefinition) {
337 if (keyType instanceof Uint8TypeDefinition) {
340 if (keyType instanceof Uint16TypeDefinition) {
343 if (keyType instanceof Uint32TypeDefinition) {
346 if (keyType instanceof Uint64TypeDefinition) {
349 if (keyType instanceof DecimalTypeDefinition) {
352 if (keyType instanceof BooleanTypeDefinition) {