summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/index/query/QueryBuilder.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/index/query/QueryBuilder.java')
-rw-r--r--java/com/google/gerrit/index/query/QueryBuilder.java350
1 files changed, 350 insertions, 0 deletions
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java
new file mode 100644
index 0000000000..c6c39c3fe0
--- /dev/null
+++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -0,0 +1,350 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index.query;
+
+import static com.google.gerrit.index.query.Predicate.and;
+import static com.google.gerrit.index.query.Predicate.not;
+import static com.google.gerrit.index.query.Predicate.or;
+import static com.google.gerrit.index.query.QueryParser.AND;
+import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
+import static com.google.gerrit.index.query.QueryParser.EXACT_PHRASE;
+import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
+import static com.google.gerrit.index.query.QueryParser.NOT;
+import static com.google.gerrit.index.query.QueryParser.OR;
+import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
+
+import com.google.common.base.Strings;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.antlr.runtime.tree.Tree;
+
+/**
+ * Base class to support writing parsers for query languages.
+ *
+ * <p>Subclasses may document their supported query operators by declaring public methods that
+ * perform the query conversion into a {@link Predicate}. For example, to support "is:starred",
+ * "is:unread", and nothing else, a subclass may write:
+ *
+ * <pre>
+ * &#064;Operator
+ * public Predicate is(String value) {
+ * if (&quot;starred&quot;.equals(value)) {
+ * return new StarredPredicate();
+ * }
+ * if (&quot;unread&quot;.equals(value)) {
+ * return new UnreadPredicate();
+ * }
+ * throw new IllegalArgumentException();
+ * }
+ * </pre>
+ *
+ * <p>The available operator methods are discovered at runtime via reflection. Method names (after
+ * being converted to lowercase), correspond to operators in the query language, method string
+ * values correspond to the operator argument. Methods must be declared {@code public}, returning
+ * {@link Predicate}, accepting one {@link String}, and annotated with the {@link Operator}
+ * annotation.
+ *
+ * <p>Subclasses may also declare a handler for values which appear without operator by overriding
+ * {@link #defaultField(String)}.
+ *
+ * @param <T> type of object the predicates can evaluate in memory.
+ */
+public abstract class QueryBuilder<T> {
+ /** Converts a value string passed to an operator into a {@link Predicate}. */
+ public interface OperatorFactory<T, Q extends QueryBuilder<T>> {
+ Predicate<T> create(Q builder, String value) throws QueryParseException;
+ }
+
+ /**
+ * Defines the operators known by a QueryBuilder.
+ *
+ * <p>This class is thread-safe and may be reused or cached.
+ *
+ * @param <T> type of object the predicates can evaluate in memory.
+ * @param <Q> type of the query builder subclass.
+ */
+ public static class Definition<T, Q extends QueryBuilder<T>> {
+ private final Map<String, OperatorFactory<T, Q>> opFactories = new HashMap<>();
+
+ public Definition(Class<Q> clazz) {
+ // Guess at the supported operators by scanning methods.
+ //
+ Class<?> c = clazz;
+ while (c != QueryBuilder.class) {
+ for (Method method : c.getDeclaredMethods()) {
+ if (method.getAnnotation(Operator.class) != null
+ && Predicate.class.isAssignableFrom(method.getReturnType())
+ && method.getParameterTypes().length == 1
+ && method.getParameterTypes()[0] == String.class
+ && (method.getModifiers() & Modifier.ABSTRACT) == 0
+ && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) {
+ final String name = method.getName().toLowerCase();
+ if (!opFactories.containsKey(name)) {
+ opFactories.put(name, new ReflectionFactory<T, Q>(name, method));
+ }
+ }
+ }
+ c = c.getSuperclass();
+ }
+ }
+ }
+
+ /**
+ * Locate a predicate in the predicate tree.
+ *
+ * @param p the predicate to find.
+ * @param clazz type of the predicate instance.
+ * @return the predicate, null if not found.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T, P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
+ if (clazz.isAssignableFrom(p.getClass())) {
+ return (P) p;
+ }
+
+ for (Predicate<T> c : p.getChildren()) {
+ P r = find(c, clazz);
+ if (r != null) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Locate a predicate in the predicate tree.
+ *
+ * @param p the predicate to find.
+ * @param clazz type of the predicate instance.
+ * @param name name of the operator.
+ * @return the first instance of a predicate having the given type, as found by a depth-first
+ * search.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T, P extends OperatorPredicate<T>> P find(
+ Predicate<T> p, Class<P> clazz, String name) {
+ if (p instanceof OperatorPredicate
+ && ((OperatorPredicate<?>) p).getOperator().equals(name)
+ && clazz.isAssignableFrom(p.getClass())) {
+ return (P) p;
+ }
+
+ for (Predicate<T> c : p.getChildren()) {
+ P r = find(c, clazz, name);
+ if (r != null) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ protected final Definition<T, ? extends QueryBuilder<T>> builderDef;
+
+ protected final Map<String, OperatorFactory<?, ?>> opFactories;
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ protected QueryBuilder(Definition<T, ? extends QueryBuilder<T>> def) {
+ builderDef = def;
+ opFactories = (Map) def.opFactories;
+ }
+
+ /**
+ * Parse a user-supplied query string into a predicate.
+ *
+ * @param query the query string.
+ * @return predicate representing the user query.
+ * @throws QueryParseException the query string is invalid and cannot be parsed by this parser.
+ * This may be due to a syntax error, may be due to an operator not being supported, or due to
+ * an invalid value being passed to a recognized operator.
+ */
+ public Predicate<T> parse(String query) throws QueryParseException {
+ if (Strings.isNullOrEmpty(query)) {
+ throw new QueryParseException("query is empty");
+ }
+ return toPredicate(QueryParser.parse(query));
+ }
+
+ /**
+ * Parse multiple user-supplied query strings into a list of predicates.
+ *
+ * @param queries the query strings.
+ * @return predicates representing the user query, in the same order as the input.
+ * @throws QueryParseException one of the query strings is invalid and cannot be parsed by this
+ * parser. This may be due to a syntax error, may be due to an operator not being supported,
+ * or due to an invalid value being passed to a recognized operator.
+ */
+ public List<Predicate<T>> parse(List<String> queries) throws QueryParseException {
+ List<Predicate<T>> predicates = new ArrayList<>(queries.size());
+ for (String query : queries) {
+ predicates.add(parse(query));
+ }
+ return predicates;
+ }
+
+ private Predicate<T> toPredicate(Tree r) throws QueryParseException, IllegalArgumentException {
+ switch (r.getType()) {
+ case AND:
+ return and(children(r));
+ case OR:
+ return or(children(r));
+ case NOT:
+ return not(toPredicate(onlyChildOf(r)));
+
+ case DEFAULT_FIELD:
+ return defaultField(onlyChildOf(r));
+
+ case FIELD_NAME:
+ return operator(r.getText(), onlyChildOf(r));
+
+ default:
+ throw error("Unsupported operator: " + r);
+ }
+ }
+
+ private Predicate<T> operator(String name, Tree val) throws QueryParseException {
+ switch (val.getType()) {
+ // Expand multiple values, "foo:(a b c)", as though they were written
+ // out with the longer form, "foo:a foo:b foo:c".
+ //
+ case AND:
+ case OR:
+ {
+ List<Predicate<T>> p = new ArrayList<>(val.getChildCount());
+ for (int i = 0; i < val.getChildCount(); i++) {
+ final Tree c = val.getChild(i);
+ if (c.getType() != DEFAULT_FIELD) {
+ throw error("Nested operator not expected: " + c);
+ }
+ p.add(operator(name, onlyChildOf(c)));
+ }
+ return val.getType() == AND ? and(p) : or(p);
+ }
+
+ case SINGLE_WORD:
+ case EXACT_PHRASE:
+ if (val.getChildCount() != 0) {
+ throw error("Expected no children under: " + val);
+ }
+ return operator(name, val.getText());
+
+ default:
+ throw error("Unsupported node in operator " + name + ": " + val);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Predicate<T> operator(String name, String value) throws QueryParseException {
+ @SuppressWarnings("rawtypes")
+ OperatorFactory f = opFactories.get(name);
+ if (f == null) {
+ throw error("Unsupported operator " + name + ":" + value);
+ }
+ return f.create(this, value);
+ }
+
+ private Predicate<T> defaultField(Tree r) throws QueryParseException {
+ switch (r.getType()) {
+ case SINGLE_WORD:
+ case EXACT_PHRASE:
+ if (r.getChildCount() != 0) {
+ throw error("Expected no children under: " + r);
+ }
+ return defaultField(r.getText());
+
+ default:
+ throw error("Unsupported node: " + r);
+ }
+ }
+
+ /**
+ * Handle a value present outside of an operator.
+ *
+ * <p>This default implementation always throws an "Unsupported query: " message containing the
+ * input text. Subclasses may override this method to perform do-what-i-mean guesses based on the
+ * input string.
+ *
+ * @param value the value supplied by itself in the query.
+ * @return predicate representing this value.
+ * @throws QueryParseException the parser does not recognize this value.
+ */
+ protected Predicate<T> defaultField(String value) throws QueryParseException {
+ throw error("Unsupported query:" + value);
+ }
+
+ private List<Predicate<T>> children(Tree r) throws QueryParseException, IllegalArgumentException {
+ List<Predicate<T>> p = new ArrayList<>(r.getChildCount());
+ for (int i = 0; i < r.getChildCount(); i++) {
+ p.add(toPredicate(r.getChild(i)));
+ }
+ return p;
+ }
+
+ private Tree onlyChildOf(Tree r) throws QueryParseException {
+ if (r.getChildCount() != 1) {
+ throw error("Expected exactly one child: " + r);
+ }
+ return r.getChild(0);
+ }
+
+ protected static QueryParseException error(String msg) {
+ return new QueryParseException(msg);
+ }
+
+ protected static QueryParseException error(String msg, Throwable why) {
+ return new QueryParseException(msg, why);
+ }
+
+ /** Denotes a method which is a query operator. */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ protected @interface Operator {}
+
+ private static class ReflectionFactory<T, Q extends QueryBuilder<T>>
+ implements OperatorFactory<T, Q> {
+ private final String name;
+ private final Method method;
+
+ ReflectionFactory(String name, Method method) {
+ this.name = name;
+ this.method = method;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Predicate<T> create(Q builder, String value) throws QueryParseException {
+ try {
+ return (Predicate<T>) method.invoke(builder, value);
+ } catch (RuntimeException | IllegalAccessException e) {
+ throw error("Error in operator " + name + ":" + value, e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof QueryParseException) {
+ throw (QueryParseException) e.getCause();
+ }
+ throw error("Error in operator " + name + ":" + value, e.getCause());
+ }
+ }
+ }
+}