Update NormalizedNodeStreamWriter
[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 com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
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.xml.transform.dom.DOMSource;
20 import org.checkerframework.checker.regex.qual.Regex;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
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.ContainerSchemaNode;
29 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
30 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
32 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
33 import org.w3c.dom.Element;
34 import org.w3c.dom.Node;
35 import org.w3c.dom.NodeList;
36 import org.w3c.dom.Text;
37
38 /**
39  * This implementation will create JSON output as output stream.
40  *
41  * <p>
42  * Values of leaf and leaf-list are NOT translated according to codecs.
43  */
44 public abstract class JSONNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
45     private static final class Exclusive extends JSONNormalizedNodeStreamWriter {
46         Exclusive(final JSONCodecFactory codecFactory, final SchemaTracker tracker, final JsonWriter writer,
47                 final JSONStreamWriterRootContext rootContext) {
48             super(codecFactory, tracker, writer, rootContext);
49         }
50
51         @Override
52         public void close() throws IOException {
53             flush();
54             closeWriter();
55         }
56     }
57
58     private static final class Nested extends JSONNormalizedNodeStreamWriter {
59         Nested(final JSONCodecFactory codecFactory, final SchemaTracker tracker, final JsonWriter writer,
60                 final JSONStreamWriterRootContext rootContext) {
61             super(codecFactory, tracker, writer, rootContext);
62         }
63
64         @Override
65         public void close() throws IOException {
66             flush();
67             // The caller "owns" the writer, let them close it
68         }
69     }
70
71     /**
72      * RFC6020 deviation: we are not required to emit empty containers unless they
73      * are marked as 'presence'.
74      */
75     private static final boolean DEFAULT_EMIT_EMPTY_CONTAINERS = true;
76
77     @Regex
78     private static final String NUMBER_STRING = "-?\\d+(\\.\\d+)?";
79     private static final Pattern NUMBER_PATTERN = Pattern.compile(NUMBER_STRING);
80
81     @Regex
82     private static final String NOT_DECIMAL_NUMBER_STRING = "-?\\d+";
83     private static final Pattern NOT_DECIMAL_NUMBER_PATTERN = Pattern.compile(NOT_DECIMAL_NUMBER_STRING);
84
85     private final SchemaTracker tracker;
86     private final JSONCodecFactory codecs;
87     private final JsonWriter writer;
88     private JSONStreamWriterContext context;
89
90     JSONNormalizedNodeStreamWriter(final JSONCodecFactory codecFactory, final SchemaTracker tracker,
91             final JsonWriter writer, final JSONStreamWriterRootContext rootContext) {
92         this.writer = requireNonNull(writer);
93         this.codecs = requireNonNull(codecFactory);
94         this.tracker = requireNonNull(tracker);
95         this.context = requireNonNull(rootContext);
96     }
97
98     /**
99      * Create a new stream writer, which writes to the specified output stream.
100      *
101      * <p>
102      * The codec factory can be reused between multiple writers.
103      *
104      * <p>
105      * Returned writer is exclusive user of JsonWriter, which means it will start
106      * top-level JSON element and ends it.
107      *
108      * <p>
109      * This instance of writer can be used only to emit one top level element,
110      * otherwise it will produce incorrect JSON. Closing this instance will close
111      * the writer too.
112      *
113      * @param codecFactory JSON codec factory
114      * @param path Schema Path
115      * @param initialNs Initial namespace
116      * @param jsonWriter JsonWriter
117      * @return A stream writer instance
118      */
119     public static NormalizedNodeStreamWriter createExclusiveWriter(final JSONCodecFactory codecFactory,
120             final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
121         return new Exclusive(codecFactory, SchemaTracker.create(codecFactory.getSchemaContext(), path), jsonWriter,
122             new JSONStreamWriterExclusiveRootContext(initialNs));
123     }
124
125     /**
126      * Create a new stream writer, which writes to the specified output stream.
127      *
128      * <p>
129      * The codec factory can be reused between multiple writers.
130      *
131      * <p>
132      * Returned writer is exclusive user of JsonWriter, which means it will start
133      * top-level JSON element and ends it.
134      *
135      * <p>
136      * This instance of writer can be used only to emit one top level element,
137      * otherwise it will produce incorrect JSON. Closing this instance will close
138      * the writer too.
139      *
140      * @param codecFactory JSON codec factory
141      * @param rootNode Root node
142      * @param initialNs Initial namespace
143      * @param jsonWriter JsonWriter
144      * @return A stream writer instance
145      */
146     public static NormalizedNodeStreamWriter createExclusiveWriter(final JSONCodecFactory codecFactory,
147             final DataNodeContainer rootNode, final URI initialNs, final JsonWriter jsonWriter) {
148         return new Exclusive(codecFactory, SchemaTracker.create(rootNode), jsonWriter,
149             new JSONStreamWriterExclusiveRootContext(initialNs));
150     }
151
152     /**
153      * Create a new stream writer, which writes to the specified output stream.
154      *
155      * <p>
156      * The codec factory can be reused between multiple writers.
157      *
158      * <p>
159      * Returned writer can be used emit multiple top level element,
160      * but does not start / close parent JSON object, which must be done
161      * by user providing {@code jsonWriter} instance in order for
162      * JSON to be valid. Closing this instance <strong>will not</strong>
163      * close the wrapped writer; the caller must take care of that.
164      *
165      * @param codecFactory JSON codec factory
166      * @param path Schema Path
167      * @param initialNs Initial namespace
168      * @param jsonWriter JsonWriter
169      * @return A stream writer instance
170      */
171     public static NormalizedNodeStreamWriter createNestedWriter(final JSONCodecFactory codecFactory,
172             final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
173         return new Nested(codecFactory, SchemaTracker.create(codecFactory.getSchemaContext(), path), jsonWriter,
174             new JSONStreamWriterSharedRootContext(initialNs));
175     }
176
177     /**
178      * Create a new stream writer, which writes to the specified output stream.
179      *
180      * <p>
181      * The codec factory can be reused between multiple writers.
182      *
183      * <p>
184      * Returned writer can be used emit multiple top level element,
185      * but does not start / close parent JSON object, which must be done
186      * by user providing {@code jsonWriter} instance in order for
187      * JSON to be valid. Closing this instance <strong>will not</strong>
188      * close the wrapped writer; the caller must take care of that.
189      *
190      * @param codecFactory JSON codec factory
191      * @param rootNode Root node
192      * @param initialNs Initial namespace
193      * @param jsonWriter JsonWriter
194      * @return A stream writer instance
195      */
196     public static NormalizedNodeStreamWriter createNestedWriter(final JSONCodecFactory codecFactory,
197             final DataNodeContainer rootNode, final URI initialNs, final JsonWriter jsonWriter) {
198         return new Nested(codecFactory, SchemaTracker.create(rootNode), jsonWriter,
199             new JSONStreamWriterSharedRootContext(initialNs));
200     }
201
202     @Override
203     public void startLeafNode(final NodeIdentifier name) throws IOException {
204         tracker.startLeafNode(name);
205         context.emittingChild(codecs.getSchemaContext(), writer);
206         context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
207     }
208
209     @Override
210     public final void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
211         tracker.startLeafSet(name);
212         context = new JSONStreamWriterListContext(context, name);
213     }
214
215     @Override
216     public void startLeafSetEntryNode(final NodeWithValue<?> name) throws IOException {
217         tracker.startLeafSetEntryNode(name);
218         context.emittingChild(codecs.getSchemaContext(), writer);
219     }
220
221     @Override
222     public final void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
223         tracker.startLeafSet(name);
224         context = new JSONStreamWriterListContext(context, name);
225     }
226
227     /*
228      * Warning suppressed due to static final constant which triggers a warning
229      * for the call to schema.isPresenceContainer().
230      */
231     @Override
232     public final void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
233         final SchemaNode schema = tracker.startContainerNode(name);
234         final boolean isPresence = schema instanceof ContainerSchemaNode
235             ? ((ContainerSchemaNode) schema).isPresenceContainer() : DEFAULT_EMIT_EMPTY_CONTAINERS;
236         context = new JSONStreamWriterNamedObjectContext(context, name, isPresence);
237     }
238
239     @Override
240     public final void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
241         tracker.startList(name);
242         context = new JSONStreamWriterListContext(context, name);
243     }
244
245     @Override
246     public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
247         tracker.startListItem(name);
248         context = new JSONStreamWriterObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS);
249     }
250
251     @Override
252     public final void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
253         tracker.startList(name);
254         context = new JSONStreamWriterListContext(context, name);
255     }
256
257     @Override
258     public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
259             throws IOException {
260         tracker.startListItem(identifier);
261         context = new JSONStreamWriterObjectContext(context, identifier, DEFAULT_EMIT_EMPTY_CONTAINERS);
262     }
263
264     @Override
265     public final void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
266         tracker.startList(name);
267         context = new JSONStreamWriterListContext(context, name);
268     }
269
270     @Override
271     public final void startChoiceNode(final NodeIdentifier name, final int childSizeHint) {
272         tracker.startChoiceNode(name);
273         context = new JSONStreamWriterInvisibleContext(context);
274     }
275
276     @Override
277     public final void startAugmentationNode(final AugmentationIdentifier identifier) {
278         tracker.startAugmentationNode(identifier);
279         context = new JSONStreamWriterInvisibleContext(context);
280     }
281
282     @Override
283     public final void startAnyxmlNode(final NodeIdentifier name) throws IOException {
284         tracker.startAnyxmlNode(name);
285         context.emittingChild(codecs.getSchemaContext(), writer);
286         context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
287     }
288
289     @Override
290     public final void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint)
291             throws IOException {
292         tracker.startYangModeledAnyXmlNode(name);
293         context = new JSONStreamWriterNamedObjectContext(context, name, true);
294     }
295
296     @Override
297     public final void endNode() throws IOException {
298         tracker.endNode();
299         context = context.endNode(codecs.getSchemaContext(), writer);
300
301         if (context instanceof JSONStreamWriterRootContext) {
302             context.emitEnd(writer);
303         }
304     }
305
306     @Override
307     public final void flush() throws IOException {
308         writer.flush();
309     }
310
311     final void closeWriter() throws IOException {
312         writer.close();
313     }
314
315     @Override
316     public void scalarValue(final Object value) throws IOException {
317         final Object current = tracker.getParent();
318         checkState(current instanceof TypedDataSchemaNode, "Cannot emit scalar %s for %s", value, current);
319         writeValue(value, codecs.codecFor((TypedDataSchemaNode) current));
320     }
321
322     @Override
323     public void domSourceValue(final DOMSource value) throws IOException {
324         final Object current = tracker.getParent();
325         checkState(current instanceof AnyXmlSchemaNode, "Cannot emit DOMSource %s for %s", value, current);
326         // FIXME: should have a codec based on this :)
327         writeAnyXmlValue(value);
328     }
329
330     @SuppressWarnings("unchecked")
331     private void writeValue(final Object value, final JSONCodec<?> codec) throws IOException {
332         ((JSONCodec<Object>) codec).writeValue(writer, value);
333     }
334
335     private void writeAnyXmlValue(final DOMSource anyXmlValue) throws IOException {
336         writeXmlNode(anyXmlValue.getNode());
337     }
338
339     private void writeXmlNode(final Node node) throws IOException {
340         if (isArrayElement(node)) {
341             writeArrayContent(node);
342             return;
343         }
344         final Element firstChildElement = getFirstChildElement(node);
345         if (firstChildElement == null) {
346             writeXmlValue(node);
347         } else {
348             writeObjectContent(firstChildElement);
349         }
350     }
351
352     private void writeArrayContent(final Node node) throws IOException {
353         writer.beginArray();
354         handleArray(node);
355         writer.endArray();
356     }
357
358     private void writeObjectContent(final Element firstChildElement) throws IOException {
359         writer.beginObject();
360         writeObject(firstChildElement);
361         writer.endObject();
362     }
363
364     private static boolean isArrayElement(final Node node) {
365         if (ELEMENT_NODE == node.getNodeType()) {
366             final String nodeName = node.getNodeName();
367             for (Node nextNode = node.getNextSibling(); nextNode != null; nextNode = nextNode.getNextSibling()) {
368                 if (ELEMENT_NODE == nextNode.getNodeType() && nodeName.equals(nextNode.getNodeName())) {
369                     return true;
370                 }
371             }
372         }
373         return false;
374     }
375
376     private void handleArray(final Node node) throws IOException {
377         final Element parentNode = (Element)node.getParentNode();
378         final NodeList elementsList = parentNode.getElementsByTagName(node.getNodeName());
379         for (int i = 0, length = elementsList.getLength(); i < length; i++) {
380             final Node arrayElement = elementsList.item(i);
381             final Element parent = (Element)arrayElement.getParentNode();
382             if (parentNode.isSameNode(parent)) {
383                 final Element firstChildElement = getFirstChildElement(arrayElement);
384                 if (firstChildElement != null) {
385                     writeObjectContent(firstChildElement);
386                 } else {
387                     // It may be scalar
388                     writeXmlValue(arrayElement);
389                 }
390             }
391         }
392     }
393
394     private void writeObject(Node node) throws IOException {
395         String previousNodeName = "";
396         while (node != null) {
397             if (ELEMENT_NODE == node.getNodeType()) {
398                 if (!node.getNodeName().equals(previousNodeName)) {
399                     previousNodeName = node.getNodeName();
400                     writer.name(node.getNodeName());
401                     writeXmlNode(node);
402                 }
403             }
404             node = node.getNextSibling();
405         }
406     }
407
408     private void writeXmlValue(final Node node) throws IOException {
409         Text firstChild = getFirstChildText(node);
410         String childNodeText = firstChild != null ? firstChild.getWholeText() : "";
411         childNodeText = childNodeText != null ? childNodeText.trim() : "";
412
413         if (NUMBER_PATTERN.matcher(childNodeText).matches()) {
414             writer.value(parseNumber(childNodeText));
415             return;
416         }
417         switch (childNodeText) {
418             case "null":
419                 writer.nullValue();
420                 break;
421             case "false":
422                 writer.value(false);
423                 break;
424             case "true":
425                 writer.value(true);
426                 break;
427             default:
428                 writer.value(childNodeText);
429         }
430     }
431
432     private static Element getFirstChildElement(final Node node) {
433         final NodeList children = node.getChildNodes();
434         for (int i = 0, length = children.getLength(); i < length; i++) {
435             final Node childNode = children.item(i);
436             if (ELEMENT_NODE == childNode.getNodeType()) {
437                 return (Element) childNode;
438             }
439         }
440         return null;
441     }
442
443     private static Text getFirstChildText(final Node node) {
444         final NodeList children = node.getChildNodes();
445         for (int i = 0, length = children.getLength(); i < length; i++) {
446             final Node childNode = children.item(i);
447             if (TEXT_NODE == childNode.getNodeType()) {
448                 return (Text) childNode;
449             }
450         }
451         return null;
452     }
453
454     // json numbers are 64 bit wide floating point numbers - in java terms it is either long or double
455     private static Number parseNumber(final String numberText) {
456         if (NOT_DECIMAL_NUMBER_PATTERN.matcher(numberText).matches()) {
457             return Long.valueOf(numberText);
458         }
459
460         return Double.valueOf(numberText);
461     }
462 }