Add MapBodyOrder 64/111464/6
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 19 Apr 2024 07:06:15 +0000 (09:06 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Sun, 21 Apr 2024 11:02:43 +0000 (13:02 +0200)
Make DefaultNormalizedNodeWriter a final class by extracting out the
child sorting logic into a pair of classes.

This exposes the fact that ordered and unordered output behaves
differently -- the latter includes map entry keys. This output is not
possible to trigger under normal circumstances, so we adjust the tests
to assert the ordered behaviour.

JIRA: NETCONF-773
Change-Id: Ia70295cd048b97dfc284f014e477cfd72a5d347a
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultMapBodyOrder.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultNormalizedNodeWriter.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/IterationMapBodyOrder.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MapBodyOrder.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedNodeWriter.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/spi/NormalizedNodeWriterDepthTest.java

diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultMapBodyOrder.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultMapBodyOrder.java
new file mode 100644 (file)
index 0000000..39fa445
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2024 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.restconf.server.spi;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+
+/**
+ * Default, standards-compliant mode. {@code leaf} children referenced in predicates are first, followed by others
+ * in iteration order.
+ */
+@NonNullByDefault
+final class DefaultMapBodyOrder extends MapBodyOrder {
+    static final DefaultMapBodyOrder INSTANCE = new DefaultMapBodyOrder();
+
+    private DefaultMapBodyOrder() {
+        // Hidden on purpose
+    }
+
+    @Override
+    Iterable<DataContainerChild> orderBody(final MapEntryNode entry) throws IOException {
+        // First things first: we will need to size our two collections...
+        final var keys = entry.name().keySet();
+        final var keySize = keys.size();
+        final var entrySize = entry.size();
+        final var otherSize = entrySize - keySize;
+
+        // ... and that allows us to establish some invariants and optimize based on them
+        if (otherSize > 0) {
+            return orderBody(entry, keys, keySize, otherSize);
+        } else if (otherSize == 0) {
+            return keySize == 1 ? orderKey(entry, keys.iterator().next()) : orderKeys(entry, keys, keySize);
+        } else {
+            throw new IOException(entry.name() + " requires " + keySize + ", have only " + entrySize);
+        }
+    }
+
+    private static Iterable<DataContainerChild> orderBody(final MapEntryNode entry, final Set<QName> qnames,
+            final int keySize, final int otherSize) throws IOException {
+        final var keys = new ArrayList<LeafNode<?>>(keySize);
+        final var others = new ArrayList<DataContainerChild>(otherSize);
+
+        // Single-pass over children, classifying them into two parts.
+        for (var child : entry.body()) {
+            if (child instanceof LeafNode<?> leaf && qnames.contains(qnameOf(leaf))) {
+                keys.add(leaf);
+            } else {
+                others.add(child);
+            }
+        }
+
+        // Check we have all the keys
+        if (keys.size() != keySize) {
+            throw new IOException("Missing leaf nodes for "
+                + Sets.difference(qnames, keys.stream().map(DefaultMapBodyOrder::qnameOf).collect(Collectors.toSet()))
+                + " in " + entry);
+        }
+
+        // Make sure key iteration order matches qnames, if not go through a sort
+        if (!Iterators.elementsEqual(qnames.iterator(),
+            Iterators.transform(keys.iterator(), DefaultMapBodyOrder::qnameOf))) {
+            sortKeys(keys, qnames);
+        }
+
+        return Iterables.concat(keys, others);
+    }
+
+    private static Iterable<DataContainerChild> orderKeys(final MapEntryNode entry, final Set<QName> qnames,
+            final int keySize) throws IOException {
+        // Every child is supposed to be a leaf, addressable via NodeIdentifier, just look each one up and be done with
+        // it.
+        final var keys = new ArrayList<DataContainerChild>(keySize);
+        for (var qname : qnames) {
+            keys.add(requireKeyLeaf(entry, qname));
+        }
+        return keys;
+    }
+
+    private static Collection<DataContainerChild> orderKey(final MapEntryNode entry, final QName key)
+            throws IOException {
+        requireKeyLeaf(entry, key);
+        return entry.body();
+    }
+
+    private static LeafNode<?> requireKeyLeaf(final MapEntryNode entry, final QName key) throws IOException {
+        final var child = entry.childByArg(new NodeIdentifier(key));
+        if (child instanceof LeafNode<?> leaf) {
+            return leaf;
+        } else if (child == null) {
+            throw new IOException("No leaf for " + key + " in " + entry.prettyTree());
+        } else {
+            throw new IOException("Child " + child + " is not a leaf");
+        }
+    }
+
+    private static void sortKeys(final ArrayList<LeafNode<?>> keys, final Set<QName> qnames) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static QName qnameOf(final NormalizedNode node) {
+        return node.name().getNodeType();
+    }
+}
index cec8ebe34eeb9ac6a4e7b723d6998065fe372270..9134e0e201faf0133f492230dce201f0e6471d9e 100644 (file)
@@ -7,15 +7,12 @@
  */
 package org.opendaylight.restconf.server.spi;
 
-import com.google.common.collect.Iterables;
 import java.io.IOException;
 import java.util.List;
-import java.util.Map.Entry;
 import java.util.Set;
 import javax.xml.transform.dom.DOMSource;
 import org.opendaylight.restconf.api.query.DepthParam;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
 import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
@@ -29,8 +26,6 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * This is an experimental iterator over a {@link NormalizedNode}. This is essentially
@@ -39,17 +34,19 @@ import org.slf4j.LoggerFactory;
  * us to write multiple nodes.
  */
 // FIXME: this is a copy&paste from yangtools' NormalizedNodeWriter then adapted for filtering
-sealed class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
+final class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
     private static final QName ROOT_DATA_QNAME = QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data");
 
+    private final MapBodyOrder mapBodyOrder;
     private final Integer maxDepth;
     private final List<Set<QName>> fields;
 
-    int currentDepth = 0;
+    private int currentDepth = 0;
 
-    DefaultNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final DepthParam depth,
-            final List<Set<QName>> fields) {
+    DefaultNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final boolean iterationOrder,
+            final DepthParam depth, final List<Set<QName>> fields) {
         super(writer);
+        mapBodyOrder = iterationOrder ? IterationMapBodyOrder.INSTANCE : DefaultMapBodyOrder.INSTANCE;
         maxDepth = depth == null ? null : depth.value();
         this.fields = fields;
     }
@@ -145,9 +142,13 @@ sealed class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
     @Override
     protected void writeMapEntry(final MapEntryNode node) throws IOException {
         writer.startMapEntryNode(node.name(), node.size());
-        currentDepth++;
-        writeMapEntryChildren(node);
-        currentDepth--;
+        if (selectedByParameters(node, false)) {
+            currentDepth++;
+            writeChildren(mapBodyOrder.orderBody(node), false);
+            currentDepth--;
+        } else {
+            writer.endNode();
+        }
     }
 
     @Override
@@ -171,7 +172,7 @@ sealed class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
      * @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
      * @return {@code true} if node will be written, {@code false} otherwise
      */
-    protected boolean selectedByParameters(final NormalizedNode node, final boolean mixinParent) {
+    private boolean selectedByParameters(final NormalizedNode node, final boolean mixinParent) {
         // nodes to be written are not limited by fields, only by depth
         if (fields == null) {
             return maxDepth == null || currentDepth < maxDepth;
@@ -198,7 +199,7 @@ sealed class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
      * @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
      * @throws IOException when the writer reports it
      */
-    protected final void writeChildren(final Iterable<? extends NormalizedNode> children, final boolean mixinParent)
+    private void writeChildren(final Iterable<? extends NormalizedNode> children, final boolean mixinParent)
             throws IOException {
         for (var child : children) {
             if (selectedByParameters(child, mixinParent)) {
@@ -207,60 +208,4 @@ sealed class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
         }
         writer.endNode();
     }
-
-    private void writeMapEntryChildren(final MapEntryNode mapEntryNode) throws IOException {
-        if (selectedByParameters(mapEntryNode, false)) {
-            writeChildren(mapEntryNode.body(), false);
-        } else if (fields == null && maxDepth != null && currentDepth == maxDepth) {
-            writeOnlyKeys(mapEntryNode.name().entrySet());
-        }
-    }
-
-    private void writeOnlyKeys(final Set<Entry<QName, Object>> entries) throws IOException {
-        for (var entry : entries) {
-            writer.startLeafNode(new NodeIdentifier(entry.getKey()));
-            writer.scalarValue(entry.getValue());
-            writer.endNode();
-        }
-        writer.endNode();
-    }
-
-    static final class OrderedRestconfNormalizedNodeWriter extends DefaultNormalizedNodeWriter {
-        private static final Logger LOG = LoggerFactory.getLogger(OrderedRestconfNormalizedNodeWriter.class);
-
-        OrderedRestconfNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final DepthParam depth,
-                final List<Set<QName>> fields) {
-            super(writer, depth, fields);
-        }
-
-        @Override
-        protected void writeMapEntry(final MapEntryNode node) throws IOException {
-            writer.startMapEntryNode(node.name(), node.size());
-
-            final var qnames = node.name().keySet();
-            // Write out all the key children
-            currentDepth++;
-            for (var qname : qnames) {
-                final var child = node.childByArg(new NodeIdentifier(qname));
-                if (child != null) {
-                    if (selectedByParameters(child, false)) {
-                        write(child);
-                    }
-                } else {
-                    LOG.info("No child for key element {} found", qname);
-                }
-            }
-
-            // Write all the rest
-            writeChildren(Iterables.filter(node.body(), input -> {
-                if (!qnames.contains(input.name().getNodeType())) {
-                    return true;
-                }
-
-                LOG.debug("Skipping key child {}", input);
-                return false;
-            }), false);
-            currentDepth--;
-        }
-    }
 }
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/IterationMapBodyOrder.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/IterationMapBodyOrder.java
new file mode 100644 (file)
index 0000000..7af5950
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2024 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.restconf.server.spi;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+
+@NonNullByDefault
+final class IterationMapBodyOrder extends MapBodyOrder {
+    static final IterationMapBodyOrder INSTANCE = new IterationMapBodyOrder();
+
+    private IterationMapBodyOrder() {
+        // Hidden on purpose
+    }
+
+    @Override
+    Iterable<DataContainerChild> orderBody(final MapEntryNode entry) {
+        return entry.body();
+    }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MapBodyOrder.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MapBodyOrder.java
new file mode 100644 (file)
index 0000000..06685e1
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2024 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.restconf.server.spi;
+
+import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+
+/**
+ * The order in which {@link MapEntryNode}s of a particular {@link MapNode} should be emitted in. This is a sealed
+ * internal implementation detail.
+ *
+ * @apiNote This contract exposes two singleton implementations, which we could model via an enum. We went the way
+ *          of a sealed abstract for two reasons:
+ *          - we do not want to expose orderChildren
+ *          - we actually do not want to load IterationMapBodyOrder unless requested, so that the JVM does not have
+ *            to worry about it unless requested
+ *          -
+ */
+@NonNullByDefault
+abstract sealed class MapBodyOrder permits DefaultMapBodyOrder, IterationMapBodyOrder {
+    /**
+     * Order the body of specified node.
+     *
+     * @param entry map entry
+     * @return The body in the order it should be written out
+     */
+    abstract Iterable<DataContainerChild> orderBody(MapEntryNode entry) throws IOException;
+}
\ No newline at end of file
index 5f2ef0e44f9b600a8d606cc5d50d57dbe3a4ee2e..88d3a533ec2897c38905444fdf3182004585aaa0 100644 (file)
@@ -17,7 +17,6 @@ import java.util.Set;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.query.DepthParam;
-import org.opendaylight.restconf.server.spi.DefaultNormalizedNodeWriter.OrderedRestconfNormalizedNodeWriter;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
 import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
@@ -89,8 +88,7 @@ public abstract class NormalizedNodeWriter implements Flushable, Closeable {
      */
     public static final NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
             final boolean orderKeyLeaves, final @Nullable DepthParam depth, final @Nullable List<Set<QName>> fields) {
-        return orderKeyLeaves ? new OrderedRestconfNormalizedNodeWriter(writer, depth, fields)
-                : new DefaultNormalizedNodeWriter(writer, depth, fields);
+        return new DefaultNormalizedNodeWriter(writer, !orderKeyLeaves, depth, fields);
     }
 
     @Override
index 0f48c51b58544a10034230974f38594056aab5ef..7cdc80b49670c1a8694dc622147bd5192d090483 100644 (file)
@@ -185,10 +185,7 @@ class NormalizedNodeWriterDepthTest extends AbstractNormalizedNodeWriterTest {
 
         final var inOrder = inOrder(writer);
         inOrder.verify(writer).startMapEntryNode(mapEntryNodeIdentifier, 2);
-        // write only the key
-        inOrder.verify(writer).startLeafNode(KEY_FIELD_NID);
-        inOrder.verify(writer).scalarValue(keyLeafNodeValue);
-        inOrder.verify(writer, times(2)).endNode();
+        inOrder.verify(writer).endNode();
     }
 
     /**