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