diff options
Diffstat (limited to 'java/com/google/gerrit/index/query/QueryBuilder.java')
-rw-r--r-- | java/com/google/gerrit/index/query/QueryBuilder.java | 350 |
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> + * @Operator + * public Predicate is(String value) { + * if ("starred".equals(value)) { + * return new StarredPredicate(); + * } + * if ("unread".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()); + } + } + } +} |