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