diff --git a/pom.xml b/pom.xml index 7d5664a1d2..969568e204 100644 --- a/pom.xml +++ b/pom.xml @@ -290,7 +290,20 @@ limitations under the License. - + + + org.antlr + antlr3-maven-plugin + 3.1.1 + + + + antlr + + + + + org.apache.maven.plugins maven-compiler-plugin @@ -570,6 +583,13 @@ limitations under the License. 2.1.2 + + org.antlr + antlr + 3.1.1 + compile + + bouncycastle bcpg-jdk15 @@ -618,6 +638,13 @@ limitations under the License. compile + + junit + junit + 3.8.1 + test + + com.google.gwt diff --git a/src/main/antlr/com/google/gerrit/server/query/Query.g b/src/main/antlr/com/google/gerrit/server/query/Query.g new file mode 100644 index 0000000000..d17a9de2f2 --- /dev/null +++ b/src/main/antlr/com/google/gerrit/server/query/Query.g @@ -0,0 +1,171 @@ +// 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. + +grammar Query; +options { + language = Java; + output = AST; +} + +tokens { + FIELD_NAME; + DEFAULT_FIELD; + SINGLE_WORD; + EXACT_PHRASE; + AND; + OR; + NOT; +} + +@header { +package com.google.gerrit.server.query; +} +@members { + static class QueryParseInternalException extends RuntimeException { + QueryParseInternalException(final String msg) { + super(msg); + } + } + + public static Tree parse(final String str) + throws QueryParseException { + try { + final QueryParser p = new QueryParser( + new TokenRewriteStream( + new QueryLexer( + new ANTLRStringStream(str) + ) + ) + ); + return (Tree)p.query().getTree(); + } catch (QueryParseInternalException e) { + throw new QueryParseException(e.getMessage()); + } catch (RecognitionException e) { + throw new QueryParseException(e.getMessage()); + } + } + + static boolean isSingleWord(final String value) { + try { + final QueryLexer lexer = new QueryLexer(new ANTLRStringStream(value)); + lexer.mSINGLE_WORD(); + return lexer.nextToken().getType() == QueryParser.EOF; + } catch (RecognitionException e) { + return false; + } + } + + @Override + public void displayRecognitionError(String[] tokenNames, + RecognitionException e) { + String hdr = getErrorHeader(e); + String msg = getErrorMessage(e, tokenNames); + throw new QueryParseInternalException(hdr + " " + msg); + } +} + +@lexer::header { +package com.google.gerrit.server.query; +} +@lexer::members { +} + +query + : conditionOr + ; + +conditionOr + : (conditionAnd OR) + => conditionAnd OR^ conditionAnd (OR! conditionAnd)* + | conditionAnd + ; + +conditionAnd + : (conditionNot AND) + => i+=conditionNot (i+=conditionAnd2)* + -> ^(AND $i+) + | (conditionNot conditionNot) + => i+=conditionNot (i+=conditionAnd2)* + -> ^(AND $i+) + | conditionNot + ; +conditionAnd2 + : AND! conditionNot + | conditionNot + ; + +conditionNot + : '-' conditionBase -> ^(NOT conditionBase) + | NOT^ conditionBase + | conditionBase + ; +conditionBase + : (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue + | fieldValue -> ^(DEFAULT_FIELD fieldValue) + ; + +fieldValue + : n=FIELD_NAME -> SINGLE_WORD[n] + | SINGLE_WORD + | EXACT_PHRASE + | '('! conditionOr ')'! + ; + +AND: 'AND' ; +OR: 'OR' ; +NOT: 'NOT' ; + +WS + : ( ' ' | '\r' | '\t' | '\n' ) { $channel=HIDDEN; } + ; + +FIELD_NAME + : ('a'..'z')+ + ; + +EXACT_PHRASE + : '"' ( ~('"') )* '"' { + String s = $text; + setText(s.substring(1, s.length() - 1)); + } + ; + +SINGLE_WORD + : ~( '-' | NON_WORD ) ( ~( NON_WORD ) )* + ; +fragment NON_WORD + : ( '\u0000'..' ' + | '!' + | '"' + | '#' + | '$' + | '%' + | '&' + | '\'' + | '(' | ')' + // '*' permit + // '+' permit + // ',' permit + // '-' permit + // '.' permit + // '/' permit + | ':' + | ';' + | '<' | '=' | '>' + | '?' + | '[' | ']' + | '{' | '}' + | '~' + ) + ; diff --git a/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/src/main/java/com/google/gerrit/server/query/AndPredicate.java new file mode 100644 index 0000000000..5e95c18c2b --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/AndPredicate.java @@ -0,0 +1,86 @@ +// 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.server.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** Requires all predicates to be true. */ +public final class AndPredicate extends Predicate { + private final Predicate[] children; + + public AndPredicate(final Predicate... that) { + this(Arrays.asList(that)); + } + + public AndPredicate(final Collection that) { + final ArrayList tmp = new ArrayList(that.size()); + for (Predicate p : that) { + if (p instanceof AndPredicate) { + tmp.addAll(p.getChildren()); + } else { + tmp.add(p); + } + } + if (tmp.size() < 2) { + throw new IllegalArgumentException("Need at least two predicates"); + } + children = new Predicate[tmp.size()]; + tmp.toArray(children); + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(Arrays.asList(children)); + } + + @Override + public int getChildCount() { + return children.length; + } + + @Override + public Predicate getChild(final int i) { + return children[i]; + } + + @Override + public int hashCode() { + return children[0].hashCode() * 31 + children[1].hashCode(); + } + + @Override + public boolean equals(final Object other) { + return other instanceof AndPredicate + && getChildren().equals(((AndPredicate) other).getChildren()); + } + + @Override + public String toString() { + final StringBuilder r = new StringBuilder(); + r.append("("); + for (int i = 0; i < children.length; i++) { + if (i != 0) { + r.append(" "); + } + r.append(children[i]); + } + r.append(")"); + return r.toString(); + } +} diff --git a/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java b/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java new file mode 100644 index 0000000000..4bd20ad89f --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java @@ -0,0 +1,79 @@ +// 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.server.query; + +import com.google.gerrit.client.reviewdb.RevId; +import com.google.inject.Singleton; + +import org.spearce.jgit.lib.AbbreviatedObjectId; + +/** + * Parses a query string meant to be applied to change objects. + *

+ * This class is thread-safe, and may be reused across threads to parse queries. + */ +@Singleton +public class ChangeQueryBuilder extends QueryBuilder { + public static final String FIELD_CHANGE = "change"; + public static final String FIELD_COMMIT = "commit"; + public static final String FIELD_REVIEWER = "reviewer"; + public static final String FIELD_OWNER = "owner"; + + private static final String CHANGE_RE = "^[1-9][0-9]*$"; + private static final String COMMIT_RE = + "^([0-9a-fA-F]{4," + RevId.LEN + "})$"; + + @Operator + public Predicate change(final String value) { + match(value, CHANGE_RE); + return new OperatorPredicate(FIELD_CHANGE, value); + } + + @Operator + public Predicate commit(final String value) { + final AbbreviatedObjectId id = AbbreviatedObjectId.fromString(value); + return new ObjectIdPredicate(FIELD_COMMIT, id); + } + + @Operator + public Predicate owner(final String value) { + return new OperatorPredicate(FIELD_OWNER, value); + } + + @Operator + public Predicate reviewer(final String value) { + return new OperatorPredicate(FIELD_REVIEWER, value); + } + + @Override + protected Predicate defaultField(final String value) + throws QueryParseException { + if (value.matches(CHANGE_RE)) { + return change(value); + + } else if (value.matches(COMMIT_RE)) { + return commit(value); + + } else { + throw error("Unsupported query:" + value); + } + } + + private static void match(String val, String re) { + if (!val.matches(re)) { + throw new IllegalArgumentException("Invalid value :" + val); + } + } +} diff --git a/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/src/main/java/com/google/gerrit/server/query/NotPredicate.java new file mode 100644 index 0000000000..ddb03a6803 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/NotPredicate.java @@ -0,0 +1,53 @@ +// 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.server.query; + +import java.util.Collections; +import java.util.List; + +/** Negates the result of another predicate. */ +public final class NotPredicate extends Predicate { + private final Predicate that; + + public NotPredicate(final Predicate that) { + this.that = that; + } + + @Override + public Predicate not() { + return that; + } + + @Override + public List getChildren() { + return Collections.singletonList(that); + } + + @Override + public int hashCode() { + return ~that.hashCode(); + } + + @Override + public boolean equals(final Object other) { + return other instanceof NotPredicate + && getChildren().equals(((Predicate) other).getChildren()); + } + + @Override + public String toString() { + return "-" + that.toString(); + } +} diff --git a/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java b/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java new file mode 100644 index 0000000000..2da042cd55 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java @@ -0,0 +1,60 @@ +// 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.server.query; + +import org.spearce.jgit.lib.AbbreviatedObjectId; +import org.spearce.jgit.lib.ObjectId; + + +/** Predicate for a field of {@link ObjectId}. */ +public final class ObjectIdPredicate extends OperatorPredicate { + private final AbbreviatedObjectId id; + + public ObjectIdPredicate(final String name, final AbbreviatedObjectId id) { + super(name, id.name()); + this.id = id; + } + + public boolean isComplete() { + return id.isComplete(); + } + + public AbbreviatedObjectId abbreviated() { + return id; + } + + public ObjectId full() { + return id.toObjectId(); + } + + @Override + public int hashCode() { + return getOperator().hashCode() * 31 + id.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ObjectIdPredicate) { + final ObjectIdPredicate p = (ObjectIdPredicate) other; + return getOperator().equals(p.getOperator()) && id.equals(p.id); + } + return false; + } + + @Override + public String toString() { + return getOperator() + ":" + id.name(); + } +} diff --git a/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java b/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java new file mode 100644 index 0000000000..fbd6af1225 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java @@ -0,0 +1,60 @@ +// 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.server.query; + + +/** Predicate to filter a field by matching value. */ +public class OperatorPredicate extends Predicate { + private final String name; + private final String value; + + public OperatorPredicate(final String name, final String value) { + this.name = name; + this.value = value; + } + + public String getOperator() { + return name; + } + + public String getValue() { + return value; + } + + @Override + public int hashCode() { + return getOperator().hashCode() * 31 + getValue().hashCode(); + } + + @Override + public boolean equals(final Object other) { + if (getClass() == other.getClass()) { + final OperatorPredicate p = (OperatorPredicate) other; + return getOperator().equals(p.getOperator()) + && getValue().equals(p.getValue()); + } + return false; + } + + @Override + public String toString() { + final String val = getValue(); + if (QueryParser.isSingleWord(val)) { + return getOperator() + ":" + val; + } else { + return getOperator() + ":\"" + val + "\""; + } + } +} diff --git a/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/src/main/java/com/google/gerrit/server/query/OrPredicate.java new file mode 100644 index 0000000000..a8b8d2b8d0 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/OrPredicate.java @@ -0,0 +1,86 @@ +// 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.server.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** Requires one predicate to be true. */ +public final class OrPredicate extends Predicate { + private final Predicate[] children; + + public OrPredicate(final Predicate... that) { + this(Arrays.asList(that)); + } + + public OrPredicate(final Collection that) { + final ArrayList tmp = new ArrayList(that.size()); + for (Predicate p : that) { + if (p instanceof OrPredicate) { + tmp.addAll(p.getChildren()); + } else { + tmp.add(p); + } + } + if (tmp.size() < 2) { + throw new IllegalArgumentException("Need at least two predicates"); + } + children = new Predicate[tmp.size()]; + tmp.toArray(children); + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(Arrays.asList(children)); + } + + @Override + public int getChildCount() { + return children.length; + } + + @Override + public Predicate getChild(final int i) { + return children[i]; + } + + @Override + public int hashCode() { + return children[0].hashCode() * 31 + children[1].hashCode(); + } + + @Override + public boolean equals(final Object other) { + return other instanceof OrPredicate + && getChildren().equals(((OrPredicate) other).getChildren()); + } + + @Override + public String toString() { + final StringBuilder r = new StringBuilder(); + r.append("("); + for (int i = 0; i < children.length; i++) { + if (i != 0) { + r.append(" OR "); + } + r.append(children[i]); + } + r.append(")"); + return r.toString(); + } +} diff --git a/src/main/java/com/google/gerrit/server/query/Predicate.java b/src/main/java/com/google/gerrit/server/query/Predicate.java new file mode 100644 index 0000000000..70da79db8a --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/Predicate.java @@ -0,0 +1,85 @@ +// 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.server.query; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * An abstract predicate tree for any form of query. + *

+ * Implementations should be immutable, and therefore also be thread-safe. They + * also should ensure their immutable promise by defensively copying any + * structures which might be modified externally, but were passed into the + * object's constructor. + *

+ * Predicates should support deep inspection whenever possible, so that generic + * algorithms can be written to operate against them. Predicates which contain + * other predicates should override {@link #getChildren()} to return the list of + * children nested within the predicate. + */ +public abstract class Predicate { + /** Combine the passed predicates into a single AND node. */ + public static Predicate and(final Predicate... that) { + return new AndPredicate(that); + } + + /** Combine the passed predicates into a single AND node. */ + public static Predicate and(final Collection that) { + return new AndPredicate(that); + } + + /** Combine the passed predicates into a single OR node. */ + public static Predicate or(final Predicate... that) { + return new OrPredicate(that); + } + + /** Combine the passed predicates into a single OR node. */ + public static Predicate or(final Collection that) { + return new OrPredicate(that); + } + + /** Invert the passed node; same as {@code that.not()}. */ + public static Predicate not(final Predicate that) { + return that.not(); + } + + /** Get the children of this predicate, if any. */ + public List getChildren() { + return Collections.emptyList(); + } + + /** Same as {@code getChildren().size()} */ + public int getChildCount() { + return getChildren().size(); + } + + /** Same as {@code getChildren().get(i)} */ + public Predicate getChild(final int i) { + return getChildren().get(i); + } + + /** Obtain the inverse of this predicate. */ + public Predicate not() { + return new NotPredicate(this); + } + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object other); +} diff --git a/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/src/main/java/com/google/gerrit/server/query/QueryBuilder.java new file mode 100644 index 0000000000..da967f45ce --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/QueryBuilder.java @@ -0,0 +1,262 @@ +// 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.server.query; + +import static com.google.gerrit.server.query.Predicate.and; +import static com.google.gerrit.server.query.Predicate.not; +import static com.google.gerrit.server.query.Predicate.or; +import static com.google.gerrit.server.query.QueryParser.AND; +import static com.google.gerrit.server.query.QueryParser.DEFAULT_FIELD; +import static com.google.gerrit.server.query.QueryParser.EXACT_PHRASE; +import static com.google.gerrit.server.query.QueryParser.FIELD_NAME; +import static com.google.gerrit.server.query.QueryParser.NOT; +import static com.google.gerrit.server.query.QueryParser.OR; +import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD; + +import org.antlr.runtime.tree.Tree; + +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.HashMap; +import java.util.Map; + +/** + * Base class to support writing parsers for query languages. + *

+ * This class is thread-safe, and may be reused across threads to parse queries, + * so implementations of this class should also strive to be thread-safe. + *

+ * 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(final 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)}. + */ +public abstract class QueryBuilder { + private final Map opFactories = + new HashMap(); + + protected QueryBuilder() { + // Guess at the supported operators by scanning methods. + // + Class c = getClass(); + while (c != QueryBuilder.class) { + for (final 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(name, method)); + } + } + } + c = c.getSuperclass(); + } + } + + /** + * 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(final String query) throws QueryParseException { + return toPredicate(QueryParser.parse(query)); + } + + private Predicate toPredicate(final 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 operator(final String name, final 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: { + final Predicate[] p = new Predicate[val.getChildCount()]; + for (int i = 0; i < p.length; i++) { + final Tree c = val.getChild(i); + if (c.getType() != DEFAULT_FIELD) { + throw error("Nested operator not expected: " + c); + } + p[i] = 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); + } + } + + private Predicate operator(final String name, final String value) + throws QueryParseException { + final OperatorFactory f = opFactories.get(name); + if (f == null) { + throw error("Unsupported operator " + name + ":" + value); + } + return f.create(value); + } + + private Predicate defaultField(final 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. + *

+ * 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(final String value) + throws QueryParseException { + throw error("Unsupported query:" + value); + } + + private Predicate[] children(final Tree r) throws QueryParseException, + IllegalArgumentException { + final Predicate[] p = new Predicate[r.getChildCount()]; + for (int i = 0; i < p.length; i++) { + p[i] = toPredicate(r.getChild(i)); + } + return p; + } + + private Tree onlyChildOf(final 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); + } + + /** Converts a value string passed to an operator into a {@link Predicate}. */ + protected interface OperatorFactory { + Predicate create(String value) throws QueryParseException; + } + + /** Denotes a method which is a query operator. */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + protected @interface Operator { + } + + private class ReflectionFactory implements OperatorFactory { + private final String name; + private final Method method; + + ReflectionFactory(final String name, final Method method) { + this.name = name; + this.method = method; + } + + @Override + public Predicate create(final String value) throws QueryParseException { + try { + return (Predicate) method.invoke(QueryBuilder.this, value); + } catch (RuntimeException e) { + throw error("Error in operator " + name + ":" + value, e); + } catch (IllegalAccessException e) { + throw error("Error in operator " + name + ":" + value, e); + } catch (InvocationTargetException e) { + throw error("Error in operator " + name + ":" + value, e.getCause()); + } + } + } +} diff --git a/src/main/java/com/google/gerrit/server/query/QueryParseException.java b/src/main/java/com/google/gerrit/server/query/QueryParseException.java new file mode 100644 index 0000000000..346e99de3a --- /dev/null +++ b/src/main/java/com/google/gerrit/server/query/QueryParseException.java @@ -0,0 +1,25 @@ +// 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.server.query; + +public class QueryParseException extends Exception { + public QueryParseException(final String message) { + super(message); + } + + public QueryParseException(final String msg, final Throwable why) { + super(msg, why); + } +} diff --git a/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java b/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java new file mode 100644 index 0000000000..11702ccfe0 --- /dev/null +++ b/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java @@ -0,0 +1,210 @@ +// 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.server.query; + +import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_CHANGE; +import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_COMMIT; +import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_OWNER; +import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_REVIEWER; +import static com.google.gerrit.server.query.Predicate.and; +import static com.google.gerrit.server.query.Predicate.not; +import static com.google.gerrit.server.query.Predicate.or; + +import junit.framework.TestCase; + +import org.spearce.jgit.lib.AbbreviatedObjectId; + +public class ChangeQueryBuilderTest extends TestCase { + private static OperatorPredicate f(final String name, final String value) { + return new OperatorPredicate(name, value); + } + + private static Predicate owner(final String who) { + return f(FIELD_OWNER, who); + } + + private static Predicate reviewer(final String who) { + return f(FIELD_REVIEWER, who); + } + + private static Predicate commit(final String idstr) { + final AbbreviatedObjectId id = AbbreviatedObjectId.fromString(idstr); + return new ObjectIdPredicate(FIELD_COMMIT, id); + } + + private static Predicate p(final String str) throws QueryParseException { + return new ChangeQueryBuilder().parse(str); + } + + public void testEmptyQuery() { + try { + p(""); + fail("expected exception"); + } catch (QueryParseException e) { + assertEquals("line 0:-1 no viable alternative at input ''", e + .getMessage()); + } + } + + public void testFailInvalidOperator() { + final String op = "thiswillneverbeaqueryoperatoritistoolongtotype"; + final String val = "true"; + try { + p(op + ":" + val); + fail("expected exception"); + } catch (QueryParseException e) { + assertEquals("Unsupported operator " + op + ":" + val, e.getMessage()); + } + } + + public void testFailNestedOperator() { + try { + p("commit:(foo:bar whiz:bang)"); + fail("expected exception"); + } catch (QueryParseException e) { + assertEquals("Nested operator not expected: foo", e.getMessage()); + } + } + + // commit: + + public void testDefaultSHA1() throws QueryParseException { + assertEquals(commit("6ea15"), p("6ea15")); + assertEquals(commit("6ea15"), p("6EA15")); + assertEquals(commit("6ea15b73668073fd9f70b2635efcb8cf8aabda22"), + p("6ea15b73668073fd9f70b2635efcb8cf8aabda22")); + } + + public void testCommitSHA1() throws QueryParseException { + assertEquals(commit("6ea15"), p("commit:6ea15")); + assertEquals(commit("6ea15"), p("commit:6EA15")); // note: forces lowercase + assertEquals(commit("6ea15b73668073fd9f70b2635efcb8cf8aabda22"), + p("commit:6ea15b73668073fd9f70b2635efcb8cf8aabda22")); + + try { + p("commit:yonothash"); + } catch (QueryParseException e) { + assertEquals("Error in operator commit:yonothash", e.getMessage()); + } + } + + // change: + + public void testDefaultChangeID() throws QueryParseException { + assertEquals(f(FIELD_CHANGE, "1234"), p("1234")); + } + + public void testChangeID() throws QueryParseException { + assertEquals(f(FIELD_CHANGE, "1234"), p("change:1234")); + } + + // owner: + + public void testOwnerBare() throws QueryParseException { + assertEquals(owner("bob"), p("owner:bob")); + assertEquals(owner("Bob"), p("owner:Bob")); + assertEquals(owner("bob@example.com"), p("owner:bob@example.com")); + + assertEquals(owner("bob"), p("owner: bob")); + assertEquals(owner("Bob"), p("owner: Bob")); + assertEquals(owner("bob@example.com"), p("owner: bob@example.com")); + + assertEquals(owner("bob"), p("owner:\tbob")); + assertEquals(owner("Bob"), p("owner:\tBob")); + assertEquals(owner("bob@example.com"), p("owner:\tbob@example.com")); + } + + public void testOwnerQuoted() throws QueryParseException { + assertEquals(owner("bob"), p("owner:\"bob\"")); + assertEquals(owner("bob@example.com"), p("owner:\"bob@example.com\"")); + assertEquals(owner(""), p("owner:\"\"")); + assertEquals(owner("A U Thor"), p("owner:\"A U Thor\"")); + + assertEquals(owner("bob"), p("owner: \"bob\"")); + assertEquals(owner("bob@example.com"), p("owner: \"bob@example.com\"")); + assertEquals(owner(""), p("owner: \"\"")); + assertEquals(owner("A U Thor"), p("owner: \"A U Thor\"")); + + assertEquals(owner("bob"), p("owner:\t\"bob\"")); + assertEquals(owner("bob@example.com"), p("owner:\t\"bob@example.com\"")); + assertEquals(owner(""), p("owner:\t\"\"")); + assertEquals(owner("A U Thor"), p("owner:\t\"A U Thor\"")); + } + + public void testOwner_NOT() throws QueryParseException { + assertEquals(not(owner("bob")), p("-owner:bob")); + assertEquals(not(owner("Bob")), p("-owner:Bob")); + assertEquals(not(owner("bob@example.com")), p("-owner:bob@example.com")); + + assertEquals(not(owner("bob")), p("NOT owner:bob")); + assertEquals(not(owner("Bob")), p("NOT owner:Bob")); + assertEquals(not(owner("bob@example.com")), p("NOT owner:bob@example.com")); + } + + // AND + + public void testAND_Styles2() throws QueryParseException { + final Predicate exp = and(commit("6ea15"), owner("bob")); + assertEquals(exp, p("6ea15 owner:bob")); + assertEquals(exp, p("6ea15 AND owner:bob")); + } + + public void testAND_Styles3() throws QueryParseException { + final Predicate exp = and(commit("6ea15"), owner("bob"), reviewer("alice")); + assertEquals(exp, p("6ea15 owner:bob reviewer:alice")); + assertEquals(exp, p("6ea15 AND owner:bob reviewer:alice")); + assertEquals(exp, p("6ea15 owner:bob AND reviewer:alice")); + assertEquals(exp, p("6ea15 AND owner:bob AND reviewer:alice")); + } + + public void testAND_ManyValuesOneOperator() throws QueryParseException { + final Predicate exp = + and(reviewer("alice"), reviewer("bob"), reviewer("charlie")); + assertEquals(exp, p("reviewer:(alice bob charlie)")); + assertEquals(exp, p("reviewer:(alice AND bob charlie)")); + assertEquals(exp, p("reviewer:(alice bob AND charlie)")); + assertEquals(exp, p("reviewer:(alice AND bob AND charlie)")); + } + + public void testAND_FlattensOperators() throws QueryParseException { + final Predicate exp = + and(reviewer("alice"), reviewer("bob"), reviewer("charlie")); + assertEquals(exp, p("reviewer:alice reviewer:(bob charlie)")); + } + + // OR + + public void testOR_2() throws QueryParseException { + final Predicate exp = or(commit("6ea15"), owner("bob")); + assertEquals(exp, p("6ea15 OR owner:bob")); + } + + public void testOR_3() throws QueryParseException { + final Predicate exp = or(commit("6ea15"), owner("bob"), reviewer("alice")); + assertEquals(exp, p("6ea15 OR owner:bob OR reviewer:alice")); + } + + public void testOR_ManyValuesOneOperator() throws QueryParseException { + final Predicate exp = + or(reviewer("alice"), reviewer("bob"), reviewer("charlie")); + assertEquals(exp, p("reviewer:(alice OR bob OR charlie)")); + } + + public void testOR_FlattensOperators() throws QueryParseException { + final Predicate exp = + or(reviewer("alice"), reviewer("bob"), reviewer("charlie")); + assertEquals(exp, p("reviewer:alice OR reviewer:(bob OR charlie)")); + } +} diff --git a/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java b/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java new file mode 100644 index 0000000000..63cf166951 --- /dev/null +++ b/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java @@ -0,0 +1,50 @@ +// 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.server.query; + +import junit.framework.TestCase; + +public class FieldPredicateTest extends TestCase { + private static OperatorPredicate f(final String name, final String value) { + return new OperatorPredicate(name, value); + } + + public void testToString() { + assertEquals("author:bob", f("author", "bob").toString()); + assertEquals("author:\"\"", f("author", "").toString()); + assertEquals("owner:\"A U Thor\"", f("owner", "A U Thor").toString()); + } + + public void testEquals() { + assertTrue(f("author", "bob").equals(f("author", "bob"))); + assertFalse(f("author", "bob").equals(f("author", "alice"))); + assertFalse(f("owner", "bob").equals(f("author", "bob"))); + assertFalse(f("author", "bob").equals("author")); + } + + public void testHashCode() { + assertTrue(f("a", "bob").hashCode() == f("a", "bob").hashCode()); + assertFalse(f("a", "bob").hashCode() == f("a", "alice").hashCode()); + } + + public void testNameValue() { + final String name = "author"; + final String value = "alice"; + final OperatorPredicate f = f(name, value); + assertSame(name, f.getOperator()); + assertSame(value, f.getValue()); + assertEquals(0, f.getChildren().size()); + } +} diff --git a/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java new file mode 100644 index 0000000000..23ba24ed81 --- /dev/null +++ b/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java @@ -0,0 +1,84 @@ +// 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.server.query; + +import static com.google.gerrit.server.query.Predicate.not; + +import junit.framework.TestCase; + +public class NotPredicateTest extends TestCase { + private static OperatorPredicate f(final String name, final String value) { + return new OperatorPredicate(name, value); + } + + public void testNotNot() { + final OperatorPredicate p = f("author", "bob"); + final Predicate n = p.not(); + assertTrue(n instanceof NotPredicate); + assertNotSame(p, n); + assertSame(p, n.not()); + } + + public void testChildren() { + final OperatorPredicate p = f("author", "bob"); + final Predicate n = p.not(); + assertEquals(1, n.getChildCount()); + assertSame(p, n.getChild(0)); + } + + public void testChildrenUnmodifiable() { + final OperatorPredicate p = f("author", "bob"); + final Predicate n = p.not(); + + try { + n.getChildren().clear(); + } catch (RuntimeException e) { + } + assertOnlyChild("clear", p, n); + + try { + n.getChildren().remove(0); + } catch (RuntimeException e) { + } + assertOnlyChild("remove(0)", p, n); + + try { + n.getChildren().iterator().remove(); + } catch (RuntimeException e) { + } + assertOnlyChild("remove(0)", p, n); + } + + private static void assertOnlyChild(String o, Predicate c, Predicate p) { + assertEquals(o + " did not affect child", 1, p.getChildCount()); + assertSame(o + " did not affect child", c, p.getChild(0)); + } + + public void testToString() { + assertEquals("-author:bob", not(f("author", "bob")).toString()); + } + + public void testEquals() { + assertTrue(not(f("author", "bob")).equals(not(f("author", "bob")))); + assertFalse(not(f("author", "bob")).equals(not(f("author", "alice")))); + assertFalse(not(f("author", "bob")).equals(f("author", "bob"))); + assertFalse(not(f("author", "bob")).equals("author")); + } + + public void testHashCode() { + assertTrue(not(f("a", "b")).hashCode() == not(f("a", "b")).hashCode()); + assertFalse(not(f("a", "b")).hashCode() == not(f("a", "a")).hashCode()); + } +} diff --git a/src/test/java/com/google/gerrit/server/query/QueryParserTest.java b/src/test/java/com/google/gerrit/server/query/QueryParserTest.java new file mode 100644 index 0000000000..9534d2b4d5 --- /dev/null +++ b/src/test/java/com/google/gerrit/server/query/QueryParserTest.java @@ -0,0 +1,47 @@ +// 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.server.query; + + +import junit.framework.TestCase; + +import org.antlr.runtime.tree.Tree; + +public class QueryParserTest extends TestCase { + public void testProjectBare() throws QueryParseException { + Tree r; + + r = parse("project:tools/gerrit"); + assertSingleWord("project", "tools/gerrit", r); + + r = parse("project:tools/*"); + assertSingleWord("project", "tools/*", r); + } + + private static void assertSingleWord(final String name, final String value, + final Tree r) { + assertEquals(QueryParser.FIELD_NAME, r.getType()); + assertEquals(name, r.getText()); + assertEquals(1, r.getChildCount()); + final Tree c = r.getChild(0); + assertEquals(QueryParser.SINGLE_WORD, c.getType()); + assertEquals(value, c.getText()); + assertEquals(0, c.getChildCount()); + } + + private static Tree parse(final String str) throws QueryParseException { + return QueryParser.parse(str); + } +}