Define a simple query language for Gerrit

This is a draft of a basic query language for Gerrit, using a syntax
that is very similar to Apache Lucene, Google web search, or Gmail.

The current parser simply delimits expressions like "foo:bar that"
into basic boolean tree, using a simple FieldPredicate for any of
the operators present in the query.  Invalid operators are caught
and thrown back at the caller, so that they may be reported.

The abstract QueryBuilder class can be used to create a custom
query language which relies on the basic language parser for the
syntax analysis, and conversion from ANTLR trees to the predicate
format we will use elsewhere.  The current rules supported by the
ChangeListServiceImpl.searchQuery are codified in the new subclass
ChangeQueryBuilder, but we really want to support a much richer
set of operators in the near future.

JUnit tests for most of the predicate logic are also defined.

Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce 2009-08-12 19:00:42 -07:00
parent 572afee082
commit 54c91ec438
15 changed files with 1386 additions and 1 deletions

27
pom.xml
View File

@ -291,6 +291,19 @@ limitations under the License.
</configuration>
</plugin>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr3-maven-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<goals>
<goal>antlr</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -570,6 +583,13 @@ limitations under the License.
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr</artifactId>
<version>3.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>bouncycastle</groupId>
<artifactId>bcpg-jdk15</artifactId>
@ -618,6 +638,13 @@ limitations under the License.
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- GWT -->
<dependency>
<groupId>com.google.gwt</groupId>

View File

@ -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
| ':'
| ';'
| '<' | '=' | '>'
| '?'
| '[' | ']'
| '{' | '}'
| '~'
)
;

View File

@ -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<Predicate> that) {
final ArrayList<Predicate> tmp = new ArrayList<Predicate>(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<Predicate> 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();
}
}

View File

@ -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.
* <p>
* 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);
}
}
}

View File

@ -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<Predicate> 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();
}
}

View File

@ -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();
}
}

View File

@ -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 + "\"";
}
}
}

View File

@ -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<Predicate> that) {
final ArrayList<Predicate> tmp = new ArrayList<Predicate>(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<Predicate> 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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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<Predicate> 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<Predicate> 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<Predicate> 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);
}

View File

@ -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.
* <p>
* 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.
* <p>
* Subclasses may document their supported query operators by declaring public
* methods that perform the query conversion into a {@link Predicate}. For
* example, to support "is:starred", "is:unread", and nothing else, a subclass
* may write:
*
* <pre>
* &#064;Operator
* public Predicate is(final String value) {
* if (&quot;starred&quot;.equals(value)) {
* return new StarredPredicate();
* }
* if (&quot;unread&quot;.equals(value)) {
* return new UnreadPredicate();
* }
* throw new IllegalArgumentException();
* }
* </pre>
* <p>
* The available operator methods are discovered at runtime via reflection.
* Method names (after being converted to lowercase), correspond to operators in
* the query language, method string values correspond to the operator argument.
* Methods must be declared {@code public}, returning {@link Predicate},
* accepting one {@link String}, and annotated with the {@link Operator}
* annotation.
* <p>
* Subclasses may also declare a handler for values which appear without
* operator by overriding {@link #defaultField(String)}.
*/
public abstract class QueryBuilder {
private final Map<String, OperatorFactory> opFactories =
new HashMap<String, OperatorFactory>();
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.
* <p>
* This default implementation always throws an "Unsupported query: " message
* containing the input text. Subclasses may override this method to perform
* do-what-i-mean guesses based on the input string.
*
* @param value the value supplied by itself in the query.
* @return predicate representing this value.
* @throws QueryParseException the parser does not recognize this value.
*/
protected Predicate 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());
}
}
}
}

View File

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

View File

@ -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 '<EOF>'", 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("<bob@example.com>"), p("owner:\"<bob@example.com>\""));
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("<bob@example.com>"), p("owner: \"<bob@example.com>\""));
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("<bob@example.com>"), p("owner:\t\"<bob@example.com>\""));
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)"));
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

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