Use nio Channels in OpenAPI read
[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 com.fasterxml.jackson.core.JsonGenerator;
11 import java.io.BufferedReader;
12 import java.io.ByteArrayInputStream;
13 import java.io.ByteArrayOutputStream;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.InputStreamReader;
17 import java.io.Reader;
18 import java.nio.ByteBuffer;
19 import java.nio.channels.Channels;
20 import java.nio.channels.ReadableByteChannel;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Deque;
25 import java.util.Iterator;
26 import java.util.List;
27 import org.opendaylight.restconf.openapi.jaxrs.OpenApiBodyWriter;
28 import org.opendaylight.restconf.openapi.model.SchemaEntity;
29 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
30 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
33 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
35 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.Module;
37 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
38
39 public final class SchemasStream extends InputStream {
40     private static final String OBJECT_TYPE = "object";
41     private static final String INPUT_SUFFIX = "_input";
42     private static final String OUTPUT_SUFFIX = "_output";
43
44     private final Iterator<? extends Module> iterator;
45     private final OpenApiBodyWriter writer;
46     private final EffectiveModelContext context;
47     private final JsonGenerator generator;
48     private final ByteArrayOutputStream stream;
49     private final boolean isForSingleModule;
50
51     private Reader reader;
52     private ReadableByteChannel channel;
53     private boolean schemesWritten;
54
55     public SchemasStream(final EffectiveModelContext context, final OpenApiBodyWriter writer,
56             final JsonGenerator generator, final ByteArrayOutputStream stream,
57             final Iterator<? extends Module> iterator, final boolean isForSingleModule) {
58         this.iterator = iterator;
59         this.context = context;
60         this.writer = writer;
61         this.generator = generator;
62         this.stream = stream;
63         this.isForSingleModule = isForSingleModule;
64     }
65
66     @Override
67     public int read() throws IOException {
68         if (reader == null) {
69             generator.writeObjectFieldStart("schemas");
70             generator.flush();
71             reader = new BufferedReader(
72                 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
73             stream.reset();
74         }
75
76         var read = reader.read();
77         while (read == -1) {
78             if (iterator.hasNext()) {
79                 reader = new BufferedReader(
80                     new InputStreamReader(new SchemaStream(toComponents(iterator.next(), context, isForSingleModule),
81                         writer), StandardCharsets.UTF_8));
82                 read = reader.read();
83                 continue;
84             }
85             if (!schemesWritten) {
86                 generator.writeEndObject();
87                 schemesWritten = true;
88                 continue;
89             }
90             generator.flush();
91             reader = new BufferedReader(
92                 new InputStreamReader(new ByteArrayInputStream(stream.toByteArray()), StandardCharsets.UTF_8));
93             stream.reset();
94             return reader.read();
95         }
96
97         return read;
98     }
99
100     @Override
101     public int read(final byte[] array, final int off, final int len) throws IOException {
102         if (channel == null) {
103             generator.writeObjectFieldStart("schemas");
104             generator.flush();
105             channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
106             stream.reset();
107         }
108
109         var read = channel.read(ByteBuffer.wrap(array, off, len));
110         while (read == -1) {
111             if (iterator.hasNext()) {
112                 channel = Channels.newChannel(new SchemaStream(toComponents(iterator.next(), context,
113                     isForSingleModule), writer));
114                 read = channel.read(ByteBuffer.wrap(array, off, len));
115                 continue;
116             }
117             if (!schemesWritten) {
118                 generator.writeEndObject();
119                 schemesWritten = true;
120                 continue;
121             }
122             generator.flush();
123             channel = Channels.newChannel(new ByteArrayInputStream(stream.toByteArray()));
124             stream.reset();
125             return channel.read(ByteBuffer.wrap(array, off, len));
126         }
127
128         return read;
129     }
130
131     private static Deque<SchemaEntity> toComponents(final Module module, final EffectiveModelContext context,
132             final boolean isForSingleModule) {
133         final var result = new ArrayDeque<SchemaEntity>();
134         final var definitionNames = new DefinitionNames();
135         final var stack = SchemaInferenceStack.of(context);
136         final var moduleName = module.getName();
137         if (isForSingleModule) {
138             definitionNames.addUnlinkedName(moduleName + "_module");
139         }
140         final var children = new ArrayList<DataSchemaNode>();
141         for (final var rpc : module.getRpcs()) {
142             stack.enterSchemaTree(rpc.getQName());
143             final var rpcName = rpc.getQName().getLocalName();
144             final var rpcInput = rpc.getInput();
145             if (!rpcInput.getChildNodes().isEmpty()) {
146                 final var input = new SchemaEntity(rpcInput, moduleName + "_" + rpcName + INPUT_SUFFIX, null,
147                     OBJECT_TYPE, stack, moduleName, false, definitionNames, EntityType.RPC);
148                 result.add(input);
149                 stack.enterSchemaTree(rpcInput.getQName());
150                 for (final var child : rpcInput.getChildNodes()) {
151                     if (!children.contains(child)) {
152                         children.add(child);
153                         processDataAndActionNodes(child, moduleName, stack, definitionNames, result, moduleName, false);
154                     }
155                 }
156                 stack.exit();
157             }
158             final var rpcOutput = rpc.getOutput();
159             if (!rpcOutput.getChildNodes().isEmpty()) {
160                 final var output = new SchemaEntity(rpcOutput, moduleName + "_" + rpcName + OUTPUT_SUFFIX, null,
161                     OBJECT_TYPE, stack, moduleName, false, definitionNames, EntityType.RPC);
162                 result.add(output);
163                 stack.enterSchemaTree(rpcOutput.getQName());
164                 for (final var child : rpcOutput.getChildNodes()) {
165                     if (!children.contains(child)) {
166                         children.add(child);
167                         processDataAndActionNodes(child, moduleName, stack, definitionNames, result, moduleName, false);
168                     }
169                 }
170                 stack.exit();
171             }
172             stack.exit();
173         }
174
175         for (final var childNode : module.getChildNodes()) {
176             processDataAndActionNodes(childNode, moduleName, stack, definitionNames, result, moduleName,
177                 true);
178         }
179         return result;
180     }
181
182     private static void processDataAndActionNodes(final DataSchemaNode node, final String title,
183             final SchemaInferenceStack stack, final DefinitionNames definitionNames,
184             final ArrayDeque<SchemaEntity> result, final String parentName, final boolean isParentConfig) {
185         if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
186             final var newTitle = title + "_" + node.getQName().getLocalName();
187             final String discriminator;
188             if (!definitionNames.isListedNode(node)) {
189                 final var parentNameConfigLocalName = parentName + "_" + node.getQName().getLocalName();
190                 final var names = List.of(parentNameConfigLocalName);
191                 discriminator = definitionNames.pickDiscriminator(node, names);
192             } else {
193                 discriminator = definitionNames.getDiscriminator(node);
194             }
195             final var child = new SchemaEntity(node, newTitle, discriminator, OBJECT_TYPE, stack, parentName,
196                 isParentConfig, definitionNames, EntityType.NODE);
197             final var isConfig = node.isConfiguration() && isParentConfig;
198             result.add(child);
199             stack.enterSchemaTree(node.getQName());
200             processActions(node, title, stack, definitionNames, result, parentName);
201             for (final var childNode : ((DataNodeContainer) node).getChildNodes()) {
202                 processDataAndActionNodes(childNode, newTitle, stack, definitionNames, result, newTitle, isConfig);
203             }
204             stack.exit();
205         } else if (node instanceof ChoiceSchemaNode choiceNode && !choiceNode.getCases().isEmpty()) {
206             // Process default case or first case
207             final var caseNode = choiceNode.getDefaultCase()
208                 .orElseGet(() -> choiceNode.getCases().stream().findFirst()
209                     .orElseThrow(() -> new IllegalStateException("No cases found in ChoiceSchemaNode")));
210             stack.enterSchemaTree(choiceNode.getQName());
211             stack.enterSchemaTree(caseNode.getQName());
212             for (final var childNode : caseNode.getChildNodes()) {
213                 processDataAndActionNodes(childNode, title, stack, definitionNames, result, parentName, isParentConfig);
214             }
215             stack.exit(); // Exit the CaseSchemaNode context
216             stack.exit(); // Exit the ChoiceSchemaNode context
217         }
218     }
219
220     private static void processActions(final DataSchemaNode node, final String title, final SchemaInferenceStack stack,
221             final DefinitionNames definitionNames, final ArrayDeque<SchemaEntity> result, final String parentName) {
222         for (final var actionDef : ((ActionNodeContainer) node).getActions()) {
223             stack.enterSchemaTree(actionDef.getQName());
224             final var actionName = actionDef.getQName().getLocalName();
225             final var actionInput = actionDef.getInput();
226             if (!actionInput.getChildNodes().isEmpty()) {
227                 final var input = new SchemaEntity(actionInput, title + "_" + actionName + INPUT_SUFFIX, null,
228                     OBJECT_TYPE, stack, parentName, false, definitionNames, EntityType.RPC);
229                 result.add(input);
230             }
231             final var actionOutput = actionDef.getOutput();
232             if (!actionOutput.getChildNodes().isEmpty()) {
233                 final var output = new SchemaEntity(actionOutput, title + "_" + actionName + OUTPUT_SUFFIX, null,
234                     OBJECT_TYPE, stack, parentName, false, definitionNames, EntityType.RPC);
235                 result.add(output);
236             }
237             stack.exit();
238         }
239     }
240
241     public enum EntityType {
242         NODE,
243         RPC
244     }
245 }