Add support for opaque anydata XML input 66/82166/23
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 20 May 2019 06:19:18 +0000 (08:19 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 21 May 2019 12:59:35 +0000 (14:59 +0200)
This patch adds preliminary support for streaming anydata nodes
into opaque values.

JIRA: YANGTOOLS-991
Change-Id: Iebe67aaf32cc7dde6c54541e8036f9a47544f573
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
yang/yang-data-codec-xml/src/main/java/org/opendaylight/yangtools/yang/data/codec/xml/XmlParserStream.java
yang/yang-data-codec-xml/src/test/java/org/opendaylight/yangtools/yang/data/codec/xml/AnydataParseTest.java [new file with mode: 0644]
yang/yang-data-util/src/main/java/org/opendaylight/yangtools/yang/data/util/AbstractAnydataNodeDataWithSchema.java [new file with mode: 0644]
yang/yang-data-util/src/main/java/org/opendaylight/yangtools/yang/data/util/OpaqueAnydataNodeDataWithSchema.java [new file with mode: 0644]

index 80958a3a0e2836a43d4acfbb70969504b90e188c..8db667bd735e7fb1559db5b119fdb6771e2c75b0 100644 (file)
@@ -42,10 +42,16 @@ import javax.xml.transform.TransformerFactoryConfigurationError;
 import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stax.StAXSource;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.yangtools.odlext.model.api.YangModeledAnyXmlSchemaNode;
+import org.opendaylight.yangtools.rfc7952.data.util.AbstractImmutableOpaqueAnydataStreamWriter;
+import org.opendaylight.yangtools.rfc7952.data.util.ImmutableNormalizedMetadata;
 import org.opendaylight.yangtools.rfc7952.model.api.AnnotationSchemaNode;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.opaque.OpaqueData;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.util.AbstractNodeDataWithSchema;
 import org.opendaylight.yangtools.yang.data.util.AnyXmlNodeDataWithSchema;
@@ -56,10 +62,12 @@ import org.opendaylight.yangtools.yang.data.util.LeafListNodeDataWithSchema;
 import org.opendaylight.yangtools.yang.data.util.LeafNodeDataWithSchema;
 import org.opendaylight.yangtools.yang.data.util.ListEntryNodeDataWithSchema;
 import org.opendaylight.yangtools.yang.data.util.ListNodeDataWithSchema;
+import org.opendaylight.yangtools.yang.data.util.OpaqueAnydataNodeDataWithSchema;
 import org.opendaylight.yangtools.yang.data.util.OperationAsContainer;
 import org.opendaylight.yangtools.yang.data.util.ParserStreamUtils;
 import org.opendaylight.yangtools.yang.data.util.SimpleNodeDataWithSchema;
 import org.opendaylight.yangtools.yang.data.util.YangModeledAnyXmlNodeDataWithSchema;
+import org.opendaylight.yangtools.yang.model.api.AnyDataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
@@ -230,6 +238,8 @@ public final class XmlParserStream implements Closeable, Flushable {
                 nodeDataWithSchema = new LeafNodeDataWithSchema((LeafSchemaNode) parentNode);
             } else if (parentNode instanceof LeafListSchemaNode) {
                 nodeDataWithSchema = new LeafListNodeDataWithSchema((LeafListSchemaNode) parentNode);
+            } else if (parentNode instanceof AnyDataSchemaNode) {
+                nodeDataWithSchema = new OpaqueAnydataNodeDataWithSchema((AnyDataSchemaNode) parentNode);
             } else {
                 throw new IllegalStateException("Unsupported schema node type " + parentNode.getClass() + ".");
             }
@@ -331,6 +341,45 @@ public final class XmlParserStream implements Closeable, Flushable {
         return (Document) result.getNode();
     }
 
+    private void readOpaqueAnydataValue(final XMLStreamReader in, final OpaqueAnydataNodeDataWithSchema parent)
+            throws XMLStreamException {
+        final DefaultOpaqueAnydataStreamWriter opaqueWriter = new DefaultOpaqueAnydataStreamWriter();
+        final Entry<OpaqueData, ImmutableNormalizedMetadata> result;
+        while (true) {
+            final int event = in.next();
+            try {
+                switch (event) {
+                    case XMLStreamConstants.START_ELEMENT:
+                        final String nsUri = in.getNamespaceURI();
+                        final QNameModule module = resolveXmlNamespace(nsUri).orElseGet(() -> rawXmlNamespace(nsUri));
+                        opaqueWriter.startOpaqueContainer(NodeIdentifier.create(QName.create(module,
+                            in.getLocalName())));
+                        break;
+                    case XMLStreamConstants.END_ELEMENT:
+                        opaqueWriter.endOpaqueNode();
+                        break;
+                    case XMLStreamConstants.CHARACTERS:
+                        opaqueWriter.opaqueValue(in.getText());
+                        break;
+                    default:
+                        LOG.debug("Ignoring event {}", event);
+                        continue;
+                }
+            } catch (IOException e) {
+                throw new XMLStreamException("Inconsistent anydata stream", e);
+            }
+
+            final Entry<OpaqueData, ImmutableNormalizedMetadata> optResult = opaqueWriter.result();
+            if (optResult != null) {
+                result = optResult;
+                break;
+            }
+        }
+
+        setValue(parent, result.getKey(), in.getNamespaceContext());
+        parent.setMetadata(result.getValue());
+    }
+
     private void read(final XMLStreamReader in, final AbstractNodeDataWithSchema<?> parent, final String rootElement)
             throws XMLStreamException {
         if (!in.hasNext()) {
@@ -381,6 +430,20 @@ public final class XmlParserStream implements Closeable, Flushable {
             return;
         }
 
+        if (parent instanceof OpaqueAnydataNodeDataWithSchema) {
+            parent.setAttributes(getElementAttributes(in));
+            readOpaqueAnydataValue(in, (OpaqueAnydataNodeDataWithSchema) parent);
+            if (isNextEndDocument(in)) {
+                return;
+            }
+
+            if (!isAtElement(in)) {
+                in.nextTag();
+            }
+
+            return;
+        }
+
         if (parent instanceof YangModeledAnyXmlSchemaNode) {
             parent.setAttributes(getElementAttributes(in));
         }
@@ -511,15 +574,17 @@ public final class XmlParserStream implements Closeable, Flushable {
     private Object translateValueByType(final Object value, final DataSchemaNode node,
             final NamespaceContext namespaceCtx) {
         if (node instanceof AnyXmlSchemaNode) {
-
             checkArgument(value instanceof Document);
             /*
-             *  FIXME: Figure out some YANG extension dispatch, which will
-             *  reuse JSON parsing or XML parsing - anyxml is not well-defined in
-             * JSON.
+             * FIXME: Figure out some YANG extension dispatch, which will reuse JSON parsing or XML parsing -
+             *        anyxml is not well-defined in JSON.
              */
             return new DOMSource(((Document) value).getDocumentElement());
         }
+        if (node instanceof AnyDataSchemaNode) {
+            checkArgument(value instanceof OpaqueData, "Unexpected anydata value %s", value);
+            return value;
+        }
 
         checkArgument(node instanceof TypedDataSchemaNode);
         checkArgument(value instanceof String);
@@ -559,4 +624,28 @@ public final class XmlParserStream implements Closeable, Flushable {
     private QNameModule rawXmlNamespace(final String xmlNamespace) {
         return rawNamespaces.computeIfAbsent(xmlNamespace, nsUri -> QNameModule.create(URI.create(nsUri)));
     }
+
+    // Utility writer just to keep the event model intact. This requires us to build an intermediate representation,
+    // simply because we are required to perform list coalescence.
+    //
+    // FIXME: 5.0.0: we should be able to negotiate downstream's tolerance to reentrant lists, so that we are not
+    //               not required to keep all the book keeping. Immutable builder should not care as long as structure
+    //               is kept...
+    private static final class DefaultOpaqueAnydataStreamWriter extends AbstractImmutableOpaqueAnydataStreamWriter {
+        private Entry<@NonNull OpaqueData, @Nullable ImmutableNormalizedMetadata> result;
+
+        DefaultOpaqueAnydataStreamWriter() {
+            super(false);
+        }
+
+        Entry<@NonNull OpaqueData, @Nullable ImmutableNormalizedMetadata> result() {
+            return result;
+        }
+
+        @Override
+        protected void finishAnydata(final OpaqueData opaqueData, final ImmutableNormalizedMetadata metadata) {
+            checkState(result == null, "Result already set to %s", result);
+            result = new SimpleImmutableEntry<>(requireNonNull(opaqueData), metadata);
+        }
+    }
 }
diff --git a/yang/yang-data-codec-xml/src/test/java/org/opendaylight/yangtools/yang/data/codec/xml/AnydataParseTest.java b/yang/yang-data-codec-xml/src/test/java/org/opendaylight/yangtools/yang/data/codec/xml/AnydataParseTest.java
new file mode 100644 (file)
index 0000000..87f6c09
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.codec.xml;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import org.junit.Test;
+import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
+import org.opendaylight.yangtools.yang.data.util.schema.opaque.OpaqueDataBuilder;
+import org.opendaylight.yangtools.yang.data.util.schema.opaque.OpaqueDataContainerBuilder;
+import org.xml.sax.SAXException;
+
+public class AnydataParseTest extends AbstractAnydataTest {
+
+    @Test
+    public void testOpaqueAnydata() throws XMLStreamException, IOException, URISyntaxException, SAXException {
+        final XMLStreamReader reader = UntrustedXML.createXMLStreamReader(
+            toInputStream("<foo xmlns=\"test-anydata\"><bar/></foo>"));
+
+        final NormalizedNodeResult result = new NormalizedNodeResult();
+        final NormalizedNodeStreamWriter streamWriter = ImmutableNormalizedNodeStreamWriter.from(result);
+        final XmlParserStream xmlParser = XmlParserStream.create(streamWriter, SCHEMA_CONTEXT,
+            SCHEMA_CONTEXT.findDataChildByName(FOO_QNAME).get(), true);
+        xmlParser.parse(reader);
+
+        final NormalizedNode<?, ?> parsed = result.getResult();
+        assertEquals(Builders.opaqueAnydataBuilder().withNodeIdentifier(FOO_NODEID)
+            .withValue(new OpaqueDataBuilder().withAccurateLists(false)
+                .withRoot(new OpaqueDataContainerBuilder().withIdentifier(BAR_NODEID).build()).build())
+            .build(), parsed);
+    }
+
+    private static InputStream toInputStream(final String str) {
+        return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
+    }
+}
diff --git a/yang/yang-data-util/src/main/java/org/opendaylight/yangtools/yang/data/util/AbstractAnydataNodeDataWithSchema.java b/yang/yang-data-util/src/main/java/org/opendaylight/yangtools/yang/data/util/AbstractAnydataNodeDataWithSchema.java
new file mode 100644 (file)
index 0000000..7e8d695
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.Beta;
+import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata;
+import org.opendaylight.yangtools.yang.model.api.AnyDataSchemaNode;
+
+@Beta
+public abstract class AbstractAnydataNodeDataWithSchema<T> extends SimpleNodeDataWithSchema<AnyDataSchemaNode> {
+    private NormalizedMetadata metadata;
+
+    protected AbstractAnydataNodeDataWithSchema(final AnyDataSchemaNode dataSchemaNode) {
+        super(dataSchemaNode);
+    }
+
+    @Override
+    public T getValue() {
+        return objectModelClass().cast(super.getValue());
+    }
+
+    @Override
+    public void setValue(final Object value) {
+        final Class<T> clazz = objectModelClass();
+        checkArgument(clazz.isInstance(value), "Value %s is not compatible with %s", clazz);
+        super.setValue(value);
+    }
+
+    public final NormalizedMetadata getMetadata() {
+        return metadata;
+    }
+
+    public final void setMetadata(final NormalizedMetadata value) {
+        checkState(metadata == null, "Metadata already set to %s", metadata);
+        metadata = value;
+    }
+
+    protected abstract Class<T> objectModelClass();
+}
diff --git a/yang/yang-data-util/src/main/java/org/opendaylight/yangtools/yang/data/util/OpaqueAnydataNodeDataWithSchema.java b/yang/yang-data-util/src/main/java/org/opendaylight/yangtools/yang/data/util/OpaqueAnydataNodeDataWithSchema.java
new file mode 100644 (file)
index 0000000..e5853c8
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.util;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.List;
+import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata;
+import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadataStreamWriter;
+import org.opendaylight.yangtools.rfc7952.data.api.OpaqueAnydataStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.opaque.OpaqueData;
+import org.opendaylight.yangtools.yang.data.api.schema.opaque.OpaqueDataContainer;
+import org.opendaylight.yangtools.yang.data.api.schema.opaque.OpaqueDataList;
+import org.opendaylight.yangtools.yang.data.api.schema.opaque.OpaqueDataNode;
+import org.opendaylight.yangtools.yang.data.api.schema.opaque.OpaqueDataValue;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.OpaqueAnydataExtension;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.OpaqueAnydataExtension.StreamWriter;
+import org.opendaylight.yangtools.yang.model.api.AnyDataSchemaNode;
+
+@Beta
+public final class OpaqueAnydataNodeDataWithSchema extends AbstractAnydataNodeDataWithSchema<OpaqueData> {
+    public OpaqueAnydataNodeDataWithSchema(final AnyDataSchemaNode dataSchemaNode) {
+        super(dataSchemaNode);
+    }
+
+    @Override
+    protected Class<OpaqueData> objectModelClass() {
+        return OpaqueData.class;
+    }
+
+    @Override
+    protected void write(final NormalizedNodeStreamWriter writer, final NormalizedMetadataStreamWriter metaWriter)
+            throws IOException {
+        final OpaqueAnydataExtension ext = writer.getExtensions().getInstance(OpaqueAnydataExtension.class);
+        if (ext != null) {
+            writer.nextDataSchemaNode(getSchema());
+            streamOpaqueAnydataNode(ext, provideNodeIdentifier(), getValue(), getMetadata());
+            writer.endNode();
+        }
+    }
+
+    private static void streamOpaqueAnydataNode(final OpaqueAnydataExtension ext, final NodeIdentifier name,
+            final OpaqueData data, final NormalizedMetadata metadata) throws IOException {
+        if (metadata != null) {
+            final StreamWriter dataWriter = ext.startOpaqueAnydataNode(name, data.hasAccurateLists());
+            final OpaqueDataNode node = data.getRoot();
+            if (dataWriter instanceof OpaqueAnydataStreamWriter) {
+                streamOpaqueDataNode((OpaqueAnydataStreamWriter) dataWriter, node, metadata);
+            } else {
+                dataWriter.streamOpaqueDataNode(node);
+            }
+        } else {
+            ext.streamOpaqueAnydataNode(name, data);
+        }
+    }
+
+    private static void streamOpaqueDataNode(final OpaqueAnydataStreamWriter writer, final OpaqueDataNode node,
+            final NormalizedMetadata metadata) throws IOException {
+        if (node instanceof OpaqueDataValue) {
+            writer.startOpaqueContainer(node.getIdentifier(), 0);
+            writeMetadata(writer, metadata);
+            writer.opaqueValue(((OpaqueDataValue) node).getValue());
+            writer.endOpaqueNode();
+            return;
+        }
+        final List<? extends OpaqueDataNode> children;
+        if (node instanceof OpaqueDataList) {
+            children = ((OpaqueDataList) node).getChildren();
+            writer.startOpaqueList(node.getIdentifier(), children.size());
+        } else if (node instanceof OpaqueDataContainer) {
+            children = ((OpaqueDataContainer) node).getChildren();
+            writer.startOpaqueContainer(node.getIdentifier(), children.size());
+        } else {
+            throw new IllegalStateException("Unhandled node " + node);
+        }
+
+        writeMetadata(writer, metadata);
+        for (OpaqueDataNode child : children) {
+            streamOpaqueDataNode(writer, child, metadata.getChildren().get(child.getIdentifier()));
+        }
+        writer.endOpaqueNode();
+    }
+
+    private static void writeMetadata(final OpaqueAnydataStreamWriter writer, final NormalizedMetadata metadata)
+            throws IOException {
+        if (metadata != null) {
+            writer.metadata(ImmutableMap.copyOf(metadata.getAnnotations()));
+        }
+    }
+}