19c05c251c1395f9a4c1a72ed71a08e4953e06a4
[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.io.IOException;
14 import java.net.URI;
15 import java.util.Set;
16 import java.util.concurrent.ConcurrentHashMap;
17 import javax.xml.XMLConstants;
18 import javax.xml.namespace.NamespaceContext;
19 import javax.xml.stream.XMLStreamConstants;
20 import javax.xml.stream.XMLStreamException;
21 import javax.xml.stream.XMLStreamReader;
22 import javax.xml.stream.XMLStreamWriter;
23 import org.opendaylight.yangtools.yang.common.QName;
24 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedAnydata;
25 import org.opendaylight.yangtools.yang.data.util.SingleChildDataNodeContainer;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 /**
30  * The sole implementation of {@link ValueWriter}, tasked with synchronizing access to XMLStreamWriter state. The only
31  * class referencing this class should be {@link XMLStreamNormalizedNodeStreamWriter}.
32  */
33 final class StreamWriterFacade extends ValueWriter {
34     private static final Logger LOG = LoggerFactory.getLogger(StreamWriterFacade.class);
35     private static final Set<String> BROKEN_NAMESPACES = ConcurrentHashMap.newKeySet();
36     private static final Set<String> LEGACY_ATTRIBUTES = ConcurrentHashMap.newKeySet();
37
38     private final XMLStreamWriter writer;
39     private final RandomPrefix prefixes;
40
41     // QName of an element we delayed emitting. This only happens if it is a naked element, without any attributes,
42     // namespace declarations or value.
43     private QName openElement;
44
45     StreamWriterFacade(final XMLStreamWriter writer) {
46         this.writer = requireNonNull(writer);
47         prefixes = new RandomPrefix(writer.getNamespaceContext());
48     }
49
50     void writeCharacters(final String text) throws XMLStreamException {
51         if (!Strings.isNullOrEmpty(text)) {
52             flushElement();
53             writer.writeCharacters(text);
54         }
55     }
56
57     @Override
58     void writeNamespace(final String prefix, final String namespaceURI) throws XMLStreamException {
59         flushElement();
60         writer.writeNamespace(prefix, namespaceURI);
61     }
62
63     @Override
64     void writeAttribute(final String localName, final String value) throws XMLStreamException {
65         flushElement();
66         writer.writeAttribute(localName, value);
67     }
68
69     @Override
70     void writeAttribute(final String prefix, final String namespaceURI, final String localName, final String value)
71             throws XMLStreamException {
72         flushElement();
73         writer.writeAttribute(prefix, namespaceURI, localName, value);
74     }
75
76     @Override
77     NamespaceContext getNamespaceContext() {
78         // Accessing namespace context is okay, because a delayed element is known to have no effect on the result
79         return writer.getNamespaceContext();
80     }
81
82     private void flushElement() throws XMLStreamException {
83         if (openElement != null) {
84             writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, openElement.getLocalName(),
85                 openElement.getNamespace().toString());
86             openElement = null;
87         }
88     }
89
90     void writeStartElement(final QName qname) throws XMLStreamException {
91         flushElement();
92
93         final String namespace = qname.getNamespace().toString();
94         final NamespaceContext context = writer.getNamespaceContext();
95         final boolean reuseNamespace;
96         if (context != null) {
97             reuseNamespace = namespace.equals(context.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX));
98         } else {
99             reuseNamespace = XMLConstants.DEFAULT_NS_PREFIX.equals(writer.getPrefix(namespace));
100         }
101
102         if (!reuseNamespace) {
103             writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, qname.getLocalName(), namespace);
104             writer.writeDefaultNamespace(namespace);
105         } else {
106             openElement = qname;
107         }
108     }
109
110     void writeEndElement() throws XMLStreamException {
111         if (openElement != null) {
112             writer.writeEmptyElement(XMLConstants.DEFAULT_NS_PREFIX, openElement.getLocalName(),
113                 openElement.getNamespace().toString());
114             openElement = null;
115         } else {
116             writer.writeEndElement();
117         }
118     }
119
120     String getPrefix(final URI uri, final String str) throws XMLStreamException {
121         final String prefix = writer.getPrefix(str);
122         if (prefix != null) {
123             return prefix;
124         }
125
126         // This is needed to recover from attributes emitted while the namespace was not declared. Ordinarily
127         // attribute namespaces would be bound in the writer, so the resulting XML is efficient, but we cannot rely
128         // on that having been done.
129         if (BROKEN_NAMESPACES.add(str)) {
130             LOG.info("Namespace {} was not bound, please fix the caller", str, new Throwable());
131         }
132
133         return prefixes.encodePrefix(uri);
134     }
135
136     void close() throws XMLStreamException {
137         // Mighty careful stepping here, we must end up closing the writer
138         XMLStreamException failure = null;
139         try {
140             flushElement();
141         } catch (XMLStreamException e) {
142             failure = e;
143             throw e;
144         } finally {
145             try {
146                 writer.close();
147             } catch (XMLStreamException e) {
148                 if (failure != null) {
149                     failure.addSuppressed(e);
150                 } else {
151                     throw e;
152                 }
153             }
154         }
155     }
156
157     void flush() throws XMLStreamException {
158         flushElement();
159         writer.flush();
160     }
161
162     void anydataWriteStreamReader(final XMLStreamReader reader) throws XMLStreamException {
163         flushElement();
164
165         // Do not emit top-level element
166         int depth = 0;
167         while (reader.hasNext()) {
168             final int event = reader.next();
169             switch (event) {
170                 case XMLStreamConstants.START_ELEMENT:
171                     if (depth != 0) {
172                         forwardStartElement(reader);
173                     } else {
174                         // anydata: forward namespaces only, skipping the default namespace
175                         for (int i = 0; i < reader.getNamespaceCount(); ++i) {
176                             final String prefix = reader.getNamespacePrefix(i);
177                             if (!XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
178                                 writer.writeNamespace(prefix, reader.getNamespaceURI(i));
179                             }
180                         }
181                     }
182                     ++depth;
183                     break;
184                 case XMLStreamConstants.END_ELEMENT:
185                     --depth;
186                     if (depth != 0) {
187                         writer.writeEndElement();
188                     }
189                     break;
190                 case XMLStreamConstants.CHARACTERS:
191                     writer.writeCharacters(reader.getText());
192                     break;
193                 case XMLStreamConstants.COMMENT:
194                 case XMLStreamConstants.SPACE:
195                     // Ignore comments and insignificant whitespace
196                     break;
197                 case XMLStreamConstants.START_DOCUMENT:
198                 case XMLStreamConstants.END_DOCUMENT:
199                     // We are embedded: ignore start/end document events
200                     break;
201                 case XMLStreamConstants.ATTRIBUTE:
202                     forwardAttributes(reader);
203                     break;
204                 case XMLStreamConstants.CDATA:
205                     writer.writeCData(reader.getText());
206                     break;
207                 case XMLStreamConstants.NAMESPACE:
208                     forwardNamespaces(reader);
209                     break;
210                 case XMLStreamConstants.DTD:
211                 case XMLStreamConstants.NOTATION_DECLARATION:
212                 case XMLStreamConstants.ENTITY_DECLARATION:
213                 case XMLStreamConstants.ENTITY_REFERENCE:
214                 case XMLStreamConstants.PROCESSING_INSTRUCTION:
215                 default:
216                     throw new IllegalStateException("Unhandled event " + event);
217             }
218         }
219     }
220
221     void anyxmlWriteStreamReader(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
222         flushElement();
223
224         // Do not emit top-level element
225         int depth = 0;
226         while (reader.hasNext()) {
227             final int event = reader.next();
228             switch (event) {
229                 case XMLStreamConstants.START_ELEMENT:
230                     if (depth != 0) {
231                         forwardStartElement(reader);
232                     } else {
233                         forwardNamespaces(reader);
234                         // anyxml, hence we need to forward attributes
235                         forwardAttributes(reader);
236                     }
237                     ++depth;
238                     break;
239                 case XMLStreamConstants.END_ELEMENT:
240                     --depth;
241                     if (depth != 0) {
242                         writer.writeEndElement();
243                     }
244                     break;
245                 case XMLStreamConstants.PROCESSING_INSTRUCTION:
246                     forwardProcessingInstruction(reader);
247                     break;
248                 case XMLStreamConstants.CHARACTERS:
249                     writer.writeCharacters(reader.getText());
250                     break;
251                 case XMLStreamConstants.COMMENT:
252                     writer.writeComment(reader.getText());
253                     break;
254                 case XMLStreamConstants.SPACE:
255                     // Ignore insignificant whitespace
256                     break;
257                 case XMLStreamConstants.START_DOCUMENT:
258                 case XMLStreamConstants.END_DOCUMENT:
259                     // We are embedded: ignore start/end document events
260                     break;
261                 case XMLStreamConstants.ENTITY_REFERENCE:
262                     writer.writeEntityRef(reader.getLocalName());
263                     break;
264                 case XMLStreamConstants.ATTRIBUTE:
265                     forwardAttributes(reader);
266                     break;
267                 case XMLStreamConstants.DTD:
268                     writer.writeDTD(reader.getText());
269                     break;
270                 case XMLStreamConstants.CDATA:
271                     writer.writeCData(reader.getText());
272                     break;
273                 case XMLStreamConstants.NAMESPACE:
274                     forwardNamespaces(reader);
275                     break;
276                 case XMLStreamConstants.NOTATION_DECLARATION:
277                 case XMLStreamConstants.ENTITY_DECLARATION:
278                 default:
279                     throw new IllegalStateException("Unhandled event " + event);
280             }
281         }
282     }
283
284     void emitNormalizedAnydata(final NormalizedAnydata anydata) throws XMLStreamException {
285         flushElement();
286         try {
287             anydata.writeTo(XMLStreamNormalizedNodeStreamWriter.create(writer, anydata.getSchemaContext(),
288                 new SingleChildDataNodeContainer(anydata.getContextNode())));
289         } catch (IOException e) {
290             throw new XMLStreamException("Failed to emit anydata " + anydata, e);
291         }
292     }
293
294     static void warnLegacyAttribute(final String localName) {
295         if (LEGACY_ATTRIBUTES.add(localName)) {
296             LOG.info("Encountered annotation {} not bound to module. Please examine the call stack and fix this "
297                     + "warning by defining a proper YANG annotation to cover it", localName,
298                     new Throwable("Call stack"));
299         }
300     }
301
302     private void forwardAttributes(final XMLStreamReader reader) throws XMLStreamException {
303         for (int i = 0, count = reader.getAttributeCount(); i < count; ++i) {
304             final String localName = reader.getAttributeLocalName(i);
305             final String value = reader.getAttributeValue(i);
306             final String prefix = reader.getAttributePrefix(i);
307             if (prefix != null) {
308                 writer.writeAttribute(prefix, reader.getAttributeNamespace(i), localName, value);
309             } else {
310                 writer.writeAttribute(localName, value);
311             }
312         }
313     }
314
315     private void forwardNamespaces(final XMLStreamReader reader) throws XMLStreamException {
316         for (int i = 0; i < reader.getNamespaceCount(); ++i) {
317             writer.writeNamespace(reader.getNamespacePrefix(i), reader.getNamespaceURI(i));
318         }
319     }
320
321     private void forwardProcessingInstruction(final XMLStreamReader reader) throws XMLStreamException {
322         final String target = reader.getPITarget();
323         final String data = reader.getPIData();
324         if (data != null) {
325             writer.writeProcessingInstruction(target, data);
326         } else {
327             writer.writeProcessingInstruction(target);
328         }
329     }
330
331     private void forwardStartElement(final XMLStreamReader reader) throws XMLStreamException {
332         final String localName = reader.getLocalName();
333         final String prefix = reader.getPrefix();
334         if (prefix != null) {
335             writer.writeStartElement(prefix, localName, reader.getNamespaceURI());
336         } else {
337             writer.writeStartElement(localName);
338         }
339
340         forwardNamespaces(reader);
341         forwardAttributes(reader);
342     }
343 }