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