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