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 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.ByteBuffer;
20 import java.nio.channels.Channels;
21 import java.nio.channels.ReadableByteChannel;
22 import java.nio.charset.StandardCharsets;
23 import java.util.ArrayDeque;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Deque;
27 import java.util.List;
28 import java.util.stream.Collectors;
29 import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
30 import org.opendaylight.restconf.openapi.model.DeleteEntity;
31 import org.opendaylight.restconf.openapi.model.GetEntity;
32 import org.opendaylight.restconf.openapi.model.OpenApiEntity;
33 import org.opendaylight.restconf.openapi.model.ParameterEntity;
34 import org.opendaylight.restconf.openapi.model.ParameterSchemaEntity;
35 import org.opendaylight.restconf.openapi.model.PatchEntity;
36 import org.opendaylight.restconf.openapi.model.PathEntity;
37 import org.opendaylight.restconf.openapi.model.PathsEntity;
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 Collection<? extends Module> modules;
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;
71 private static final String OPERATIONS = "operations";
72 private static final String DATA = "data";
73 private boolean hasRootPostLink;
74 private boolean hasAddedDataStore;
75 private Reader reader;
76 private ReadableByteChannel channel;
78 public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
79 final String deviceName, final String urlPrefix, final boolean isForSingleModule,
80 final boolean includeDataStore, final Collection<? extends Module> modules, final String basePath) {
81 this.modules = modules;
83 this.schemaContext = schemaContext;
84 this.isForSingleModule = isForSingleModule;
85 this.deviceName = deviceName;
86 this.urlPrefix = urlPrefix;
87 this.includeDataStore = includeDataStore;
88 this.basePath = basePath;
89 hasRootPostLink = false;
90 hasAddedDataStore = false;
94 public int read() throws IOException {
96 reader = new BufferedReader(
97 new InputStreamReader(new ByteArrayInputStream(writeNextEntity(new PathsEntity(toPaths()))),
98 StandardCharsets.UTF_8));
100 return reader.read();
104 public int read(final byte[] array, final int off, final int len) throws IOException {
105 if (channel == null) {
106 channel = Channels.newChannel(new ByteArrayInputStream(writeNextEntity(new PathsEntity(toPaths()))));
108 return channel.read(ByteBuffer.wrap(array, off, len));
111 private byte[] writeNextEntity(final OpenApiEntity next) throws IOException {
112 writer.writeTo(next, null, null, null, null, null, null);
113 return writer.readFrom();
116 private Deque<PathEntity> toPaths() {
117 final var result = new ArrayDeque<PathEntity>();
118 for (final var module : modules) {
119 if (includeDataStore && !hasAddedDataStore) {
120 final var dataPath = basePath + DATA + urlPrefix;
121 result.add(new PathEntity(dataPath, null, null, null,
122 new GetEntity(null, deviceName, "data", null, null, false),
124 final var operationsPath = basePath + OPERATIONS + urlPrefix;
125 result.add(new PathEntity(operationsPath, null, null, null,
126 new GetEntity(null, deviceName, "operations", null, null, false),
128 hasAddedDataStore = true;
130 // RPC operations (via post) - RPCs have their own path
131 for (final var rpc : module.getRpcs()) {
132 final var localName = rpc.getQName().getLocalName();
133 final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), localName, null);
134 final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
135 final var entity = new PathEntity(resolvedPath, post, null, null, null, null);
138 for (final var node : module.getChildNodes()) {
139 final var moduleName = module.getName();
140 final boolean isConfig = node.isConfiguration();
141 final var nodeLocalName = node.getQName().getLocalName();
143 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
144 if (isConfig && !hasRootPostLink && isForSingleModule) {
145 final var resolvedPath = basePath + DATA + urlPrefix;
146 result.add(new PathEntity(resolvedPath, new PostEntity(node, deviceName, moduleName,
147 new ArrayList<>(), nodeLocalName, module), null, null, null, null));
148 hasRootPostLink = true;
151 final var pathParams = new ArrayList<ParameterEntity>();
152 final var localName = moduleName + ":" + nodeLocalName;
153 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
154 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
155 deviceName, basePath, node);
162 private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
163 final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
164 final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
165 final String basePath, final SchemaNode parentNode) {
166 final var resourcePath = basePath + DATA + path;
167 final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
168 final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
169 if (firstChild != null && node instanceof ContainerSchemaNode) {
170 result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
171 fullName, firstChild, deviceName));
173 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
174 isConfig, fullName, deviceName));
176 final var childNodes = ((DataNodeContainer) node).getChildNodes();
177 if (node instanceof ActionNodeContainer actionContainer) {
178 final var actionParams = new ArrayList<>(pathParams);
179 actionContainer.getActions().forEach(actionDef -> {
180 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
181 node.getQName(), schemaContext);
182 final var childPath = basePath + OPERATIONS + resourceActionPath;
183 result.add(processRootAndActionPathEntity(actionDef, childPath, actionParams, moduleName,
184 refPath, deviceName, parentNode));
187 for (final var childNode : childNodes) {
188 final var childParams = new ArrayList<>(pathParams);
189 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
190 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
191 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
192 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
193 final var newConfig = isConfig && childNode.isConfiguration();
194 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
195 schemaContext, deviceName, basePath, node);
200 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
201 return node.getChildNodes().stream()
202 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
203 .findFirst().orElse(null);
206 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
207 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
208 final boolean isConfig, final String fullName, final String deviceName) {
210 return new PathEntity(resourcePath, null,
211 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
212 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
213 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
214 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
216 return new PathEntity(resourcePath, null, null, null,
217 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
221 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
222 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
223 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
225 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
226 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
227 if (!((DataSchemaNode) childNode).isConfiguration()) {
228 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
230 return new PathEntity(resourcePath, post,
231 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
232 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
233 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
234 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
236 return new PathEntity(resourcePath, null, null, null,
237 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
241 private static PathEntity processRootAndActionPathEntity(final SchemaNode node, final String resourcePath,
242 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
243 final String deviceName, final SchemaNode parentNode) {
244 return new PathEntity(resourcePath,
245 new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode),
246 null, null, null, null);
249 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
250 final String localName) {
251 final var path = new StringBuilder();
252 path.append(localName);
253 final var parameters = pathParams.stream()
254 .map(ParameterEntity::name)
255 .collect(Collectors.toSet());
257 if (node instanceof ListSchemaNode listSchemaNode) {
259 var discriminator = 1;
260 for (final var listKey : listSchemaNode.getKeyDefinition()) {
261 final var keyName = listKey.getLocalName();
262 var paramName = keyName;
263 while (!parameters.add(paramName)) {
264 paramName = keyName + discriminator;
268 final var pathParamIdentifier = prefix + "{" + paramName + "}";
270 path.append(pathParamIdentifier);
272 final var description = listSchemaNode.findDataChildByName(listKey)
273 .flatMap(DataSchemaNode::getDescription).orElse(null);
275 pathParams.add(new ParameterEntity(paramName, "path", true,
276 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
279 return path.toString();
282 private static String getAllowedType(final ListSchemaNode list, final QName key) {
283 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
285 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
286 // see: https://swagger.io/docs/specification/data-models/data-types/
287 // TODO: Java 21 use pattern matching for switch
288 if (keyType instanceof Int8TypeDefinition) {
291 if (keyType instanceof Int16TypeDefinition) {
294 if (keyType instanceof Int32TypeDefinition) {
297 if (keyType instanceof Int64TypeDefinition) {
300 if (keyType instanceof Uint8TypeDefinition) {
303 if (keyType instanceof Uint16TypeDefinition) {
306 if (keyType instanceof Uint32TypeDefinition) {
309 if (keyType instanceof Uint64TypeDefinition) {
313 if (keyType instanceof DecimalTypeDefinition) {
317 if (keyType instanceof BooleanTypeDefinition) {