7293b70e4c32c5c48de388d5a4ae39557db982bf
[yangtools.git] / yang / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / XmlParserStream.java
1 /*
2  * Copyright (c) 2016 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
9 package org.opendaylight.yangtools.yang.data.codec.xml;
10
11 import com.google.common.annotations.Beta;
12 import com.google.common.base.Preconditions;
13 import java.io.Closeable;
14 import java.io.Flushable;
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.util.Deque;
20 import java.util.HashSet;
21 import java.util.Set;
22 import javax.annotation.concurrent.NotThreadSafe;
23 import javax.xml.namespace.NamespaceContext;
24 import javax.xml.parsers.ParserConfigurationException;
25 import javax.xml.stream.Location;
26 import javax.xml.stream.XMLStreamConstants;
27 import javax.xml.stream.XMLStreamException;
28 import javax.xml.stream.XMLStreamReader;
29 import javax.xml.transform.dom.DOMSource;
30 import org.opendaylight.yangtools.util.xml.UntrustedXML;
31 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
32 import org.opendaylight.yangtools.yang.data.util.AbstractNodeDataWithSchema;
33 import org.opendaylight.yangtools.yang.data.util.AnyXmlNodeDataWithSchema;
34 import org.opendaylight.yangtools.yang.data.util.CompositeNodeDataWithSchema;
35 import org.opendaylight.yangtools.yang.data.util.ContainerNodeDataWithSchema;
36 import org.opendaylight.yangtools.yang.data.util.LeafListEntryNodeDataWithSchema;
37 import org.opendaylight.yangtools.yang.data.util.LeafListNodeDataWithSchema;
38 import org.opendaylight.yangtools.yang.data.util.LeafNodeDataWithSchema;
39 import org.opendaylight.yangtools.yang.data.util.ListEntryNodeDataWithSchema;
40 import org.opendaylight.yangtools.yang.data.util.ListNodeDataWithSchema;
41 import org.opendaylight.yangtools.yang.data.util.ParserStreamUtils;
42 import org.opendaylight.yangtools.yang.data.util.RpcAsContainer;
43 import org.opendaylight.yangtools.yang.data.util.SimpleNodeDataWithSchema;
44 import org.opendaylight.yangtools.yang.data.util.YangModeledAnyXmlNodeDataWithSchema;
45 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
52 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
53 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.TypedSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.YangModeledAnyXmlSchemaNode;
56 import org.w3c.dom.Document;
57 import org.xml.sax.InputSource;
58 import org.xml.sax.SAXException;
59
60 /**
61  * This class provides functionality for parsing an XML source containing YANG-modeled data. It disallows multiple
62  * instances of the same element except for leaf-list and list entries. It also expects that the YANG-modeled data in
63  * the XML source are wrapped in a root element.
64  */
65 @Beta
66 @NotThreadSafe
67 public final class XmlParserStream implements Closeable, Flushable {
68     private final NormalizedNodeStreamWriter writer;
69     private final XmlCodecFactory codecs;
70     private final DataSchemaNode parentNode;
71     private final boolean strictParsing;
72
73     private XmlParserStream(final NormalizedNodeStreamWriter writer, final XmlCodecFactory codecs,
74             final DataSchemaNode parentNode, final boolean strictParsing) {
75         this.writer = Preconditions.checkNotNull(writer);
76         this.codecs = Preconditions.checkNotNull(codecs);
77         this.parentNode = parentNode;
78         this.strictParsing = strictParsing;
79     }
80
81     /**
82      * Construct a new {@link XmlParserStream} with strict parsing mode switched on.
83      *
84      * @param writer Output writer
85      * @param codecs Shared codecs
86      * @param parentNode Parent root node
87      * @return A new stream instance
88      */
89     public static XmlParserStream create(final NormalizedNodeStreamWriter writer, final XmlCodecFactory codecs,
90             final SchemaNode parentNode) {
91         return create(writer, codecs, parentNode, true);
92     }
93
94     /**
95      * Construct a new {@link XmlParserStream}.
96      *
97      * @param writer Output writer
98      * @param codecs Shared codecs
99      * @param parentNode Parent root node
100      * @param strictParsing parsing mode
101      *            if set to true, the parser will throw an exception if it encounters unknown child nodes
102      *            (nodes, that are not defined in the provided SchemaContext) in containers and lists
103      *            if set to false, the parser will skip unknown child nodes
104      * @return A new stream instance
105      */
106     public static XmlParserStream create(final NormalizedNodeStreamWriter writer, final XmlCodecFactory codecs,
107             final SchemaNode parentNode, final boolean strictParsing) {
108         if (parentNode instanceof RpcDefinition) {
109             return new XmlParserStream(writer, codecs, new RpcAsContainer((RpcDefinition) parentNode), strictParsing);
110         }
111         Preconditions.checkArgument(parentNode instanceof DataSchemaNode, "Instance of DataSchemaNode class awaited.");
112         return new XmlParserStream(writer, codecs, (DataSchemaNode) parentNode, strictParsing);
113     }
114
115     /**
116      * Construct a new {@link XmlParserStream}.
117      *
118      * @deprecated Use {@link #create(NormalizedNodeStreamWriter, SchemaContext, SchemaNode)} instead.
119      */
120     @Deprecated
121     public static XmlParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext) {
122         return create(writer, schemaContext, schemaContext);
123     }
124
125     /**
126      * Utility method for use when caching {@link XmlCodecFactory} is not feasible. Users with high performance
127      * requirements should use {@link #create(NormalizedNodeStreamWriter, XmlCodecFactory, SchemaNode)} instead and
128      * maintain a {@link XmlCodecFactory} to match the current {@link SchemaContext}.
129      */
130     public static XmlParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext,
131             final SchemaNode parentNode) {
132         return create(writer, schemaContext, parentNode, true);
133     }
134
135     /**
136      * Utility method for use when caching {@link XmlCodecFactory} is not feasible. Users with high performance
137      * requirements should use {@link #create(NormalizedNodeStreamWriter, XmlCodecFactory, SchemaNode)} instead and
138      * maintain a {@link XmlCodecFactory} to match the current {@link SchemaContext}.
139      */
140     public static XmlParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext,
141             final SchemaNode parentNode, final boolean strictParsing) {
142         return create(writer, XmlCodecFactory.create(schemaContext), parentNode, strictParsing);
143     }
144
145     /**
146      * This method parses the XML source and emits node events into a NormalizedNodeStreamWriter based on the
147      * YANG-modeled data contained in the XML source.
148      *
149      * @param reader
150      *              StAX reader which is to used to walk through the XML source
151      * @return
152      *              instance of XmlParserStream
153      * @throws XMLStreamException
154      *              if a well-formedness error or an unexpected processing condition occurs while parsing the XML
155      * @throws URISyntaxException
156      *              if the namespace URI of an XML element contains a syntax error
157      * @throws IOException
158      *              if an error occurs while parsing the value of an anyxml node
159      * @throws ParserConfigurationException
160      *              if an error occurs while parsing the value of an anyxml node
161      * @throws SAXException
162      *              if an error occurs while parsing the value of an anyxml node
163      */
164     public XmlParserStream parse(final XMLStreamReader reader) throws XMLStreamException, URISyntaxException,
165             IOException, ParserConfigurationException, SAXException {
166         if (reader.hasNext()) {
167             reader.nextTag();
168             final AbstractNodeDataWithSchema nodeDataWithSchema;
169             if (parentNode instanceof ContainerSchemaNode) {
170                 nodeDataWithSchema = new ContainerNodeDataWithSchema(parentNode);
171             } else if (parentNode instanceof ListSchemaNode) {
172                 nodeDataWithSchema = new ListNodeDataWithSchema(parentNode);
173             } else if (parentNode instanceof YangModeledAnyXmlSchemaNode) {
174                 nodeDataWithSchema = new YangModeledAnyXmlNodeDataWithSchema((YangModeledAnyXmlSchemaNode) parentNode);
175             } else if (parentNode instanceof AnyXmlSchemaNode) {
176                 nodeDataWithSchema = new AnyXmlNodeDataWithSchema(parentNode);
177             } else if (parentNode instanceof LeafSchemaNode) {
178                 nodeDataWithSchema = new LeafNodeDataWithSchema(parentNode);
179             } else if (parentNode instanceof LeafListSchemaNode) {
180                 nodeDataWithSchema = new LeafListNodeDataWithSchema(parentNode);
181             } else {
182                 throw new IllegalStateException("Unsupported schema node type " + parentNode.getClass() + ".");
183             }
184
185             read(reader, nodeDataWithSchema, reader.getLocalName());
186             nodeDataWithSchema.write(writer);
187         }
188
189         return this;
190     }
191
192     private static String readAnyXmlValue(final XMLStreamReader in) throws XMLStreamException {
193         final StringBuilder sb = new StringBuilder();
194         final String anyXmlElementName = in.getLocalName();
195         sb.append('<').append(anyXmlElementName).append(" xmlns=\"").append(in.getNamespaceURI()).append("\">");
196
197         while (in.hasNext()) {
198             final int eventType = in.next();
199
200             if (eventType == XMLStreamConstants.START_ELEMENT) {
201                 sb.append('<').append(in.getLocalName()).append('>');
202             } else if (eventType == XMLStreamConstants.END_ELEMENT) {
203                 sb.append("</").append(in.getLocalName()).append('>');
204
205                 if (in.getLocalName().equals(anyXmlElementName)) {
206                     break;
207                 }
208
209             } else if (eventType == XMLStreamConstants.CHARACTERS) {
210                 sb.append(in.getText());
211             }
212         }
213
214         return sb.toString();
215     }
216
217     private void read(final XMLStreamReader in, final AbstractNodeDataWithSchema parent, final String rootElement)
218             throws XMLStreamException, URISyntaxException, ParserConfigurationException, SAXException, IOException {
219         if (!in.hasNext()) {
220             return;
221         }
222
223         if (parent instanceof LeafNodeDataWithSchema || parent instanceof LeafListEntryNodeDataWithSchema) {
224             setValue(parent, in.getElementText().trim(), in.getNamespaceContext());
225             if (isNextEndDocument(in)) {
226                 return;
227             }
228
229             if (!isAtElement(in)) {
230                 in.nextTag();
231             }
232             return;
233         }
234
235         if (parent instanceof LeafListNodeDataWithSchema || parent instanceof ListNodeDataWithSchema) {
236             String xmlElementName = in.getLocalName();
237             while (xmlElementName.equals(parent.getSchema().getQName().getLocalName())) {
238                 read(in, newEntryNode(parent), rootElement);
239                 if (in.getEventType() == XMLStreamConstants.END_DOCUMENT) {
240                     break;
241                 }
242                 xmlElementName = in.getLocalName();
243             }
244
245             return;
246         }
247
248         if (parent instanceof AnyXmlNodeDataWithSchema) {
249             setValue(parent, readAnyXmlValue(in), in.getNamespaceContext());
250             if (isNextEndDocument(in)) {
251                 return;
252             }
253
254             if (!isAtElement(in)) {
255                 in.nextTag();
256             }
257
258             return;
259         }
260
261         switch (in.nextTag()) {
262             case XMLStreamConstants.START_ELEMENT:
263                 final Set<String> namesakes = new HashSet<>();
264                 while (in.hasNext()) {
265                     final String xmlElementName = in.getLocalName();
266
267                     DataSchemaNode parentSchema = parent.getSchema();
268
269                     final String parentSchemaName = parentSchema.getQName().getLocalName();
270                     if (parentSchemaName.equals(xmlElementName)
271                             && in.getEventType() == XMLStreamConstants.END_ELEMENT) {
272                         if (isNextEndDocument(in)) {
273                             break;
274                         }
275
276                         if (!isAtElement(in)) {
277                             in.nextTag();
278                         }
279                         break;
280                     }
281
282                     if (in.isEndElement() && rootElement.equals(xmlElementName)) {
283                         break;
284                     }
285
286                     if (parentSchema instanceof YangModeledAnyXmlSchemaNode) {
287                         parentSchema = ((YangModeledAnyXmlSchemaNode) parentSchema).getSchemaOfAnyXmlData();
288                     }
289
290                     if (!namesakes.add(xmlElementName)) {
291                         final Location loc = in.getLocation();
292                         throw new IllegalStateException(String.format(
293                                 "Duplicate element \"%s\" in XML input at: line %s column %s", xmlElementName,
294                                 loc.getLineNumber(), loc.getColumnNumber()));
295                     }
296
297                     final String xmlElementNamespace = in.getNamespaceURI();
298                     final Deque<DataSchemaNode> childDataSchemaNodes =
299                             ParserStreamUtils.findSchemaNodeByNameAndNamespace(parentSchema, xmlElementName,
300                                     new URI(xmlElementNamespace));
301
302                     if (childDataSchemaNodes.isEmpty()) {
303                         Preconditions.checkState(!strictParsing,
304                                 "Schema for node with name %s and namespace %s doesn't exist.", xmlElementName,
305                                 xmlElementNamespace);
306                         skipUnknownNode(in);
307                         continue;
308                     }
309
310                     read(in, ((CompositeNodeDataWithSchema) parent).addChild(childDataSchemaNodes), rootElement);
311                 }
312                 break;
313             case XMLStreamConstants.END_ELEMENT:
314                 if (isNextEndDocument(in)) {
315                     break;
316                 }
317
318                 if (!isAtElement(in)) {
319                     in.nextTag();
320                 }
321                 break;
322             default:
323                 break;
324         }
325     }
326
327     private static boolean isNextEndDocument(final XMLStreamReader in) throws XMLStreamException {
328         return in.next() == XMLStreamConstants.END_DOCUMENT;
329     }
330
331     private static boolean isAtElement(final XMLStreamReader in) {
332         return in.getEventType() == XMLStreamConstants.START_ELEMENT
333                 || in.getEventType() == XMLStreamConstants.END_ELEMENT;
334     }
335
336     private static void skipUnknownNode(final XMLStreamReader in) throws XMLStreamException {
337         // in case when the unknown node and at least one of its descendant nodes have the same name
338         // we cannot properly reach the end just by checking if the current node is an end element and has the same name
339         // as the root unknown element. therefore we ignore the names completely and just track the level of nesting
340         int levelOfNesting = 0;
341         while (in.hasNext()) {
342             // in case there are text characters in an element, we cannot skip them by calling nextTag()
343             // therefore we skip them by calling next(), and then proceed to next element
344             in.next();
345             if (!isAtElement(in)) {
346                 in.nextTag();
347             }
348             if (in.isStartElement()) {
349                 levelOfNesting++;
350             }
351
352             if (in.isEndElement()) {
353                 if (levelOfNesting == 0) {
354                     break;
355                 }
356
357                 levelOfNesting--;
358             }
359         }
360
361         in.nextTag();
362     }
363
364     private void setValue(final AbstractNodeDataWithSchema parent, final String value, final NamespaceContext nsContext)
365             throws ParserConfigurationException, SAXException, IOException {
366         Preconditions.checkArgument(parent instanceof SimpleNodeDataWithSchema, "Node %s is not a simple type",
367                 parent.getSchema().getQName());
368         final SimpleNodeDataWithSchema parentSimpleNode = (SimpleNodeDataWithSchema) parent;
369         Preconditions.checkArgument(parentSimpleNode.getValue() == null, "Node '%s' has already set its value to '%s'",
370                 parentSimpleNode.getSchema().getQName(), parentSimpleNode.getValue());
371
372         parentSimpleNode.setValue(translateValueByType(value, parentSimpleNode.getSchema(), nsContext));
373     }
374
375     private Object translateValueByType(final String value, final DataSchemaNode node,
376             final NamespaceContext namespaceCtx) throws IOException, SAXException, ParserConfigurationException {
377         if (node instanceof AnyXmlSchemaNode) {
378             /*
379              *  FIXME: Figure out some YANG extension dispatch, which will
380              *  reuse JSON parsing or XML parsing - anyxml is not well-defined in
381              * JSON.
382              */
383             final Document doc = UntrustedXML.newDocumentBuilder().parse(new InputSource(new StringReader(value)));
384             doc.normalize();
385
386             return new DOMSource(doc.getDocumentElement());
387         }
388
389         Preconditions.checkArgument(node instanceof TypedSchemaNode);
390         return codecs.codecFor((TypedSchemaNode) node).parseValue(namespaceCtx, value);
391     }
392
393     private static AbstractNodeDataWithSchema newEntryNode(final AbstractNodeDataWithSchema parent) {
394         final AbstractNodeDataWithSchema newChild;
395         if (parent instanceof ListNodeDataWithSchema) {
396             newChild = new ListEntryNodeDataWithSchema(parent.getSchema());
397         } else {
398             newChild = new LeafListEntryNodeDataWithSchema(parent.getSchema());
399         }
400         ((CompositeNodeDataWithSchema) parent).addChild(newChild);
401         return newChild;
402     }
403
404     @Override
405     public void close() throws IOException {
406         writer.flush();
407         writer.close();
408     }
409
410     @Override
411     public void flush() throws IOException {
412         writer.flush();
413     }
414 }