All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.index.query.QueryBuilder Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// 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.common.base.Preconditions.checkArgument;
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.COLON;
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.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.Extension;
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.Collections;
import java.util.List;
import java.util.Map;
import org.antlr.runtime.tree.Tree;

/**
 * Base class to support writing parsers for query languages.
 *
 * 

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: * *

 * @Operator
 * public Predicate is(String value) {
 *   if ("starred".equals(value)) {
 *     return new StarredPredicate();
 *   }
 *   if ("unread".equals(value)) {
 *     return new UnreadPredicate();
 *   }
 *   throw new IllegalArgumentException();
 * }
 * 
* *

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. * *

Subclasses may also declare a handler for values which appear without operator by overriding * {@link #defaultField(String)}. * *

Instances are non-singletons and should only be used once, in order to rescan the {@code * DynamicMap} of plugin-provided operators on each query invocation. * * @param type of object the predicates can evaluate in memory. */ public abstract class QueryBuilder> { /** Converts a value string passed to an operator into a {@link Predicate}. */ public interface OperatorFactory> { Predicate create(Q builder, String value) throws QueryParseException; } /** * Defines the operators known by a QueryBuilder. * *

Operators are discovered by scanning for methods annotated with {@link Operator}. Operator * methods must be public, non-abstract, return a {@code Predicate}, and take a single string as * an argument. * *

This class is deeply immutable. * * @param type of object the predicates can evaluate. * @param type of the query builder subclass. */ public static class Definition> { private final ImmutableMap> opFactories; public Definition(Class clazz) { ImmutableMap.Builder> b = ImmutableMap.builder(); Class c = clazz; while (c != QueryBuilder.class) { for (Method method : c.getDeclaredMethods()) { if (method.getAnnotation(Operator.class) == null) { continue; } checkArgument( CharMatcher.ascii().matchesAllOf(method.getName()), "method name must be ASCII: %s", method.getName()); checkArgument( Predicate.class.isAssignableFrom(method.getReturnType()) && method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == String.class && Modifier.isPublic(method.getModifiers()) && !Modifier.isAbstract(method.getModifiers()), "method must be of the form \"@%s public Predicate %s(String value)\": %s", Operator.class.getSimpleName(), method.getName(), method); String name = Ascii.toLowerCase(method.getName()); b.put(name, new ReflectionFactory<>(name, method)); } c = c.getSuperclass(); } opFactories = b.build(); } } /** * 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 > P find(Predicate p, Class

clazz) { if (clazz.isAssignableFrom(p.getClass())) { return (P) p; } for (Predicate 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 > P find( Predicate p, Class

clazz, String name) { if (p instanceof OperatorPredicate && ((OperatorPredicate) p).getOperator().equals(name) && clazz.isAssignableFrom(p.getClass())) { return (P) p; } for (Predicate c : p.getChildren()) { P r = find(c, clazz, name); if (r != null) { return r; } } return null; } protected final Definition builderDef; private final ImmutableMap> opFactories; protected Map opAliases = Collections.emptyMap(); protected QueryBuilder( Definition def, @Nullable DynamicMap> dynamicOpFactories) { builderDef = def; if (dynamicOpFactories != null) { ImmutableMap.Builder> opFactoriesBuilder = ImmutableMap.builder(); opFactoriesBuilder.putAll(def.opFactories); for (Extension> e : dynamicOpFactories) { String name = e.getExportName() + "_" + e.getPluginName(); opFactoriesBuilder.put(name, e.getProvider().get()); } opFactories = opFactoriesBuilder.build(); } else { opFactories = 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 parse(String query) throws QueryParseException { if (Strings.isNullOrEmpty(query)) { throw new QueryParseException("query is empty"); } return toPredicate(QueryParser.parse(query)); } public void setOperatorAliases(Map opAliases) { this.opAliases = opAliases; } /** * 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> parse(List queries) throws QueryParseException { List> predicates = new ArrayList<>(queries.size()); for (String query : queries) { predicates.add(parse(query)); } return predicates; } private Predicate 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(concatenateChildText(r)); case FIELD_NAME: return operator(r.getText(), concatenateChildText(r)); default: throw error("Unsupported operator: " + r); } } private static String concatenateChildText(Tree r) throws QueryParseException { if (r.getChildCount() == 0) { throw error("Expected children under: " + r); } if (r.getChildCount() == 1) { return getFieldValue(r.getChild(0)); } StringBuilder sb = new StringBuilder(); for (int i = 0; i < r.getChildCount(); i++) { sb.append(getFieldValue(r.getChild(i))); } return sb.toString(); } private static String getFieldValue(Tree r) throws QueryParseException { if (r.getChildCount() != 0) { throw error("Expected no children under: " + r); } switch (r.getType()) { case SINGLE_WORD: case COLON: case EXACT_PHRASE: return r.getText(); default: throw error( String.format( "Unsupported %s node in operator %s: %s", QueryParser.tokenNames[r.getType()], r.getParent(), r)); } } @SuppressWarnings("unchecked") private Predicate operator(String name, String value) throws QueryParseException { String opName = MoreObjects.firstNonNull(opAliases.get(name), name); @SuppressWarnings("rawtypes") OperatorFactory f = opFactories.get(opName); if (f == null && !opName.equals(name)) { f = opFactories.get(name); } if (f == null) { throw error("Unsupported operator " + name + ":" + value); } return f.create(this, value); } /** * Handle a value present outside of an operator. * *

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 defaultField(String value) throws QueryParseException { throw error("Unsupported query:" + value); } private List> children(Tree r) throws QueryParseException, IllegalArgumentException { List> 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> implements OperatorFactory { private final String name; private final Method method; ReflectionFactory(String name, Method method) { this.name = name; this.method = method; } @SuppressWarnings("unchecked") @Override public Predicate create(Q builder, String value) throws QueryParseException { try { return (Predicate) 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()); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy