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.ByteArrayInputStream;
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.InputStreamReader;
19 import java.io.Reader;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayDeque;
22 import java.util.ArrayList;
23 import java.util.Deque;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
29 import org.opendaylight.restconf.openapi.model.DeleteEntity;
30 import org.opendaylight.restconf.openapi.model.GetEntity;
31 import org.opendaylight.restconf.openapi.model.ParameterEntity;
32 import org.opendaylight.restconf.openapi.model.ParameterSchemaEntity;
33 import org.opendaylight.restconf.openapi.model.PatchEntity;
34 import org.opendaylight.restconf.openapi.model.PathEntity;
35 import org.opendaylight.restconf.openapi.model.PostEntity;
36 import org.opendaylight.restconf.openapi.model.PutEntity;
37 import org.opendaylight.yangtools.yang.common.QName;
38 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
39 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
41 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
43 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.Module;
46 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
49 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
52 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
53 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
54 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
55 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
56 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
58 public final class PathsStream extends InputStream {
59 private final Iterator<? extends Module> iterator;
60 private final JsonGenerator generator;
61 private final OpenApiBodyWriter writer;
62 private final ByteArrayOutputStream stream;
63 private final EffectiveModelContext schemaContext;
64 private final String deviceName;
65 private final String urlPrefix;
66 private final boolean isForSingleModule;
67 private final boolean includeDataStore;
69 private static final String OPERATIONS = "/rests/operations";
70 private static final String DATA = "/rests/data";
72 private boolean hasRootPostLink;
73 private boolean hasAddedDataStore;
75 private Reader reader;
78 public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
79 final JsonGenerator generator, final ByteArrayOutputStream stream, final String deviceName,
80 final String urlPrefix, final boolean isForSingleModule, final boolean includeDataStore,
81 final Iterator<? extends Module> iterator) {
82 this.iterator = iterator;
83 this.generator = generator;
86 this.schemaContext = schemaContext;
87 this.isForSingleModule = isForSingleModule;
88 this.deviceName = deviceName;
89 this.urlPrefix = urlPrefix;
90 this.includeDataStore = includeDataStore;
91 hasRootPostLink = false;
92 hasAddedDataStore = false;
96 public int read() throws IOException {
100 if (reader == null) {
101 generator.writeObjectFieldStart("paths");
103 reader = new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8);
107 var read = reader.read();
109 if (iterator.hasNext()) {
110 reader = new InputStreamReader(new PathStream(toPaths(iterator.next()), writer),
111 StandardCharsets.UTF_8);
112 read = reader.read();
115 generator.writeEndObject();
117 reader = new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8);
120 return reader.read();
127 public int read(final byte @NonNull [] array, final int off, final int len) throws IOException {
128 return super.read(array, off, len);
131 private Deque<PathEntity> toPaths(final Module module) {
132 final var result = new ArrayDeque<PathEntity>();
133 if (includeDataStore && !hasAddedDataStore) {
134 final var dataPath = DATA + urlPrefix;
135 result.add(new PathEntity(dataPath, null, null, null,
136 new GetEntity(null, deviceName, "data", null, null, false),
138 final var operationsPath = OPERATIONS + urlPrefix;
139 result.add(new PathEntity(operationsPath, null, null, null,
140 new GetEntity(null, deviceName, "operations", null, null, false),
142 hasAddedDataStore = true;
144 // RPC operations (via post) - RPCs have their own path
145 for (final var rpc : module.getRpcs()) {
146 // TODO connect path with payload
147 final var localName = rpc.getQName().getLocalName();
148 final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), localName, null);
149 final var resolvedPath = OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
150 final var entity = new PathEntity(resolvedPath, post, null, null, null, null);
153 for (final var node : module.getChildNodes()) {
154 final var moduleName = module.getName();
155 final boolean isConfig = node.isConfiguration();
156 final var nodeLocalName = node.getQName().getLocalName();
158 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
159 if (isConfig && !hasRootPostLink && isForSingleModule) {
160 final var resolvedPath = DATA + urlPrefix;
161 result.add(new PathEntity(resolvedPath, new PostEntity(node, deviceName, moduleName,
162 new ArrayList<>(), nodeLocalName, module), null, null, null, null));
163 hasRootPostLink = true;
166 final var pathParams = new ArrayList<ParameterEntity>();
167 final var localName = moduleName + ":" + nodeLocalName;
168 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
169 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
176 private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
177 final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
178 final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName) {
179 final var resourcePath = DATA + path;
180 final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
181 final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
182 if (firstChild != null && node instanceof ContainerSchemaNode) {
183 result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
184 fullName, firstChild, deviceName));
186 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
187 isConfig, fullName, deviceName));
189 final var childNodes = ((DataNodeContainer) node).getChildNodes();
190 if (node instanceof ActionNodeContainer actionContainer) {
191 final var actionParams = new ArrayList<>(pathParams);
192 actionContainer.getActions().forEach(actionDef -> {
193 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
194 node.getQName(), schemaContext);
195 final var childPath = OPERATIONS + resourceActionPath;
196 result.add(processRootAndActionPathEntity(actionDef, childPath, actionParams, moduleName,
197 refPath, deviceName));
200 for (final var childNode : childNodes) {
201 final var childParams = new ArrayList<>(pathParams);
202 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
203 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
204 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
205 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
206 final var newConfig = isConfig && childNode.isConfiguration();
207 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
208 schemaContext, deviceName);
213 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
214 return node.getChildNodes().stream()
215 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
216 .findFirst().orElse(null);
219 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
220 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
221 final boolean isConfig, final String fullName, final String deviceName) {
223 return new PathEntity(resourcePath, null,
224 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
225 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
226 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
227 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
229 return new PathEntity(resourcePath, null, null, null,
230 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
234 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
235 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
236 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
238 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
239 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
240 if (!((DataSchemaNode) childNode).isConfiguration()) {
241 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
243 return new PathEntity(resourcePath, post,
244 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
245 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
246 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
247 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
249 return new PathEntity(resourcePath, null, null, null,
250 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
254 private static PathEntity processRootAndActionPathEntity(final SchemaNode node, final String resourcePath,
255 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
256 final String deviceName) {
257 return new PathEntity(resourcePath,
258 new PostEntity(node, deviceName, moduleName, pathParams, refPath, null),
259 null, null, null, null);
262 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
263 final String localName) {
264 final var path = new StringBuilder();
265 path.append(localName);
266 final var parameters = pathParams.stream()
267 .map(ParameterEntity::name)
268 .collect(Collectors.toSet());
270 if (node instanceof ListSchemaNode listSchemaNode) {
272 var discriminator = 1;
273 for (final var listKey : listSchemaNode.getKeyDefinition()) {
274 final var keyName = listKey.getLocalName();
275 var paramName = keyName;
276 while (!parameters.add(paramName)) {
277 paramName = keyName + discriminator;
281 final var pathParamIdentifier = prefix + "{" + paramName + "}";
283 path.append(pathParamIdentifier);
285 final var description = listSchemaNode.findDataChildByName(listKey)
286 .flatMap(DataSchemaNode::getDescription).orElse(null);
288 pathParams.add(new ParameterEntity(paramName, "path", true,
289 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
292 return path.toString();
295 private static String getAllowedType(final ListSchemaNode list, final QName key) {
296 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
298 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
299 // see: https://swagger.io/docs/specification/data-models/data-types/
300 // TODO: Java 21 use pattern matching for switch
301 if (keyType instanceof Int8TypeDefinition) {
304 if (keyType instanceof Int16TypeDefinition) {
307 if (keyType instanceof Int32TypeDefinition) {
310 if (keyType instanceof Int64TypeDefinition) {
313 if (keyType instanceof Uint8TypeDefinition) {
316 if (keyType instanceof Uint16TypeDefinition) {
319 if (keyType instanceof Uint32TypeDefinition) {
322 if (keyType instanceof Uint64TypeDefinition) {
326 if (keyType instanceof DecimalTypeDefinition) {
330 if (keyType instanceof BooleanTypeDefinition) {