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.charset.StandardCharsets;
20 import java.util.ArrayDeque;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Deque;
24 import java.util.List;
25 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
28 import org.opendaylight.restconf.openapi.model.DeleteEntity;
29 import org.opendaylight.restconf.openapi.model.GetEntity;
30 import org.opendaylight.restconf.openapi.model.OpenApiEntity;
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.PathsEntity;
36 import org.opendaylight.restconf.openapi.model.PostEntity;
37 import org.opendaylight.restconf.openapi.model.PutEntity;
38 import org.opendaylight.yangtools.yang.common.QName;
39 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
40 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
42 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
44 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.Module;
47 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
49 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
52 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
53 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
54 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
55 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
56 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
59 public final class PathsStream extends InputStream {
60 private final Collection<? extends Module> modules;
61 private final OpenApiBodyWriter writer;
62 private final EffectiveModelContext schemaContext;
63 private final String deviceName;
64 private final String urlPrefix;
65 private final String basePath;
66 private final boolean isForSingleModule;
67 private final boolean includeDataStore;
69 private static final String OPERATIONS = "operations";
70 private static final String DATA = "data";
71 private boolean hasRootPostLink;
72 private boolean hasAddedDataStore;
73 private Reader reader;
75 public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
76 final String deviceName, final String urlPrefix, final boolean isForSingleModule,
77 final boolean includeDataStore, final Collection<? extends Module> modules, final String basePath) {
78 this.modules = modules;
80 this.schemaContext = schemaContext;
81 this.isForSingleModule = isForSingleModule;
82 this.deviceName = deviceName;
83 this.urlPrefix = urlPrefix;
84 this.includeDataStore = includeDataStore;
85 this.basePath = basePath;
86 hasRootPostLink = false;
87 hasAddedDataStore = false;
91 public int read() throws IOException {
93 reader = new BufferedReader(
94 new InputStreamReader(new ByteArrayInputStream(writeNextEntity(new PathsEntity(toPaths()))),
95 StandardCharsets.UTF_8));
101 public int read(final byte @NonNull [] array, final int off, final int len) throws IOException {
102 return super.read(array, off, len);
105 private byte[] writeNextEntity(final OpenApiEntity next) throws IOException {
106 writer.writeTo(next, null, null, null, null, null, null);
107 return writer.readFrom();
110 private Deque<PathEntity> toPaths() {
111 final var result = new ArrayDeque<PathEntity>();
112 for (final var module : modules) {
113 if (includeDataStore && !hasAddedDataStore) {
114 final var dataPath = basePath + DATA + urlPrefix;
115 result.add(new PathEntity(dataPath, null, null, null,
116 new GetEntity(null, deviceName, "data", null, null, false),
118 final var operationsPath = basePath + OPERATIONS + urlPrefix;
119 result.add(new PathEntity(operationsPath, null, null, null,
120 new GetEntity(null, deviceName, "operations", null, null, false),
122 hasAddedDataStore = true;
124 // RPC operations (via post) - RPCs have their own path
125 for (final var rpc : module.getRpcs()) {
126 final var localName = rpc.getQName().getLocalName();
127 final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), localName, null);
128 final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
129 final var entity = new PathEntity(resolvedPath, post, null, null, null, null);
132 for (final var node : module.getChildNodes()) {
133 final var moduleName = module.getName();
134 final boolean isConfig = node.isConfiguration();
135 final var nodeLocalName = node.getQName().getLocalName();
137 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
138 if (isConfig && !hasRootPostLink && isForSingleModule) {
139 final var resolvedPath = basePath + DATA + urlPrefix;
140 result.add(new PathEntity(resolvedPath, new PostEntity(node, deviceName, moduleName,
141 new ArrayList<>(), nodeLocalName, module), null, null, null, null));
142 hasRootPostLink = true;
145 final var pathParams = new ArrayList<ParameterEntity>();
146 final var localName = moduleName + ":" + nodeLocalName;
147 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
148 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
149 deviceName, basePath);
156 private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
157 final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
158 final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
159 final String basePath) {
160 final var resourcePath = basePath + DATA + path;
161 final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
162 final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
163 if (firstChild != null && node instanceof ContainerSchemaNode) {
164 result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
165 fullName, firstChild, deviceName));
167 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
168 isConfig, fullName, deviceName));
170 final var childNodes = ((DataNodeContainer) node).getChildNodes();
171 if (node instanceof ActionNodeContainer actionContainer) {
172 final var actionParams = new ArrayList<>(pathParams);
173 actionContainer.getActions().forEach(actionDef -> {
174 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
175 node.getQName(), schemaContext);
176 final var childPath = basePath + OPERATIONS + resourceActionPath;
177 result.add(processRootAndActionPathEntity(actionDef, childPath, actionParams, moduleName,
178 refPath, deviceName));
181 for (final var childNode : childNodes) {
182 final var childParams = new ArrayList<>(pathParams);
183 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
184 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
185 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
186 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
187 final var newConfig = isConfig && childNode.isConfiguration();
188 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
189 schemaContext, deviceName, basePath);
194 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
195 return node.getChildNodes().stream()
196 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
197 .findFirst().orElse(null);
200 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
201 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
202 final boolean isConfig, final String fullName, final String deviceName) {
204 return new PathEntity(resourcePath, null,
205 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
206 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
207 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
208 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
210 return new PathEntity(resourcePath, null, null, null,
211 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
215 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
216 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
217 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
219 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
220 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
221 if (!((DataSchemaNode) childNode).isConfiguration()) {
222 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
224 return new PathEntity(resourcePath, post,
225 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
226 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
227 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
228 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
230 return new PathEntity(resourcePath, null, null, null,
231 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
235 private static PathEntity processRootAndActionPathEntity(final SchemaNode node, final String resourcePath,
236 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
237 final String deviceName) {
238 return new PathEntity(resourcePath,
239 new PostEntity(node, deviceName, moduleName, pathParams, refPath, null),
240 null, null, null, null);
243 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
244 final String localName) {
245 final var path = new StringBuilder();
246 path.append(localName);
247 final var parameters = pathParams.stream()
248 .map(ParameterEntity::name)
249 .collect(Collectors.toSet());
251 if (node instanceof ListSchemaNode listSchemaNode) {
253 var discriminator = 1;
254 for (final var listKey : listSchemaNode.getKeyDefinition()) {
255 final var keyName = listKey.getLocalName();
256 var paramName = keyName;
257 while (!parameters.add(paramName)) {
258 paramName = keyName + discriminator;
262 final var pathParamIdentifier = prefix + "{" + paramName + "}";
264 path.append(pathParamIdentifier);
266 final var description = listSchemaNode.findDataChildByName(listKey)
267 .flatMap(DataSchemaNode::getDescription).orElse(null);
269 pathParams.add(new ParameterEntity(paramName, "path", true,
270 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
273 return path.toString();
276 private static String getAllowedType(final ListSchemaNode list, final QName key) {
277 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
279 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
280 // see: https://swagger.io/docs/specification/data-models/data-types/
281 // TODO: Java 21 use pattern matching for switch
282 if (keyType instanceof Int8TypeDefinition) {
285 if (keyType instanceof Int16TypeDefinition) {
288 if (keyType instanceof Int32TypeDefinition) {
291 if (keyType instanceof Int64TypeDefinition) {
294 if (keyType instanceof Uint8TypeDefinition) {
297 if (keyType instanceof Uint16TypeDefinition) {
300 if (keyType instanceof Uint32TypeDefinition) {
303 if (keyType instanceof Uint64TypeDefinition) {
307 if (keyType instanceof DecimalTypeDefinition) {
311 if (keyType instanceof BooleanTypeDefinition) {