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