Rework NormalizedNodeStreamWriter
[yangtools.git] / yang / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / StreamWriterFacade.java
1 /*
2  * Copyright (c) 2019 Pantheon Technologies, s.r.o. 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 java.util.Objects.requireNonNull;
11
12 import com.google.common.base.Strings;
13 import java.net.URI;
14 import java.util.Map;
15 import java.util.Map.Entry;
16 import java.util.Set;
17 import java.util.concurrent.ConcurrentHashMap;
18 import javax.xml.XMLConstants;
19 import javax.xml.namespace.NamespaceContext;
20 import javax.xml.stream.XMLStreamConstants;
21 import javax.xml.stream.XMLStreamException;
22 import javax.xml.stream.XMLStreamWriter;
23 import org.opendaylight.yangtools.yang.common.QName;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26
27 /**
28  * The sole implementation of {@link ValueWriter}, tasked with synchronizing access to XMLStreamWriter state. The only
29  * class referencing this class should be {@link XMLStreamNormalizedNodeStreamWriter}.
30  */
31 final class StreamWriterFacade extends ValueWriter {
32     private static final Logger LOG = LoggerFactory.getLogger(StreamWriterFacade.class);
33     private static final Set<String> BROKEN_NAMESPACES = ConcurrentHashMap.newKeySet();
34
35     private final XMLStreamWriter writer;
36     private final RandomPrefix prefixes;
37
38     // QName of an element we delayed emitting. This only happens if it is a naked element, without any attributes,
39     // namespace declarations or value.
40     private QName openElement;
41
42     StreamWriterFacade(final XMLStreamWriter writer) {
43         this.writer = requireNonNull(writer);
44         prefixes = new RandomPrefix(writer.getNamespaceContext());
45     }
46
47     @Override
48     void writeCharacters(final String text) throws XMLStreamException {
49         if (!Strings.isNullOrEmpty(text)) {
50             flushElement();
51             writer.writeCharacters(text);
52         }
53     }
54
55     @Override
56     void writeNamespace(final String prefix, final String namespaceURI) throws XMLStreamException {
57         flushElement();
58         writer.writeNamespace(prefix, namespaceURI);
59     }
60
61     @Override
62     void writeAttribute(final String localName, final String value) throws XMLStreamException {
63         flushElement();
64         writer.writeAttribute(localName, value);
65     }
66
67     @Override
68     void writeAttribute(final String prefix, final String namespaceURI, final String localName, final String value)
69             throws XMLStreamException {
70         flushElement();
71         writer.writeAttribute(prefix, namespaceURI, localName, value);
72     }
73
74     @Override
75     NamespaceContext getNamespaceContext() {
76         // Accessing namespace context is okay, because a delayed element is known to have no effect on the result
77         return writer.getNamespaceContext();
78     }
79
80     private void flushElement() throws XMLStreamException {
81         if (openElement != null) {
82             writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, openElement.getLocalName(),
83                 openElement.getNamespace().toString());
84             openElement = null;
85         }
86     }
87
88     void writeStartElement(final QName qname) throws XMLStreamException {
89         flushElement();
90
91         final String ns = qname.getNamespace().toString();
92         final NamespaceContext context = writer.getNamespaceContext();
93         final boolean reuseNamespace;
94         if (context != null) {
95             reuseNamespace = ns.equals(context.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX));
96         } else {
97             reuseNamespace = XMLConstants.DEFAULT_NS_PREFIX.equals(writer.getPrefix(ns));
98         }
99
100         if (!reuseNamespace) {
101             writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, qname.getLocalName(), ns);
102             writer.writeDefaultNamespace(ns);
103         } else {
104             openElement = qname;
105         }
106     }
107
108     void writeEndElement() throws XMLStreamException {
109         if (openElement != null) {
110             writer.writeEmptyElement(XMLConstants.DEFAULT_NS_PREFIX, openElement.getLocalName(),
111                 openElement.getNamespace().toString());
112             openElement = null;
113         } else {
114             writer.writeEndElement();
115         }
116     }
117
118     void writeAttributes(final Map<QName, String> attributes) throws XMLStreamException {
119         flushElement();
120         for (final Entry<QName, String> entry : attributes.entrySet()) {
121             final QName qname = entry.getKey();
122             final String namespace = qname.getNamespace().toString();
123
124             if (!Strings.isNullOrEmpty(namespace)) {
125                 final String prefix = getPrefix(qname.getNamespace(), namespace);
126                 writer.writeAttribute(prefix, namespace, qname.getLocalName(), entry.getValue());
127             } else {
128                 writer.writeAttribute(qname.getLocalName(), entry.getValue());
129             }
130         }
131     }
132
133     private String getPrefix(final URI uri, final String str) throws XMLStreamException {
134         final String prefix = writer.getPrefix(str);
135         if (prefix != null) {
136             return prefix;
137         }
138
139         // This is needed to recover from attributes emitted while the namespace was not declared. Ordinarily
140         // attribute namespaces would be bound in the writer, so the resulting XML is efficient, but we cannot rely
141         // on that having been done.
142         if (BROKEN_NAMESPACES.add(str)) {
143             LOG.info("Namespace {} was not bound, please fix the caller", str, new Throwable());
144         }
145
146         return prefixes.encodePrefix(uri);
147     }
148
149     void close() throws XMLStreamException {
150         // Mighty careful stepping here, we must end up closing the writer
151         XMLStreamException failure = null;
152         try {
153             flushElement();
154         } catch (XMLStreamException e) {
155             failure = e;
156             throw e;
157         } finally {
158             try {
159                 writer.close();
160             } catch (XMLStreamException e) {
161                 if (failure != null) {
162                     failure.addSuppressed(e);
163                 } else {
164                     throw e;
165                 }
166             }
167         }
168     }
169
170     void flush() throws XMLStreamException {
171         flushElement();
172         writer.flush();
173     }
174
175     void writeStreamReader(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
176         flushElement();
177
178         // We track depth, as we do not want to output the top-most element
179         int depth = 0;
180         while (reader.hasNext()) {
181             final int event = reader.next();
182             switch (event) {
183                 case XMLStreamConstants.START_ELEMENT:
184                     if (depth != 0) {
185                         forwardStartElement(reader);
186                     }
187                     ++depth;
188                     break;
189                 case XMLStreamConstants.END_ELEMENT:
190                     if (depth != 0) {
191                         writer.writeEndElement();
192                     }
193                     --depth;
194                     break;
195                 case XMLStreamConstants.PROCESSING_INSTRUCTION:
196                     forwardProcessingInstruction(reader);
197                     break;
198                 case XMLStreamConstants.CHARACTERS:
199                     writer.writeCharacters(reader.getText());
200                     break;
201                 case XMLStreamConstants.COMMENT:
202                     writer.writeComment(reader.getText());
203                     break;
204                 case XMLStreamConstants.SPACE:
205                     // Ignore insignificant whitespace
206                     break;
207                 case XMLStreamConstants.START_DOCUMENT:
208                 case XMLStreamConstants.END_DOCUMENT:
209                     // We are embedded: ignore start/end document events
210                     break;
211                 case XMLStreamConstants.ENTITY_REFERENCE:
212                     writer.writeEntityRef(reader.getLocalName());
213                     break;
214                 case XMLStreamConstants.ATTRIBUTE:
215                     forwardAttributes(reader);
216                     break;
217                 case XMLStreamConstants.DTD:
218                     writer.writeDTD(reader.getText());
219                     break;
220                 case XMLStreamConstants.CDATA:
221                     writer.writeCData(reader.getText());
222                     break;
223                 case XMLStreamConstants.NAMESPACE:
224                     forwardNamespaces(reader);
225                     break;
226                 case XMLStreamConstants.NOTATION_DECLARATION:
227                 case XMLStreamConstants.ENTITY_DECLARATION:
228                 default:
229                     throw new IllegalStateException("Unhandled event " + event);
230             }
231         }
232     }
233
234     private void forwardAttributes(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
235         for (int i = 0; i < reader.getAttributeCount(); ++i) {
236             final String localName = reader.getAttributeLocalName(i);
237             final String value = reader.getAttributeValue(i);
238             final String prefix = reader.getAttributePrefix(i);
239             if (prefix != null) {
240                 writer.writeAttribute(prefix, reader.getAttributeNamespace(i), localName, value);
241             } else {
242                 writer.writeAttribute(localName, value);
243             }
244         }
245     }
246
247     private void forwardNamespaces(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
248         for (int i = 0; i < reader.getNamespaceCount(); ++i) {
249             writer.writeNamespace(reader.getNamespacePrefix(i), reader.getNamespaceURI(i));
250         }
251     }
252
253     private void forwardProcessingInstruction(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
254         final String target = reader.getPITarget();
255         final String data = reader.getPIData();
256         if (data != null) {
257             writer.writeProcessingInstruction(target, data);
258         } else {
259             writer.writeProcessingInstruction(target);
260         }
261     }
262
263     private void forwardStartElement(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
264         final String localName = reader.getLocalName();
265         final String prefix = reader.getPrefix();
266         if (prefix != null) {
267             writer.writeStartElement(prefix, localName, reader.getNamespaceURI());
268         } else {
269             writer.writeStartElement(localName);
270         }
271
272         forwardNamespaces(reader);
273         forwardAttributes(reader);
274     }
275 }