Add support for opaque anydata XML output
[yangtools.git] / yang / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / XMLStreamNormalizedNodeStreamWriter.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. 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.collect.ClassToInstanceMap;
13 import com.google.common.collect.ImmutableMap;
14 import java.io.IOException;
15 import java.util.ArrayDeque;
16 import java.util.Deque;
17 import java.util.Map.Entry;
18 import java.util.Set;
19 import java.util.concurrent.ConcurrentHashMap;
20 import javax.xml.stream.XMLStreamException;
21 import javax.xml.stream.XMLStreamWriter;
22 import javax.xml.transform.dom.DOMSource;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.opendaylight.yangtools.concepts.ObjectExtensions;
25 import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadataStreamWriter;
26 import org.opendaylight.yangtools.rfc7952.data.api.OpaqueAnydataStreamWriter;
27 import org.opendaylight.yangtools.yang.common.QName;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
32 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriterExtension;
33 import org.opendaylight.yangtools.yang.data.api.schema.stream.OpaqueAnydataExtension;
34 import org.opendaylight.yangtools.yang.data.impl.codec.SchemaTracker;
35 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
36 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
37 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40 import org.w3c.dom.Node;
41
42 /**
43  * A {@link NormalizedNodeStreamWriter} which translates the events into an {@link XMLStreamWriter}, resulting in an
44  * RFC6020 XML encoding. There are 2 versions of this class, one that takes a SchemaContext and encodes values
45  * appropriately according to the YANG schema. The other is schema-less and merely outputs values using toString. The
46  * latter is intended for debugging where doesn't have a SchemaContext available and isn't meant for production use.
47  *
48  * <p>
49  * Due to backwards compatibility reasons this writer recognizes RFC7952 metadata include keys QNames with empty URI
50  * (as exposed via {@link XmlParserStream#LEGACY_ATTRIBUTE_NAMESPACE}) as their QNameModule. These indicate an
51  * unqualified XML attribute and their value can be assumed to be a String. Furthermore, this extends to qualified
52  * attributes, which uses the proper namespace, but will not bind to a proper module revision. This caveat will be
53  * removed in a future version.
54  */
55 public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements NormalizedNodeStreamWriter,
56         NormalizedMetadataStreamWriter, OpaqueAnydataExtension {
57     private static final Logger LOG = LoggerFactory.getLogger(XMLStreamNormalizedNodeStreamWriter.class);
58     private static final Set<String> BROKEN_ATTRIBUTES = ConcurrentHashMap.newKeySet();
59
60     @SuppressWarnings("rawtypes")
61     static final ObjectExtensions.Factory<XMLStreamNormalizedNodeStreamWriter, NormalizedNodeStreamWriter,
62         NormalizedNodeStreamWriterExtension> EXTENSIONS_BUILDER = ObjectExtensions.factory(
63             XMLStreamNormalizedNodeStreamWriter.class, NormalizedMetadataStreamWriter.class,
64             OpaqueAnydataExtension.class);
65
66     private final @NonNull StreamWriterFacade facade;
67
68     XMLStreamNormalizedNodeStreamWriter(final XMLStreamWriter writer) {
69         facade = new StreamWriterFacade(writer);
70     }
71
72     /**
73      * Create a new writer with the specified context as its root.
74      *
75      * @param writer Output {@link XMLStreamWriter}
76      * @param context Associated {@link SchemaContext}.
77      * @return A new {@link NormalizedNodeStreamWriter}
78      */
79     public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer,
80             final SchemaContext context) {
81         return create(writer, context, context);
82     }
83
84     /**
85      * Create a new writer with the specified context and rooted at the specified node.
86      *
87      * @param writer Output {@link XMLStreamWriter}
88      * @param context Associated {@link SchemaContext}.
89      * @param rootNode Root node
90      * @return A new {@link NormalizedNodeStreamWriter}
91      */
92     public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context,
93             final DataNodeContainer rootNode) {
94         return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context, SchemaTracker.create(rootNode));
95     }
96
97     /**
98      * Create a new writer with the specified context and rooted in the specified schema path.
99      *
100      * @param writer Output {@link XMLStreamWriter}
101      * @param context Associated {@link SchemaContext}.
102      * @param path path
103      * @return A new {@link NormalizedNodeStreamWriter}
104      */
105     public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context,
106             final SchemaPath path) {
107         return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context, SchemaTracker.create(context, path));
108     }
109
110     /**
111      * Create a new schema-less writer. Note that this version is intended for debugging
112      * where doesn't have a SchemaContext available and isn't meant for production use.
113      *
114      * @param writer Output {@link XMLStreamWriter}
115      *
116      * @return A new {@link NormalizedNodeStreamWriter}
117      */
118     public static @NonNull NormalizedNodeStreamWriter createSchemaless(final XMLStreamWriter writer) {
119         return new SchemalessXMLStreamNormalizedNodeStreamWriter(writer);
120     }
121
122     @Override
123     public final ClassToInstanceMap<NormalizedNodeStreamWriterExtension> getExtensions() {
124         return EXTENSIONS_BUILDER.newInstance(this);
125     }
126
127     abstract T startAnydata(NodeIdentifier name);
128
129     abstract void startList(NodeIdentifier name);
130
131     abstract void startListItem(PathArgument name) throws IOException;
132
133     abstract String encodeAnnotationValue(@NonNull ValueWriter xmlWriter, @NonNull QName qname, @NonNull Object value)
134             throws XMLStreamException;
135
136     abstract String encodeValue(@NonNull ValueWriter xmlWriter, @NonNull Object value, T context)
137             throws XMLStreamException;
138
139     final void writeValue(final @NonNull Object value, final T context) throws IOException {
140         try {
141             facade.writeCharacters(encodeValue(facade, value, context));
142         } catch (XMLStreamException e) {
143             throw new IOException("Failed to write value", e);
144         }
145     }
146
147     final void startElement(final QName qname) throws IOException {
148         try {
149             facade.writeStartElement(qname);
150         } catch (XMLStreamException e) {
151             throw new IOException("Failed to start element", e);
152         }
153     }
154
155     final void endElement() throws IOException {
156         try {
157             facade.writeEndElement();
158         } catch (XMLStreamException e) {
159             throw new IOException("Failed to end element", e);
160         }
161     }
162
163     final void anyxmlValue(final DOMSource domSource) throws IOException {
164         if (domSource != null) {
165             final Node domNode = requireNonNull(domSource.getNode());
166             try {
167                 facade.writeStreamReader(new DOMSourceXMLStreamReader(domSource));
168             } catch (XMLStreamException e) {
169                 throw new IOException("Unable to transform anyXml value: " + domNode, e);
170             }
171         }
172     }
173
174     @Override
175     public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
176         startListItem(name);
177     }
178
179     @Override
180     public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
181             throws IOException {
182         startListItem(identifier);
183     }
184
185     @Override
186     public final void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) {
187         startList(name);
188     }
189
190     @Override
191     public final void startMapNode(final NodeIdentifier name, final int childSizeHint) {
192         startList(name);
193     }
194
195     @Override
196     public final void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) {
197         startList(name);
198     }
199
200     @Override
201     public final void close() throws IOException {
202         try {
203             facade.close();
204         } catch (XMLStreamException e) {
205             throw new IOException("Failed to close writer", e);
206         }
207     }
208
209     @Override
210     public final void flush() throws IOException {
211         try {
212             facade.flush();
213         } catch (XMLStreamException e) {
214             throw new IOException("Failed to flush writer", e);
215         }
216     }
217
218     @Override
219     public final void metadata(final ImmutableMap<QName, Object> attributes) throws IOException {
220         for (final Entry<QName, Object> entry : attributes.entrySet()) {
221             final QName qname = entry.getKey();
222             final String namespace = qname.getNamespace().toString();
223             final String localName = qname.getLocalName();
224             final Object value = entry.getValue();
225
226             // FIXME: remove this handling once we have complete mapping to metadata
227             try {
228                 if (namespace.isEmpty()) {
229                     // Legacy attribute, which is expected to be a String
230                     StreamWriterFacade.warnLegacyAttribute(localName);
231                     if (!(value instanceof String)) {
232                         if (BROKEN_ATTRIBUTES.add(localName)) {
233                             LOG.warn("Unbound annotation {} does not have a String value, ignoring it. Please fix the "
234                                     + "source of this annotation either by formatting it to a String or removing its "
235                                     + "use", localName, new Throwable("Call stack"));
236                         }
237                         LOG.debug("Ignoring annotation {} value {}", localName, value);
238                     } else {
239                         facade.writeAttribute(localName, (String) value);
240                         continue;
241                     }
242                 } else {
243                     final String prefix = facade.getPrefix(qname.getNamespace(), namespace);
244                     final String attrValue = encodeAnnotationValue(facade, qname, value);
245                     facade.writeAttribute(prefix, namespace, localName, attrValue);
246                 }
247             } catch (final XMLStreamException e) {
248                 throw new IOException("Unable to emit attribute " + qname, e);
249             }
250         }
251     }
252
253     @Override
254     public final OpaqueAnydataExtension.StreamWriter startOpaqueAnydataNode(final NodeIdentifier name,
255             final boolean accurateLists) throws IOException {
256         final T schema = startAnydata(name);
257         startElement(name.getNodeType());
258         return new XMLOpaqueStreamWriter(schema);
259     }
260
261     private final class XMLOpaqueStreamWriter implements OpaqueAnydataStreamWriter {
262         private final Deque<Boolean> stack = new ArrayDeque<>();
263         private final T valueContext;
264
265         XMLOpaqueStreamWriter(final T valueContext) {
266             this.valueContext = valueContext;
267         }
268
269         @Override
270         public void startOpaqueList(final NodeIdentifier name, final int childSizeHint) throws IOException {
271             stack.push(Boolean.FALSE);
272         }
273
274         @Override
275         public void startOpaqueContainer(final NodeIdentifier name, final int childSizeHint) throws IOException {
276             stack.push(Boolean.TRUE);
277             startElement(name.getNodeType());
278         }
279
280         @Override
281         public void opaqueValue(final Object value) throws IOException {
282             writeValue(value, valueContext);
283         }
284
285         @Override
286         public void endOpaqueNode() throws IOException {
287             if (stack.pop()) {
288                 endElement();
289             }
290         }
291
292         @Override
293         public boolean requireMetadataFirst() {
294             return true;
295         }
296
297         @Override
298         public void metadata(final ImmutableMap<QName, Object> metadata) throws IOException {
299             if (stack.peek()) {
300                 XMLStreamNormalizedNodeStreamWriter.this.metadata(metadata);
301             } else {
302                 LOG.debug("Ignoring metadata {}", metadata);
303             }
304         }
305     }
306 }