Cleanup use of Guava library
[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 static java.util.Objects.requireNonNull;
11 import static org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream.ANYXML_ARRAY_ELEMENT_ID;
12 import static org.w3c.dom.Node.ELEMENT_NODE;
13 import static org.w3c.dom.Node.TEXT_NODE;
14
15 import com.google.gson.stream.JsonWriter;
16 import java.io.IOException;
17 import java.net.URI;
18 import java.util.regex.Pattern;
19 import javax.annotation.RegEx;
20 import javax.xml.transform.dom.DOMSource;
21 import org.opendaylight.yangtools.yang.common.QName;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
25 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
26 import org.opendaylight.yangtools.yang.data.impl.codec.SchemaTracker;
27 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
29 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34
35 /**
36  * This implementation will create JSON output as output stream.
37  *
38  * <p>
39  * Values of leaf and leaf-list are NOT translated according to codecs.
40  */
41 public abstract class JSONNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
42     private static final class Exclusive extends JSONNormalizedNodeStreamWriter {
43         Exclusive(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter writer,
44                 final JSONStreamWriterRootContext rootContext) {
45             super(codecFactory, path, writer, rootContext);
46         }
47
48         @Override
49         public void close() throws IOException {
50             flush();
51             closeWriter();
52         }
53     }
54
55     private static final class Nested extends JSONNormalizedNodeStreamWriter {
56         Nested(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter writer,
57                 final JSONStreamWriterRootContext rootContext) {
58             super(codecFactory, path, writer, rootContext);
59         }
60
61         @Override
62         public void close() throws IOException {
63             flush();
64             // The caller "owns" the writer, let them close it
65         }
66     }
67
68     /**
69      * RFC6020 deviation: we are not required to emit empty containers unless they
70      * are marked as 'presence'.
71      */
72     private static final boolean DEFAULT_EMIT_EMPTY_CONTAINERS = true;
73
74     @RegEx
75     private static final String NUMBER_STRING = "-?\\d+(\\.\\d+)?";
76     private static final Pattern NUMBER_PATTERN = Pattern.compile(NUMBER_STRING);
77
78     @RegEx
79     private static final String NOT_DECIMAL_NUMBER_STRING = "-?\\d+";
80     private static final Pattern NOT_DECIMAL_NUMBER_PATTERN = Pattern.compile(NOT_DECIMAL_NUMBER_STRING);
81
82     private final SchemaTracker tracker;
83     private final JSONCodecFactory codecs;
84     private final JsonWriter writer;
85     private JSONStreamWriterContext context;
86
87     JSONNormalizedNodeStreamWriter(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter writer,
88             final JSONStreamWriterRootContext rootContext) {
89         this.writer = requireNonNull(writer);
90         this.codecs = requireNonNull(codecFactory);
91         this.tracker = SchemaTracker.create(codecFactory.getSchemaContext(), path);
92         this.context = requireNonNull(rootContext);
93     }
94
95     /**
96      * Create a new stream writer, which writes to the specified output stream.
97      *
98      * <p>
99      * The codec factory can be reused between multiple writers.
100      *
101      * <p>
102      * Returned writer is exclusive user of JsonWriter, which means it will start
103      * top-level JSON element and ends it.
104      *
105      * <p>
106      * This instance of writer can be used only to emit one top level element,
107      * otherwise it will produce incorrect JSON. Closing this instance will close
108      * the writer too.
109      *
110      * @param codecFactory JSON codec factory
111      * @param path Schema Path
112      * @param initialNs Initial namespace
113      * @param jsonWriter JsonWriter
114      * @return A stream writer instance
115      */
116     public static NormalizedNodeStreamWriter createExclusiveWriter(final JSONCodecFactory codecFactory,
117             final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
118         return new Exclusive(codecFactory, path, jsonWriter, new JSONStreamWriterExclusiveRootContext(initialNs));
119     }
120
121     /**
122      * Create a new stream writer, which writes to the specified output stream.
123      *
124      * <p>
125      * The codec factory can be reused between multiple writers.
126      *
127      * <p>
128      * Returned writer can be used emit multiple top level element,
129      * but does not start / close parent JSON object, which must be done
130      * by user providing {@code jsonWriter} instance in order for
131      * JSON to be valid. Closing this instance <strong>will not</strong>
132      * close the wrapped writer; the caller must take care of that.
133      *
134      * @param codecFactory JSON codec factory
135      * @param path Schema Path
136      * @param initialNs Initial namespace
137      * @param jsonWriter JsonWriter
138      * @return A stream writer instance
139      */
140     public static NormalizedNodeStreamWriter createNestedWriter(final JSONCodecFactory codecFactory,
141             final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
142         return new Nested(codecFactory, path, jsonWriter, new JSONStreamWriterSharedRootContext(initialNs));
143     }
144
145     @Override
146     public final void leafNode(final NodeIdentifier name, final Object value) throws IOException {
147         final LeafSchemaNode schema = tracker.leafNode(name);
148         final JSONCodec<?> codec = codecs.codecFor(schema);
149         context.emittingChild(codecs.getSchemaContext(), writer);
150         context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
151         writeValue(value, codec);
152     }
153
154     @Override
155     public final void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
156         tracker.startLeafSet(name);
157         context = new JSONStreamWriterListContext(context, name);
158     }
159
160     @Override
161     public final void leafSetEntryNode(final QName name, final Object value) throws IOException {
162         final LeafListSchemaNode schema = tracker.leafSetEntryNode(name);
163         final JSONCodec<?> codec = codecs.codecFor(schema);
164         context.emittingChild(codecs.getSchemaContext(), writer);
165         writeValue(value, codec);
166     }
167
168     @Override
169     public final void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
170         tracker.startLeafSet(name);
171         context = new JSONStreamWriterListContext(context, name);
172     }
173
174     /*
175      * Warning suppressed due to static final constant which triggers a warning
176      * for the call to schema.isPresenceContainer().
177      */
178     @SuppressWarnings("unused")
179     @Override
180     public final void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
181         final SchemaNode schema = tracker.startContainerNode(name);
182
183         // FIXME this code ignores presence for containers
184         // but datastore does as well and it needs be fixed first (2399)
185         context = new JSONStreamWriterNamedObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS);
186     }
187
188     @Override
189     public final void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
190         tracker.startList(name);
191         context = new JSONStreamWriterListContext(context, name);
192     }
193
194     @Override
195     public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
196         tracker.startListItem(name);
197         context = new JSONStreamWriterObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS);
198     }
199
200     @Override
201     public final void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
202         tracker.startList(name);
203         context = new JSONStreamWriterListContext(context, name);
204     }
205
206     @Override
207     public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
208             throws IOException {
209         tracker.startListItem(identifier);
210         context = new JSONStreamWriterObjectContext(context, identifier, DEFAULT_EMIT_EMPTY_CONTAINERS);
211     }
212
213     @Override
214     public final void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
215         tracker.startList(name);
216         context = new JSONStreamWriterListContext(context, name);
217     }
218
219     @Override
220     public final void startChoiceNode(final NodeIdentifier name, final int childSizeHint) {
221         tracker.startChoiceNode(name);
222         context = new JSONStreamWriterInvisibleContext(context);
223     }
224
225     @Override
226     public final void startAugmentationNode(final AugmentationIdentifier identifier) {
227         tracker.startAugmentationNode(identifier);
228         context = new JSONStreamWriterInvisibleContext(context);
229     }
230
231     @Override
232     public final void anyxmlNode(final NodeIdentifier name, final Object value) throws IOException {
233         @SuppressWarnings("unused")
234         final AnyXmlSchemaNode schema = tracker.anyxmlNode(name);
235         // FIXME: should have a codec based on this :)
236
237         context.emittingChild(codecs.getSchemaContext(), writer);
238         context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
239
240         writeAnyXmlValue((DOMSource) value);
241     }
242
243     @Override
244     public final void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint)
245             throws IOException {
246         tracker.startYangModeledAnyXmlNode(name);
247         context = new JSONStreamWriterNamedObjectContext(context, name, true);
248     }
249
250     @Override
251     public final void endNode() throws IOException {
252         tracker.endNode();
253         context = context.endNode(codecs.getSchemaContext(), writer);
254
255         if (context instanceof JSONStreamWriterRootContext) {
256             context.emitEnd(writer);
257         }
258     }
259
260     @Override
261     public final void flush() throws IOException {
262         writer.flush();
263     }
264
265     final void closeWriter() throws IOException {
266         writer.close();
267     }
268
269     @SuppressWarnings("unchecked")
270     private void writeValue(final Object value, final JSONCodec<?> codec) throws IOException {
271         ((JSONCodec<Object>) codec).writeValue(writer, value);
272     }
273
274     private void writeAnyXmlValue(final DOMSource anyXmlValue) throws IOException {
275         final Node documentNode = anyXmlValue.getNode();
276         final Node firstChild = documentNode.getFirstChild();
277         if (ELEMENT_NODE == firstChild.getNodeType() && !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName())) {
278             writer.beginObject();
279             traverseAnyXmlValue(documentNode);
280             writer.endObject();
281         } else {
282             traverseAnyXmlValue(documentNode);
283         }
284     }
285
286     private void traverseAnyXmlValue(final Node node) throws IOException {
287         final NodeList children = node.getChildNodes();
288         boolean inArray = false;
289
290         for (int i = 0, length = children.getLength(); i < length; i++) {
291             final Node childNode = children.item(i);
292             boolean inObject = false;
293
294             if (ELEMENT_NODE == childNode.getNodeType()) {
295                 final Node firstChild = childNode.getFirstChild();
296                 // beginning of an array
297                 if (ANYXML_ARRAY_ELEMENT_ID.equals(childNode.getNodeName()) && !inArray) {
298                     writer.beginArray();
299                     inArray = true;
300                     // object at the beginning of the array
301                     if (isJsonObjectInArray(childNode, firstChild)) {
302                         writer.beginObject();
303                         inObject = true;
304                     }
305                     // object in the array
306                 } else if (isJsonObjectInArray(childNode, firstChild)) {
307                     writer.beginObject();
308                     inObject = true;
309                     // object
310                 } else if (isJsonObject(firstChild)) {
311                     writer.name(childNode.getNodeName());
312                     writer.beginObject();
313                     inObject = true;
314                     // name
315                 } else if (!inArray) {
316                     writer.name(childNode.getNodeName());
317                 }
318             }
319
320             // text value, i.e. a number, string, boolean or null
321             if (TEXT_NODE == childNode.getNodeType()) {
322                 final String childNodeText = childNode.getNodeValue();
323                 if (NUMBER_PATTERN.matcher(childNodeText).matches()) {
324                     writer.value(parseNumber(childNodeText));
325                 } else if ("true".equals(childNodeText) || "false".equals(childNodeText)) {
326                     writer.value(Boolean.parseBoolean(childNodeText));
327                 } else if ("null".equals(childNodeText)) {
328                     writer.nullValue();
329                 } else {
330                     writer.value(childNodeText);
331                 }
332
333                 return;
334             }
335
336             traverseAnyXmlValue(childNode);
337
338             if (inObject) {
339                 writer.endObject();
340             }
341         }
342
343         if (inArray) {
344             writer.endArray();
345         }
346     }
347
348     // json numbers are 64 bit wide floating point numbers - in java terms it is either long or double
349     private static Number parseNumber(final String numberText) {
350         if (NOT_DECIMAL_NUMBER_PATTERN.matcher(numberText).matches()) {
351             return Long.valueOf(numberText);
352         }
353
354         return Double.valueOf(numberText);
355     }
356
357     private static boolean isJsonObject(final Node firstChild) {
358         return !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName()) && TEXT_NODE != firstChild.getNodeType();
359     }
360
361     private static boolean isJsonObjectInArray(final Node node, final Node firstChild) {
362         return ANYXML_ARRAY_ELEMENT_ID.equals(node.getNodeName())
363                 && !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName())
364                 && TEXT_NODE != firstChild.getNodeType();
365     }
366 }