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.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
27 import org.opendaylight.restconf.openapi.model.DeleteEntity;
28 import org.opendaylight.restconf.openapi.model.GetEntity;
29 import org.opendaylight.restconf.openapi.model.OpenApiEntity;
30 import org.opendaylight.restconf.openapi.model.ParameterEntity;
31 import org.opendaylight.restconf.openapi.model.ParameterSchemaEntity;
32 import org.opendaylight.restconf.openapi.model.PatchEntity;
33 import org.opendaylight.restconf.openapi.model.PathEntity;
34 import org.opendaylight.restconf.openapi.model.PathsEntity;
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 Collection<? extends Module> modules;
60 private final OpenApiBodyWriter writer;
61 private final EffectiveModelContext schemaContext;
62 private final String deviceName;
63 private final String urlPrefix;
64 private final String basePath;
65 private final boolean isForSingleModule;
66 private final boolean includeDataStore;
68 private static final String OPERATIONS = "operations";
69 private static final String DATA = "data";
70 private boolean hasRootPostLink;
71 private boolean hasAddedDataStore;
72 private Reader reader;
74 public PathsStream(final EffectiveModelContext schemaContext, final OpenApiBodyWriter writer,
75 final String deviceName, final String urlPrefix, final boolean isForSingleModule,
76 final boolean includeDataStore, final Collection<? extends Module> modules, final String basePath) {
77 this.modules = modules;
79 this.schemaContext = schemaContext;
80 this.isForSingleModule = isForSingleModule;
81 this.deviceName = deviceName;
82 this.urlPrefix = urlPrefix;
83 this.includeDataStore = includeDataStore;
84 this.basePath = basePath;
85 hasRootPostLink = false;
86 hasAddedDataStore = false;
90 public int read() throws IOException {
92 reader = new BufferedReader(
93 new InputStreamReader(new ByteArrayInputStream(writeNextEntity(new PathsEntity(toPaths()))),
94 StandardCharsets.UTF_8));
100 public int read(final byte[] array, final int off, final int len) throws IOException {
101 return super.read(array, off, len);
104 private byte[] writeNextEntity(final OpenApiEntity next) throws IOException {
105 writer.writeTo(next, null, null, null, null, null, null);
106 return writer.readFrom();
109 private Deque<PathEntity> toPaths() {
110 final var result = new ArrayDeque<PathEntity>();
111 for (final var module : modules) {
112 if (includeDataStore && !hasAddedDataStore) {
113 final var dataPath = basePath + DATA + urlPrefix;
114 result.add(new PathEntity(dataPath, null, null, null,
115 new GetEntity(null, deviceName, "data", null, null, false),
117 final var operationsPath = basePath + OPERATIONS + urlPrefix;
118 result.add(new PathEntity(operationsPath, null, null, null,
119 new GetEntity(null, deviceName, "operations", null, null, false),
121 hasAddedDataStore = true;
123 // RPC operations (via post) - RPCs have their own path
124 for (final var rpc : module.getRpcs()) {
125 final var localName = rpc.getQName().getLocalName();
126 final var post = new PostEntity(rpc, deviceName, module.getName(), new ArrayList<>(), localName, null);
127 final var resolvedPath = basePath + OPERATIONS + urlPrefix + "/" + module.getName() + ":" + localName;
128 final var entity = new PathEntity(resolvedPath, post, null, null, null, null);
131 for (final var node : module.getChildNodes()) {
132 final var moduleName = module.getName();
133 final boolean isConfig = node.isConfiguration();
134 final var nodeLocalName = node.getQName().getLocalName();
136 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
137 if (isConfig && !hasRootPostLink && isForSingleModule) {
138 final var resolvedPath = basePath + DATA + urlPrefix;
139 result.add(new PathEntity(resolvedPath, new PostEntity(node, deviceName, moduleName,
140 new ArrayList<>(), nodeLocalName, module), null, null, null, null));
141 hasRootPostLink = true;
144 final var pathParams = new ArrayList<ParameterEntity>();
145 final var localName = moduleName + ":" + nodeLocalName;
146 final var path = urlPrefix + "/" + processPath(node, pathParams, localName);
147 processChildNode(node, pathParams, moduleName, result, path, nodeLocalName, isConfig, schemaContext,
148 deviceName, basePath, node);
155 private static void processChildNode(final DataSchemaNode node, final List<ParameterEntity> pathParams,
156 final String moduleName, final Deque<PathEntity> result, final String path, final String refPath,
157 final boolean isConfig, final EffectiveModelContext schemaContext, final String deviceName,
158 final String basePath, final SchemaNode parentNode) {
159 final var resourcePath = basePath + DATA + path;
160 final var fullName = resolveFullNameFromNode(node.getQName(), schemaContext);
161 final var firstChild = getListOrContainerChildNode((DataNodeContainer) node);
162 if (firstChild != null && node instanceof ContainerSchemaNode) {
163 result.add(processTopPathEntity(node, resourcePath, pathParams, moduleName, refPath, isConfig,
164 fullName, firstChild, deviceName));
166 result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
167 isConfig, fullName, deviceName));
169 final var childNodes = ((DataNodeContainer) node).getChildNodes();
170 if (node instanceof ActionNodeContainer actionContainer) {
171 final var actionParams = new ArrayList<>(pathParams);
172 actionContainer.getActions().forEach(actionDef -> {
173 final var resourceActionPath = path + "/" + resolvePathArgumentsName(actionDef.getQName(),
174 node.getQName(), schemaContext);
175 final var childPath = basePath + OPERATIONS + resourceActionPath;
176 result.add(processRootAndActionPathEntity(actionDef, childPath, actionParams, moduleName,
177 refPath, deviceName, parentNode));
180 for (final var childNode : childNodes) {
181 final var childParams = new ArrayList<>(pathParams);
182 final var newRefPath = refPath + "_" + childNode.getQName().getLocalName();
183 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
184 final var localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
185 final var resourceDataPath = path + "/" + processPath(childNode, childParams, localName);
186 final var newConfig = isConfig && childNode.isConfiguration();
187 processChildNode(childNode, childParams, moduleName, result, resourceDataPath, newRefPath, newConfig,
188 schemaContext, deviceName, basePath, node);
193 private static <T extends DataNodeContainer> DataSchemaNode getListOrContainerChildNode(final T node) {
194 return node.getChildNodes().stream()
195 .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
196 .findFirst().orElse(null);
199 private static PathEntity processDataPathEntity(final SchemaNode node, final String resourcePath,
200 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
201 final boolean isConfig, final String fullName, final String deviceName) {
203 return new PathEntity(resourcePath, null,
204 new PatchEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
205 new PutEntity(node, deviceName, moduleName, pathParams, refPath, fullName),
206 new GetEntity(node, deviceName, moduleName, pathParams, refPath, true),
207 new DeleteEntity(node, deviceName, moduleName, pathParams, refPath));
209 return new PathEntity(resourcePath, null, null, null,
210 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
214 private static PathEntity processTopPathEntity(final SchemaNode node, final String resourcePath,
215 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
216 final boolean isConfig, final String fullName, final SchemaNode childNode, final String deviceName) {
218 final var childNodeRefPath = refPath + "_" + childNode.getQName().getLocalName();
219 var post = new PostEntity(childNode, deviceName, moduleName, pathParams, childNodeRefPath, node);
220 if (!((DataSchemaNode) childNode).isConfiguration()) {
221 post = new PostEntity(node, deviceName, moduleName, pathParams, refPath, null);
223 return new PathEntity(resourcePath, post,
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 processRootAndActionPathEntity(final SchemaNode node, final String resourcePath,
235 final List<ParameterEntity> pathParams, final String moduleName, final String refPath,
236 final String deviceName, final SchemaNode parentNode) {
237 return new PathEntity(resourcePath,
238 new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode),
239 null, null, null, null);
242 private static String processPath(final DataSchemaNode node, final List<ParameterEntity> pathParams,
243 final String localName) {
244 final var path = new StringBuilder();
245 path.append(localName);
246 final var parameters = pathParams.stream()
247 .map(ParameterEntity::name)
248 .collect(Collectors.toSet());
250 if (node instanceof ListSchemaNode listSchemaNode) {
252 var discriminator = 1;
253 for (final var listKey : listSchemaNode.getKeyDefinition()) {
254 final var keyName = listKey.getLocalName();
255 var paramName = keyName;
256 while (!parameters.add(paramName)) {
257 paramName = keyName + discriminator;
261 final var pathParamIdentifier = prefix + "{" + paramName + "}";
263 path.append(pathParamIdentifier);
265 final var description = listSchemaNode.findDataChildByName(listKey)
266 .flatMap(DataSchemaNode::getDescription).orElse(null);
268 pathParams.add(new ParameterEntity(paramName, "path", true,
269 new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
272 return path.toString();
275 private static String getAllowedType(final ListSchemaNode list, final QName key) {
276 final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
278 // see: https://datatracker.ietf.org/doc/html/rfc7950#section-4.2.4
279 // see: https://swagger.io/docs/specification/data-models/data-types/
280 // TODO: Java 21 use pattern matching for switch
281 if (keyType instanceof Int8TypeDefinition) {
284 if (keyType instanceof Int16TypeDefinition) {
287 if (keyType instanceof Int32TypeDefinition) {
290 if (keyType instanceof Int64TypeDefinition) {
293 if (keyType instanceof Uint8TypeDefinition) {
296 if (keyType instanceof Uint16TypeDefinition) {
299 if (keyType instanceof Uint32TypeDefinition) {
302 if (keyType instanceof Uint64TypeDefinition) {
306 if (keyType instanceof DecimalTypeDefinition) {
310 if (keyType instanceof BooleanTypeDefinition) {