Add support for keyed entries 98/93398/7
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 26 Oct 2020 22:54:40 +0000 (23:54 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 28 Oct 2020 01:31:31 +0000 (02:31 +0100)
When we enter a NodeIdentifierWithPredicates for a Map, we can
service that with a direct map lookup instead of iteration. As we
now have the tools to access this step, add a unit test and fix
the implementation.

These predicates add another dimension to matching, where we have
to deal with Maps, something that NormalizedNodes does not help
with.

Predicate matching is split into its own class with a clear entry
point, so as to separate it from other complex pieces. This also
solves the tension between Evaluator and Iterator by shifting
ownership to a third class.

Since a predicate now be matching multiple nodes, we have to also
introduce an explicit DOMQueryPredicate.Not, so that its effects
can be properly accounted for.

JIRA: MDSAL-612
Change-Id: I34511517c5746efc5d245a56da89083c44100752
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/query/AbstractValueMatchBuilder.java
binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/query/QueryBuilderTest.java
dom/mdsal-dom-api/src/main/java/org/opendaylight/mdsal/dom/api/query/DOMQueryPredicate.java
dom/mdsal-dom-spi/src/main/java/org/opendaylight/mdsal/dom/spi/query/DOMQueryEvaluator.java
dom/mdsal-dom-spi/src/main/java/org/opendaylight/mdsal/dom/spi/query/DOMQueryMatcher.java [new file with mode: 0644]
dom/mdsal-dom-spi/src/main/java/org/opendaylight/mdsal/dom/spi/query/LazyDOMQueryResultIterator.java

index 080a5c826fded00fdf43791a97f516ba32eeb2be..47a32b96bdf0ffba13a639fa6dae1fb0131b955c 100644 (file)
@@ -15,7 +15,6 @@ import org.opendaylight.mdsal.binding.api.query.ValueMatchBuilder;
 import org.opendaylight.mdsal.binding.dom.adapter.query.QueryBuilderState.BoundMethod;
 import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate;
 import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate.Exists;
-import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate.NotExists;
 import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate.ValueEquals;
 import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
@@ -41,7 +40,7 @@ abstract class AbstractValueMatchBuilder<T extends DataObject, V> implements Val
 
     @Override
     public final ValueMatch<T> isNull() {
-        return withPredicate(new NotExists(relativePath()));
+        return withPredicate(new Exists(relativePath()).negate());
     }
 
     @Override
index 7fdc35ca7156aa0041a16f376aeb8b959570f359..d0a58eda8b6d9498ad7a600795027ebe13697c05 100644 (file)
@@ -21,7 +21,6 @@ import org.opendaylight.mdsal.binding.api.query.QueryExpression;
 import org.opendaylight.mdsal.binding.api.query.QueryFactory;
 import org.opendaylight.mdsal.binding.api.query.QueryResult;
 import org.opendaylight.mdsal.binding.api.query.QueryResult.Item;
-import org.opendaylight.mdsal.binding.api.query.QueryStructureException;
 import org.opendaylight.mdsal.binding.dom.codec.api.BindingCodecTree;
 import org.opendaylight.mdsal.binding.dom.codec.impl.DefaultBindingCodecTreeFactory;
 import org.opendaylight.mdsal.binding.runtime.spi.BindingRuntimeHelpers;
@@ -29,6 +28,7 @@ import org.opendaylight.yang.gen.v1.mdsal.query.norev.Foo;
 import org.opendaylight.yang.gen.v1.mdsal.query.norev.FooBuilder;
 import org.opendaylight.yang.gen.v1.mdsal.query.norev.first.grp.System;
 import org.opendaylight.yang.gen.v1.mdsal.query.norev.first.grp.SystemBuilder;
+import org.opendaylight.yang.gen.v1.mdsal.query.norev.first.grp.SystemKey;
 import org.opendaylight.yang.gen.v1.mdsal.query.norev.second.grp.Alarms;
 import org.opendaylight.yang.gen.v1.mdsal.query.norev.second.grp.AlarmsBuilder;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.Top;
@@ -95,7 +95,7 @@ public class QueryBuilderTest {
     }
 
     @Test
-    public void bar() throws QueryStructureException {
+    public void bar() {
         final Stopwatch sw = Stopwatch.createStarted();
         final QueryExpression<TopLevelList> query = factory.querySubtree(InstanceIdentifier.create(Top.class))
                 .extractChild(TopLevelList.class)
@@ -110,7 +110,7 @@ public class QueryBuilderTest {
     }
 
     @Test
-    public void testFindCriticalAlarms() throws QueryStructureException {
+    public void testFindCriticalAlarms() {
         final Stopwatch sw = Stopwatch.createStarted();
         final QueryExpression<Alarms> query = factory.querySubtree(InstanceIdentifier.create(Foo.class))
             .extractChild(System.class)
@@ -125,7 +125,7 @@ public class QueryBuilderTest {
     }
 
     @Test
-    public void testFindNonCriticalAlarms() throws QueryStructureException {
+    public void testFindNonCriticalAlarms() {
         final Stopwatch sw = Stopwatch.createStarted();
         final QueryExpression<Alarms> query = factory.querySubtree(InstanceIdentifier.create(Foo.class))
             .extractChild(System.class)
@@ -140,7 +140,7 @@ public class QueryBuilderTest {
     }
 
     @Test
-    public void testFindZeroAlarms() throws QueryStructureException {
+    public void testFindZeroAlarms() {
         final Stopwatch sw = Stopwatch.createStarted();
         final QueryExpression<Alarms> query = factory.querySubtree(InstanceIdentifier.create(Foo.class))
             .extractChild(System.class)
@@ -154,6 +154,37 @@ public class QueryBuilderTest {
         assertEquals(2, items.size());
     }
 
+    @Test
+    public void testFindSystemFirstAlarmOne() {
+        final Stopwatch sw = Stopwatch.createStarted();
+        final QueryExpression<Alarms> query = factory.querySubtree(InstanceIdentifier.create(Foo.class))
+            .extractChild(System.class, new SystemKey("first"))
+            .extractChild(Alarms.class)
+                .matching()
+                    .leaf(Alarms::getId).valueEquals(Uint64.ZERO)
+                .build();
+        LOG.info("Query built in {}", sw);
+
+        final List<? extends Item<@NonNull Alarms>> items = execute(query).getItems();
+        assertEquals(1, items.size());
+    }
+
+    @Test
+    public void testFindSystemFirstWithAlarmOne() {
+        final Stopwatch sw = Stopwatch.createStarted();
+        final QueryExpression<System> query = factory.querySubtree(InstanceIdentifier.create(Foo.class))
+            .extractChild(System.class, new SystemKey("first"))
+                .matching()
+                    .childObject(Alarms.class)
+                        .leaf(Alarms::getId).valueEquals(Uint64.ZERO)
+                .build();
+        LOG.info("Query built in {}", sw);
+
+        final List<? extends Item<@NonNull System>> items = execute(query).getItems();
+        assertEquals(1, items.size());
+    }
+
+
     private <T extends @NonNull DataObject> QueryResult<T> execute(final QueryExpression<T> query) {
         final Stopwatch sw = Stopwatch.createStarted();
         final QueryResult<T> result = executor.executeQuery(query);
index 7a36bd7b206a3391219882c12e2a4e45106400c9..80db6f34420efd8bced760979e9a1007a30e057e 100644 (file)
@@ -93,14 +93,26 @@ public abstract class DOMQueryPredicate implements Immutable, Predicate<Normaliz
         }
     }
 
-    public static final class NotExists extends DOMQueryPredicate {
-        public NotExists(final YangInstanceIdentifier relativePath) {
-            super(relativePath);
+    public static final class Not extends DOMQueryPredicate {
+        private final DOMQueryPredicate predicate;
+
+        Not(final DOMQueryPredicate predicate) {
+            super(predicate.relativePath);
+            this.predicate = predicate;
+        }
+
+        public @NonNull DOMQueryPredicate predicate() {
+            return predicate;
+        }
+
+        @Override
+        public DOMQueryPredicate negate() {
+            return predicate;
         }
 
         @Override
         public boolean test(final NormalizedNode<?, ?> data) {
-            return data == null;
+            return !predicate.test(data);
         }
     }
 
@@ -222,6 +234,11 @@ public abstract class DOMQueryPredicate implements Immutable, Predicate<Normaliz
         return relativePath;
     }
 
+    @Override
+    public @NonNull DOMQueryPredicate negate() {
+        return new Not(this);
+    }
+
     @Override
     public abstract boolean test(@Nullable NormalizedNode<?, ?> data);
 
index e5483eb35008719467cc0f83540151fbc654f5bd..5dceb89b79cc5bada1ce918031fdc75e2eabf5ac 100644 (file)
@@ -60,7 +60,7 @@ public final class DOMQueryEvaluator {
     }
 
     private static DOMQueryResult evalSingle(final DOMQuery query, final NormalizedNode<?, ?> data) {
-        return LazyDOMQueryResultIterator.matches(data, query.getPredicates()) ? DOMQueryResult.of()
+        return DOMQueryMatcher.matches(data, query.getPredicates()) ? DOMQueryResult.of()
                 : DOMQueryResult.of(new SimpleImmutableEntry<>(query.getRoot(), data));
     }
 }
diff --git a/dom/mdsal-dom-spi/src/main/java/org/opendaylight/mdsal/dom/spi/query/DOMQueryMatcher.java b/dom/mdsal-dom-spi/src/main/java/org/opendaylight/mdsal/dom/spi/query/DOMQueryMatcher.java
new file mode 100644 (file)
index 0000000..4b9bd9f
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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.mdsal.dom.spi.query;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.Optional;
+import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate;
+import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate.Not;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodes;
+
+/**
+ * Generalized utility for matching predicates. Split out of {@link LazyDOMQueryResultIterator} for simplicity.
+ */
+final class DOMQueryMatcher {
+    private DOMQueryMatcher() {
+        // Utility class
+    }
+
+    static boolean matches(final NormalizedNode<?, ?> data, final List<? extends DOMQueryPredicate> predicates) {
+        // TODO: it would be nice if predicates were somehow structured -- can we perhaps sort them by their
+        //       InstanceIdentifier? If the predicates are sharing a common subpath. Hence if we can guarantee
+        //       predicates are in a certain order, we would not end up in subsequent re-lookups of the same node.
+        Deque<PathArgument> pathArgs = null;
+        for (DOMQueryPredicate pred : predicates) {
+            // So now, dealing with implementations: YangInstanceIdentifier.getLastPathArgument() is always cheap.
+            // If its parent is YangInstanceIdentifier.ROOT (i.e. isEmpty() == true), we are dealing with a last-step
+            // lookup -- in which case we forgo iteration:
+            final YangInstanceIdentifier path = pred.getPath();
+            if (path.coerceParent().isEmpty()) {
+                if (pred instanceof Not) {
+                    if (matchesChild(((Not) pred).predicate(), data, path.getLastPathArgument())) {
+                        return false;
+                    }
+                } else if (!matchesChild(pred, data, path.getLastPathArgument())) {
+                    return false;
+                }
+                continue;
+            }
+
+            // We are leaking path arguments in a bid for object reuse: we end up reusing same object as needed
+            if (pathArgs == null) {
+                pathArgs = new ArrayDeque<>();
+            }
+            pathArgs.addAll(path.getPathArguments());
+
+            // The stage is set, we now have to deal with potential negation.
+            if (pred instanceof Not) {
+                if (matchesAny(((Not) pred).predicate(), data, pathArgs)) {
+                    return false;
+                }
+            } else if (!matchesAny(pred, data, pathArgs)) {
+                return false;
+            }
+
+            pathArgs.clear();
+        }
+        return true;
+    }
+
+    private static boolean matchesAny(final DOMQueryPredicate pred, final NormalizedNode<?, ?> data,
+            final Deque<PathArgument> pathArgs) {
+        // Guaranteed to have at least one item
+        final PathArgument pathArg = pathArgs.pop();
+        // Ultimate item -- reuse lookup & match
+        if (pathArgs.isEmpty()) {
+            pathArgs.push(pathArg);
+            return matchesChild(pred, data, pathArg);
+        }
+
+        final Optional<NormalizedNode<?, ?>> direct = NormalizedNodes.getDirectChild(data, pathArg);
+        if (direct.isPresent()) {
+            final boolean ret = matchesAny(pred, direct.orElseThrow(), pathArgs);
+            pathArgs.push(pathArg);
+            return ret;
+        }
+
+        // We may be dealing with a wildcard here. NodeIdentifier is a final class, hence this is as fast as it gets.
+        if (pathArg instanceof NodeIdentifier && data instanceof MapNode) {
+            for (MapEntryNode child : ((MapNode) data).getValue()) {
+                if (matchesAny(pred, child, pathArgs)) {
+                    pathArgs.push(pathArg);
+                    return true;
+                }
+            }
+        }
+
+        pathArgs.push(pathArg);
+        return false;
+    }
+
+    private static boolean matchesChild(final DOMQueryPredicate pred, final NormalizedNode<?, ?> data,
+            final PathArgument pathArg) {
+        // Try the direct approach...
+        final Optional<NormalizedNode<?, ?>> direct = NormalizedNodes.getDirectChild(data, pathArg);
+        if (direct.isPresent()) {
+            return pred.test(direct.orElseThrow());
+        }
+
+        // We may be dealing with a wildcard here. NodeIdentifier is a final class, hence this is as fast as it gets.
+        if (pathArg instanceof NodeIdentifier && data instanceof MapNode) {
+            for (MapEntryNode child : ((MapNode) data).getValue()) {
+                if (pred.test(child)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+}
index dd615d15dc43f6f2822b2eaaf75610a5a500ac22..265c1b882e05fa45f52912276b38d357a5765594 100644 (file)
@@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.mdsal.dom.api.query.DOMQuery;
 import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+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.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
@@ -185,9 +186,7 @@ final class LazyDOMQueryResultIterator extends AbstractIterator<Entry<YangInstan
                 // This is the ultimate step in lookup, process it without churning the stack by imposing a dedicated
                 // Frame. In either case we are done with this frame, unwinding it in both cases.
                 if (matches(child)) {
-                    final YangInstanceIdentifier childPath = createIdentifier(child);
-                    unwind(current, next);
-                    return new SimpleImmutableEntry<>(childPath, child);
+                    return unwindAndReturn(current, next, child);
                 }
 
                 current = unwind(current, next);
@@ -197,12 +196,47 @@ final class LazyDOMQueryResultIterator extends AbstractIterator<Entry<YangInstan
             // Push our state back, it's just a placeholder for 'currentSelect'. Current path points at us and so does
             // the saved Frame.
             currentPath.addLast(current.data.getIdentifier());
-            frames.push(current);
 
             // Now decide what sort of entry to push. For maps we want to start an iterator already, so it gets
             // picked up as a continuation.
-            current = child instanceof MapNode ? new MapFrame(child, next, ((MapNode) child).getValue().iterator())
-                : new Frame(child, next);
+            if (child instanceof MapNode) {
+                final MapNode map = (MapNode) child;
+                final PathArgument target = remainingSelect.peek();
+                if (target instanceof NodeIdentifierWithPredicates) {
+                    final Optional<MapEntryNode> optEntry = map.getChild((NodeIdentifierWithPredicates) target);
+                    if (optEntry.isPresent()) {
+                        final MapEntryNode entry = optEntry.orElseThrow();
+                        if (remainingSelect.size() != 1) {
+                            // We need to perform further selection push this frame, an empty frame for the map and
+                            // finally a frame for the map entry.
+                            remainingSelect.pop();
+                            frames.push(current);
+                            currentPath.addLast(map.getIdentifier());
+                            frames.push(new Frame(map, next));
+                            current = new Frame(entry, target);
+                            continue;
+                        }
+
+                        // We have selected entry, see it it matches. In any case rewind, potentially returning
+                        // the match
+                        if (matches(entry)) {
+                            return unwindAndReturn(current, next, entry);
+                        }
+                    }
+
+                    // We failed to find a matching entry, unwind
+                    current = unwind(current, next);
+                    continue;
+                }
+
+                // We have a wildcard, expand it
+                frames.push(current);
+                current = new MapFrame(child, next, map.getValue().iterator());
+            } else {
+                // Next step in iteration, deal with it
+                frames.push(current);
+                current = new Frame(child, next);
+            }
         }
 
         // All done, there be entries no more.
@@ -231,6 +265,14 @@ final class LazyDOMQueryResultIterator extends AbstractIterator<Entry<YangInstan
         return new SimpleImmutableEntry<>(childPath, child);
     }
 
+    // Unwind any leftover frames and return a matching item
+    private Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> unwindAndReturn(final Frame frame,
+            final PathArgument next, final NormalizedNode<?, ?> child) {
+        final YangInstanceIdentifier childPath = createIdentifier(child);
+        unwind(frame, next);
+        return new SimpleImmutableEntry<>(childPath, child);
+    }
+
     /**
      * Unwind the stack, discarding current frame, and possibly some others. The unwind starts with pushing {@code next}
      * to {@link #remainingSelect}, hence we remember to handle it next time around. It then defers to
@@ -287,25 +329,6 @@ final class LazyDOMQueryResultIterator extends AbstractIterator<Entry<YangInstan
     }
 
     private boolean matches(final NormalizedNode<?, ?> data) {
-        return matches(data, predicates);
-    }
-
-    static boolean matches(final NormalizedNode<?, ?> data, final List<? extends DOMQueryPredicate> predicates) {
-        for (DOMQueryPredicate pred : predicates) {
-            // Okay, now we need to deal with predicates, but do it in a smart fashion, so we do not end up iterating
-            // all over the place. Typically we will be matching just a leaf.
-            final YangInstanceIdentifier path = pred.getPath();
-            final Optional<NormalizedNode<?, ?>> node;
-            if (path.coerceParent().isEmpty()) {
-                node = NormalizedNodes.getDirectChild(data, path.getLastPathArgument());
-            } else {
-                node = NormalizedNodes.findNode(data, path);
-            }
-
-            if (!pred.test(node.orElse(null))) {
-                return false;
-            }
-        }
-        return true;
+        return DOMQueryMatcher.matches(data, predicates);
     }
 }