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