Add MapBodyOrder
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / spi / DefaultMapBodyOrder.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();
+    }
+}