Merge "Added hardcoded URLs back for repository section"
[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 int currentDepth = 0;
89     private URI currentNamespace;
90
91     private JSONNormalizedNodeStreamWriter(final SchemaContext schemaContext,
92             final Writer writer, final int indentSize) {
93         this(schemaContext, SchemaPath.ROOT, writer, null, indentSize);
94     }
95
96     private JSONNormalizedNodeStreamWriter(final SchemaContext schemaContext, final SchemaPath path,
97             final Writer writer, final URI initialNs, final int indentSize) {
98         this.schemaContext = Preconditions.checkNotNull(schemaContext);
99         this.writer = Preconditions.checkNotNull(writer);
100
101         Preconditions.checkArgument(indentSize >= 0, "Indent size must be non-negative");
102         if (indentSize != 0) {
103             indent = Strings.repeat(" ", indentSize);
104         } else {
105             indent = null;
106         }
107         this.codecs = CodecFactory.create(schemaContext);
108         this.tracker = SchemaTracker.create(schemaContext, path);
109
110         this.currentNamespace = initialNs;
111     }
112
113     /**
114      * Create a new stream writer, which writes to the specified {@link Writer}.
115      *
116      * @param schemaContext Schema context
117      * @param writer Output writer
118      * @return A stream writer instance
119      */
120     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final Writer writer) {
121         return new JSONNormalizedNodeStreamWriter(schemaContext, writer, 0);
122     }
123
124     /**
125      * Create a new stream writer, which writes to the specified {@link Writer}.
126      *
127      * @param schemaContext Schema context
128      * @param writer Output writer
129      * @return A stream writer instance
130      */
131     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final SchemaPath path,final Writer writer) {
132         return new JSONNormalizedNodeStreamWriter(schemaContext, path, writer, null, 0);
133     }
134
135     /**
136      * Create a new stream writer, which writes to the specified {@link Writer}.
137      *
138      * @param schemaContext Schema context
139      * @param writer Output writer
140      * @param initialNs Initial namespace
141      * @return A stream writer instance
142      */
143     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final SchemaPath path,final URI initialNs, final Writer writer) {
144         return new JSONNormalizedNodeStreamWriter(schemaContext, path, writer, initialNs, 0);
145     }
146
147     /**
148      * Create a new stream writer, which writes to the specified output stream.
149      *
150      * @param schemaContext Schema context
151      * @param writer Output writer
152      * @param indentSize indentation size
153      * @return A stream writer instance
154      */
155     public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final Writer writer, final int indentSize) {
156         return new JSONNormalizedNodeStreamWriter(schemaContext, writer, indentSize);
157     }
158
159     @Override
160     public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
161         final LeafSchemaNode schema = tracker.leafNode(name);
162         final Codec<Object, Object> codec = codecs.codecFor(schema.getType());
163
164         separateElementFromPreviousElement();
165         writeJsonIdentifier(name);
166         currentNamespace = stack.peek().getNamespace();
167         writeValue(codec.serialize(value));
168         separateNextSiblingsWithComma();
169     }
170
171     @Override
172     public void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
173         tracker.startLeafSet(name);
174
175         separateElementFromPreviousElement();
176         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
177         writeJsonIdentifier(name);
178         writeStartList();
179         indentRight();
180     }
181
182     @Override
183     public void leafSetEntryNode(final Object value) throws IOException {
184         final LeafListSchemaNode schema = tracker.leafSetEntryNode();
185         final Codec<Object, Object> codec = codecs.codecFor(schema.getType());
186
187         separateElementFromPreviousElement();
188         writeValue(codec.serialize(value));
189         separateNextSiblingsWithComma();
190     }
191
192     @Override
193     public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
194         tracker.startContainerNode(name);
195
196         separateElementFromPreviousElement();
197         stack.push(new TypeInfo(NodeType.OBJECT, name.getNodeType().getNamespace()));
198         writeJsonIdentifier(name);
199         writeStartObject();
200         indentRight();
201     }
202
203     @Override
204     public void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
205         tracker.startList(name);
206
207         separateElementFromPreviousElement();
208         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
209         writeJsonIdentifier(name);
210         writeStartList();
211         indentRight();
212     }
213
214     @Override
215     public void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
216         tracker.startListItem(name);
217
218         separateElementFromPreviousElement();
219         stack.push(new TypeInfo(NodeType.OBJECT, name.getNodeType().getNamespace()));
220         writeStartObject();
221         indentRight();
222     }
223
224     @Override
225     public void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
226         tracker.startList(name);
227
228         separateElementFromPreviousElement();
229         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
230         writeJsonIdentifier(name);
231         writeStartList();
232         indentRight();
233     }
234
235     @Override
236     public void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
237             throws IOException {
238         tracker.startListItem(identifier);
239         separateElementFromPreviousElement();
240         stack.push(new TypeInfo(NodeType.OBJECT, identifier.getNodeType().getNamespace()));
241
242
243         writeStartObject();
244         indentRight();
245     }
246
247     @Override
248     public void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
249         tracker.startListItem(name);
250
251         stack.push(new TypeInfo(NodeType.LIST, name.getNodeType().getNamespace()));
252         separateElementFromPreviousElement();
253         writeJsonIdentifier(name);
254         writeStartList();
255         indentRight();
256     }
257
258     @Override
259     public void startChoiceNode(final NodeIdentifier name, final int childSizeHint) throws IllegalArgumentException {
260         tracker.startChoiceNode(name);
261         handleInvisibleNode(name.getNodeType().getNamespace());
262     }
263
264     @Override
265     public void startAugmentationNode(final AugmentationIdentifier identifier) throws IllegalArgumentException {
266         tracker.startAugmentationNode(identifier);
267         handleInvisibleNode(currentNamespace);
268     }
269
270     @Override
271     public void anyxmlNode(final NodeIdentifier name, final Object value) throws IOException {
272         final AnyXmlSchemaNode schema = tracker.anyxmlNode(name);
273         // FIXME: should have a codec based on this :)
274
275         separateElementFromPreviousElement();
276         writeJsonIdentifier(name);
277         currentNamespace = stack.peek().getNamespace();
278         writeValue(value);
279         separateNextSiblingsWithComma();
280     }
281
282     @Override
283     public void endNode() throws IOException {
284         tracker.endNode();
285
286         final TypeInfo t = stack.pop();
287         switch (t.getType()) {
288         case LIST:
289             indentLeft();
290             newLine();
291             writer.append(']');
292             break;
293         case OBJECT:
294             indentLeft();
295             newLine();
296             writer.append('}');
297             break;
298         default:
299             break;
300         }
301
302         currentNamespace = stack.isEmpty() ? null : stack.peek().getNamespace();
303         separateNextSiblingsWithComma();
304     }
305
306     private void separateElementFromPreviousElement() throws IOException {
307         if (!stack.isEmpty() && stack.peek().hasAtLeastOneChild()) {
308             writer.append(',');
309         }
310         newLine();
311     }
312
313     private void newLine() throws IOException {
314         if (indent != null) {
315             writer.append('\n');
316
317             for (int i = 0; i < currentDepth; i++) {
318                 writer.append(indent);
319             }
320         }
321     }
322
323     private void separateNextSiblingsWithComma() {
324         if (!stack.isEmpty()) {
325             stack.peek().setHasAtLeastOneChild(true);
326         }
327     }
328
329     /**
330      * Invisible nodes have to be also pushed to stack because of pairing of start*() and endNode() methods. Information
331      * about child existing (due to printing comma) has to be transfered to invisible node.
332      */
333     private void handleInvisibleNode(final URI uri) {
334         TypeInfo typeInfo = new TypeInfo(NodeType.OTHER, uri);
335         typeInfo.setHasAtLeastOneChild(stack.peek().hasAtLeastOneChild());
336         stack.push(typeInfo);
337     }
338
339     private void writeStartObject() throws IOException {
340         writer.append('{');
341     }
342
343     private void writeStartList() throws IOException {
344         writer.append('[');
345     }
346
347     private void writeModulName(final URI namespace) throws IOException {
348         if (this.currentNamespace == null || namespace != this.currentNamespace) {
349             Module module = schemaContext.findModuleByNamespaceAndRevision(namespace, null);
350             writer.append(module.getName());
351             writer.append(':');
352             currentNamespace = namespace;
353         }
354     }
355
356     private void writeValue(final Object value) throws IOException {
357         final String str = String.valueOf(value);
358
359         if (!NUMERIC_CLASSES.contains(value.getClass())) {
360             writer.append('"');
361             writer.append(str);
362             writer.append('"');
363         } else {
364             writer.append(str);
365         }
366     }
367
368     private void writeJsonIdentifier(final NodeIdentifier name) throws IOException {
369         writer.append('"');
370         writeModulName(name.getNodeType().getNamespace());
371         writer.append(name.getNodeType().getLocalName());
372         writer.append("\":");
373     }
374
375     private void indentRight() {
376         currentDepth++;
377     }
378
379     private void indentLeft() {
380         currentDepth--;
381     }
382
383     @Override
384     public void flush() throws IOException {
385         writer.flush();
386     }
387
388     @Override
389     public void close() throws IOException {
390         writer.flush();
391         writer.close();
392     }
393
394 }