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