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