Add a simple Binding query language 52/88552/11
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 20 Mar 2020 16:22:09 +0000 (17:22 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Thu, 2 Apr 2020 15:17:17 +0000 (17:17 +0200)
Offloading some amount of data matching towards the storage engine
can give use two-fold improvement:

1) skip instantiation of Binding DTOs, as the query can be realized
   in terms of DOM matches
2) potentially skip transfer of large amount of data

For Binding layer we want to have something type-safe, where we are
in control of the flow the language.

This provides a first cut at the structure, which may end up being
further evolved.

Change-Id: Ica52158c889c0df9a6004227c0b64092590f67af
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
15 files changed:
binding/mdsal-binding-api/pom.xml
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ComparableMatchBuilder.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/DescendantQueryBuilder.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/MatchBuilderPath.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryExpression.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryFactory.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryResult.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryStructureException.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/StringMatchBuilder.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/StructuralBuilder.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ValueMatch.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ValueMatchBuilder.java [new file with mode: 0644]
binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/package-info.java [new file with mode: 0644]
binding/mdsal-binding-api/src/test/java/org/opendaylight/mdsal/binding/api/query/QueryBuilderExamples.java [new file with mode: 0644]
binding/mdsal-binding-test-model/src/main/yang/mdsal-query.yang [new file with mode: 0644]

index fb2f15e038f91622567bebee470f462e671464df..4d301c5b6c1a8996a98c25d5df7de73bc089b629 100644 (file)
             <artifactId>yang-data-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.opendaylight.mdsal</groupId>
+            <artifactId>mdsal-binding-test-model</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ComparableMatchBuilder.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ComparableMatchBuilder.java
new file mode 100644 (file)
index 0000000..c7b28a7
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Builder for a match of a leaf value which define a total ordering by implementing the {@link Comparable} interface.
+ *
+ * <p>
+ * Note that value comparison is preconditioned on target leaf existence. If the leaf does not exist any total ordering
+ * checks will not match it -- thus a non-existent leaf does not match {@link #greaterThan(Comparable)} and at the same
+ * time it does not match {@link #lessThanOrEqual(Comparable)}.
+ *
+ * @param <T> query result type
+ * @param <V> value type
+ */
+@Beta
+public interface ComparableMatchBuilder<T extends DataObject, V extends Comparable<V>> extends ValueMatchBuilder<T, V> {
+    /**
+     * Match if the leaf exists and its value is less than the specified value.
+     *
+     * @param value value to check against
+     * @return A ValueMatch
+     * @throws NullPointerException if value is null
+     */
+    @NonNull ValueMatch<T> lessThan(V value);
+
+    /**
+     * Match if the leaf exists and its value is less than, or equal to, the specified value.
+     *
+     * @param value value to check against
+     * @return A ValueMatch
+     * @throws NullPointerException if value is null
+     */
+    @NonNull ValueMatch<T> lessThanOrEqual(V value);
+
+    /**
+     * Match if the leaf exists and its value is greater than the specified value.
+     *
+     * @param value value to check against
+     * @return A ValueMatch
+     * @throws NullPointerException if value is null
+     */
+    @NonNull ValueMatch<T> greaterThan(V value);
+
+    /**
+     * Match if the leaf exists and its value is greater than, or equal to the specified value.
+     *
+     * @param value value to check against
+     * @return A ValueMatch
+     * @throws NullPointerException if value is null
+     */
+    @NonNull ValueMatch<T> greaterThanOrEqual(V value);
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/DescendantQueryBuilder.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/DescendantQueryBuilder.java
new file mode 100644 (file)
index 0000000..a42ad16
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.ChildOf;
+import org.opendaylight.yangtools.yang.binding.ChoiceIn;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Intermediate Query builder stage, which allows the specification of the query result type to be built up via
+ * {@link #extractChild(Class)} and {@link #extractChild(Class, Class)} methods. Once completed, use either
+ * {@link #build()} to create a simple query, or {@link #matching()} to transition to specify predicates.
+ *
+ * @param <T> Query result type
+ */
+@Beta
+public interface DescendantQueryBuilder<T extends DataObject> extends StructuralBuilder<QueryExpression<T>> {
+    /**
+     * Add a child path component to the specification of what needs to be extracted. This method, along with its
+     * alternatives, can be used to specify which object type to select from the root path.
+     *
+     * @param <N> Container type
+     * @param childClass child container class
+     * @return This builder
+     * @throws NullPointerException if childClass is null
+     */
+    <N extends ChildOf<? super T>> @NonNull DescendantQueryBuilder<N> extractChild(Class<N> childClass);
+
+    /**
+     * Add a child path component to the specification of what needs to be extracted. This method, along with its
+     * alternatives, can be used to specify which object type to select from the root path.
+     *
+     * @param <C> Case type
+     * @param <N> Container type
+     * @param caseClass child case class
+     * @param childClass child container class
+     * @return This builder
+     * @throws NullPointerException if any argument is null
+     */
+    <C extends ChoiceIn<? super T> & DataObject, N extends ChildOf<? super C>>
+        @NonNull DescendantQueryBuilder<N> extractChild(Class<C> caseClass, Class<N> childClass);
+
+    /**
+     * Start specifying type match predicates.
+     *
+     * @return A predicate match builder based on current result type
+     */
+    @NonNull MatchBuilderPath<T, T> matching();
+
+    @Override
+    QueryExpression<T> build();
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/MatchBuilderPath.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/MatchBuilderPath.java
new file mode 100644 (file)
index 0000000..66c2f5a
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import java.io.Serializable;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.Mutable;
+import org.opendaylight.yangtools.yang.binding.BaseIdentity;
+import org.opendaylight.yangtools.yang.binding.ChildOf;
+import org.opendaylight.yangtools.yang.binding.ChoiceIn;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.TypeObject;
+import org.opendaylight.yangtools.yang.common.Empty;
+import org.opendaylight.yangtools.yang.common.Uint16;
+import org.opendaylight.yangtools.yang.common.Uint32;
+import org.opendaylight.yangtools.yang.common.Uint64;
+import org.opendaylight.yangtools.yang.common.Uint8;
+
+@Beta
+public interface MatchBuilderPath<O extends DataObject, T extends DataObject> extends Mutable {
+    /**
+     * Descend match into a child container.
+     *
+     * @param <N> Child container type
+     * @param childClass Child container type class
+     * @return This builder
+     * @throws NullPointerException if childClass is null
+     */
+    <N extends ChildOf<? super T>> @NonNull MatchBuilderPath<O, N> childObject(Class<N> childClass);
+
+    /**
+     * Descend match into a child container in a particular case.
+     *
+     * @param <C> Case type
+     * @param <N> Child container type
+     * @param childClass Child container type class
+     * @return This builder
+     * @throws NullPointerException if any argument is null
+     */
+    <C extends ChoiceIn<? super T> & DataObject, N extends ChildOf<? super C>>
+        @NonNull MatchBuilderPath<O, N> extractChild(Class<C> caseClass, Class<N> childClass);
+
+    /**
+     * Match an {@code empty} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ValueMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ValueMatchBuilder<O, Empty> leaf(EmptyLeafReference<T> methodRef);
+
+    /**
+     * Match an {@code string} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link StringMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull StringMatchBuilder<O> leaf(StringLeafReference<T> methodRef);
+
+    /**
+     * Match an {@code int8} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Byte> leaf(Int8LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code int16} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Short> leaf(Int16LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code int32} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Integer> leaf(Int32LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code int64} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Long> leaf(Int64LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code uint8} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Uint8> leaf(Uint8LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code uint16} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Uint16> leaf(Uint16LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code uint32} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Uint32> leaf(Uint32LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code uint64} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ComparableMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    @NonNull ComparableMatchBuilder<O, Uint64> leaf(Uint64LeafReference<T> methodRef);
+
+    /**
+     * Match an {@code identityref} leaf's value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ValueMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    <I extends BaseIdentity> @NonNull ValueMatchBuilder<O, I> leaf(IdentityLeafReference<T, I> methodRef);
+
+    /**
+     * Match a generic leaf value.
+     *
+     * @param methodRef method reference to the getter method
+     * @return A {@link ValueMatchBuilder}
+     * @throws NullPointerException if methodRef is null
+     */
+    <C extends TypeObject> @NonNull ValueMatchBuilder<O, C> leaf(TypeObjectLeafReference<T, C> methodRef);
+
+    /**
+     * Base interface for capturing binding getter method references through lambda expressions. This interface should
+     * never be used directly, but rather through one of its specializations.
+     *
+     * <p>
+     * This interface uncharacteristically extends {@link Serializable} for the purposes of making the resulting lambda
+     * also Serializable. This part is critical for the process of introspection into the lambda and identifying the
+     * method being invoked.
+     *
+     * @param <P> Parent type
+     * @param <C> Child type
+     */
+    @FunctionalInterface
+    public interface LeafReference<P, C> extends Serializable {
+        /**
+         * Dummy method to express the method signature of a typical getter. This method
+         *
+         * @param parent Parent object
+         * @return Leaf value
+         * @deprecated This method is present only for technical realization of taking the method reference and should
+         *             never be involved directly.
+         */
+        @Deprecated(forRemoval = true)
+        C dummyMethod(P parent);
+    }
+
+    @FunctionalInterface
+    public interface EmptyLeafReference<P> extends LeafReference<P, Empty> {
+
+    }
+
+    @FunctionalInterface
+    public interface StringLeafReference<P> extends LeafReference<P, String> {
+
+    }
+
+    @FunctionalInterface
+    public interface Int8LeafReference<P> extends LeafReference<P, Byte> {
+
+    }
+
+    @FunctionalInterface
+    public interface Int16LeafReference<P> extends LeafReference<P, Short> {
+
+    }
+
+    @FunctionalInterface
+    public interface Int32LeafReference<P> extends LeafReference<P, Integer> {
+
+    }
+
+    @FunctionalInterface
+    public interface Int64LeafReference<P> extends LeafReference<P, Long> {
+
+    }
+
+    @FunctionalInterface
+    public interface Uint8LeafReference<P> extends LeafReference<P, Uint8> {
+
+    }
+
+    @FunctionalInterface
+    public interface Uint16LeafReference<P> extends LeafReference<P, Uint16> {
+
+    }
+
+    @FunctionalInterface
+    public interface Uint32LeafReference<P> extends LeafReference<P, Uint32> {
+
+    }
+
+    @FunctionalInterface
+    public interface Uint64LeafReference<P> extends LeafReference<P, Uint64> {
+
+    }
+
+    @FunctionalInterface
+    public interface IdentityLeafReference<P, T extends BaseIdentity> extends LeafReference<P, T> {
+
+    }
+
+    @FunctionalInterface
+    public interface TypeObjectLeafReference<P, T extends TypeObject> extends LeafReference<P, T> {
+
+    }
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryExpression.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryExpression.java
new file mode 100644 (file)
index 0000000..a9c6834
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * An opaque query expression. A query execution results in a {@link QueryResult}, which is composed of zero or more
+ * objects of the same type. Implementations of this interface are expected to be effectively-immutable and therefore
+ * thread-safe and reusable.
+ *
+ * <p>
+ * While this interface does not expose any useful methods, it represents a well-defined concept, which is composed of
+ * three distinct parts:
+ * <ul>
+ *   <li>root path, which defines the subtree on which the expression is executed</li>
+ *   <li>select path, which is a strict subset of the root path and defines which objects should be selected</li>
+ *   <li>a set of predicates, which are to be evaluated on selected objects</li>
+ * </ul>
+ * When the expression is evaluated, its QueryResult will contain only those selected objects which also match all
+ * predicates in the expression.
+ *
+ * <p>
+ * For the purposes of illustration of how these three parts work together, let's imagine the following simple model:
+ *
+ * <pre>
+ *   module foo {
+ *     list foo {
+ *       key name;
+ *
+ *       leaf name {
+ *         type string;
+ *       }
+ *
+ *       leaf alias {
+ *         type string;
+ *       }
+ *
+ *       container bar {
+ *         list baz {
+ *           key id;
+ *
+ *           leaf id {
+ *             type uint64;
+ *           }
+ *
+ *           leaf value {
+ *             type string;
+ *           }
+ *         }
+ *       }
+ *     }
+ *   }
+ * </pre>
+ *
+ * <p>
+ * We have two nested lists, each having two leaves -- one addressable as a key, one a plain property. There is a number
+ * of different queries we could perform on such a model:
+ * <ol>
+ *   <li>select all {@code baz}es which have {@code value="foo"}</li>
+ *   <li>select all {@code baz}es under {@code foo[name="xyzzy"]}, which have {@code value="foo"}</li>
+ *   <li>select all {@code foo}s which have {@code alias="xyzzy"}</li>
+ *   <li>select all {@code foo}s which have {@code alias="xyzzy"} and contain a {@code baz[value="foo"]}</li>
+ * </ol>
+ *
+ * <p>
+ * Note how the first and second options differ in what is being searched:
+ * <ul>
+ *   <li>search for all {@code baz} entries needs to traverse all {@code foo} entries</li>
+ *   <li>search for all {@code baz} entries for {@code foo[name="xyzzy"]} needs to traverse only a single
+ *       directly-addressable entry.</li>
+ * </ul>
+ * The distinction here is made by selecting different root paths: the first will specify the entire {@code foo} list,
+ * while the second will select a specific {@code foo} entry.
+ *
+ *
+ *
+ * @param <T> Result object type
+ */
+@Beta
+public interface QueryExpression<T extends DataObject> extends Immutable {
+
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryFactory.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryFactory.java
new file mode 100644 (file)
index 0000000..31bbb18
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Primary entry point to creating {@link QueryExpression} instances.
+ */
+@Beta
+public interface QueryFactory {
+    /**
+     * Create a new {@link DescendantQueryBuilder} for a specified root path. Root path must be a non-wildcard
+     * InstanceIdentifier in general sense. If the target type represents a list, the last path argument may be a
+     * wildcard, in which case the path is interpreted to search the specified list. Inner path elements have to be
+     * always non-wildcarded.
+     *
+     * @param <T> Target object type
+     * @param rootPath Subtree root
+     * @return a subtree query instance
+     * @throws IllegalArgumentException if rootPath is incorrect
+     * @throws NullPointerException if rootPath is null
+     */
+    <T extends DataObject> @NonNull DescendantQueryBuilder<T> querySubtree(InstanceIdentifier<T> rootPath);
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryResult.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryResult.java
new file mode 100644 (file)
index 0000000..55b3703
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Result of executing a {@link QueryExpression}. It is composed of one or more result values, which can be accessed via
+ * {@link #spliterator()}, {@link #stream()} and {@link #getValues()} methods.
+ *
+ * @param <T> Result object type
+ */
+@Beta
+public interface QueryResult<T extends DataObject> {
+    /**
+     * Returns a spliterator over values of the result.
+     *
+     * @return Returns the a spliterator which visits query results.
+     */
+    // TODO: @throws IllegalStateException if values have been already been consumed?
+    // FIXME: we really may want to wrap each entry in a CheckedValue, so that we can communicate fetch problems
+    @NonNull Spliterator<? extends T> spliterator();
+
+    /**
+     * Returns a sequential {@link Stream} of values from the result.
+     *
+     * @return A stream of non-null values.
+     */
+    default @NonNull Stream<? extends T> stream() {
+        return StreamSupport.stream(spliterator(), false);
+    }
+
+    /**
+     * Returns a parallel {@link Stream} of values from the result.
+     *
+     * @return A stream of non-null values.
+     */
+    default @NonNull Stream<? extends T> parallelStream() {
+        return StreamSupport.stream(spliterator(), true);
+    }
+
+    default @NonNull List<? extends T> getValues() {
+        return stream().collect(Collectors.toUnmodifiableList());
+    }
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryStructureException.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/QueryStructureException.java
new file mode 100644 (file)
index 0000000..ab8ccbb
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Exception reported when the proposed query has a structural problem. This may be either a mismatch with underlying
+ * schema, a value type problem, or a general DTO relationship issue.
+ */
+@Beta
+public class QueryStructureException extends IllegalStateException {
+    private static final long serialVersionUID = 1L;
+
+    public QueryStructureException(final String message) {
+        super(message);
+    }
+
+    public QueryStructureException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/StringMatchBuilder.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/StringMatchBuilder.java
new file mode 100644 (file)
index 0000000..5e7ed3c
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import java.util.regex.Pattern;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Builder for a match of a String leaf value.
+ *
+ * @param <T> query result type
+ */
+@Beta
+public interface StringMatchBuilder<T extends DataObject> extends ValueMatchBuilder<T, String> {
+    /**
+     * Match if the leaf exists and its value starts with specified string, i.e. its {@link String#startsWith(String)}
+     * would return true.
+     *
+     * @param str string to match as prefix
+     * @return A ValueMatch
+     * @throws NullPointerException if str is null
+     */
+    @NonNull ValueMatch<T> startsWith(String str);
+
+    /**
+     * Match if the leaf exists and its value ends with specified string. i.e. its {@link String#endsWith(String)}
+     * would return true.
+     *
+     * @param str string to match as suffix
+     * @return A ValueMatch
+     * @throws NullPointerException if str is null
+     */
+    @NonNull ValueMatch<T> endsWith(String str);
+
+    /**
+     * Match if the leaf exists and its value contains specified string, i.e. its {@link String#contains(CharSequence)}
+     * would return true.
+     *
+     * @param str the string to search for
+     * @return A ValueMatch
+     * @throws NullPointerException if str is null
+     */
+    @NonNull ValueMatch<T> contains(String str);
+
+    /**
+     * Match if the leaf exists and its value matches with specified pattern.
+     *
+     * @param pattern pattern to check against
+     * @return A ValueMatch
+     * @throws NullPointerException if pattern is null
+     */
+    @NonNull ValueMatch<T> matchesPattern(Pattern pattern);
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/StructuralBuilder.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/StructuralBuilder.java
new file mode 100644 (file)
index 0000000..eb62b23
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.opendaylight.yangtools.concepts.CheckedBuilder;
+
+/**
+ * A specialization of {@link CheckedBuilder}, whose {@code build()} method throws a {@link QueryStructureException}.
+ *
+ * @param <P> Product of builder
+ */
+@Beta
+public interface StructuralBuilder<P> extends CheckedBuilder<P, QueryStructureException> {
+
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ValueMatch.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ValueMatch.java
new file mode 100644 (file)
index 0000000..7977fea
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * A value-based match executed from some point in the data tree.
+ *
+ * @param <T> query result type
+ */
+@Beta
+public interface ValueMatch<T extends DataObject> extends StructuralBuilder<QueryExpression<T>> {
+    /**
+     * Start chaining an additional match for the query. Query results are guaranteed both this match and that
+     * additional match at the same time.
+     *
+     * @return A {@link MatchBuilderPath}
+     */
+    @NonNull MatchBuilderPath<T, T> and();
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ValueMatchBuilder.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/ValueMatchBuilder.java
new file mode 100644 (file)
index 0000000..edc650b
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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.binding.api.query;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Basic builder for a match of a leaf value.
+ *
+ * @param <T> query result type
+ * @param <V> value type
+ */
+@Beta
+public interface ValueMatchBuilder<T extends DataObject, V> {
+    /**
+     * Match any existing value.
+     *
+     * @return A ValueMatch
+     */
+    @NonNull ValueMatch<T> nonNull();
+
+    /**
+     * Match exact value.
+     *
+     * @param value value to match
+     * @return A ValueMatch
+     * @throws NullPointerException if value is null
+     */
+    @NonNull ValueMatch<T> valueEquals(@NonNull V value);
+}
diff --git a/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/package-info.java b/binding/mdsal-binding-api/src/main/java/org/opendaylight/mdsal/binding/api/query/package-info.java
new file mode 100644 (file)
index 0000000..113ab40
--- /dev/null
@@ -0,0 +1,11 @@
+/*
+ * 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
+ */
+/**
+ * Simple type-safe query language based on YANG concepts as manifested by its Java Bindings.
+ */
+package org.opendaylight.mdsal.binding.api.query;
diff --git a/binding/mdsal-binding-api/src/test/java/org/opendaylight/mdsal/binding/api/query/QueryBuilderExamples.java b/binding/mdsal-binding-api/src/test/java/org/opendaylight/mdsal/binding/api/query/QueryBuilderExamples.java
new file mode 100644 (file)
index 0000000..5dd8da5
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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.binding.api.query;
+
+import org.mockito.Mock;
+import org.opendaylight.yang.gen.v1.mdsal.query.norev.Foo;
+import org.opendaylight.yang.gen.v1.mdsal.query.norev.first.grp.System;
+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.third.grp.AffectedUsers;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.common.Uint64;
+
+public class QueryBuilderExamples {
+    @Mock
+    private QueryFactory factory;
+
+    /*
+     * Return all of /foo. Equivalent to a read() of the same identifier.
+     */
+    public QueryExpression<Foo> selectFoo() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .build();
+    }
+
+    /*
+     * Read all of /foo/system[name="some"]. Equivalent to a read() of the same identifier.
+     */
+    public QueryExpression<System> selectFooSystemSome() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class).child(System.class, new SystemKey("some")))
+                .build();
+    }
+
+    /*
+     * Read all entries in /foo/system. Equivalent to a read(/foo).get().nonnullSystem().
+     */
+    public QueryExpression<System> selectFooSystem() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .extractChild(System.class)
+                .build();
+    }
+
+    /*
+     * Read all entries in /foo/system, which have 'alias' set to 'some'.
+     */
+    public QueryExpression<System> selectFooSystemAliasSome() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .extractChild(System.class)
+                .matching()
+                    .leaf(System::getAlias)
+                    .valueEquals("some")
+                .build();
+    }
+
+    /*
+     * Read all entries in /foo/system, which have 'alias' containing the string 'needle'.
+     */
+    public QueryExpression<System> selectFooSystemAliasWithNeedle() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .extractChild(System.class)
+                .matching()
+                    .leaf(System::getAlias)
+                    .contains("needle")
+                .build();
+    }
+
+    /*
+     * Read all entries in /foo/system/alarms, which have 'critical' leaf present.
+     */
+    public QueryExpression<Alarms> selectFooSystemAlarmsCritical() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .extractChild(System.class)
+                .extractChild(Alarms.class)
+                .matching()
+                    .leaf(Alarms::getCritical)
+                    .nonNull()
+                .build();
+    }
+
+    /*
+     * Read all entries in /foo/system/alarms, which have 'critical' leaf present and have an entry in 'affected-users'
+     * with 'uid' larger than 10.
+     *
+     * Note this is the same expression as selectFooSystemCriticalUid(), but selects Alarms objects.
+     */
+    public QueryExpression<Alarms> selectFooSystemAlarmsCriticalUid() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .extractChild(System.class)
+                .extractChild(Alarms.class)
+                .matching()
+                    .leaf(Alarms::getCritical)
+                    .nonNull()
+                .and()
+                    .childObject(AffectedUsers.class)
+                    .leaf(AffectedUsers::getUid)
+                    .greaterThan(Uint64.TEN)
+                .build();
+    }
+
+
+    /*
+     * Read all entries in /foo/system, which have 'critical' leaf present and have an entry in 'affected-users'
+     * with 'uid' larger than 10.
+     *
+     * Note this is the same expression as selectFooSystemAlarmsCriticalUid(), but selects System objects.
+     */
+    public QueryExpression<System> selectFooSystemCriticalUid() {
+        return factory
+                .querySubtree(InstanceIdentifier.create(Foo.class))
+                .extractChild(System.class)
+                .matching()
+                    .childObject(Alarms.class)
+                    .leaf(Alarms::getCritical)
+                    .nonNull()
+                .and()
+                    .childObject(Alarms.class)
+                    .childObject(AffectedUsers.class)
+                    .leaf(AffectedUsers::getUid)
+                    .greaterThan(Uint64.TEN)
+                .build();
+    }
+}
diff --git a/binding/mdsal-binding-test-model/src/main/yang/mdsal-query.yang b/binding/mdsal-binding-test-model/src/main/yang/mdsal-query.yang
new file mode 100644 (file)
index 0000000..a5b36bd
--- /dev/null
@@ -0,0 +1,55 @@
+module mdsal-query {
+  namespace "mdsal-query";
+  prefix mq;
+
+  grouping first-grp {
+    list system {
+      key name;
+
+      leaf name {
+        type string;
+      }
+
+      leaf alias {
+        type string;
+      }
+
+      uses second-grp;
+    }
+  }
+
+  grouping second-grp {
+    list alarms {
+      key id;
+
+      leaf id {
+        type uint64;
+      }
+
+      leaf critical {
+        type empty;
+      }
+
+      uses third-grp;
+    }
+  }
+
+  grouping third-grp {
+    list affected-users {
+      key uid;
+
+      leaf uid {
+        type uint64;
+      }
+
+      leaf attr {
+        type uint8;
+      }
+    }
+  }
+
+  container foo {
+    uses first-grp;
+  }
+}
+