Add RuleBase implementation

Change-Id: I8a93cbaa34df7295c1c90d044f71f974e4a9951a
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
index de7709d..0fcf032 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
@@ -21,12 +21,14 @@
 import com.googlesource.gerrit.plugins.hooks.its.ItsName;
 import com.googlesource.gerrit.plugins.hooks.validation.ItsValidateComment;
 import com.googlesource.gerrit.plugins.hooks.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.hooks.workflow.Condition;
 import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddComment;
 import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddRelatedLinkToChangeId;
 import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddRelatedLinkToGitWeb;
 import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterChangeState;
 import com.googlesource.gerrit.plugins.hooks.workflow.ActionController;
 import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+import com.googlesource.gerrit.plugins.hooks.workflow.Rule;
 
 public class ItsHookModule extends FactoryModule {
 
@@ -53,5 +55,7 @@
         ActionController.class);
     factory(ActionRequest.Factory.class);
     factory(Property.Factory.class);
+    factory(Condition.Factory.class);
+    factory(Rule.Factory.class);
   }
 }
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
new file mode 100644
index 0000000..1bb1564
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Condition.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+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.
+ */
+public class Condition {
+  private final String key;
+  private final Set<String> values;
+
+  public interface Factory {
+    Condition create(@Assisted("key") String key,
+        @Assisted("values") String values);
+  }
+
+  /**
+   * Constructs a condition.
+   * @param key The key to use for values.
+   * @param values A comma separated list of values to associate to the key.
+   */
+  @Inject
+  public Condition(@Assisted("key") String key,
+      @Nullable @Assisted("values") String values) {
+    this.key = key;
+    Set<String> modifyableValues;
+    if (values == null) {
+      modifyableValues = Collections.emptySet();
+    } else {
+      modifyableValues = Sets.newHashSet(Arrays.asList(values.split(",")));
+    }
+    this.values = Collections.unmodifiableSet(modifyableValues);
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public Set<String> getValues() {
+    return values;
+  }
+
+  /**
+   * 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.
+   */
+  public boolean isMetBy(Iterable<Property> properties) {
+    for (Property property : properties) {
+      String propertyKey = property.getKey();
+      if ((key == null && propertyKey == null)
+          || (key != null && key.equals(propertyKey))) {
+        if (values.contains(property.getValue())) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "[" + key + " = " + values + "]";
+  }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Rule.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Rule.java
new file mode 100644
index 0000000..512af73
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Rule.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * A single rule that associates {@code Action}s to {@code Condition}s.
+ */
+public class Rule {
+  private final String name;
+  private List<ActionRequest> actionRequests;
+  private Set<Condition> conditions;
+
+  public interface Factory {
+    Rule create(String name);
+  }
+
+  @Inject
+  public Rule(@Assisted String name) {
+    this.name = name;
+    this.actionRequests = Lists.newLinkedList();
+    this.conditions = Sets.newHashSet();
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Adds a condition to the rule.
+   *
+   * @param condition The condition to add.
+   */
+  public void addCondition(Condition condition) {
+    conditions.add(condition);
+  }
+
+  /**
+   * Adds an action to the rule.
+   *
+   * @param action The action to add.
+   */
+  public void addActionRequest(ActionRequest actionRequest) {
+    actionRequests.add(actionRequest);
+  }
+
+  /**
+   * Gets this rule's the action requests for a given set of properties.
+   *
+   * If the given set of properties meets all of the rule's conditions, the
+   * rule's actions are returned. Otherwise the empty collection is returned.
+   *
+   * @param properties The properties to check against the rule's conditions.
+   * @return The actions that should get fired.
+   */
+  public Collection<ActionRequest> actionRequestsFor(
+      Iterable<Property> properties) {
+    for (Condition condition : conditions) {
+      if (!condition.isMetBy(properties)) {
+        return Collections.emptyList();
+      }
+    }
+    return Collections.unmodifiableList(actionRequests);
+  }
+
+  @Override
+  public String toString() {
+    return "[" + name + ", " + conditions + " -> " + actionRequests + "]";
+  }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBase.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBase.java
index e7ef399..101dfa6 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBase.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBase.java
@@ -14,14 +14,116 @@
 
 package com.googlesource.gerrit.plugins.hooks.workflow;
 
+import java.io.File;
 import java.util.Collection;
+import java.util.Collections;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.config.SitePath;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Collection and matcher agains {@link Rule}s.
  */
 public class RuleBase {
-  public RuleBase() {
-    // TODO construct rule base
+  private static final Logger log = LoggerFactory.getLogger(RuleBase.class);
+
+  /**
+   * File (relative to site) to load rules from
+   */
+  private static final String ITS_CONFIG_FILE = "etc" + File.separatorChar +
+      "its" + File.separator + "action.config";
+
+  /**
+   * The section for rules within {@link #ITS_CONFIG_FILE}
+   */
+  private static final String RULE_SECTION = "rule";
+
+  /**
+   * The key for actions within {@link #ITS_CONFIG_FILE}
+   */
+  private static final String ACTION_KEY = "action";
+
+  private final File sitePath;
+  private final Rule.Factory ruleFactory;
+  private final Condition.Factory conditionFactory;
+  private final ActionRequest.Factory actionRequestFactory;
+
+  private Collection<Rule> rules;
+
+  public interface Factory {
+    RuleBase create();
+  }
+
+  @Inject
+  public RuleBase(@SitePath File sitePath, Rule.Factory ruleFactory,
+      Condition.Factory conditionFactory,
+      ActionRequest.Factory actionRequestFactory) {
+    this.sitePath = sitePath;
+    this.ruleFactory = ruleFactory;
+    this.conditionFactory = conditionFactory;
+    this.actionRequestFactory = actionRequestFactory;
+    loadRules();
+  }
+
+  /**
+   * Loads the rules for the RuleBase.
+   *
+   * Consider using {@link #loadRules()@}, as that method only loads the rules,
+   * if they have not yet been loaded.
+   */
+  private void forceLoadRules() throws Exception {
+    File configFile = new File(sitePath, ITS_CONFIG_FILE);
+    if (configFile.exists()) {
+      FileBasedConfig cfg = new FileBasedConfig(configFile, FS.DETECTED);
+      cfg.load();
+
+      rules = Lists.newArrayList();
+      Collection<String> subsections = cfg.getSubsections(RULE_SECTION);
+      for (String subsection : subsections) {
+        Rule rule = ruleFactory.create(subsection);
+        Collection<String> keys = cfg.getNames(RULE_SECTION, subsection);
+        for (String key : keys) {
+          String values[] = cfg.getStringList(RULE_SECTION, subsection, key);
+          if (ACTION_KEY.equals(key)) {
+            for (String value : values) {
+              ActionRequest actionRequest = actionRequestFactory.create(value);
+              rule.addActionRequest(actionRequest);
+            }
+          } else {
+            for (String value : values) {
+              Condition condition = conditionFactory.create(key, value);
+              rule.addCondition(condition);
+            }
+          }
+        }
+        rules.add(rule);
+      }
+    } else {
+      // configFile does not exist.
+      log.warn("ITS actions configuration file (" + configFile + ") does not exist.");
+      rules = Collections.emptySet();
+    }
+  }
+
+  /**
+   * Loads the rules for the RuleBase, if they have not yet been loaded.
+   */
+  private void loadRules() {
+    if (rules == null) {
+      try {
+        forceLoadRules();
+      } catch (Exception e) {
+        log.error("Invalid ITS action configuration", e);
+        rules = Collections.emptySet();
+      }
+    }
   }
 
   /**
@@ -32,7 +134,10 @@
    */
   public Collection<ActionRequest> actionRequestsFor(
       Iterable<Property> properties) {
-    // TODO implement
-    throw new RuntimeException("unimplemented");
+    Collection<ActionRequest> ret = Lists.newLinkedList();
+    for (Rule rule : rules) {
+      ret.addAll(rule.actionRequestsFor(properties));
+    }
+    return ret;
   }
 }
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
new file mode 100644
index 0000000..d672ab7
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ConditionTest.java
@@ -0,0 +1,229 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.hooks.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+
+public class ConditionTest  extends LoggingMockingTestCase {
+  private Injector injector;
+
+  public void testSimpleValue() {
+    Condition condition = createCondition("testKey", "testValue");
+    assertEquals("key not matching 'testKey'", "testKey", condition.getKey());
+    Set<String> expectedValues = Sets.newHashSet();
+    expectedValues.add("testValue");
+    assertEquals("values do not match", expectedValues, condition.getValues());
+  }
+
+  public void testGetKeyNull() {
+    Condition condition = new Condition(null, "testValues");
+    assertNull("key is not null", condition.getKey());
+  }
+
+  public void testGetValuesNull() {
+    Condition condition = createCondition("testKey", null);
+    Set<String> values = condition.getValues();
+    assertNotNull("values is null", values);
+    assertTrue("values is not empty", values.isEmpty());
+  }
+
+  public void testOredValue() {
+    Condition condition = createCondition("testKey", "value1,value2,value3");
+    assertEquals("key not matching 'testKey'", "testKey", condition.getKey());
+    Set<String> expectedValues = Sets.newLinkedHashSet();
+    expectedValues.add("value1");
+    expectedValues.add("value2");
+    expectedValues.add("value3");
+    assertEquals("values do not match", expectedValues, condition.getValues());
+  }
+
+  public void testIsMetBySimple() {
+    Condition condition = createCondition("testKey", "testValue");
+
+    Property property1 = createMock(Property.class);
+    expect(property1.getKey()).andReturn("testKey").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 testIsMetBySimpleEmpty() {
+    Condition condition = createCondition("testKey", "testValue");
+
+    Collection<Property> properties = Collections.emptySet();
+
+    replayMocks();
+
+    assertFalse("isMetBy gave true", condition.isMetBy(properties));
+  }
+
+  public void testIsMetByMismatchedKey() {
+    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();
+
+    assertFalse("isMetBy gave true", condition.isMetBy(properties));
+  }
+
+  public void testIsMetByMismatchedValue() {
+    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();
+
+    assertFalse("isMetBy gave true", condition.isMetBy(properties));
+  }
+
+  public void testIsMetByOredSingle() {
+    Condition condition = createCondition("testKey", "value1,value2,value3");
+
+    Property property1 = createMock(Property.class);
+    expect(property1.getKey()).andReturn("testKey").anyTimes();
+    expect(property1.getValue()).andReturn("value2").anyTimes();
+
+    Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+    properties.add(property1);
+
+    replayMocks();
+
+    assertTrue("isMetBy gave false", condition.isMetBy(properties));
+  }
+
+  public void testIsMetByOredMultiple() {
+    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();
+
+    assertTrue("isMetBy gave false", condition.isMetBy(properties));
+  }
+
+  public void testIsMetByOredAll() {
+    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();
+
+    assertTrue("isMetBy gave false", condition.isMetBy(properties));
+  }
+
+  public void testIsMetByOredOvershoot() {
+    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();
+
+    assertTrue("isMetBy gave false", condition.isMetBy(properties));
+  }
+
+  public void testUnmodifiableValue() {
+    Condition condition = createCondition("testKey", "testValue");
+    Set<String> values = condition.getValues();
+    try {
+      values.add("value2");
+      fail("value is not unmodifyable");
+    } catch (UnsupportedOperationException e) {
+    }
+  }
+
+  private Condition createCondition(String key, String value) {
+    Condition.Factory factory = injector.getInstance(Condition.Factory.class);
+    return factory.create(key, value);
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      factory(Condition.Factory.class);
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBaseTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBaseTest.java
new file mode 100644
index 0000000..2c6b466
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBaseTest.java
@@ -0,0 +1,279 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.hooks.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import org.eclipse.jgit.util.FileUtils;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.SitePath;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+
+public class RuleBaseTest extends LoggingMockingTestCase {
+  private Injector injector;
+
+  private File sitePath;
+  private Rule.Factory ruleFactory;
+  private Condition.Factory conditionFactory;
+  private ActionRequest.Factory actionRequestFactory;
+
+  private boolean cleanupSitePath;
+
+  public void testWarnNonExistingRuleBase() {
+    replayMocks();
+
+    createRuleBase();
+
+    assertLogMessageContains("does not exist");
+  }
+
+  public void testEmptyRuleBase() throws IOException {
+    injectRuleBase("");
+
+    replayMocks();
+
+    createRuleBase();
+  }
+
+  public void testSimpleRuleBase() throws IOException {
+    injectRuleBase("[rule \"rule1\"]\n" +
+        "\tconditionA = value1\n" +
+        "\taction = action1");
+
+    Rule rule1 = createMock(Rule.class);
+    expect(ruleFactory.create("rule1")).andReturn(rule1);
+
+    Condition condition1 = createMock(Condition.class);
+    expect(conditionFactory.create("conditionA", "value1")).andReturn(condition1);
+    rule1.addCondition(condition1);
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action1")).andReturn(actionRequest1);
+    rule1.addActionRequest(actionRequest1);
+
+    replayMocks();
+
+    createRuleBase();
+  }
+
+  public void testBasicRuleBase() throws IOException {
+    injectRuleBase("[rule \"rule1\"]\n" +
+        "\tconditionA = value1,value2\n" +
+        "\tconditionA = value3,value of 4\n" +
+        "\tconditionB = value5\n" +
+        "\taction = action1\n" +
+        "\taction = action2 param\n" +
+        "\n" +
+        "[ruleXZ \"nonrule\"]\n" +
+        "\tconditionA = value1\n" +
+        "\taction = action2\n" +
+        "[rule \"rule2\"]\n" +
+        "\tconditionC = value6\n" +
+        "\taction = action3");
+
+    Rule rule1 = createMock(Rule.class);
+    expect(ruleFactory.create("rule1")).andReturn(rule1);
+
+    Condition condition1 = createMock(Condition.class);
+    expect(conditionFactory.create("conditionA", "value1,value2")).
+        andReturn(condition1);
+    rule1.addCondition(condition1);
+
+    Condition condition2 = createMock(Condition.class);
+    expect(conditionFactory.create("conditionA", "value3,value of 4")).
+        andReturn(condition2);
+    rule1.addCondition(condition2);
+
+    Condition condition3 = createMock(Condition.class);
+    expect(conditionFactory.create("conditionB", "value5")).
+        andReturn(condition3);
+    rule1.addCondition(condition3);
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action1")).andReturn(actionRequest1);
+    rule1.addActionRequest(actionRequest1);
+
+    ActionRequest actionRequest2 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action2 param")).andReturn(actionRequest2);
+    rule1.addActionRequest(actionRequest2);
+
+    Rule rule2 = createMock(Rule.class);
+    expect(ruleFactory.create("rule2")).andReturn(rule2);
+
+    Condition condition4 = createMock(Condition.class);
+    expect(conditionFactory.create("conditionC", "value6")).
+        andReturn(condition4);
+    rule2.addCondition(condition4);
+
+    ActionRequest actionRequest3 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action3")).andReturn(actionRequest3);
+    rule2.addActionRequest(actionRequest3);
+
+    replayMocks();
+
+    createRuleBase();
+  }
+
+  public void testActionRequestsForSimple() throws IOException {
+    injectRuleBase("[rule \"rule1\"]\n" +
+        "\taction = action1");
+
+    Rule rule1 = createMock(Rule.class);
+    expect(ruleFactory.create("rule1")).andReturn(rule1);
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action1")).andReturn(actionRequest1);
+    rule1.addActionRequest(actionRequest1);
+
+    Collection<Property> properties = Collections.emptySet();
+
+    List<ActionRequest> rule1Match = Lists.newArrayListWithCapacity(1);
+    rule1Match.add(actionRequest1);
+    expect(rule1.actionRequestsFor(properties)).andReturn(rule1Match);
+
+    replayMocks();
+
+    RuleBase ruleBase = createRuleBase();
+    Collection<ActionRequest> actual = ruleBase.actionRequestsFor(properties);
+
+    List<ActionRequest> expected = Lists.newArrayListWithCapacity(3);
+    expected.add(actionRequest1);
+
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
+  public void testActionRequestsForExtended() throws IOException {
+    injectRuleBase("[rule \"rule1\"]\n" +
+        "\taction = action1\n" +
+        "\taction = action2\n" +
+        "\n" +
+        "[rule \"rule2\"]\n" +
+        "\taction = action3");
+
+    Rule rule1 = createMock(Rule.class);
+    expect(ruleFactory.create("rule1")).andReturn(rule1);
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action1")).andReturn(actionRequest1);
+    rule1.addActionRequest(actionRequest1);
+
+    ActionRequest actionRequest2 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action2")).andReturn(actionRequest2);
+    rule1.addActionRequest(actionRequest2);
+
+    Rule rule2 = createMock(Rule.class);
+    expect(ruleFactory.create("rule2")).andReturn(rule2);
+
+    ActionRequest actionRequest3 = createMock(ActionRequest.class);
+    expect(actionRequestFactory.create("action3")).andReturn(actionRequest3);
+    rule2.addActionRequest(actionRequest3);
+
+    Collection<Property> properties = Lists.newArrayListWithCapacity(1);
+    Property property1 = createMock(Property.class);
+    properties.add(property1);
+
+    List<ActionRequest> rule1Match = Lists.newArrayListWithCapacity(2);
+    rule1Match.add(actionRequest1);
+    rule1Match.add(actionRequest2);
+    expect(rule1.actionRequestsFor(properties)).andReturn(rule1Match);
+
+    List<ActionRequest> rule2Match = Lists.newArrayListWithCapacity(1);
+    rule2Match.add(actionRequest3);
+    expect(rule2.actionRequestsFor(properties)).andReturn(rule2Match);
+
+    replayMocks();
+
+    RuleBase ruleBase = createRuleBase();
+    Collection<ActionRequest> actual = ruleBase.actionRequestsFor(properties);
+
+    List<ActionRequest> expected = Lists.newArrayListWithCapacity(3);
+    expected.add(actionRequest1);
+    expected.add(actionRequest2);
+    expected.add(actionRequest3);
+
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
+  private RuleBase createRuleBase() {
+    return injector.getInstance(RuleBase.class);
+  }
+
+  private void injectRuleBase(String rules) throws IOException {
+    File ruleBaseFile = new File(sitePath, "etc" + File.separatorChar + "its" +
+        File.separator + "action.config");
+    File ruleBaseParentFile = ruleBaseFile.getParentFile();
+    assertTrue("Failed to create parent (" + ruleBaseParentFile + ") for " +
+        "rule base", ruleBaseParentFile.mkdirs());
+    FileWriter unbufferedWriter = new FileWriter(ruleBaseFile);
+    BufferedWriter writer = new BufferedWriter(unbufferedWriter);
+    writer.write(rules);
+    writer.close();
+    unbufferedWriter.close();
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+    cleanupSitePath = false;
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  public void tearDown() throws Exception {
+    if (cleanupSitePath) {
+      if (sitePath.exists()) {
+        FileUtils.delete(sitePath, FileUtils.RECURSIVE);
+      }
+    }
+    super.tearDown();
+  }
+
+  private File randomTargetFile() {
+    final File t = new File("target");
+    return new File(t, "random-name-" + UUID.randomUUID().toString());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+
+      sitePath = randomTargetFile();
+      assertFalse("sitePath already (" + sitePath + ") already exists",
+          sitePath.exists());
+      cleanupSitePath = true;
+
+      bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+
+      ruleFactory = createMock(Rule.Factory.class);
+      bind(Rule.Factory.class).toInstance(ruleFactory);
+
+      conditionFactory = createMock(Condition.Factory.class);
+      bind(Condition.Factory.class).toInstance(conditionFactory);
+
+      actionRequestFactory = createMock(ActionRequest.Factory.class);
+      bind(ActionRequest.Factory.class).toInstance(actionRequestFactory);
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleTest.java
new file mode 100644
index 0000000..d09a790
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleTest.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.hooks.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+
+public class RuleTest extends LoggingMockingTestCase {
+  private Injector injector;
+
+  public void testGetName() {
+    Rule rule = createRule("testRule");
+    assertEquals("Rule name does not match", "testRule", rule.getName());
+  }
+
+  public void testActionsForUnconditionalRule() {
+    Collection<Property> properties = Collections.emptySet();
+
+    Rule rule = createRule("testRule");
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    rule.addActionRequest(actionRequest1);
+
+    replayMocks();
+
+    Collection<ActionRequest> actual = rule.actionRequestsFor(properties);
+
+    Collection<ActionRequest> expected = Lists.newArrayListWithCapacity(1);
+    expected.add(actionRequest1);
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
+  public void testActionRequestsForConditionalRuleEmptyProperties() {
+    Collection<Property> properties = Collections.emptySet();
+
+    Rule rule = createRule("testRule");
+
+    Condition condition1 = createMock(Condition.class);
+    expect(condition1.isMetBy(properties)).andReturn(false).anyTimes();
+    rule.addCondition(condition1);
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    rule.addActionRequest(actionRequest1);
+
+    replayMocks();
+
+    Collection<ActionRequest> actual = rule.actionRequestsFor(properties);
+
+    List<ActionRequest> expected = Collections.emptyList();
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
+  public void testActionRequestsForConditionalRules() {
+    Collection<Property> properties = Collections.emptySet();
+
+    Rule rule = createRule("testRule");
+
+    Condition condition1 = createMock(Condition.class);
+    expect(condition1.isMetBy(properties)).andReturn(true).anyTimes();
+    rule.addCondition(condition1);
+
+    Condition condition2 = createMock(Condition.class);
+    expect(condition2.isMetBy(properties)).andReturn(false).anyTimes();
+    rule.addCondition(condition2);
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    rule.addActionRequest(actionRequest1);
+
+    replayMocks();
+
+    Collection<ActionRequest> actual = rule.actionRequestsFor(properties);
+
+    List<ActionRequest> expected = Collections.emptyList();
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
+  public void testActionRequestsForMultipleActionRequests() {
+    Collection<Property> properties = Collections.emptySet();
+
+    Rule rule = createRule("testRule");
+
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    rule.addActionRequest(actionRequest1);
+
+    ActionRequest actionRequest2 = createMock(ActionRequest.class);
+    rule.addActionRequest(actionRequest2);
+
+    replayMocks();
+
+    Collection<ActionRequest> actual = rule.actionRequestsFor(properties);
+
+    List<ActionRequest> expected = Lists.newArrayListWithCapacity(1);
+    expected.add(actionRequest1);
+    expected.add(actionRequest2);
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
+  private Rule createRule(String name) {
+    Rule.Factory factory = injector.getInstance(Rule.Factory.class);
+    return factory.create(name);
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      factory(Rule.Factory.class);
+    }
+  }
+}
\ No newline at end of file