Split out NormalizedContainer
[yangtools.git] / data / yang-data-api / src / main / java / org / opendaylight / yangtools / yang / data / api / schema / stream / NormalizedNodeWriter.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.api.schema.stream;
9
10 import static com.google.common.base.Verify.verify;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.annotations.Beta;
14 import com.google.common.collect.Iterables;
15 import java.io.Closeable;
16 import java.io.Flushable;
17 import java.io.IOException;
18 import java.util.Set;
19 import javax.xml.stream.XMLStreamReader;
20 import javax.xml.transform.dom.DOMSource;
21 import org.eclipse.jdt.annotation.NonNull;
22 import org.opendaylight.yangtools.yang.common.QName;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
24 import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
25 import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
26 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
27 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
28 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
29 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
30 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
31 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
33 import org.opendaylight.yangtools.yang.data.api.schema.SystemLeafSetNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
35 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
36 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
37 import org.opendaylight.yangtools.yang.data.api.schema.UserLeafSetNode;
38 import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * This is an experimental iterator over a {@link NormalizedNode}. This is essentially
44  * the opposite of a {@link XMLStreamReader} -- unlike instantiating an iterator over
45  * the backing data, this encapsulates a {@link NormalizedNodeStreamWriter} and allows
46  * us to write multiple nodes.
47  */
48 @Beta
49 public class NormalizedNodeWriter implements Closeable, Flushable {
50     private static final Logger LOG = LoggerFactory.getLogger(NormalizedNodeWriter.class);
51
52     private final @NonNull NormalizedNodeStreamWriter writer;
53
54     protected NormalizedNodeWriter(final NormalizedNodeStreamWriter writer) {
55         this.writer = requireNonNull(writer);
56     }
57
58     protected final NormalizedNodeStreamWriter getWriter() {
59         return writer;
60     }
61
62     /**
63      * Create a new writer backed by a {@link NormalizedNodeStreamWriter}.
64      *
65      * @param writer Back-end writer
66      * @return A new instance.
67      */
68     public static NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer) {
69         return forStreamWriter(writer, true);
70     }
71
72     /**
73      * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. Unlike the simple
74      * {@link #forStreamWriter(NormalizedNodeStreamWriter)} method, this allows the caller to switch off RFC6020 XML
75      * compliance, providing better throughput. The reason is that the XML mapping rules in RFC6020 require
76      * the encoding to emit leaf nodes which participate in a list's key first and in the order in which they are
77      * defined in the key. For JSON, this requirement is completely relaxed and leaves can be ordered in any way we
78      * see fit. The former requires a bit of work: first a lookup for each key and then for each emitted node we need
79      * to check whether it was already emitted.
80      *
81      * @param writer Back-end writer
82      * @param orderKeyLeaves whether the returned instance should be RFC6020 XML compliant.
83      * @return A new instance.
84      */
85     public static NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
86             final boolean orderKeyLeaves) {
87         return orderKeyLeaves ? new OrderedNormalizedNodeWriter(writer) : new NormalizedNodeWriter(writer);
88     }
89
90     /**
91      * Iterate over the provided {@link NormalizedNode} and emit write
92      * events to the encapsulated {@link NormalizedNodeStreamWriter}.
93      *
94      * @param node Node
95      * @return NormalizedNodeWriter this
96      * @throws IOException when thrown from the backing writer.
97      */
98     public NormalizedNodeWriter write(final NormalizedNode node) throws IOException {
99         if (wasProcessedAsCompositeNode(node)) {
100             return this;
101         }
102
103         if (wasProcessAsSimpleNode(node)) {
104             return this;
105         }
106
107         throw new IllegalStateException("It wasn't possible to serialize node " + node);
108     }
109
110     @Override
111     public void flush() throws IOException {
112         writer.flush();
113     }
114
115     @Override
116     public void close() throws IOException {
117         writer.flush();
118         writer.close();
119     }
120
121     protected boolean wasProcessAsSimpleNode(final NormalizedNode node) throws IOException {
122         if (node instanceof LeafSetEntryNode<?> nodeAsLeafList) {
123             writer.startLeafSetEntryNode(nodeAsLeafList.name());
124             writer.scalarValue(nodeAsLeafList.body());
125             writer.endNode();
126             return true;
127         } else if (node instanceof LeafNode<?> nodeAsLeaf) {
128             writer.startLeafNode(nodeAsLeaf.name());
129             writer.scalarValue(nodeAsLeaf.body());
130             writer.endNode();
131             return true;
132         } else if (node instanceof AnyxmlNode<?> anyxmlNode) {
133             final Class<?> model = anyxmlNode.bodyObjectModel();
134             if (writer.startAnyxmlNode(anyxmlNode.name(), model)) {
135                 final Object value = node.body();
136                 if (DOMSource.class.isAssignableFrom(model)) {
137                     verify(value instanceof DOMSource, "Inconsistent anyxml node %s", anyxmlNode);
138                     writer.domSourceValue((DOMSource) value);
139                 } else {
140                     writer.scalarValue(value);
141                 }
142                 writer.endNode();
143                 return true;
144             }
145
146             LOG.debug("Ignoring unhandled anyxml node {}", anyxmlNode);
147         } else if (node instanceof AnydataNode<?> anydata) {
148             final Class<?> model = anydata.bodyObjectModel();
149             if (writer.startAnydataNode(anydata.name(), model)) {
150                 writer.scalarValue(anydata.body());
151                 writer.endNode();
152                 return true;
153             }
154
155             LOG.debug("Writer {} does not support anydata in form of {}", writer, model);
156         }
157
158         return false;
159     }
160
161     /**
162      * Emit events for all children and then emit an endNode() event.
163      *
164      * @param children Child iterable
165      * @return True
166      * @throws IOException when the writer reports it
167      */
168     protected boolean writeChildren(final Iterable<? extends NormalizedNode> children) throws IOException {
169         for (var child : children) {
170             write(child);
171         }
172
173         writer.endNode();
174         return true;
175     }
176
177     protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException {
178         writer.startMapEntryNode(node.name(), node.size());
179         return writeChildren(node.body());
180     }
181
182     protected boolean wasProcessedAsCompositeNode(final NormalizedNode node) throws IOException {
183         if (node instanceof ContainerNode n) {
184             writer.startContainerNode(n.name(), n.size());
185             return writeChildren(n.body());
186         } else if (node instanceof MapEntryNode n) {
187             return writeMapEntryNode(n);
188         } else if (node instanceof UnkeyedListEntryNode n) {
189             writer.startUnkeyedListItem(n.name(), n.size());
190             return writeChildren(n.body());
191         } else if (node instanceof ChoiceNode n) {
192             writer.startChoiceNode(n.name(), n.size());
193             return writeChildren(n.body());
194         } else if (node instanceof UnkeyedListNode n) {
195             writer.startUnkeyedList(n.name(), n.size());
196             return writeChildren(n.body());
197         } else if (node instanceof UserMapNode n) {
198             writer.startOrderedMapNode(n.name(), n.size());
199             return writeChildren(n.body());
200         } else if (node instanceof SystemMapNode n) {
201             writer.startMapNode(n.name(), n.size());
202             return writeChildren(n.body());
203         } else if (node instanceof UserLeafSetNode<?> n) {
204             writer.startOrderedLeafSet(n.name(), n.size());
205             return writeChildren(n.body());
206         } else if (node instanceof SystemLeafSetNode<?> n) {
207             writer.startLeafSet(n.name(), n.size());
208             return writeChildren(n.body());
209         }
210         return false;
211     }
212
213     private static final class OrderedNormalizedNodeWriter extends NormalizedNodeWriter {
214         private static final Logger LOG = LoggerFactory.getLogger(OrderedNormalizedNodeWriter.class);
215
216         OrderedNormalizedNodeWriter(final NormalizedNodeStreamWriter writer) {
217             super(writer);
218         }
219
220         @Override
221         protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException {
222             final NormalizedNodeStreamWriter nnWriter = getWriter();
223             nnWriter.startMapEntryNode(node.name(), node.size());
224
225             final Set<QName> qnames = node.name().keySet();
226             // Write out all the key children
227             for (final QName qname : qnames) {
228                 final DataContainerChild child = node.childByArg(new NodeIdentifier(qname));
229                 if (child != null) {
230                     write(child);
231                 } else {
232                     LOG.info("No child for key element {} found", qname);
233                 }
234             }
235
236             // Write all the rest
237             return writeChildren(Iterables.filter(node.body(), input -> {
238                 if (qnames.contains(input.name().getNodeType())) {
239                     LOG.debug("Skipping key child {}", input);
240                     return false;
241                 }
242                 return true;
243             }));
244         }
245     }
246 }