Bind operation prefix to correct namespace 10/79610/20
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 17 Jan 2019 12:05:32 +0000 (13:05 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 7 Jul 2020 10:54:41 +0000 (12:54 +0200)
During NetconfUtil.writeNormalizedNode() we failed
to set a prefix for the netconf operation leading to a warning.
Make sure we set the correct NamespaceContext and fallback
to setPrefix() in case the underlying DOM writer does not
support setNamespaceContext().

JIRA: NETCONF-603
Change-Id: Icff5e045c4e901bb53990177d448a1ea43952c62
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
Signed-off-by: Tomas Cere <tomas.cere@pantheon.tech>
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/AnyXmlNamespaceContext.java [new file with mode: 0644]
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java
netconf/netconf-util/src/test/java/org/opendaylight/netconf/util/NetconfUtilTest.java

diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/AnyXmlNamespaceContext.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/AnyXmlNamespaceContext.java
new file mode 100644 (file)
index 0000000..3cd7e73
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2020 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.netconf.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE;
+import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
+import static javax.xml.XMLConstants.XML_NS_PREFIX;
+import static javax.xml.XMLConstants.XML_NS_URI;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableListMultimap;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.xml.namespace.NamespaceContext;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+final class AnyXmlNamespaceContext implements Immutable, NamespaceContext {
+    private static final ImmutableBiMap<String, String> FIXED_PREFIX_TO_URI = ImmutableBiMap.of(
+        XML_NS_PREFIX, XML_NS_URI,
+        XMLNS_ATTRIBUTE, XMLNS_ATTRIBUTE_NS_URI);
+
+    private final ImmutableListMultimap<String, String> uriToPrefix;
+    private final ImmutableBiMap<String, String> prefixToUri;
+
+    AnyXmlNamespaceContext(final Map<String, String> prefixToUri) {
+        final ImmutableListMultimap.Builder<String, String> uriToPrefixBuilder = ImmutableListMultimap.builder();
+        final BiMap<String, String> prefixToUriBuilder = HashBiMap.create();
+
+        // Populate well-known prefixes
+        prefixToUriBuilder.putAll(FIXED_PREFIX_TO_URI);
+        uriToPrefixBuilder.putAll(FIXED_PREFIX_TO_URI.inverse().entrySet());
+
+        // Deal with default namespace first ...
+        final String defaultURI = prefixToUri.get("");
+        if (defaultURI != null) {
+            checkMapping("", defaultURI);
+            uriToPrefixBuilder.put(defaultURI, "");
+            prefixToUriBuilder.putIfAbsent("", defaultURI);
+        }
+
+        // ... and then process all the rest
+        for (Entry<String, String> entry : prefixToUri.entrySet()) {
+            final String prefix = requireNonNull(entry.getKey());
+            if (!prefix.isEmpty()) {
+                final String namespaceURI = requireNonNull(entry.getValue());
+                checkMapping(prefix, namespaceURI);
+                uriToPrefixBuilder.put(namespaceURI, prefix);
+                prefixToUriBuilder.putIfAbsent(prefix, namespaceURI);
+            }
+        }
+
+        this.uriToPrefix = uriToPrefixBuilder.build();
+        this.prefixToUri = ImmutableBiMap.copyOf(prefixToUriBuilder);
+    }
+
+    @Override
+    public String getNamespaceURI(final String prefix) {
+        return getValue(prefixToUri, prefix, "");
+    }
+
+    @Override
+    public String getPrefix(final String namespaceURI) {
+        return getValue(prefixToUri.inverse(), namespaceURI, null);
+    }
+
+    @Override
+    public Iterator<String> getPrefixes(final String namespaceURI) {
+        checkArgument(namespaceURI != null);
+        return uriToPrefix.get(namespaceURI).iterator();
+    }
+
+    Collection<Entry<String, String>> uriPrefixEntries() {
+        return uriToPrefix.entries();
+    }
+
+    private static void checkMapping(final String prefix, final String namespaceURI) {
+        checkArgument(!namespaceURI.isEmpty(), "Namespace must not be empty (%s)", prefix);
+        checkArgument(!FIXED_PREFIX_TO_URI.containsKey(prefix), "Cannot bind prefix %s", prefix);
+        checkArgument(!FIXED_PREFIX_TO_URI.containsValue(namespaceURI), "Cannot bind namespace %s", namespaceURI);
+    }
+
+    private static String getValue(final ImmutableBiMap<String, String> map, final String key,
+            final String defaultValue) {
+        checkArgument(key != null);
+        final String found;
+        return (found = map.get(key)) == null ? defaultValue : found;
+    }
+}
index 3bf93f8d01068dc142c9de68ff7efdef9a45caba..2bf085609283c0ec2d058c8eb2b176a0f374a28c 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.netconf.util;
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.Iterator;
@@ -49,6 +50,45 @@ import org.w3c.dom.Document;
 import org.xml.sax.SAXException;
 
 public final class NetconfUtil {
+    /**
+     * Shim interface to handle differences around namespace handling between various XMLStreamWriter implementations.
+     * Specifically:
+     * <ul>
+     *   <li>OpenJDK DOM writer (com.sun.xml.internal.stream.writers.XMLDOMWriterImpl) throws
+     *       UnsupportedOperationException from its setNamespaceContext() method</li>
+     *   <li>Woodstox DOM writer (com.ctc.wstx.dom.WstxDOMWrappingWriter) works with namespace context, but treats
+     *       setPrefix() calls as hints -- which are not discoverable.</li>
+     * </ul>
+     *
+     * <p>
+     * Due to this we perform a quick test for behavior and decide the appropriate strategy.
+     */
+    @FunctionalInterface
+    private interface NamespaceSetter {
+        void initializeNamespace(XMLStreamWriter writer) throws XMLStreamException;
+
+        static NamespaceSetter forFactory(final XMLOutputFactory xmlFactory) {
+            final String netconfNamespace = NETCONF_QNAME.getNamespace().toString();
+            final AnyXmlNamespaceContext namespaceContext = new AnyXmlNamespaceContext(ImmutableMap.of(
+                "op", netconfNamespace));
+
+            try {
+                final XMLStreamWriter testWriter = xmlFactory.createXMLStreamWriter(new DOMResult(
+                    XmlUtil.newDocument()));
+                testWriter.setNamespaceContext(namespaceContext);
+            } catch (final UnsupportedOperationException e) {
+                // This happens with JDK's DOM writer, which we may be using
+                LOG.warn("Unable to set namespace context, falling back to setPrefix()", e);
+                return writer -> writer.setPrefix("op", netconfNamespace);
+            } catch (XMLStreamException e) {
+                throw new ExceptionInInitializerError(e);
+            }
+
+            // Success, we can use setNamespaceContext()
+            return writer -> writer.setNamespaceContext(namespaceContext);
+        }
+    }
+
     private static final Logger LOG = LoggerFactory.getLogger(NetconfUtil.class);
 
     // FIXME: document what exactly this QName means, as it is not referring to a tangible node nor the ietf-module.
@@ -60,6 +100,7 @@ public final class NetconfUtil {
         Revision.of("2011-06-01")), "netconf").intern();
     // FIXME: is this the device-bound revision?
     public static final QName NETCONF_DATA_QNAME = QName.create(NETCONF_QNAME, "data").intern();
+
     public static final XMLOutputFactory XML_FACTORY;
 
     static {
@@ -69,6 +110,8 @@ public final class NetconfUtil {
         XML_FACTORY = f;
     }
 
+    private static final NamespaceSetter XML_NAMESPACE_SETTER = NamespaceSetter.forFactory(XML_FACTORY);
+
     private NetconfUtil() {
         // No-op
     }
@@ -123,6 +166,7 @@ public final class NetconfUtil {
         }
 
         final XMLStreamWriter writer = XML_FACTORY.createXMLStreamWriter(result);
+        XML_NAMESPACE_SETTER.initializeNamespace(writer);
         try (
              NormalizedNodeStreamWriter normalizedNodeStreamWriter =
                      XMLStreamNormalizedNodeStreamWriter.create(writer, context, schemaPath);
index d2b3e17a47b76bf30fbd3d2ae9851678425b903a..16842040a57372f1bb35be0f48dedacb76050e08 100644 (file)
@@ -77,8 +77,8 @@ public class NetconfUtilTest {
         final DOMResult result = new DOMResult(XmlUtil.newDocument());
         final SchemaPath path = SchemaPath.create(true, NetconfState.QNAME);
         NetconfUtil.writeNormalizedNode(sessions, result, path, context);
-        final Document expected = XmlUtil.readXmlToDocument(getClass().getResourceAsStream("/sessions.xml"));
         final Document actual = (Document) result.getNode();
+        final Document expected = XmlUtil.readXmlToDocument(getClass().getResourceAsStream("/sessions.xml"));
         final Diff diff = XMLUnit.compareXML(expected, actual);
         Assert.assertTrue(diff.toString(), diff.similar());
     }