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