Fix xml->CompositeNode transformation for rpc replies for rpcs with no output
[yangtools.git] / yang / yang-data-codec-gson / src / main / java / org / opendaylight / yangtools / yang / data / codec / gson / JSONNormalizedNodeStreamWriter.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. 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.yangtools.yang.data.codec.gson;
9
10 import com.google.common.base.Preconditions;
11 import com.google.common.base.Strings;
12 import com.google.common.collect.ImmutableSet;
13 import com.google.gson.stream.JsonWriter;
14
15 import java.io.IOException;
16 import java.io.Writer;
17 import java.math.BigDecimal;
18 import java.math.BigInteger;
19 import java.net.URI;
20 import java.util.ArrayDeque;
21 import java.util.Collection;
22 import java.util.Deque;
23
24 import org.opendaylight.yangtools.concepts.Codec;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
28 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
29 import org.opendaylight.yangtools.yang.data.impl.codec.SchemaTracker;
30 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.Module;
34 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
35 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
36
37 /**
38  * This implementation will create JSON output as output stream.
39  *
40  * Values of leaf and leaf-list are NOT translated according to codecs.
41  *
42  * FIXME: rewrite this in terms of {@link JsonWriter}.
43  */
44 public class JSONNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
45
46     private static enum NodeType {
47         OBJECT,
48         LIST,
49         OTHER,
50     }
51
52     private static class TypeInfo {
53         private boolean hasAtLeastOneChild = false;
54         private final NodeType type;
55         private final URI uri;
56
57         public TypeInfo(final NodeType type, final URI uri) {
58             this.type = type;
59             this.uri = uri;
60         }
61
62         public void setHasAtLeastOneChild(final boolean hasChildren) {
63             this.hasAtLeastOneChild = hasChildren;
64         }
65
66         public NodeType getType() {
67             return type;
68         }
69
70         public URI getNamespace() {
71             return uri;
72         }
73
74         public boolean hasAtLeastOneChild() {
75             return hasAtLeastOneChild;
76         }
77     }
78
79     private static final Collection<Class<?>> NUMERIC_CLASSES =
80             ImmutableSet.<Class<?>>of(Byte.class, Short.class, Integer.class, Long.class, BigInteger.class, BigDecimal.class);
81     private final Deque<TypeInfo> stack = new ArrayDeque<>();
82     private final SchemaContext schemaContext;
83     private final CodecFactory codecs;
84     private final SchemaTracker tracker;
85     private final Writer writer;
86     private final String indent;
87
88     private URI currentNamespace = null;
89     private int currentDepth = 0;
90
91     private JSONNormalizedNodeStreamWriter(final SchemaContext schemaContext,
92             final Writer writer, final int indentSize) {
93         this.schemaContext = Preconditions.checkNotNull(schemaContext);
94         this.writer = Preconditions.checkNotNull(writer);
95
96         Preconditions.checkArgument(indentSize >= 0, "Indent size must be non-negative");
97         if (indentSize != 0) {
98             indent = Strings.repeat(" ", indentSize);
99         } else {
100             indent = null;
101         }
102
103         this.codecs = CodecFactory.create(schemaContext);
104         this.tracker = SchemaTracker.create(schemaContext);
105     }
106
107     private JSONNormalizedNodeStreamWriter(final SchemaContext schemaContext, final SchemaPath path,
108             final Writer writer, final URI initialNs,final int indentSize) {
109         this.schemaContext = Preconditions.checkNotNull(schemaContext);
110         this.writer = Preconditions.checkNotNull(writer);
111
112         Preconditions.checkArgument(indentSize >= 0, "Indent size must be non-negative");
113         if (indentSize != 0) {
114             indent = Strings.repeat(" ", indentSize);
115         } else {
116             indent = null;
117         }
118         this.currentNamespace = initialNs;
119         this.codecs = CodecFactory.create(schemaContext);
120         this.tracker = SchemaTracker.create(schemaContext,path);
121     }
122
123     /**
124      * Create a new stream writer, which writes to the specified {@link Writer}.
125      *
126      * @param schemaContext Schema context
127      * @param writer Output writer
128      * @return A stream writer instance
129      */
130     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final Writer writer) {
131         return new JSONNormalizedNodeStreamWriter(schemaContext, writer, 0);
132     }
133
134     /**
135      * Create a new stream writer, which writes to the specified {@link Writer}.
136      *
137      * @param schemaContext Schema context
138      * @param writer Output writer
139      * @return A stream writer instance
140      */
141     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final SchemaPath path,final Writer writer) {
142         return new JSONNormalizedNodeStreamWriter(schemaContext, path, writer, null, 0);
143     }
144
145     /**
146      * Create a new stream writer, which writes to the specified {@link Writer}.
147      *
148      * @param schemaContext Schema context
149      * @param writer Output writer
150      * @param initialNs Initial namespace
151      * @return A stream writer instance
152      */
153     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final SchemaPath path,final URI initialNs, final Writer writer) {
154         return new JSONNormalizedNodeStreamWriter(schemaContext, path, writer, initialNs, 0);
155     }
156
157     /**
158      * Create a new stream writer, which writes to the specified output stream.
159      *
160      * @param schemaContext Schema context
161      * @param writer Output writer
162      * @param indentSize indentation size
163      * @return A stream writer instance
164      */
165     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final Writer writer, final int indentSize) {
166         return new JSONNormalizedNodeStreamWriter(schemaContext, writer, indentSize);
167     }
168
169     @Override
170     public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
171         final LeafSchemaNode schema = tracker.leafNode(name);
172         final Codec<Object, Object> codec = codecs.codecFor(schema.getType());
173
174         separateElementFromPreviousElement();
175         writeJsonIdentifier(name);
176         currentNamespace = stack.peek().getNamespace();
177         writeValue(codec.serialize(value));
178         separateNextSiblingsWithComma();
179     }
180
181     @Override
182     public void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
183         tracker.startLeafSet(name);
184
185         separateElementFromPreviousElement();
186         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
187         writeJsonIdentifier(name);
188         writeStartList();
189         indentRight();
190     }
191
192     @Override
193     public void leafSetEntryNode(final Object value) throws IOException {
194         final LeafListSchemaNode schema = tracker.leafSetEntryNode();
195         final Codec<Object, Object> codec = codecs.codecFor(schema.getType());
196
197         separateElementFromPreviousElement();
198         writeValue(codec.serialize(value));
199         separateNextSiblingsWithComma();
200     }
201
202     @Override
203     public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
204         tracker.startContainerNode(name);
205
206         separateElementFromPreviousElement();
207         stack.push(new TypeInfo(NodeType.OBJECT, name.getNodeType().getNamespace()));
208         writeJsonIdentifier(name);
209         writeStartObject();
210         indentRight();
211     }
212
213     @Override
214     public void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
215         tracker.startList(name);
216
217         separateElementFromPreviousElement();
218         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
219         writeJsonIdentifier(name);
220         writeStartList();
221         indentRight();
222     }
223
224     @Override
225     public void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
226         tracker.startListItem(name);
227
228         separateElementFromPreviousElement();
229         stack.push(new TypeInfo(NodeType.OBJECT, name.getNodeType().getNamespace()));
230         writeStartObject();
231         indentRight();
232     }
233
234     @Override
235     public void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
236         tracker.startList(name);
237
238         separateElementFromPreviousElement();
239         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
240         writeJsonIdentifier(name);
241         writeStartList();
242         indentRight();
243     }
244
245     @Override
246     public void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
247             throws IOException {
248         tracker.startListItem(identifier);
249         separateElementFromPreviousElement();
250         stack.push(new TypeInfo(NodeType.OBJECT, identifier.getNodeType().getNamespace()));
251
252
253         writeStartObject();
254         indentRight();
255     }
256
257     @Override
258     public void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
259         tracker.startListItem(name);
260
261         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
262         separateElementFromPreviousElement();
263         writeJsonIdentifier(name);
264         writeStartList();
265         indentRight();
266     }
267
268     @Override
269     public void startChoiceNode(final NodeIdentifier name, final int childSizeHint) throws IllegalArgumentException {
270         tracker.startChoiceNode(name);
271         handleInvisibleNode(name.getNodeType().getNamespace());
272     }
273
274     @Override
275     public void startAugmentationNode(final AugmentationIdentifier identifier) throws IllegalArgumentException {
276         tracker.startAugmentationNode(identifier);
277         handleInvisibleNode(currentNamespace);
278     }
279
280     @Override
281     public void anyxmlNode(final NodeIdentifier name, final Object value) throws IOException {
282         final AnyXmlSchemaNode schema = tracker.anyxmlNode(name);
283         // FIXME: should have a codec based on this :)
284
285         separateElementFromPreviousElement();
286         writeJsonIdentifier(name);
287         currentNamespace = stack.peek().getNamespace();
288         writeValue(value);
289         separateNextSiblingsWithComma();
290     }
291
292     @Override
293     public void endNode() throws IOException {
294         tracker.endNode();
295
296         final TypeInfo t = stack.pop();
297         switch (t.getType()) {
298         case LIST:
299             indentLeft();
300             newLine();
301             writer.append(']');
302             break;
303         case OBJECT:
304             indentLeft();
305             newLine();
306             writer.append('}');
307             break;
308         default:
309             break;
310         }
311
312         currentNamespace = stack.isEmpty() ? null : stack.peek().getNamespace();
313         separateNextSiblingsWithComma();
314     }
315
316     private void separateElementFromPreviousElement() throws IOException {
317         if (!stack.isEmpty() && stack.peek().hasAtLeastOneChild()) {
318             writer.append(',');
319         }
320         newLine();
321     }
322
323     private void newLine() throws IOException {
324         if (indent != null) {
325             writer.append('\n');
326
327             for (int i = 0; i < currentDepth; i++) {
328                 writer.append(indent);
329             }
330         }
331     }
332
333     private void separateNextSiblingsWithComma() {
334         if (!stack.isEmpty()) {
335             stack.peek().setHasAtLeastOneChild(true);
336         }
337     }
338
339     /**
340      * Invisible nodes have to be also pushed to stack because of pairing of start*() and endNode() methods. Information
341      * about child existing (due to printing comma) has to be transfered to invisible node.
342      */
343     private void handleInvisibleNode(final URI uri) {
344         TypeInfo typeInfo = new TypeInfo(NodeType.OTHER, uri);
345         typeInfo.setHasAtLeastOneChild(stack.peek().hasAtLeastOneChild());
346         stack.push(typeInfo);
347     }
348
349     private void writeStartObject() throws IOException {
350         writer.append('{');
351     }
352
353     private void writeStartList() throws IOException {
354         writer.append('[');
355     }
356
357     private void writeModulName(final URI namespace) throws IOException {
358         if (this.currentNamespace == null || namespace != this.currentNamespace) {
359             Module module = schemaContext.findModuleByNamespaceAndRevision(namespace, null);
360             writer.append(module.getName());
361             writer.append(':');
362             currentNamespace = namespace;
363         }
364     }
365
366     private void writeValue(final Object value) throws IOException {
367         final String str = String.valueOf(value);
368
369         if (!NUMERIC_CLASSES.contains(value.getClass())) {
370             writer.append('"');
371             writer.append(str);
372             writer.append('"');
373         } else {
374             writer.append(str);
375         }
376     }
377
378     private void writeJsonIdentifier(final NodeIdentifier name) throws IOException {
379         writer.append('"');
380         writeModulName(name.getNodeType().getNamespace());
381         writer.append(name.getNodeType().getLocalName());
382         writer.append("\":");
383     }
384
385     private void indentRight() {
386         currentDepth++;
387     }
388
389     private void indentLeft() {
390         currentDepth--;
391     }
392
393     @Override
394     public void flush() throws IOException {
395         writer.flush();
396     }
397
398     @Override
399     public void close() throws IOException {
400         writer.flush();
401         writer.close();
402     }
403
404 }