Fix wrong path reference - schema for actions
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / impl / PathsStream.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.openapi.impl;
9
10 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
11 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolvePathArgumentsName;
12
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;
58
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;
68
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;
74
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;
79         this.writer = writer;
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;
88     }
89
90     @Override
91     public int read() throws IOException {
92         if (reader == null) {
93             reader = new BufferedReader(
94                 new InputStreamReader(new ByteArrayInputStream(writeNextEntity(new PathsEntity(toPaths()))),
95                     StandardCharsets.UTF_8));
96         }
97         return reader.read();
98     }
99
100     @Override
101     public int read(final byte @NonNull [] array, final int off, final int len) throws IOException {
102         return super.read(array, off, len);
103     }
104
105     private byte[] writeNextEntity(final OpenApiEntity next) throws IOException {
106         writer.writeTo(next, null, null, null, null, null, null);
107         return writer.readFrom();
108     }
109
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),
117                     null));
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),
121                     null));
122                 hasAddedDataStore = true;
123             }
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);
130                 result.add(entity);
131             }
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();
136
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;
143                     }
144                     //process first node
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, node);
150                 }
151             }
152         }
153         return result;
154     }
155
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, final SchemaNode parentNode) {
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));
166         } else {
167             result.add(processDataPathEntity(node, resourcePath, pathParams, moduleName, refPath,
168                 isConfig, fullName, deviceName));
169         }
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, parentNode));
179             });
180         }
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, node);
190             }
191         }
192     }
193
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);
198     }
199
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) {
203         if (isConfig) {
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));
209         } else {
210             return new PathEntity(resourcePath, null, null, null,
211                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
212         }
213     }
214
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) {
218         if (isConfig) {
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);
223             }
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));
229         } else {
230             return new PathEntity(resourcePath, null, null, null,
231                 new GetEntity(node, deviceName, moduleName, pathParams, refPath, false), null);
232         }
233     }
234
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, final SchemaNode parentNode) {
238         return new PathEntity(resourcePath,
239             new PostEntity(node, deviceName, moduleName, pathParams, refPath, parentNode),
240             null, null, null, null);
241     }
242
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());
250
251         if (node instanceof ListSchemaNode listSchemaNode) {
252             var prefix = "=";
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;
259                     discriminator++;
260                 }
261
262                 final var pathParamIdentifier = prefix + "{" + paramName + "}";
263                 prefix = ",";
264                 path.append(pathParamIdentifier);
265
266                 final var description = listSchemaNode.findDataChildByName(listKey)
267                     .flatMap(DataSchemaNode::getDescription).orElse(null);
268
269                 pathParams.add(new ParameterEntity(paramName, "path", true,
270                     new ParameterSchemaEntity(getAllowedType(listSchemaNode, listKey), null), description));
271             }
272         }
273         return path.toString();
274     }
275
276     private static String getAllowedType(final ListSchemaNode list, final QName key) {
277         final var keyType = ((LeafSchemaNode) list.getDataChildByName(key)).getType();
278
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) {
283             return "integer";
284         }
285         if (keyType instanceof Int16TypeDefinition) {
286             return "integer";
287         }
288         if (keyType instanceof Int32TypeDefinition) {
289             return "integer";
290         }
291         if (keyType instanceof Int64TypeDefinition) {
292             return "integer";
293         }
294         if (keyType instanceof Uint8TypeDefinition) {
295             return "integer";
296         }
297         if (keyType instanceof Uint16TypeDefinition) {
298             return "integer";
299         }
300         if (keyType instanceof Uint32TypeDefinition) {
301             return "integer";
302         }
303         if (keyType instanceof Uint64TypeDefinition) {
304             return "integer";
305         }
306
307         if (keyType instanceof DecimalTypeDefinition) {
308             return "number";
309         }
310
311         if (keyType instanceof BooleanTypeDefinition) {
312             return "boolean";
313         }
314
315         return "string";
316     }
317 }