60f49e6a59aa84f36703d43a27b435bb76b32e5b
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / impl / SchemasStream.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 java.util.Objects.requireNonNullElse;
11 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.widthList;
12
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;
45
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";
50
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;
58
59     private Reader reader;
60     private ReadableByteChannel channel;
61     private boolean eof;
62
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;
68         this.writer = writer;
69         this.isForSingleModule = isForSingleModule;
70         this.stream = stream;
71         this.generator = generator;
72         this.width = requireNonNullElse(width, 0);
73     }
74
75     @Override
76     public int read() throws IOException {
77         if (eof) {
78             return -1;
79         }
80         if (reader == null) {
81             generator.writeObjectFieldStart("schemas");
82             generator.flush();
83             reader = new BufferedReader(
84                 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
85             stream.reset();
86         }
87         var read = reader.read();
88         while (read == -1) {
89             if (iterator.hasNext()) {
90                 reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
91                     writeNextEntity(new SchemasEntity(toComponents(iterator.next(), modelContext, isForSingleModule,
92                             width)))),
93                         StandardCharsets.UTF_8));
94                 read = reader.read();
95                 continue;
96             }
97             generator.writeEndObject();
98             generator.flush();
99             reader = new BufferedReader(
100                 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
101             stream.reset();
102             eof = true;
103             return reader.read();
104         }
105         return read;
106     }
107
108     @Override
109     public int read(final byte[] array, final int off, final int len) throws IOException {
110         if (eof) {
111             return -1;
112         }
113         if (channel == null) {
114             generator.writeObjectFieldStart("schemas");
115             generator.flush();
116             channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
117             stream.reset();
118         }
119         var read = channel.read(ByteBuffer.wrap(array, off, len));
120         while (read == -1) {
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));
125                 continue;
126             }
127             generator.writeEndObject();
128             generator.flush();
129             channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
130             stream.reset();
131             eof = true;
132             return channel.read(ByteBuffer.wrap(array, off, len));
133         }
134         return read;
135     }
136
137     private byte[] writeNextEntity(final OpenApiEntity entity) throws IOException {
138         writer.writeTo(entity, null, null, null, null, null, null);
139         return writer.readFrom();
140     }
141
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");
150         }
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);
159                 result.add(input);
160                 stack.enterSchemaTree(rpcInput.getQName());
161                 for (final var child : rpcInput.getChildNodes()) {
162                     if (!children.contains(child)) {
163                         children.add(child);
164                         processDataAndActionNodes(child, moduleName, stack, definitionNames, result, moduleName,
165                             false, width);
166                     }
167                 }
168                 stack.exit();
169             }
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);
174                 result.add(output);
175                 stack.enterSchemaTree(rpcOutput.getQName());
176                 for (final var child : rpcOutput.getChildNodes()) {
177                     if (!children.contains(child)) {
178                         children.add(child);
179                         processDataAndActionNodes(child, moduleName, stack, definitionNames, result, moduleName,
180                             false, width);
181                     }
182                 }
183                 stack.exit();
184             }
185             stack.exit();
186         }
187
188         final var childNodes = widthList(module, width);
189         for (final var childNode : childNodes) {
190             processDataAndActionNodes(childNode, moduleName, stack, definitionNames, result, moduleName,
191                 true, width);
192         }
193         return result;
194     }
195
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,
199             final int width) {
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
204                 return;
205             }
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;
210             result.add(child);
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,
216                     width);
217             }
218             stack.exit();
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);
230             }
231             stack.exit(); // Exit the CaseSchemaNode context
232             stack.exit(); // Exit the ChoiceSchemaNode context
233         }
234     }
235
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,
238             final int width) {
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);
246                 result.add(input);
247             }
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);
252                 result.add(output);
253             }
254             stack.exit();
255         }
256     }
257 }