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.ParameterEntity;
35 import org.opendaylight.restconf.openapi.model.ParameterSchemaEntity;
36 import org.opendaylight.restconf.openapi.model.PatchEntity;
37 import org.opendaylight.restconf.openapi.model.PathEntity;
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;
61 public final class PathsStream extends InputStream {
62 private final Iterator<? extends Module> iterator;
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 private final ByteArrayOutputStream stream;
71 private final JsonGenerator generator;
73 private static final String OPERATIONS = "operations";
74 private static final String DATA = "data";
75 private boolean hasRootPostLink;
76 private boolean hasAddedDataStore;
77 private Reader reader;
78 private ReadableByteChannel channel;
81 public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
82 final String deviceName, final String urlPrefix, final boolean isForSingleModule,
83 final boolean includeDataStore, final Iterator<? extends Module> iterator, final String basePath,
84 final ByteArrayOutputStream stream, final JsonGenerator generator) {
85 this.iterator = iterator;
87 this.schemaContext = schemaContext;
88 this.isForSingleModule = isForSingleModule;
89 this.deviceName = deviceName;
90 this.urlPrefix = urlPrefix;
91 this.includeDataStore = includeDataStore;
92 this.basePath = basePath;
94 this.generator = generator;
95 hasRootPostLink = false;
96 hasAddedDataStore = false;
100 public int read() throws IOException {
104 if (reader == null) {
105 generator.writeObjectFieldStart("paths");
107 reader = new BufferedReader(
108 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
111 var read = reader.read();
113 if (iterator.hasNext()) {
114 reader = new BufferedReader(
115 new InputStreamReader(new PathStream(toPaths(iterator.next()), writer), StandardCharsets.UTF_8));
116 read = reader.read();
119 generator.writeEndObject();
121 reader = new BufferedReader(
122 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
125 return reader.read();
131 public int read(final byte[] array, final int off, final int len) throws IOException {
135 if (channel == null) {
136 generator.writeObjectFieldStart("paths");
138 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
141 var read = channel.read(ByteBuffer.wrap(array, off, len));
143 if (iterator.hasNext()) {
144 channel = Channels.newChannel(new PathStream(toPaths(iterator.next()), writer));
145 read = channel.read(ByteBuffer.wrap(array, off, len));
148 generator.writeEndObject();
150 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
153 return channel.read(ByteBuffer.wrap(array, off, len));
158 private Deque<PathEntity> toPaths(final Module module) {
159 final var result = new ArrayDeque<PathEntity>();
160 if (includeDataStore && !hasAddedDataStore) {
161 final var dataPath = basePath + DATA + urlPrefix;
162 result.add(new PathEntity(dataPath, null, null, null,
163 new GetEntity(null, deviceName, "data", null, null, false),
165 final var operationsPath = basePath + OPERATIONS + urlPrefix;
166 result.add(new PathEntity(operationsPath, null, null, null,
167 new GetEntity(null, deviceName, "operations", null, null, false),
169 hasAddedDataStore = true;
171 // RPC operations (via post) - RPCs have their own path
172 for (final var rpc : module.getRpcs()) {
173 final var localName = rpc.getQName().getLocalName();
174 final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), localName, null);
175 final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
176 final var entity = new PathEntity(resolvedPath, post, null, null, null, null);
179 for (final var node : module.getChildNodes()) {
180 final var moduleName = module.getName();
181 final boolean isConfig = node.isConfiguration();
182 final var nodeLocalName = node.getQName().getLocalName();
184 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
185 if (isConfig && !hasRootPostLink && isForSingleModule) {
186 final var resolvedPath = basePath + DATA + urlPrefix;
187 result.add(new PathEntity(resolvedPath, new PostEntity(node, deviceName, moduleName,
188 new ArrayList<>(), nodeLocalName, module), null, null, null, null));
189 hasRootPostLink = true;
192 final var pathParams = new ArrayList<ParameterEntity>();
193 final var localName = moduleName + ":" + nodeLocalName;
194 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
195 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
196 deviceName, basePath, null);
202 private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
203 final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
204 final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
205 final String basePath, final SchemaNode parentNode) {
206 final var resourcePath = basePath + DATA + path;
207 final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
208 final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
209 if (firstChild != null && node instanceof ContainerSchemaNode) {
210 result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
211 fullName, firstChild, deviceName));
213 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
214 isConfig, fullName, deviceName));
216 final var childNodes = ((DataNodeContainer) node).getChildNodes();
217 if (node instanceof ActionNodeContainer actionContainer) {
218 final var actionParams = new ArrayList<>(pathParams);
219 actionContainer.getActions().forEach(actionDef -> {
220 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
221 node.getQName(), schemaContext);
222 final var childPath = basePath + OPERATIONS + resourceActionPath;
223 result.add(processRootAndActionPathEntity(actionDef, childPath, actionParams, moduleName,
224 refPath, deviceName, parentNode));
227 for (final var childNode : childNodes) {
228 final var childParams = new ArrayList<>(pathParams);
229 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
230 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
231 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
232 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
233 final var newConfig = isConfig && childNode.isConfiguration();
234 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
235 schemaContext, deviceName, basePath, node);
240 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
241 return node.getChildNodes().stream()
242 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
243 .findFirst().orElse(null);
246 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
247 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
248 final boolean isConfig, final String fullName, final String deviceName) {
250 return new PathEntity(resourcePath, null,
251 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
252 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
253 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
254 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
256 return new PathEntity(resourcePath, null, null, null,
257 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
261 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
262 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
263 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
265 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
266 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
267 if (!((DataSchemaNode) childNode).isConfiguration()) {
268 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
270 return new PathEntity(resourcePath, post,
271 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
272 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
273 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
274 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
276 return new PathEntity(resourcePath, null, null, null,
277 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
281 private static PathEntity processRootAndActionPathEntity(final SchemaNode node, final String resourcePath,
282 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
283 final String deviceName, final SchemaNode parentNode) {
284 return new PathEntity(resourcePath,
285 new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode),
286 null, null, null, null);
289 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
290 final String localName) {
291 final var path = new StringBuilder();
292 path.append(localName);
293 final var parameters = pathParams.stream()
294 .map(ParameterEntity::name)
295 .collect(Collectors.toSet());
297 if (node instanceof ListSchemaNode listSchemaNode) {
299 var discriminator = 1;
300 for (final var listKey : listSchemaNode.getKeyDefinition()) {
301 final var keyName = listKey.getLocalName();
302 var paramName = keyName;
303 while (!parameters.add(paramName)) {
304 paramName = keyName + discriminator;
308 final var pathParamIdentifier = prefix + "{" + paramName + "}";
310 path.append(pathParamIdentifier);
312 final var description = listSchemaNode.findDataChildByName(listKey)
313 .flatMap(DataSchemaNode::getDescription).orElse(null);
315 pathParams.add(new ParameterEntity(paramName, "path", true,
316 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
319 return path.toString();
322 private static String getAllowedType(final ListSchemaNode list, final QName key) {
323 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
325 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
326 // see: https://swagger.io/docs/specification/data-models/data-types/
327 // TODO: Java 21 use pattern matching for switch
328 if (keyType instanceof Int8TypeDefinition) {
331 if (keyType instanceof Int16TypeDefinition) {
334 if (keyType instanceof Int32TypeDefinition) {
337 if (keyType instanceof Int64TypeDefinition) {
340 if (keyType instanceof Uint8TypeDefinition) {
343 if (keyType instanceof Uint16TypeDefinition) {
346 if (keyType instanceof Uint32TypeDefinition) {
349 if (keyType instanceof Uint64TypeDefinition) {
353 if (keyType instanceof DecimalTypeDefinition) {
357 if (keyType instanceof BooleanTypeDefinition) {