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