Bug 1441: Bug fixes, clean-up and test migration
[yangtools.git] / yang / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / XMLStreamNormalizedNodeStreamWriter.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.codec.xml;
9
10 import com.google.common.base.Preconditions;
11 import java.io.IOException;
12 import java.io.StringWriter;
13 import java.util.Map;
14 import javax.annotation.Nonnull;
15 import javax.annotation.Nullable;
16 import javax.xml.XMLConstants;
17 import javax.xml.namespace.NamespaceContext;
18 import javax.xml.stream.XMLStreamException;
19 import javax.xml.stream.XMLStreamWriter;
20 import javax.xml.transform.OutputKeys;
21 import javax.xml.transform.Transformer;
22 import javax.xml.transform.TransformerException;
23 import javax.xml.transform.TransformerFactory;
24 import javax.xml.transform.TransformerFactoryConfigurationError;
25 import javax.xml.transform.dom.DOMSource;
26 import javax.xml.transform.stax.StAXResult;
27 import javax.xml.transform.stream.StreamResult;
28 import org.opendaylight.yangtools.yang.common.QName;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
32 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamAttributeWriter;
33 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
34 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
35 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.w3c.dom.Element;
39
40 /**
41  * A {@link NormalizedNodeStreamWriter} which translates the events into an {@link XMLStreamWriter},
42  * resulting in a RFC 6020 XML encoding. There are 2 versions of this class, one that takes a
43  * SchemaContext and encodes values appropriately according to the yang schema. The other is
44  * schema-less and merely outputs values using toString. The latter is intended for debugging
45  * where doesn't have a SchemaContext available and isn't meant for production use.
46  */
47 public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements NormalizedNodeStreamAttributeWriter {
48     private static final Logger LOG = LoggerFactory.getLogger(XMLStreamNormalizedNodeStreamWriter.class);
49     private static final String COM_SUN_TRANSFORMER = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl";
50
51     private static final TransformerFactory TRANSFORMER_FACTORY;
52     static {
53         TransformerFactory f = TransformerFactory.newInstance();
54         if (!f.getFeature(StAXResult.FEATURE)) {
55             LOG.warn("Platform-default TransformerFactory {} does not support StAXResult, attempting fallback to {}",
56                     f, COM_SUN_TRANSFORMER);
57             f = TransformerFactory.newInstance(COM_SUN_TRANSFORMER, null);
58             if (!f.getFeature(StAXResult.FEATURE)) {
59                 throw new TransformerFactoryConfigurationError("No TransformerFactory supporting StAXResult found.");
60             }
61         }
62
63         TRANSFORMER_FACTORY = f;
64     }
65
66     final XMLStreamWriter writer;
67
68     XMLStreamNormalizedNodeStreamWriter(final XMLStreamWriter writer) {
69         this.writer = Preconditions.checkNotNull(writer);
70     }
71
72     /**
73      * Create a new writer with the specified context as its root.
74      *
75      * @param writer Output {@link XMLStreamWriter}
76      * @param context Associated {@link SchemaContext}.
77      * @return A new {@link NormalizedNodeStreamWriter}
78      */
79     public static NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context) {
80         return create( writer, context, SchemaPath.ROOT);
81     }
82
83     /**
84      * Create a new writer with the specified context and rooted in the specified schema path
85      *
86      * @param writer Output {@link XMLStreamWriter}
87      * @param context Associated {@link SchemaContext}.
88      * @param path path
89      *
90      * @return A new {@link NormalizedNodeStreamWriter}
91      */
92     public static NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context,
93             final SchemaPath path) {
94         return SchemaAwareXMLStreamNormalizedNodeStreamWriter.newInstance(writer, context, path);
95     }
96
97     /**
98      * Create a new schema-less writer. Note that this version is intended for debugging
99      * where doesn't have a SchemaContext available and isn't meant for production use.
100      *
101      * @param writer Output {@link XMLStreamWriter}
102      *
103      * @return A new {@link NormalizedNodeStreamWriter}
104      */
105     public static NormalizedNodeStreamWriter createSchemaless(final XMLStreamWriter writer) {
106         return SchemalessXMLStreamNormalizedNodeStreamWriter.newInstance(writer);
107     }
108
109     abstract void writeAttributes(@Nonnull final Map<QName, String> attributes) throws IOException;
110
111     abstract void writeValue(final XMLStreamWriter xmlWriter, final QName qname,
112             @Nonnull final Object value, T context) throws IOException, XMLStreamException;
113
114     abstract void startList(final NodeIdentifier name);
115
116     abstract void startListItem(final PathArgument name) throws IOException;
117
118     abstract void endNode(XMLStreamWriter xmlWriter) throws IOException, XMLStreamException;
119
120     private void writeStartElement(final QName qname) throws XMLStreamException {
121         String ns = qname.getNamespace().toString();
122         writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, qname.getLocalName(), ns);
123         if (writer.getNamespaceContext() != null) {
124             String parentNs = writer.getNamespaceContext().getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX);
125             if (!ns.equals(parentNs)) {
126                 writer.writeDefaultNamespace(ns);
127             }
128         }
129     }
130
131     void writeElement(final QName qname, final Object value, @Nullable final Map<QName, String> attributes,
132             final T context) throws IOException {
133         try {
134             writeStartElement(qname);
135
136             writeAttributes(attributes);
137             if (value != null) {
138                 writeValue(writer, qname, value, context);
139             }
140             writer.writeEndElement();
141         } catch (XMLStreamException e) {
142             throw new IOException("Failed to emit element", e);
143         }
144     }
145
146     void startElement(final QName qname) throws IOException {
147         try {
148             writeStartElement(qname);
149         } catch (XMLStreamException e) {
150             throw new IOException("Failed to start element", e);
151         }
152     }
153
154     void anyxmlNode(final QName qname, final Object value) throws IOException {
155         if (value != null) {
156             Preconditions.checkArgument(value instanceof DOMSource, "AnyXML value must be DOMSource, not %s", value);
157             final DOMSource domSource = (DOMSource) value;
158             Preconditions.checkNotNull(domSource.getNode());
159             Preconditions.checkArgument(domSource.getNode().getNodeName().equals(qname.getLocalName()));
160             Preconditions.checkArgument(domSource.getNode().getNamespaceURI().equals(qname.getNamespace().toString()));
161             try {
162                 // TODO can the transformer be a constant ? is it thread safe ?
163                 final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
164                 // Writer has to be wrapped in a wrapper that ignores endDocument event
165                 // EndDocument event forbids any other modification to the writer so a nested anyXml breaks serialization
166                 transformer.transform(domSource, new StAXResult(new DelegateWriterNoEndDoc(writer)));
167             } catch (final TransformerException e) {
168                 throw new IOException("Unable to transform anyXml(" + qname + ") value: " + value, e);
169             }
170         }
171     }
172
173     @Override
174     public final void startContainerNode(final NodeIdentifier name, final int childSizeHint,
175                                          final Map<QName, String> attributes) throws IOException {
176         startContainerNode(name, childSizeHint);
177         writeAttributes(attributes);
178     }
179
180     @Override
181     public final void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint,
182                                                  final Map<QName, String> attributes) throws IOException {
183         startYangModeledAnyXmlNode(name, childSizeHint);
184         writeAttributes(attributes);
185     }
186
187     @Override
188     public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint,
189                                            final Map<QName, String> attributes) throws IOException {
190         startUnkeyedListItem(name, childSizeHint);
191         writeAttributes(attributes);
192     }
193
194     @Override
195     public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint,
196                                         final Map<QName, String> attributes) throws IOException {
197         startMapEntryNode(identifier, childSizeHint);
198         writeAttributes(attributes);
199     }
200
201     @Override
202     public final void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) {
203         startList(name);
204     }
205
206     @Override
207     public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
208         startListItem(name);
209     }
210
211     @Override
212     public final void startMapNode(final NodeIdentifier name, final int childSizeHint) {
213         startList(name);
214     }
215
216     @Override
217     public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
218             throws IOException {
219         startListItem(identifier);
220     }
221
222     @Override
223     public final void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) {
224         startList(name);
225     }
226
227     public static String toString(final Element xml) {
228         try {
229             final Transformer transformer = TransformerFactory.newInstance().newTransformer();
230             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
231
232             final StreamResult result = new StreamResult(new StringWriter());
233             final DOMSource source = new DOMSource(xml);
234             transformer.transform(source, result);
235
236             return result.getWriter().toString();
237         } catch (IllegalArgumentException | TransformerFactoryConfigurationError | TransformerException e) {
238             throw new RuntimeException("Unable to serialize xml element " + xml, e);
239         }
240     }
241
242     @Override
243     public final void endNode() throws IOException {
244         try {
245             endNode(writer);
246         } catch (XMLStreamException e) {
247             throw new IOException("Failed to end element", e);
248         }
249     }
250
251     @Override
252     public final void close() throws IOException {
253         try {
254             writer.close();
255         } catch (XMLStreamException e) {
256             throw new IOException("Failed to close writer", e);
257         }
258     }
259
260     @Override
261     public final void flush() throws IOException {
262         try {
263             writer.flush();
264         } catch (XMLStreamException e) {
265             throw new IOException("Failed to flush writer", e);
266         }
267     }
268
269     /**
270      * Delegate writer that ignores writeEndDocument event. Used for AnyXml serialization.
271      */
272     private static final class DelegateWriterNoEndDoc implements XMLStreamWriter {
273         private final XMLStreamWriter writer;
274
275         public DelegateWriterNoEndDoc(final XMLStreamWriter writer) {
276             this.writer = writer;
277         }
278
279         @Override
280         public void writeStartElement(final String localName) throws XMLStreamException {
281             writer.writeStartElement(localName);
282         }
283
284         @Override
285         public void writeStartElement(final String namespaceURI, final String localName) throws XMLStreamException {
286             writer.writeStartElement(namespaceURI, localName);
287         }
288
289         @Override
290         public void writeStartElement(final String prefix, final String localName, final String namespaceURI)
291                 throws XMLStreamException {
292             writer.writeStartElement(prefix, localName, namespaceURI);
293         }
294
295         @Override
296         public void writeEmptyElement(final String namespaceURI, final String localName) throws XMLStreamException {
297             writer.writeEmptyElement(namespaceURI, localName);
298         }
299
300         @Override
301         public void writeEmptyElement(final String prefix, final String localName, final String namespaceURI)
302                 throws XMLStreamException {
303             writer.writeEmptyElement(prefix, localName, namespaceURI);
304         }
305
306         @Override
307         public void writeEmptyElement(final String localName) throws XMLStreamException {
308             writer.writeEmptyElement(localName);
309         }
310
311         @Override
312         public void writeEndElement() throws XMLStreamException {
313             writer.writeEndElement();
314
315         }
316
317         @Override
318         public void writeEndDocument() throws XMLStreamException {
319             // End document is disabled
320         }
321
322         @Override
323         public void close() throws XMLStreamException {
324             writer.close();
325         }
326
327         @Override
328         public void flush() throws XMLStreamException {
329             writer.flush();
330         }
331
332         @Override
333         public void writeAttribute(final String localName, final String value) throws XMLStreamException {
334             writer.writeAttribute(localName, value);
335         }
336
337         @Override
338         public void writeAttribute(final String prefix, final String namespaceURI, final String localName,
339                                    final String value) throws XMLStreamException {
340             writer.writeAttribute(prefix, namespaceURI, localName, value);
341         }
342
343         @Override
344         public void writeAttribute(final String namespaceURI, final String localName, final String value)
345                 throws XMLStreamException {
346             writer.writeAttribute(namespaceURI, localName, value);
347         }
348
349         @Override
350         public void writeNamespace(final String prefix, final String namespaceURI) throws XMLStreamException {
351             // Workaround for default namespace
352             // If a namespace is not prefixed, it is is still treated as prefix namespace.
353             // This results in the NamespaceSupport class ignoring the namespace since xmlns is not a valid prefix
354             // Write the namespace at least as an attribute
355             // TODO this is a hotfix, the transformer itself should write namespaces passing the namespace
356             // in writeStartElement method
357             if (prefix.equals("xml") || prefix.equals("xmlns")) {
358                 writer.writeAttribute(prefix, namespaceURI);
359             } else {
360                 writer.writeNamespace(prefix, namespaceURI);
361             }
362         }
363
364         @Override
365         public void writeDefaultNamespace(final String namespaceURI) throws XMLStreamException {
366             writer.writeDefaultNamespace(namespaceURI);
367         }
368
369         @Override
370         public void writeComment(final String data) throws XMLStreamException {
371             writer.writeComment(data);
372         }
373
374         @Override
375         public void writeProcessingInstruction(final String target) throws XMLStreamException {
376             writer.writeProcessingInstruction(target);
377         }
378
379         @Override
380         public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException {
381             writer.writeProcessingInstruction(target, data);
382         }
383
384         @Override
385         public void writeCData(final String data) throws XMLStreamException {
386             writer.writeCData(data);
387         }
388
389         @Override
390         public void writeDTD(final String dtd) throws XMLStreamException {
391             writer.writeDTD(dtd);
392         }
393
394         @Override
395         public void writeEntityRef(final String name) throws XMLStreamException {
396             writer.writeEntityRef(name);
397         }
398
399         @Override
400         public void writeStartDocument() throws XMLStreamException {
401         }
402
403         @Override
404         public void writeStartDocument(final String version) throws XMLStreamException {
405         }
406
407         @Override
408         public void writeStartDocument(final String encoding, final String version) throws XMLStreamException {
409         }
410
411         @Override
412         public void writeCharacters(final String text) throws XMLStreamException {
413             writer.writeCharacters(text);
414         }
415
416         @Override
417         public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException {
418             writer.writeCharacters(text, start, len);
419         }
420
421         @Override
422         public String getPrefix(final String uri) throws XMLStreamException {
423             return writer.getPrefix(uri);
424         }
425
426         @Override
427         public void setPrefix(final String prefix, final String uri) throws XMLStreamException {
428             // Disabled since it causes exceptions in the underlying writer
429         }
430
431         @Override
432         public void setDefaultNamespace(final String uri) throws XMLStreamException {
433             writer.setDefaultNamespace(uri);
434         }
435
436         @Override
437         public void setNamespaceContext(final NamespaceContext context) throws XMLStreamException {
438             writer.setNamespaceContext(context);
439         }
440
441         @Override
442         public NamespaceContext getNamespaceContext() {
443             return writer.getNamespaceContext();
444         }
445
446         @Override
447         public Object getProperty(final String name) {
448             return writer.getProperty(name);
449         }
450     }
451 }