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);
+ }
+}