Allow to negate conditions by setting their first value to !
Thereby, we can formulate conditions as
status = !,DRAFT
to match changes that are not in DRAFT status.
Change-Id: I48d8d1814cfc5eb6357449344339487f238e4061
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Condition.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Condition.java
index 47aae91..61aa365 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Condition.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Condition.java
@@ -14,27 +14,34 @@
package com.googlesource.gerrit.plugins.hooks.workflow;
-import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
+import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
/**
* A condition as used in {@link Rule}, as precondition to {@link Action}s.
- *
- * A condition consists of a key and an associated set of values. A condition
- * is said to match a set of properties, is this set contains at least one
- * property that matches the rules key and whose value matches at least one
- * of the rule's value.
+ * <p>
+ * A condition consists of a key and an associated set of values.
+ * <p>
+ * For positive conditions (see constructor), the condition is said to match a
+ * set of properties, if this set contains at least one property that matches
+ * the rules key and whose value matches at least one of the rule's value.
+ * <p>
+ * For negated conditions (see constructor), the condition is said to match a
+ * set of properties, if this set does not contain any property that matches
+ * the rules key and whose value matches at least one of the rule's value.
*/
public class Condition {
private final String key;
private final Set<String> values;
+ private final boolean negated;
public interface Factory {
Condition create(@Assisted("key") String key,
@@ -44,19 +51,29 @@
/**
* Constructs a condition.
* @param key The key to use for values.
- * @param values A comma separated list of values to associate to the key.
+ * @param values A comma separated list of values to associate to the key. If
+ * the first value is not "!", it's a positive condition. If the first
+ * value is "!", the "!" is removed from the values and the condition is
+ * considered a negated condition.
*/
@Inject
public Condition(@Assisted("key") String key,
@Nullable @Assisted("values") String values) {
this.key = key;
Set<String> modifyableValues;
+ boolean modifyableNegated = false;
if (values == null) {
modifyableValues = Collections.emptySet();
} else {
- modifyableValues = Sets.newHashSet(Arrays.asList(values.split(",")));
+ List<String> valueList = Lists.newArrayList(values.split(","));
+ if (!valueList.isEmpty() && "!".equals(valueList.get(0))) {
+ modifyableNegated = true;
+ valueList.remove(0);
+ }
+ modifyableValues = Sets.newHashSet(valueList);
}
this.values = Collections.unmodifiableSet(modifyableValues);
+ this.negated = modifyableNegated;
}
public String getKey() {
@@ -67,8 +84,11 @@
* Checks whether or not the Condition matches the given set of properties
*
* @param properties The set of properties to match against.
- * @return True iff properties contains at least one property that matches
- * the rules key and whose value matches at least one of the rule's value.
+ * @return For positive conditions, true iff properties contains at least
+ * one property that matches the rules key and whose value matches at
+ * least one of the rule's value. For negated conditions, true iff
+ * properties does not contain any property that matches the rules key
+ * and whose value matches at least one of the rule's value.
*/
public boolean isMetBy(Iterable<Property> properties) {
for (Property property : properties) {
@@ -76,11 +96,11 @@
if ((key == null && propertyKey == null)
|| (key != null && key.equals(propertyKey))) {
if (values.contains(property.getValue())) {
- return true;
+ return !negated;
}
}
}
- return false;
+ return negated;
}
@Override
diff --git a/hooks-its/src/main/resources/Documentation/config.md b/hooks-its/src/main/resources/Documentation/config.md
index 2593171..a4ccfd1 100644
--- a/hooks-its/src/main/resources/Documentation/config.md
+++ b/hooks-its/src/main/resources/Documentation/config.md
@@ -94,9 +94,22 @@
----
name = value1, value2, ..., valueN
----
-and match if the event comes with a property 'name' having 'value1',
-or 'value2', or ..., or 'valueN'.
+and (if 'value1' is not +!+) match if the event comes with a property
+'name' having 'value1', or 'value2', or ..., or 'valueN'. So for
+example to match events that come with an 'association' property
+having 'subject', or 'footer-Bug', the following condition can be
+used:
+----
+association = subject,footer-Bug
+----
+If 'value1' is +!+, the conditon matches if the event does not come
+with a property 'name' having 'value2', or ..., or 'valueN'. So for
+example to match events that do not come with a 'status' property
+having 'DRAFT', the following condition can be used:
+----
+status = !,DRAFT
+----
[[event-properties]]
Event Properties
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ConditionTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ConditionTest.java
index 857f9e8..886ab40 100644
--- a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ConditionTest.java
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ConditionTest.java
@@ -172,6 +172,146 @@
assertTrue("isMetBy gave false", condition.isMetBy(properties));
}
+ public void testNegatedIsMetByEmpty() {
+ Condition condition = createCondition("testKey", "!,testValue");
+
+ Collection<Property> properties = Collections.emptySet();
+
+ replayMocks();
+
+ assertTrue("isMetBy gave false", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByMismatchedKey() {
+ Condition condition = createCondition("testKey", "!,testValue");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("otherKey").anyTimes();
+ expect(property1.getValue()).andReturn("testValue").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+ properties.add(property1);
+
+ replayMocks();
+
+ assertTrue("isMetBy gave false", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByMaMismatchedValue() {
+ Condition condition = createCondition("testKey", "!,testValue");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("testKey").anyTimes();
+ expect(property1.getValue()).andReturn("otherValue").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+ properties.add(property1);
+
+ replayMocks();
+
+ assertTrue("isMetBy gave false", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByOredNoMatch() {
+ Condition condition = createCondition("testKey", "!,value1,value2,value3");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("testKey").anyTimes();
+ expect(property1.getValue()).andReturn("otherValue").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+ properties.add(property1);
+
+ replayMocks();
+
+ assertTrue("isMetBy gave false", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByOredSingleMatch() {
+ Condition condition = createCondition("testKey", "!,value1,value2,value3");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("testKey").anyTimes();
+ expect(property1.getValue()).andReturn("value1").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+ properties.add(property1);
+
+ replayMocks();
+
+ assertFalse("isMetBy gave true", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByOredMultiple() {
+ Condition condition = createCondition("testKey", "!,value1,value2,value3");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("testKey").anyTimes();
+ expect(property1.getValue()).andReturn("value1").anyTimes();
+
+ Property property2 = createMock(Property.class);
+ expect(property2.getKey()).andReturn("testKey").anyTimes();
+ expect(property2.getValue()).andReturn("value3").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(2);
+ properties.add(property1);
+ properties.add(property2);
+
+ replayMocks();
+
+ assertFalse("isMetBy gave true", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByOredAll() {
+ Condition condition = createCondition("testKey", "!,value1,value2,value3");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("testKey").anyTimes();
+ expect(property1.getValue()).andReturn("value1").anyTimes();
+
+ Property property2 = createMock(Property.class);
+ expect(property2.getKey()).andReturn("testKey").anyTimes();
+ expect(property2.getValue()).andReturn("value2").anyTimes();
+
+ Property property3 = createMock(Property.class);
+ expect(property3.getKey()).andReturn("testKey").anyTimes();
+ expect(property3.getValue()).andReturn("value3").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+ properties.add(property1);
+ properties.add(property2);
+ properties.add(property3);
+
+ replayMocks();
+
+ assertFalse("isMetBy gave true", condition.isMetBy(properties));
+ }
+
+ public void testNegatedIsMetByOredOvershoot() {
+ Condition condition = createCondition("testKey", "!,value1,value2,value3");
+
+ Property property1 = createMock(Property.class);
+ expect(property1.getKey()).andReturn("testKey").anyTimes();
+ expect(property1.getValue()).andReturn("otherValue1").anyTimes();
+
+ Property property2 = createMock(Property.class);
+ expect(property2.getKey()).andReturn("testKey").anyTimes();
+ expect(property2.getValue()).andReturn("value2").anyTimes();
+
+ Property property3 = createMock(Property.class);
+ expect(property3.getKey()).andReturn("testKey").anyTimes();
+ expect(property3.getValue()).andReturn("otherValue3").anyTimes();
+
+ Collection<Property> properties = Lists.newArrayListWithCapacity(3);
+ properties.add(property1);
+ properties.add(property2);
+ properties.add(property3);
+
+ replayMocks();
+
+ assertFalse("isMetBy gave true", condition.isMetBy(properties));
+ }
+
private Condition createCondition(String key, String value) {
Condition.Factory factory = injector.getInstance(Condition.Factory.class);
return factory.create(key, value);