ChangeStatusPredicate: Silently ignore invalid status
Instead of throwing an exception from the ChangeStatusPredicate
constructor complaining about an invalid change status, return a real
index predicate that is guaranteed to match no changes in the index.
This is similar to the behavior of other query engines that are vaguely
Gerrit-like; for example, in Gmail, searching for [has:attachment] has
meaning, whereas [has:fhqwhgads] successfully returns no results.
The proximate motivation for this change is so user dashboards and other
links containing [is:draft] predicates continue to work after removing
the draft workflow. While it might be reasonable to return 400 for a
pure [is:draft] search after drafts are removed, a user might expect
[owner:self (is:draft OR is:new)] to continue working. Similarly, we can
write a migration that removes all [is:draft] custom dashboard entries
from user preferences, but there is no single migration step that works
in the general case of a query containing [is:draft] at arbitrary points
in the query tree.
This change is straightforward and general, but is not the only way to
achieve this goal. Another possibility would be to special-case
[is:draft] specifically to return ChangeStatusPredicate.NONE. That would
be the most minimal behavior change, but requires more messy
hard-coding.
Change-Id: I32119ff660d914a64da64bcd0769b0434d68a8d6
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 28843c9..824fd4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -83,7 +83,8 @@
private static EnumSet<Change.Status> extractStatus(Predicate<ChangeData> in) {
if (in instanceof ChangeStatusPredicate) {
- return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
+ Status status = ((ChangeStatusPredicate) in).getStatus();
+ return status != null ? EnumSet.of(status) : null;
} else if (in instanceof NotPredicate) {
EnumSet<Status> s = extractStatus(in.getChild(0));
return s != null ? EnumSet.complementOf(s) : null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 309f15c..beff5f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -496,7 +496,7 @@
}
@Operator
- public Predicate<ChangeData> status(String statusName) throws QueryParseException {
+ public Predicate<ChangeData> status(String statusName) {
if ("reviewed".equalsIgnoreCase(statusName)) {
return IsReviewedPredicate.create();
}
@@ -619,13 +619,7 @@
throw new QueryParseException("'is:wip' operator is not supported by change index version");
}
- try {
- return status(value);
- } catch (IllegalArgumentException e) {
- // not status: alias?
- }
-
- throw error("Invalid query");
+ return status(value);
}
@Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index edead5a..155b016 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -14,8 +14,11 @@
package com.google.gerrit.server.query.change;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.server.index.change.ChangeField;
@@ -24,6 +27,7 @@
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
+import java.util.Objects;
import java.util.TreeMap;
/**
@@ -36,6 +40,9 @@
* <p>Status names are looked up by prefix case-insensitively.
*/
public final class ChangeStatusPredicate extends ChangeIndexPredicate {
+ private static final String INVALID_STATUS = "__invalid__";
+ private static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
+
private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
private static final Predicate<ChangeData> CLOSED;
private static final Predicate<ChangeData> OPEN;
@@ -46,8 +53,14 @@
List<Predicate<ChangeData>> closed = new ArrayList<>();
for (Change.Status s : Change.Status.values()) {
- ChangeStatusPredicate p = new ChangeStatusPredicate(s);
- PREDICATES.put(canonicalize(s), p);
+ ChangeStatusPredicate p = forStatus(s);
+ String str = canonicalize(s);
+ checkState(
+ !INVALID_STATUS.equals(str),
+ "invalid status sentinel %s cannot match canonicalized status string %s",
+ INVALID_STATUS,
+ str);
+ PREDICATES.put(str, p);
(s.isOpen() ? open : closed).add(p);
}
@@ -63,7 +76,7 @@
return status.name().toLowerCase();
}
- public static Predicate<ChangeData> parse(String value) throws QueryParseException {
+ public static Predicate<ChangeData> parse(String value) {
String lower = value.toLowerCase();
NavigableMap<String, Predicate<ChangeData>> head = PREDICATES.tailMap(lower, true);
if (!head.isEmpty()) {
@@ -73,7 +86,7 @@
return e.getValue();
}
}
- throw new QueryParseException("invalid change status: " + value);
+ return NONE;
}
public static Predicate<ChangeData> open() {
@@ -84,13 +97,23 @@
return CLOSED;
}
- private final Change.Status status;
+ public static ChangeStatusPredicate forStatus(Change.Status status) {
+ return new ChangeStatusPredicate(checkNotNull(status));
+ }
- public ChangeStatusPredicate(Change.Status status) {
- super(ChangeField.STATUS, canonicalize(status));
+ @Nullable private final Change.Status status;
+
+ private ChangeStatusPredicate(@Nullable Change.Status status) {
+ super(ChangeField.STATUS, status != null ? canonicalize(status) : INVALID_STATUS);
this.status = status;
}
+ /**
+ * Get the status for this predicate.
+ *
+ * @return the status, or null if this predicate is intended to never match any changes.
+ */
+ @Nullable
public Change.Status getStatus() {
return status;
}
@@ -98,7 +121,7 @@
@Override
public boolean match(ChangeData object) throws OrmException {
Change change = object.change();
- return change != null && status.equals(change.getStatus());
+ return change != null && Objects.equals(status, change.getStatus());
}
@Override
@@ -108,16 +131,13 @@
@Override
public int hashCode() {
- return status.hashCode();
+ return Objects.hashCode(status);
}
@Override
public boolean equals(Object other) {
- if (other instanceof ChangeStatusPredicate) {
- final ChangeStatusPredicate p = (ChangeStatusPredicate) other;
- return status.equals(p.status);
- }
- return false;
+ return (other instanceof ChangeStatusPredicate)
+ && Objects.equals(status, ((ChangeStatusPredicate) other).status);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 2316bd0..4d10c0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -66,7 +66,7 @@
}
private static Predicate<ChangeData> status(Change.Status status) {
- return new ChangeStatusPredicate(status);
+ return ChangeStatusPredicate.forStatus(status);
}
private static Predicate<ChangeData> commit(String id) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 97d8ae8..4a6663a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -175,13 +175,16 @@
@Test
public void getPossibleStatus() throws Exception {
- assertThat(status("file:a")).isEqualTo(EnumSet.allOf(Change.Status.class));
+ Set<Change.Status> all = EnumSet.allOf(Change.Status.class);
+ assertThat(status("file:a")).isEqualTo(all);
assertThat(status("is:new")).containsExactly(NEW);
assertThat(status("is:new OR is:merged")).containsExactly(NEW, MERGED);
+ assertThat(status("is:new OR is:x")).isEqualTo(all);
assertThat(status("is:new is:merged")).isEmpty();
assertThat(status("(is:new) (is:merged)")).isEmpty();
assertThat(status("(is:new) (is:merged)")).isEmpty();
+ assertThat(status("is:new is:x")).containsExactly(NEW);
}
@Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index b1e600a..1e722fc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -324,6 +324,8 @@
assertQuery("is:new", change1);
assertQuery("status:merged", change2);
assertQuery("is:merged", change2);
+ assertQuery("status:draft");
+ assertQuery("is:draft");
}
@Test
@@ -381,10 +383,8 @@
assertQuery("status:N", change1);
assertQuery("status:nE", change1);
assertQuery("status:neW", change1);
- assertThatQueryException("status:nx").hasMessageThat().isEqualTo("invalid change status: nx");
- assertThatQueryException("status:newx")
- .hasMessageThat()
- .isEqualTo("invalid change status: newx");
+ assertQuery("status:nx");
+ assertQuery("status:newx");
}
@Test