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
index e5bd7a21b235b86938418570bee85a4c9fab62d6..e239845e23a1396684df605754a989b56679f957 100644 (file)
@@ -7,42 +7,61 @@
  */
 package org.opendaylight.yangtools.yang.data.codec.xml;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ClassToInstanceMap;
+import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
-import java.io.StringWriter;
-import java.util.Map;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
 import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.concepts.ObjectExtensions;
+import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadataStreamWriter;
+import org.opendaylight.yangtools.rfc7952.data.api.OpaqueAnydataStreamWriter;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
-import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamAttributeWriter;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriterExtension;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.OpaqueAnydataExtension;
+import org.opendaylight.yangtools.yang.data.impl.codec.SchemaTracker;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
-import org.w3c.dom.Element;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.w3c.dom.Node;
 
 /**
- * A {@link NormalizedNodeStreamWriter} which translates the events into an {@link XMLStreamWriter},
- * resulting in a RFC 6020 XML encoding. There are 2 versions of this class, one that takes a
- * SchemaContext and encodes values appropriately according to the yang schema. The other is
- * schema-less and merely outputs values using toString. The latter is intended for debugging
- * where doesn't have a SchemaContext available and isn't meant for production use.
+ * A {@link NormalizedNodeStreamWriter} which translates the events into an {@link XMLStreamWriter}, resulting in an
+ * RFC6020 XML encoding. There are 2 versions of this class, one that takes a SchemaContext and encodes values
+ * appropriately according to the YANG schema. The other is schema-less and merely outputs values using toString. The
+ * latter is intended for debugging where doesn't have a SchemaContext available and isn't meant for production use.
+ *
+ * <p>
+ * Due to backwards compatibility reasons this writer recognizes RFC7952 metadata include keys QNames with empty URI
+ * (as exposed via {@link XmlParserStream#LEGACY_ATTRIBUTE_NAMESPACE}) as their QNameModule. These indicate an
+ * unqualified XML attribute and their value can be assumed to be a String. Furthermore, this extends to qualified
+ * attributes, which uses the proper namespace, but will not bind to a proper module revision. This caveat will be
+ * removed in a future version.
  */
-public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements NormalizedNodeStreamAttributeWriter {
-    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
+public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements NormalizedNodeStreamWriter,
+        NormalizedMetadataStreamWriter, OpaqueAnydataExtension {
+    private static final Logger LOG = LoggerFactory.getLogger(XMLStreamNormalizedNodeStreamWriter.class);
+    private static final Set<String> BROKEN_ATTRIBUTES = ConcurrentHashMap.newKeySet();
+
+    @SuppressWarnings("rawtypes")
+    static final ObjectExtensions.Factory<XMLStreamNormalizedNodeStreamWriter, NormalizedNodeStreamWriter,
+        NormalizedNodeStreamWriterExtension> EXTENSIONS_BUILDER = ObjectExtensions.factory(
+            XMLStreamNormalizedNodeStreamWriter.class, NormalizedMetadataStreamWriter.class,
+            OpaqueAnydataExtension.class);
 
     private final @NonNull StreamWriterFacade facade;
 
@@ -57,8 +76,22 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
      * @param context Associated {@link SchemaContext}.
      * @return A new {@link NormalizedNodeStreamWriter}
      */
-    public static NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context) {
-        return create(writer, context, SchemaPath.ROOT);
+    public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer,
+            final SchemaContext context) {
+        return create(writer, context, context);
+    }
+
+    /**
+     * Create a new writer with the specified context and rooted at the specified node.
+     *
+     * @param writer Output {@link XMLStreamWriter}
+     * @param context Associated {@link SchemaContext}.
+     * @param rootNode Root node
+     * @return A new {@link NormalizedNodeStreamWriter}
+     */
+    public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context,
+            final DataNodeContainer rootNode) {
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context, SchemaTracker.create(rootNode));
     }
 
     /**
@@ -67,12 +100,11 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
      * @param writer Output {@link XMLStreamWriter}
      * @param context Associated {@link SchemaContext}.
      * @param path path
-     *
      * @return A new {@link NormalizedNodeStreamWriter}
      */
     public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer, final SchemaContext context,
             final SchemaPath path) {
-        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context, path);
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context, SchemaTracker.create(context, path));
     }
 
     /**
@@ -87,47 +119,29 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
         return new SchemalessXMLStreamNormalizedNodeStreamWriter(writer);
     }
 
-    /**
-     * Utility method for formatting an {@link Element} to a string.
-     *
-     * @deprecated This method not used anywhere, users are advised to use their own formatting.
-     */
-    @Deprecated
-    public static String toString(final Element xml) {
-        try {
-            final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
-            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
-
-            final StreamResult result = new StreamResult(new StringWriter());
-            transformer.transform(new DOMSource(xml), result);
-
-            return result.getWriter().toString();
-        } catch (IllegalArgumentException | TransformerException e) {
-            throw new IllegalStateException("Unable to serialize xml element " + xml, e);
-        }
+    @Override
+    public final ClassToInstanceMap<NormalizedNodeStreamWriterExtension> getExtensions() {
+        return EXTENSIONS_BUILDER.newInstance(this);
     }
 
-    abstract void writeValue(@NonNull ValueWriter xmlWriter, QName qname, @NonNull Object value, T context)
-            throws IOException, XMLStreamException;
+    abstract T startAnydata(NodeIdentifier name);
 
     abstract void startList(NodeIdentifier name);
 
     abstract void startListItem(PathArgument name) throws IOException;
 
-    final void writeElement(final QName qname, final Object value, final @Nullable Map<QName, String> attributes,
-            final T context) throws IOException {
-        startElement(qname);
-        if (attributes != null) {
-            writeAttributes(attributes);
-        }
-        if (value != null) {
-            try {
-                writeValue(facade, qname, value, context);
-            } catch (XMLStreamException e) {
-                throw new IOException("Failed to write value", e);
-            }
+    abstract String encodeAnnotationValue(@NonNull ValueWriter xmlWriter, @NonNull QName qname, @NonNull Object value)
+            throws XMLStreamException;
+
+    abstract String encodeValue(@NonNull ValueWriter xmlWriter, @NonNull Object value, T context)
+            throws XMLStreamException;
+
+    final void writeValue(final @NonNull Object value, final T context) throws IOException {
+        try {
+            facade.writeCharacters(encodeValue(facade, value, context));
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to write value", e);
         }
-        endElement();
     }
 
     final void startElement(final QName qname) throws IOException {
@@ -146,55 +160,22 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
         }
     }
 
-    final void anyxmlNode(final QName qname, final Object value) throws IOException {
-        if (value != null) {
-            checkArgument(value instanceof DOMSource, "AnyXML value must be DOMSource, not %s", value);
-            final DOMSource domSource = (DOMSource) value;
+    final void anyxmlValue(final DOMSource domSource) throws IOException {
+        if (domSource != null) {
             final Node domNode = requireNonNull(domSource.getNode());
-            checkArgument(domNode.getNodeName().equals(qname.getLocalName()));
-            checkArgument(domNode.getNamespaceURI().equals(qname.getNamespace().toString()));
-
             try {
                 facade.writeStreamReader(new DOMSourceXMLStreamReader(domSource));
             } catch (XMLStreamException e) {
-                throw new IOException("Unable to transform anyXml(" + qname + ") value: " + domNode, e);
+                throw new IOException("Unable to transform anyXml value: " + domNode, e);
             }
         }
     }
 
-    @Override
-    public final void startContainerNode(final NodeIdentifier name, final int childSizeHint,
-                                         final Map<QName, String> attributes) throws IOException {
-        startContainerNode(name, childSizeHint);
-        writeAttributes(attributes);
-    }
-
-    @Override
-    public final void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint,
-                                                 final Map<QName, String> attributes) throws IOException {
-        startYangModeledAnyXmlNode(name, childSizeHint);
-        writeAttributes(attributes);
-    }
-
-    @Override
-    public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint,
-                                           final Map<QName, String> attributes) throws IOException {
-        startUnkeyedListItem(name, childSizeHint);
-        writeAttributes(attributes);
-    }
-
     @Override
     public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
         startListItem(name);
     }
 
-    @Override
-    public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint,
-                                        final Map<QName, String> attributes) throws IOException {
-        startMapEntryNode(identifier, childSizeHint);
-        writeAttributes(attributes);
-    }
-
     @Override
     public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
             throws IOException {
@@ -234,12 +215,91 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
         }
     }
 
-    private void writeAttributes(final @NonNull Map<QName, String> attributes) throws IOException {
-        if (!attributes.isEmpty()) {
+    @Override
+    public final void metadata(final ImmutableMap<QName, Object> attributes) throws IOException {
+        for (final Entry<QName, Object> entry : attributes.entrySet()) {
+            final QName qname = entry.getKey();
+            final String namespace = qname.getNamespace().toString();
+            final String localName = qname.getLocalName();
+            final Object value = entry.getValue();
+
+            // FIXME: remove this handling once we have complete mapping to metadata
             try {
-                facade.writeAttributes(attributes);
+                if (namespace.isEmpty()) {
+                    // Legacy attribute, which is expected to be a String
+                    StreamWriterFacade.warnLegacyAttribute(localName);
+                    if (!(value instanceof String)) {
+                        if (BROKEN_ATTRIBUTES.add(localName)) {
+                            LOG.warn("Unbound annotation {} does not have a String value, ignoring it. Please fix the "
+                                    + "source of this annotation either by formatting it to a String or removing its "
+                                    + "use", localName, new Throwable("Call stack"));
+                        }
+                        LOG.debug("Ignoring annotation {} value {}", localName, value);
+                    } else {
+                        facade.writeAttribute(localName, (String) value);
+                        continue;
+                    }
+                } else {
+                    final String prefix = facade.getPrefix(qname.getNamespace(), namespace);
+                    final String attrValue = encodeAnnotationValue(facade, qname, value);
+                    facade.writeAttribute(prefix, namespace, localName, attrValue);
+                }
             } catch (final XMLStreamException e) {
-                throw new IOException("Unable to emit attributes " + attributes, e);
+                throw new IOException("Unable to emit attribute " + qname, e);
+            }
+        }
+    }
+
+    @Override
+    public final OpaqueAnydataExtension.StreamWriter startOpaqueAnydataNode(final NodeIdentifier name,
+            final boolean accurateLists) throws IOException {
+        final T schema = startAnydata(name);
+        startElement(name.getNodeType());
+        return new XMLOpaqueStreamWriter(schema);
+    }
+
+    private final class XMLOpaqueStreamWriter implements OpaqueAnydataStreamWriter {
+        private final Deque<Boolean> stack = new ArrayDeque<>();
+        private final T valueContext;
+
+        XMLOpaqueStreamWriter(final T valueContext) {
+            this.valueContext = valueContext;
+        }
+
+        @Override
+        public void startOpaqueList(final NodeIdentifier name, final int childSizeHint) throws IOException {
+            stack.push(Boolean.FALSE);
+        }
+
+        @Override
+        public void startOpaqueContainer(final NodeIdentifier name, final int childSizeHint) throws IOException {
+            stack.push(Boolean.TRUE);
+            startElement(name.getNodeType());
+        }
+
+        @Override
+        public void opaqueValue(final Object value) throws IOException {
+            writeValue(value, valueContext);
+        }
+
+        @Override
+        public void endOpaqueNode() throws IOException {
+            if (stack.pop()) {
+                endElement();
+            }
+        }
+
+        @Override
+        public boolean requireMetadataFirst() {
+            return true;
+        }
+
+        @Override
+        public void metadata(final ImmutableMap<QName, Object> metadata) throws IOException {
+            if (stack.peek()) {
+                XMLStreamNormalizedNodeStreamWriter.this.metadata(metadata);
+            } else {
+                LOG.debug("Ignoring metadata {}", metadata);
             }
         }
     }