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