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 java.util.Objects.requireNonNullElse;
11 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.widthList;
13 import com.fasterxml.jackson.core.JsonGenerator;
14 import java.io.BufferedReader;
15 import java.io.ByteArrayInputStream;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InputStreamReader;
20 import java.io.Reader;
21 import java.nio.ByteBuffer;
22 import java.nio.channels.Channels;
23 import java.nio.channels.ReadableByteChannel;
24 import java.nio.charset.StandardCharsets;
25 import java.util.ArrayDeque;
26 import java.util.ArrayList;
27 import java.util.Deque;
28 import java.util.Iterator;
29 import java.util.List;
30 import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
31 import org.opendaylight.restconf.openapi.model.NodeSchemaEntity;
32 import org.opendaylight.restconf.openapi.model.OpenApiEntity;
33 import org.opendaylight.restconf.openapi.model.RpcSchemaEntity;
34 import org.opendaylight.restconf.openapi.model.SchemaEntity;
35 import org.opendaylight.restconf.openapi.model.SchemasEntity;
36 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
37 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
40 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
42 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.Module;
44 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
46 public final class SchemasStream extends InputStream {
47 private static final String OBJECT_TYPE = "object";
48 private static final String INPUT_SUFFIX = "_input";
49 private static final String OUTPUT_SUFFIX = "_output";
51 private final Iterator<? extends Module> iterator;
52 private final OpenApiBodyWriter writer;
53 private final EffectiveModelContext modelContext;
54 private final boolean isForSingleModule;
55 private final ByteArrayOutputStream stream;
56 private final JsonGenerator generator;
57 private final Integer width;
59 private Reader reader;
60 private ReadableByteChannel channel;
63 public SchemasStream(final EffectiveModelContext modelContext, final OpenApiBodyWriter writer,
64 final Iterator<? extends Module> iterator, final boolean isForSingleModule,
65 final ByteArrayOutputStream stream, final JsonGenerator generator, final Integer width) {
66 this.iterator = iterator;
67 this.modelContext = modelContext;
69 this.isForSingleModule = isForSingleModule;
71 this.generator = generator;
72 this.width = requireNonNullElse(width, 0);
76 public int read() throws IOException {
81 generator.writeObjectFieldStart("schemas");
83 reader = new BufferedReader(
84 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
87 var read = reader.read();
89 if (iterator.hasNext()) {
90 reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
91 writeNextEntity(new SchemasEntity(toComponents(iterator.next(), modelContext, isForSingleModule,
93 StandardCharsets.UTF_8));
97 generator.writeEndObject();
99 reader = new BufferedReader(
100 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
103 return reader.read();
109 public int read(final byte[] array, final int off, final int len) throws IOException {
113 if (channel == null) {
114 generator.writeObjectFieldStart("schemas");
116 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
119 var read = channel.read(ByteBuffer.wrap(array, off, len));
121 if (iterator.hasNext()) {
122 channel = Channels.newChannel(new ByteArrayInputStream(writeNextEntity(
123 new SchemasEntity(toComponents(iterator.next(), modelContext, isForSingleModule, width)))));
124 read = channel.read(ByteBuffer.wrap(array, off, len));
127 generator.writeEndObject();
129 channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
132 return channel.read(ByteBuffer.wrap(array, off, len));
137 private byte[] writeNextEntity(final OpenApiEntity entity) throws IOException {
138 writer.writeTo(entity, null, null, null, null, null, null);
139 return writer.readFrom();
142 private static Deque<SchemaEntity> toComponents(final Module module, final EffectiveModelContext modelContext,
143 final boolean isForSingleModule, final int width) {
144 final var result = new ArrayDeque<SchemaEntity>();
145 final var definitionNames = new DefinitionNames();
146 final var stack = SchemaInferenceStack.of(modelContext);
147 final var moduleName = module.getName();
148 if (isForSingleModule) {
149 definitionNames.addUnlinkedName(moduleName + "_module");
151 final var children = new ArrayList<DataSchemaNode>();
152 for (final var rpc : module.getRpcs()) {
153 stack.enterSchemaTree(rpc.getQName());
154 final var rpcName = rpc.getQName().getLocalName();
155 final var rpcInput = rpc.getInput();
156 if (!rpcInput.getChildNodes().isEmpty()) {
157 final var input = new RpcSchemaEntity(rpcInput, moduleName + "_" + rpcName + INPUT_SUFFIX, null,
158 OBJECT_TYPE, stack, moduleName, false, definitionNames, width);
160 stack.enterSchemaTree(rpcInput.getQName());
161 for (final var child : rpcInput.getChildNodes()) {
162 if (!children.contains(child)) {
164 processDataAndActionNodes(child, moduleName, stack, definitionNames, result, moduleName,
170 final var rpcOutput = rpc.getOutput();
171 if (!rpcOutput.getChildNodes().isEmpty()) {
172 final var output = new RpcSchemaEntity(rpcOutput, moduleName + "_" + rpcName + OUTPUT_SUFFIX, null,
173 OBJECT_TYPE, stack, moduleName, false, definitionNames, width);
175 stack.enterSchemaTree(rpcOutput.getQName());
176 for (final var child : rpcOutput.getChildNodes()) {
177 if (!children.contains(child)) {
179 processDataAndActionNodes(child, moduleName, stack, definitionNames, result, moduleName,
188 final var childNodes = widthList(module, width);
189 for (final var childNode : childNodes) {
190 processDataAndActionNodes(childNode, moduleName, stack, definitionNames, result, moduleName,
196 private static void processDataAndActionNodes(final DataSchemaNode node, final String title,
197 final SchemaInferenceStack stack, final DefinitionNames definitionNames,
198 final ArrayDeque<SchemaEntity> result, final String parentName, final boolean isParentConfig,
200 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
201 final var newTitle = title + "_" + node.getQName().getLocalName();
202 if (definitionNames.isListedNode(node, newTitle)) {
203 // This means schema for this node is already processed
206 final var discriminator = definitionNames.pickDiscriminator(node, List.of(newTitle));
207 final var child = new NodeSchemaEntity(node, newTitle, discriminator, OBJECT_TYPE, stack, parentName,
208 isParentConfig, definitionNames, width);
209 final var isConfig = node.isConfiguration() && isParentConfig;
211 stack.enterSchemaTree(node.getQName());
212 processActions(node, newTitle, stack, definitionNames, result, parentName, width);
213 final var childNodes = widthList((DataNodeContainer) node, width);
214 for (final var childNode : childNodes) {
215 processDataAndActionNodes(childNode, newTitle, stack, definitionNames, result, newTitle, isConfig,
219 } else if (node instanceof ChoiceSchemaNode choiceNode && !choiceNode.getCases().isEmpty()) {
220 // Process default case or first case
221 final var caseNode = choiceNode.getDefaultCase()
222 .orElseGet(() -> choiceNode.getCases().stream().findFirst()
223 .orElseThrow(() -> new IllegalStateException("No cases found in ChoiceSchemaNode")));
224 stack.enterSchemaTree(choiceNode.getQName());
225 stack.enterSchemaTree(caseNode.getQName());
226 final var childNodes = widthList(caseNode, width);
227 for (final var childNode : childNodes) {
228 processDataAndActionNodes(childNode, title, stack, definitionNames, result, parentName,
229 isParentConfig, width);
231 stack.exit(); // Exit the CaseSchemaNode context
232 stack.exit(); // Exit the ChoiceSchemaNode context
236 private static void processActions(final DataSchemaNode node, final String title, final SchemaInferenceStack stack,
237 final DefinitionNames definitionNames, final ArrayDeque<SchemaEntity> result, final String parentName,
239 for (final var actionDef : ((ActionNodeContainer) node).getActions()) {
240 stack.enterSchemaTree(actionDef.getQName());
241 final var actionName = actionDef.getQName().getLocalName();
242 final var actionInput = actionDef.getInput();
243 if (!actionInput.getChildNodes().isEmpty()) {
244 final var input = new RpcSchemaEntity(actionInput, title + "_" + actionName + INPUT_SUFFIX, null,
245 OBJECT_TYPE, stack, parentName, false, definitionNames, width);
248 final var actionOutput = actionDef.getOutput();
249 if (!actionOutput.getChildNodes().isEmpty()) {
250 final var output = new RpcSchemaEntity(actionOutput, title + "_" + actionName + OUTPUT_SUFFIX, null,
251 OBJECT_TYPE, stack, parentName, false, definitionNames, width);