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