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 static final String OPERATIONS = "operations";
63 private static final String DATA = "data";
65 private final Iterator<? extends Module> iterator;
66 private final OpenApiBodyWriter writer;
67 private final EffectiveModelContext schemaContext;
68 private final String deviceName;
69 private final String urlPrefix;
70 private final String basePath;
71 private final boolean isForSingleModule;
72 private final boolean includeDataStore;
73 private final ByteArrayOutputStream stream;
74 private final JsonGenerator generator;
76 private boolean hasRootPostLink;
77 private boolean hasAddedDataStore;
78 private Reader reader;
79 private ReadableByteChannel channel;
82 public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
83 final String deviceName, final String urlPrefix, final boolean isForSingleModule,
84 final boolean includeDataStore, final Iterator<? extends Module> iterator, final String basePath,
85 final ByteArrayOutputStream stream, final JsonGenerator generator) {
86 this.iterator = iterator;
88 this.schemaContext = schemaContext;
89 this.isForSingleModule = isForSingleModule;
90 this.deviceName = deviceName;
91 this.urlPrefix = urlPrefix;
92 this.includeDataStore = includeDataStore;
93 this.basePath = basePath;
95 this.generator = generator;
96 hasRootPostLink = false;
97 hasAddedDataStore = false;
101 public int read() throws IOException {
105 if (reader == null) {
106 generator.writeObjectFieldStart("paths");
108 reader = new BufferedReader(
109 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
112 var read = reader.read();
114 if (iterator.hasNext()) {
115 reader = new BufferedReader(
116 new InputStreamReader(new PathStream(toPaths(iterator.next()), writer), StandardCharsets.UTF_8));
117 read = reader.read();
120 generator.writeEndObject();
122 reader = new BufferedReader(
123 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
126 return reader.read();
132 public int read(final byte[] array, final int off, final int len) throws IOException {
136 if (channel == null) {
137 generator.writeObjectFieldStart("paths");
139 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
142 var read = channel.read(ByteBuffer.wrap(array, off, len));
144 if (iterator.hasNext()) {
145 channel = Channels.newChannel(new PathStream(toPaths(iterator.next()), writer));
146 read = channel.read(ByteBuffer.wrap(array, off, len));
149 generator.writeEndObject();
151 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
154 return channel.read(ByteBuffer.wrap(array, off, len));
159 private Deque<PathEntity> toPaths(final Module module) {
160 final var result = new ArrayDeque<PathEntity>();
161 if (includeDataStore && !hasAddedDataStore) {
162 final var dataPath = basePath + DATA + urlPrefix;
163 result.add(new PathEntity(dataPath,
164 new GetEntity(null, deviceName, "data", null, null, false)));
165 final var operationsPath = basePath + OPERATIONS + urlPrefix;
166 result.add(new PathEntity(operationsPath,
167 new GetEntity(null, deviceName, "operations", null, null, false)));
168 hasAddedDataStore = true;
170 // RPC operations (via post) - RPCs have their own path
171 for (final var rpc : module.getRpcs()) {
172 final var localName = rpc.getQName().getLocalName();
173 final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), 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, new ArrayList<>(), nodeLocalName, module)));
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);
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) {
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 if (node instanceof ActionNodeContainer actionContainer) {
217 final var actionParams = new ArrayList<>(pathParams);
218 actionContainer.getActions().forEach(actionDef -> {
219 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
220 node.getQName(), schemaContext);
221 final var childPath = basePath + OPERATIONS + resourceActionPath;
222 result.add(processActionPathEntity(actionDef, childPath, actionParams, moduleName,
223 refPath, deviceName, parentNode));
226 for (final var childNode : childNodes) {
227 final var childParams = new ArrayList<>(pathParams);
228 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
229 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
230 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
231 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
232 final var newConfig = isConfig && childNode.isConfiguration();
233 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
234 schemaContext, deviceName, basePath, node);
239 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
240 return node.getChildNodes().stream()
241 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
242 .findFirst().orElse(null);
245 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
246 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
247 final boolean isConfig, final String fullName, final String deviceName) {
249 return new PathEntity(resourcePath,
250 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
251 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
252 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
253 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
255 return new PathEntity(resourcePath,
256 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
260 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
261 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
262 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
264 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
265 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
266 if (!((DataSchemaNode) childNode).isConfiguration()) {
267 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
269 return new PathEntity(resourcePath, post,
270 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
271 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
272 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
273 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
275 return new PathEntity(resourcePath,
276 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false));
280 private static PathEntity processActionPathEntity(final SchemaNode node, final String resourcePath,
281 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
282 final String deviceName, final SchemaNode parentNode) {
283 return new PathEntity(resourcePath,
284 new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode));
287 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
288 final String localName) {
289 final var path = new StringBuilder();
290 path.append(localName);
291 final var parameters = pathParams.stream()
292 .map(ParameterEntity::name)
293 .collect(Collectors.toSet());
295 if (node instanceof ListSchemaNode listSchemaNode) {
297 var discriminator = 1;
298 for (final var listKey : listSchemaNode.getKeyDefinition()) {
299 final var keyName = listKey.getLocalName();
300 var paramName = keyName;
301 while (!parameters.add(paramName)) {
302 paramName = keyName + discriminator;
306 final var pathParamIdentifier = prefix + "{" + paramName + "}";
308 path.append(pathParamIdentifier);
310 final var description = listSchemaNode.findDataChildByName(listKey)
311 .flatMap(DataSchemaNode::getDescription).orElse(null);
313 pathParams.add(new ParameterEntity(paramName, "path", true,
314 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
317 return path.toString();
320 private static String getAllowedType(final ListSchemaNode list, final QName key) {
321 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
323 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
324 // see: https://swagger.io/docs/specification/data-models/data-types/
325 // TODO: Java 21 use pattern matching for switch
326 if (keyType instanceof Int8TypeDefinition) {
329 if (keyType instanceof Int16TypeDefinition) {
332 if (keyType instanceof Int32TypeDefinition) {
335 if (keyType instanceof Int64TypeDefinition) {
338 if (keyType instanceof Uint8TypeDefinition) {
341 if (keyType instanceof Uint16TypeDefinition) {
344 if (keyType instanceof Uint32TypeDefinition) {
347 if (keyType instanceof Uint64TypeDefinition) {
350 if (keyType instanceof DecimalTypeDefinition) {
353 if (keyType instanceof BooleanTypeDefinition) {