Promote SchemaTracker to NormalizedNodeInferenceStack
[yangtools.git] / yang / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / XMLStreamNormalizedNodeStreamWriter.java
index fad67527a43301cd75aa278f582fcec7a1429d6b..f0b688cd14c20abffa91b42629df9c5321c88e6f 100644 (file)
  */
 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.base.Strings;
+import com.google.common.collect.ClassToInstanceMap;
+import com.google.common.collect.ImmutableClassToInstanceMap;
+import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
-import java.io.StringWriter;
-import java.net.URI;
-import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import javax.xml.XMLConstants;
-import javax.xml.namespace.NamespaceContext;
-import javax.xml.stream.XMLStreamConstants;
 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.rfc7952.data.api.StreamWriterMetadataExtension;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.YangConstants;
 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.NormalizedAnydata;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriterExtension;
+import org.opendaylight.yangtools.yang.data.util.NormalizedNodeInferenceStack;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.w3c.dom.Element;
 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 {
+public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements NormalizedNodeStreamWriter,
+        StreamWriterMetadataExtension {
     private static final Logger LOG = LoggerFactory.getLogger(XMLStreamNormalizedNodeStreamWriter.class);
-    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
-    private static final Set<String> BROKEN_NAMESPACES = ConcurrentHashMap.newKeySet();
+    private static final Set<String> BROKEN_ATTRIBUTES = ConcurrentHashMap.newKeySet();
 
-    private final @NonNull XMLStreamWriter writer;
-    private final RandomPrefix prefixes;
+    private final @NonNull StreamWriterFacade facade;
 
     XMLStreamNormalizedNodeStreamWriter(final XMLStreamWriter writer) {
-        this.writer = requireNonNull(writer);
-        this.prefixes = new RandomPrefix(writer.getNamespaceContext());
+        facade = new StreamWriterFacade(writer);
     }
 
     /**
      * Create a new writer with the specified context as its root.
      *
      * @param writer Output {@link XMLStreamWriter}
-     * @param context Associated {@link SchemaContext}.
+     * @param context Associated {@link EffectiveModelContext}.
+     * @return A new {@link NormalizedNodeStreamWriter}
+     */
+    public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer,
+            final EffectiveModelContext context) {
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context,
+            NormalizedNodeInferenceStack.of(context));
+    }
+
+    /**
+     * Create a new writer with the specified context and rooted at the specified node.
+     *
+     * @param writer Output {@link XMLStreamWriter}
+     * @param inference root node inference
+     * @return A new {@link NormalizedNodeStreamWriter}
+     */
+    public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer,
+            final EffectiveStatementInference inference) {
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, inference.getEffectiveModelContext(),
+            NormalizedNodeInferenceStack.of(inference));
+    }
+
+    /**
+     * Create a new writer with the specified context and rooted in the specified schema path.
+     *
+     * @param writer Output {@link XMLStreamWriter}
+     * @param context Associated {@link EffectiveModelContext}.
+     * @param path path
      * @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 EffectiveModelContext context, final SchemaPath path) {
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context,
+            NormalizedNodeInferenceStack.of(context, path));
     }
 
     /**
      * Create a new writer with the specified context and rooted in the specified schema path.
      *
      * @param writer Output {@link XMLStreamWriter}
-     * @param context Associated {@link SchemaContext}.
+     * @param context Associated {@link EffectiveModelContext}.
      * @param path path
+     * @return A new {@link NormalizedNodeStreamWriter}
+     */
+    public static @NonNull NormalizedNodeStreamWriter create(final XMLStreamWriter writer,
+            final EffectiveModelContext context, final Absolute path) {
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context,
+            NormalizedNodeInferenceStack.of(context, path));
+    }
+
+    /**
+     * Create a new writer with the specified context and rooted in the specified operation's input.
      *
+     * @param writer Output {@link XMLStreamWriter}
+     * @param context Associated {@link EffectiveModelContext}.
+     * @param operationPath Parent operation (RPC or action) 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);
+    public static @NonNull NormalizedNodeStreamWriter forInputOf(final XMLStreamWriter writer,
+            final EffectiveModelContext context, final Absolute operationPath) {
+        return forOperation(writer, context, operationPath,
+            YangConstants.operationInputQName(operationPath.lastNodeIdentifier().getModule()));
+    }
+
+    /**
+     * Create a new writer with the specified context and rooted in the specified operation's output.
+     *
+     * @param writer Output {@link XMLStreamWriter}
+     * @param context Associated {@link EffectiveModelContext}.
+     * @param operationPath Parent operation (RPC or action) path.
+     * @return A new {@link NormalizedNodeStreamWriter}
+     */
+    public static @NonNull NormalizedNodeStreamWriter forOutputOf(final XMLStreamWriter writer,
+            final EffectiveModelContext context, final Absolute operationPath) {
+        return forOperation(writer, context, operationPath,
+            YangConstants.operationOutputQName(operationPath.lastNodeIdentifier().getModule()));
+    }
+
+    private static @NonNull NormalizedNodeStreamWriter forOperation(final XMLStreamWriter writer,
+            final EffectiveModelContext context, final Absolute operationPath, final QName qname) {
+        return new SchemaAwareXMLStreamNormalizedNodeStreamWriter(writer, context,
+            NormalizedNodeInferenceStack.ofOperation(context, operationPath, qname));
     }
 
     /**
@@ -101,83 +162,34 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
         return new SchemalessXMLStreamNormalizedNodeStreamWriter(writer);
     }
 
-    abstract void writeValue(@NonNull XMLStreamWriter xmlWriter, QName qname, @NonNull Object value, T context)
-            throws IOException, XMLStreamException;
-
-    abstract void startList(NodeIdentifier name);
-
-    abstract void startListItem(PathArgument name) throws IOException;
-
-    private void writeAttributes(final @NonNull Map<QName, String> attributes) throws IOException {
-        for (final Entry<QName, String> entry : attributes.entrySet()) {
-            try {
-                final QName qname = entry.getKey();
-                final String namespace = qname.getNamespace().toString();
-
-                if (!Strings.isNullOrEmpty(namespace)) {
-                    final String prefix = getPrefix(qname.getNamespace(), namespace);
-                    writer.writeAttribute(prefix, namespace, qname.getLocalName(), entry.getValue());
-                } else {
-                    writer.writeAttribute(qname.getLocalName(), entry.getValue());
-                }
-            } catch (final XMLStreamException e) {
-                throw new IOException("Unable to emit attribute " + entry, e);
-            }
-        }
+    @Override
+    public final ClassToInstanceMap<NormalizedNodeStreamWriterExtension> getExtensions() {
+        return ImmutableClassToInstanceMap.of(StreamWriterMetadataExtension.class, this);
     }
 
-    private String getPrefix(final URI uri, final String str) throws XMLStreamException {
-        final String prefix = writer.getPrefix(str);
-        if (prefix != null) {
-            return prefix;
-        }
+    abstract void startAnydata(NodeIdentifier name);
 
-        // This is needed to recover from attributes emitted while the namespace was not declared. Ordinarily
-        // attribute namespaces would be bound in the writer, so the resulting XML is efficient, but we cannot rely
-        // on that having been done.
-        if (BROKEN_NAMESPACES.add(str)) {
-            LOG.info("Namespace {} was not bound, please fix the caller", str, new Throwable());
-        }
+    abstract void startList(NodeIdentifier name);
 
-        return prefixes.encodePrefix(uri);
-    }
+    abstract void startListItem(PathArgument name) throws IOException;
 
-    private void writeStartElement(final QName qname) throws XMLStreamException {
-        final String ns = qname.getNamespace().toString();
-        final NamespaceContext context = writer.getNamespaceContext();
-        final boolean needDefaultNs;
-        if (context != null) {
-            final String parentNs = context.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX);
-            needDefaultNs = !ns.equals(parentNs);
-        } else {
-            needDefaultNs = false;
-        }
+    abstract String encodeAnnotationValue(@NonNull ValueWriter xmlWriter, @NonNull QName qname, @NonNull Object value)
+            throws XMLStreamException;
 
-        writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, qname.getLocalName(), ns);
-        if (needDefaultNs) {
-            writer.writeDefaultNamespace(ns);
-        }
-    }
+    abstract String encodeValue(@NonNull ValueWriter xmlWriter, @NonNull Object value, T context)
+            throws XMLStreamException;
 
-    final void writeElement(final QName qname, final Object value, final @Nullable Map<QName, String> attributes,
-            final T context) throws IOException {
+    final void writeValue(final @NonNull Object value, final T context) throws IOException {
         try {
-            writeStartElement(qname);
-            if (attributes != null) {
-                writeAttributes(attributes);
-            }
-            if (value != null) {
-                writeValue(writer, qname, value, context);
-            }
-            writer.writeEndElement();
+            facade.writeCharacters(encodeValue(facade, value, context));
         } catch (XMLStreamException e) {
-            throw new IOException("Failed to emit element", e);
+            throw new IOException("Failed to write value", e);
         }
     }
 
     final void startElement(final QName qname) throws IOException {
         try {
-            writeStartElement(qname);
+            facade.writeStartElement(qname);
         } catch (XMLStreamException e) {
             throw new IOException("Failed to start element", e);
         }
@@ -185,138 +197,39 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
 
     final void endElement() throws IOException {
         try {
-            writer.writeEndElement();
+            facade.writeEndElement();
         } catch (XMLStreamException e) {
             throw new IOException("Failed to end element", e);
         }
     }
 
-    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 Node domNode = requireNonNull(domSource.getNode());
-            checkArgument(domNode.getNodeName().equals(qname.getLocalName()));
-            checkArgument(domNode.getNamespaceURI().equals(qname.getNamespace().toString()));
-
+    final void anydataValue(final Object value) throws IOException {
+        if (value instanceof DOMSourceAnydata) {
             try {
-                writeStreamReader(new DOMSourceXMLStreamReader(domSource));
+                facade.anydataWriteStreamReader(((DOMSourceAnydata) value).toStreamReader());
             } catch (XMLStreamException e) {
-                throw new IOException("Unable to transform anyXml(" + qname + ") value: " + domNode, e);
-            }
-        }
-    }
-
-    private void writeStreamReader(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
-        while (reader.hasNext()) {
-            final int event = reader.next();
-            switch (event) {
-                case XMLStreamConstants.START_ELEMENT:
-                    forwardStartElement(reader);
-                    break;
-                case XMLStreamConstants.END_ELEMENT:
-                    writer.writeEndElement();
-                    break;
-                case XMLStreamConstants.PROCESSING_INSTRUCTION:
-                    forwardProcessingInstruction(reader);
-                    break;
-                case XMLStreamConstants.CHARACTERS:
-                    writer.writeCharacters(reader.getText());
-                    break;
-                case XMLStreamConstants.COMMENT:
-                    writer.writeComment(reader.getText());
-                    break;
-                case XMLStreamConstants.SPACE:
-                    // Ignore insignificant whitespace
-                    break;
-                case XMLStreamConstants.START_DOCUMENT:
-                case XMLStreamConstants.END_DOCUMENT:
-                    // We are embedded: ignore start/end document events
-                    break;
-                case XMLStreamConstants.ENTITY_REFERENCE:
-                    writer.writeEntityRef(reader.getLocalName());
-                    break;
-                case XMLStreamConstants.ATTRIBUTE:
-                    forwardAttributes(reader);
-                    break;
-                case XMLStreamConstants.DTD:
-                    writer.writeDTD(reader.getText());
-                    break;
-                case XMLStreamConstants.CDATA:
-                    writer.writeCData(reader.getText());
-                    break;
-                case XMLStreamConstants.NAMESPACE:
-                    forwardNamespaces(reader);
-                    break;
-                case XMLStreamConstants.NOTATION_DECLARATION:
-                case XMLStreamConstants.ENTITY_DECLARATION:
-                default:
-                    throw new IllegalStateException("Unhandled event " + event);
+                throw new IOException("Unable to transform anydata value: " + value, e);
             }
-        }
-    }
-
-    private void forwardAttributes(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
-        for (int i = 0; i < reader.getAttributeCount(); ++i) {
-            final String localName = reader.getAttributeLocalName(i);
-            final String value = reader.getAttributeValue(i);
-            final String prefix = reader.getAttributePrefix(i);
-            if (prefix != null) {
-                writer.writeAttribute(prefix, reader.getAttributeNamespace(i), localName, value);
-            } else {
-                writer.writeAttribute(localName, value);
+        } else if (value instanceof NormalizedAnydata) {
+            try {
+                facade.emitNormalizedAnydata((NormalizedAnydata) value);
+            } catch (XMLStreamException e) {
+                throw new IOException("Unable to emit anydata value: " + value, e);
             }
-        }
-    }
-
-    private void forwardNamespaces(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
-        for (int i = 0; i < reader.getNamespaceCount(); ++i) {
-            writer.writeNamespace(reader.getNamespacePrefix(i), reader.getNamespaceURI(i));
-        }
-    }
-
-    private void forwardProcessingInstruction(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
-        final String target = reader.getPITarget();
-        final String data = reader.getPIData();
-        if (data != null) {
-            writer.writeProcessingInstruction(target, data);
         } else {
-            writer.writeProcessingInstruction(target);
+            throw new IllegalStateException("Unexpected anydata value " + value);
         }
     }
 
-    private void forwardStartElement(final DOMSourceXMLStreamReader reader) throws XMLStreamException {
-        final String localName = reader.getLocalName();
-        final String prefix = reader.getPrefix();
-        if (prefix != null) {
-            writer.writeStartElement(prefix, localName, reader.getNamespaceURI());
-        } else {
-            writer.writeStartElement(localName);
+    final void anyxmlValue(final DOMSource domSource) throws IOException {
+        if (domSource != null) {
+            final Node domNode = requireNonNull(domSource.getNode());
+            try {
+                facade.anyxmlWriteStreamReader(new DOMSourceXMLStreamReader(domSource));
+            } catch (XMLStreamException e) {
+                throw new IOException("Unable to transform anyXml value: " + domNode, e);
+            }
         }
-
-        forwardNamespaces(reader);
-        forwardAttributes(reader);
-    }
-
-    @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
@@ -324,13 +237,6 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
         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 {
@@ -352,24 +258,10 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
         startList(name);
     }
 
-    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 void close() throws IOException {
         try {
-            writer.close();
+            facade.close();
         } catch (XMLStreamException e) {
             throw new IOException("Failed to close writer", e);
         }
@@ -378,9 +270,55 @@ public abstract class XMLStreamNormalizedNodeStreamWriter<T> implements Normaliz
     @Override
     public final void flush() throws IOException {
         try {
-            writer.flush();
+            facade.flush();
         } catch (XMLStreamException e) {
             throw new IOException("Failed to flush writer", e);
         }
     }
+
+    @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 {
+                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 attribute " + qname, e);
+            }
+        }
+    }
+
+    @Override
+    public final boolean startAnydataNode(final NodeIdentifier name, final Class<?> objectModel) throws IOException {
+        if (DOMSourceAnydata.class.isAssignableFrom(objectModel)
+                || NormalizedAnydata.class.isAssignableFrom(objectModel)) {
+            startAnydata(name);
+            startElement(name.getNodeType());
+            return true;
+        }
+        return false;
+    }
 }