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