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