Add workflow for ActionController

ActionController's workflow gets split into three components:
* PropertyExtractor (extracts properties from an event),
* RuleBase (assigns actions to properties), and
* ActionExecutor (executes matched actions).

Change-Id: Ideb81b242a0f097c94c76bd90afff4bdb438609a
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 86097ef..de7709d 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
@@ -16,17 +16,19 @@
 
 import com.google.gerrit.common.ChangeListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.inject.AbstractModule;
 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.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;
 
-public class ItsHookModule extends AbstractModule {
+public class ItsHookModule extends FactoryModule {
 
   private String itsName;
 
@@ -49,5 +51,7 @@
         ItsValidateComment.class);
     DynamicSet.bind(binder(), ChangeListener.class).to(
         ActionController.class);
+    factory(ActionRequest.Factory.class);
+    factory(Property.Factory.class);
   }
 }
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/PropertyExtractor.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/PropertyExtractor.java
new file mode 100644
index 0000000..9cf514e
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/PropertyExtractor.java
@@ -0,0 +1,73 @@
+//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.util;
+
+import java.util.Set;
+
+import com.google.gerrit.server.events.ChangeEvent;
+
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+/**
+ * Extractor to translate an {@link ChangeEvent} to
+ * {@link Property Properties}.
+ */
+public class PropertyExtractor {
+  /**
+   * A set of property sets extracted from an event.
+   *
+   * As events may relate to more that a single issue, and properties sets are
+   * should be tied to a single issue, returning {@code Collection<Property>}
+   * is not sufficient, and we need to return
+   * {@code Collection<Collection<Property>>}. Using this approach, a
+   * PatchSetCreatedEvent for a patch set with commit message:
+   *
+   * <pre>
+   *   (bug 4711) Fix treatment of special characters in title
+   *
+   *   This commit mitigates the effects of bug 42, but does not fix them.
+   *
+   *   Change-Id: I1234567891123456789212345678931234567894
+   * </pre>
+   *
+   * may return both
+   *
+   * <pre>
+   *   issue: 4711
+   *   association: subject
+   *   event: PatchSetCreatedEvent
+   * </pre>
+   *
+   * and
+   *
+   * <pre>
+   *   issue: 42
+   *   association: body
+   *   event: PatchSetCreatedEvent
+   * </pre>
+   *
+   * Thereby, sites can choose to to cause different actions for different
+   * issues associated to the same event. So in the above example, a comment
+   * "mentioned in change 123" may be added for issue 42, and a comment
+   * "fixed by change 123” may be added for issue 4711.
+   *
+   * @param event The event to extract property sets from.
+   * @return sets of property sets extracted from the event.
+   */
+  public Set<Set<Property>> extractFrom(ChangeEvent event) {
+    // TODO implement
+    throw new RuntimeException("unimplemented");
+  }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java
index 9d6ad99..e5c4728 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java
@@ -14,19 +14,48 @@
 
 package com.googlesource.gerrit.plugins.hooks.workflow;
 
+import java.util.Collection;
+import java.util.Set;
+
 import com.google.gerrit.common.ChangeListener;
 import com.google.gerrit.server.events.ChangeEvent;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.util.PropertyExtractor;
 
+/**
+ * Controller that takes actions according to {@code ChangeEvents@}.
+ *
+ * The taken actions are typically Its related (e.g.: adding an Its comment, or
+ * changing an issue's status).
+ */
 public class ActionController implements ChangeListener {
-  public ActionController() {
-    // TODO construct rule base
+  private final PropertyExtractor propertyExtractor;
+  private final RuleBase ruleBase;
+  private final ActionExecutor actionExecutor;
+
+  @Inject
+  public ActionController(PropertyExtractor propertyExtractor,
+      RuleBase ruleBase, ActionExecutor actionExecutor) {
+    this.propertyExtractor = propertyExtractor;
+    this.ruleBase = ruleBase;
+    this.actionExecutor = actionExecutor;
   }
 
   @Override
   public void onChangeEvent(ChangeEvent event) {
-    // TODO extract conditions from event
-    // TODO find rules in rule base that match the extracted conditions
-    // TODO fire actions for matched rules
+    Set<Set<Property>> propertiesCollections =
+        propertyExtractor.extractFrom(event);
+    for (Set<Property> properties : propertiesCollections) {
+      Collection<ActionRequest> actions =
+          ruleBase.actionRequestsFor(properties);
+      if (!actions.isEmpty()) {
+        for (Property property : properties) {
+          if ("issue".equals(property.getKey())) {
+            String issue = property.getValue();
+            actionExecutor.execute(issue, actions);
+          }
+        }
+      }
+    }
   }
-
 }
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java
new file mode 100644
index 0000000..13ab6de
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java
@@ -0,0 +1,25 @@
+//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;
+
+/**
+ * Executes an {@link ActionRequest}
+ */
+public class ActionExecutor {
+  public void execute(String issue, Iterable<ActionRequest> actions) {
+    // TODO implement
+    throw new RuntimeException("unimplemented");
+  }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java
new file mode 100644
index 0000000..bbf2690
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java
@@ -0,0 +1,37 @@
+// 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 javax.annotation.Nullable;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * An action to take for an {@code ChangeEvent}.
+ *
+ * Actions are typically related to an Its (e.g.:adding an Its comment, or
+ * changing an issue's status).
+ */
+public class ActionRequest {
+
+  public interface Factory {
+    ActionRequest create(String specification);
+  }
+
+  @Inject
+  public ActionRequest(@Nullable @Assisted String specification) {
+  }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Property.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Property.java
new file mode 100644
index 0000000..e8cc634
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/Property.java
@@ -0,0 +1,83 @@
+// 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 javax.annotation.Nullable;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * A property to match against {@code Condition}s.
+ *
+ * A property is a simple key value pair.
+ */
+public class Property {
+  public interface Factory {
+    Property create(@Assisted("key") String key,
+        @Assisted("value") String value);
+  }
+
+  private final String key;
+  private final String value;
+
+  @Inject
+  public Property(@Assisted("key") String key,
+      @Nullable @Assisted("value") String value) {
+    this.key = key;
+    this.value = value;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    boolean ret = false;
+    if (other != null && other instanceof Property) {
+      Property otherProperty = (Property) other;
+      ret = true;
+
+      if (key == null) {
+        ret &= otherProperty.getKey() == null;
+      } else {
+        ret &= key.equals(otherProperty.getKey());
+      }
+
+      if (value == null) {
+        ret &= otherProperty.getValue() == null;
+      } else {
+        ret &= value.equals(otherProperty.getValue());
+      }
+    }
+    return ret;
+  }
+
+  @Override
+  public int hashCode() {
+    return (key == null ? 0 : key.hashCode()) * 31 +
+        (value == null ? 0 : value.hashCode());
+  }
+
+  @Override
+  public String toString() {
+    return "[" + key + " = " + value + "]";
+  }
+}
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
new file mode 100644
index 0000000..e7ef399
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/RuleBase.java
@@ -0,0 +1,38 @@
+// 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;
+
+/**
+ * Collection and matcher agains {@link Rule}s.
+ */
+public class RuleBase {
+  public RuleBase() {
+    // TODO construct rule base
+  }
+
+  /**
+   * Gets the action requests for a set of properties.
+   *
+   * @param properties The properties to search actions for.
+   * @return Requests for the actions that should be fired.
+   */
+  public Collection<ActionRequest> actionRequestsFor(
+      Iterable<Property> properties) {
+    // TODO implement
+    throw new RuntimeException("unimplemented");
+  }
+}
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
new file mode 100644
index 0000000..926fe8b
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
@@ -0,0 +1,200 @@
+// 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.gerrit.server.events.ChangeEvent;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.hooks.util.PropertyExtractor;
+
+public class ActionControllerTest extends LoggingMockingTestCase {
+  private Injector injector;
+
+  private PropertyExtractor propertyExtractor;
+  private RuleBase ruleBase;
+  private ActionExecutor actionExecutor;
+
+  public void testNoPropertySets() {
+    ActionController actionController = createActionController();
+
+    ChangeEvent event = createMock(ChangeEvent.class);
+
+    Set<Set<Property>> propertySets = Collections.emptySet();
+    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets)
+        .anyTimes();
+
+    replayMocks();
+
+    actionController.onChangeEvent(event);
+  }
+
+  public void testNoActions() {
+    ActionController actionController = createActionController();
+
+    ChangeEvent event = createMock(ChangeEvent.class);
+
+    Set<Set<Property>> propertySets = Sets.newHashSet();
+    Set<Property> propertySet = Collections.emptySet();
+    propertySets.add(propertySet);
+
+    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets)
+        .anyTimes();
+
+    Collection<ActionRequest> actions = Collections.emptySet();
+    expect(ruleBase.actionRequestsFor(propertySet)).andReturn(actions).once();
+
+    replayMocks();
+
+    actionController.onChangeEvent(event);
+  }
+
+  public void testNoIssues() {
+    ActionController actionController = createActionController();
+
+    ChangeEvent event = createMock(ChangeEvent.class);
+
+    Set<Set<Property>> propertySets = Sets.newHashSet();
+    Set<Property> propertySet = Collections.emptySet();
+    propertySets.add(propertySet);
+
+    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets)
+        .anyTimes();
+
+    Collection<ActionRequest> actions = Lists.newArrayListWithCapacity(1);
+    ActionRequest action1 = createMock(ActionRequest.class);
+    actions.add(action1);
+    expect(ruleBase.actionRequestsFor(propertySet)).andReturn(actions).once();
+
+    replayMocks();
+
+    actionController.onChangeEvent(event);
+  }
+
+  public void testSinglePropertySetSingleActionSingleIssue() {
+    ActionController actionController = createActionController();
+
+    ChangeEvent event = createMock(ChangeEvent.class);
+
+    Property propertyIssue1 = createMock(Property.class);
+    expect(propertyIssue1.getKey()).andReturn("issue").anyTimes();
+    expect(propertyIssue1.getValue()).andReturn("testIssue").anyTimes();
+
+    Set<Property> propertySet = Sets.newHashSet();
+    propertySet.add(propertyIssue1);
+
+    Set<Set<Property>> propertySets = Sets.newHashSet();
+    propertySets.add(propertySet);
+
+    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets)
+        .anyTimes();
+
+    Collection<ActionRequest> actionRequests =
+        Lists.newArrayListWithCapacity(1);
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    actionRequests.add(actionRequest1);
+    expect(ruleBase.actionRequestsFor(propertySet)).andReturn(actionRequests)
+        .once();
+
+    actionExecutor.execute("testIssue", actionRequests);
+
+    replayMocks();
+
+    actionController.onChangeEvent(event);
+  }
+
+  public void testMultiplePropertySetsMultipleActionMultipleIssue() {
+    ActionController actionController = createActionController();
+
+    ChangeEvent event = createMock(ChangeEvent.class);
+
+    Property propertyIssue1 = createMock(Property.class);
+    expect(propertyIssue1.getKey()).andReturn("issue").anyTimes();
+    expect(propertyIssue1.getValue()).andReturn("testIssue").anyTimes();
+
+    Property propertyIssue2 = createMock(Property.class);
+    expect(propertyIssue2.getKey()).andReturn("issue").anyTimes();
+    expect(propertyIssue2.getValue()).andReturn("testIssue2").anyTimes();
+
+    Set<Property> propertySet1 = Sets.newHashSet();
+    propertySet1.add(propertyIssue1);
+
+    Set<Property> propertySet2 = Sets.newHashSet();
+    propertySet2.add(propertyIssue1);
+    propertySet2.add(propertyIssue2);
+
+    Set<Set<Property>> propertySets = Sets.newHashSet();
+    propertySets.add(propertySet1);
+    propertySets.add(propertySet2);
+
+    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets)
+        .anyTimes();
+
+    Collection<ActionRequest> actionRequests1 =
+        Lists.newArrayListWithCapacity(1);
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    actionRequests1.add(actionRequest1);
+
+    Collection<ActionRequest> actionRequests2 =
+        Lists.newArrayListWithCapacity(2);
+    ActionRequest actionRequest2 = createMock(ActionRequest.class);
+    actionRequests2.add(actionRequest2);
+    ActionRequest actionRequest3 = createMock(ActionRequest.class);
+    actionRequests2.add(actionRequest3);
+
+    expect(ruleBase.actionRequestsFor(propertySet1)).andReturn(actionRequests1)
+        .once();
+    expect(ruleBase.actionRequestsFor(propertySet2)).andReturn(actionRequests2)
+        .once();
+
+    actionExecutor.execute("testIssue", actionRequests1);
+    actionExecutor.execute("testIssue", actionRequests2);
+    actionExecutor.execute("testIssue2", actionRequests2);
+
+    replayMocks();
+
+    actionController.onChangeEvent(event);
+  }
+  private ActionController createActionController() {
+    return injector.getInstance(ActionController.class);
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      propertyExtractor = createMock(PropertyExtractor.class);
+      bind(PropertyExtractor.class).toInstance(propertyExtractor);
+
+      ruleBase = createMock(RuleBase.class);
+      bind(RuleBase.class).toInstance(ruleBase);
+
+      actionExecutor = createMock(ActionExecutor.class);
+      bind(ActionExecutor.class).toInstance(actionExecutor);
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/PropertyTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/PropertyTest.java
new file mode 100644
index 0000000..c977208
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/PropertyTest.java
@@ -0,0 +1,127 @@
+// 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 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 PropertyTest  extends LoggingMockingTestCase {
+  private Injector injector;
+
+  public void testGetKeyNull() {
+    Property property = new Property(null, "testValue");
+    assertNull("Key is not null", property.getKey());
+  }
+
+  public void testGetKeyNonNull() {
+    Property property = createProperty("testKey", "testValue");
+    assertEquals("Key does not match", "testKey", property.getKey());
+  }
+
+  public void testGetValueNull() {
+    Property property = createProperty("testKey", null);
+    assertNull("Value is not null", property.getValue());
+  }
+
+  public void testGetValueNonNull() {
+    Property property = createProperty("testKey", "testValue");
+    assertEquals("Value does not match", "testValue", property.getValue());
+  }
+
+  public void testEqualsSelf() {
+    Property property = createProperty("testKey", "testValue");
+    assertTrue("Property not equal to itself", property.equals(property));
+  }
+
+  public void testEqualsSimilar() {
+    Property propertyA = createProperty("testKey", "testValue");
+    Property propertyB = createProperty("testKey", "testValue");
+    assertTrue("Property is equal to similar", propertyA.equals(propertyB));
+  }
+
+  public void testEqualsNull() {
+    Property property = createProperty("testKey", "testValue");
+    assertFalse("Property is equal to null", property.equals(null));
+  }
+
+  public void testEqualsNull2() {
+    Property property = new Property(null, null);
+    assertFalse("Property is equal to null", property.equals(null));
+  }
+
+  public void testEqualsNulledKey() {
+    Property propertyA = new Property(null, "testValue");
+    Property propertyB = createProperty("testKey", "testValue");
+    assertFalse("Single nulled key does match",
+        propertyA.equals(propertyB));
+  }
+
+  public void testEqualsNulledKey2() {
+    Property propertyA = createProperty("testKey", "testValue");
+    Property propertyB = new Property(null, "testValue");
+    assertFalse("Single nulled key does match",
+        propertyA.equals(propertyB));
+  }
+
+  public void testEqualsNulledValue() {
+    Property propertyA = createProperty("testKey", "testValue");
+    Property propertyB = createProperty("testKey", null);
+    assertFalse("Single nulled value does match",
+        propertyA.equals(propertyB));
+  }
+
+  public void testEqualsNulledValue2() {
+    Property propertyA = createProperty("testKey", null);
+    Property propertyB = createProperty("testKey", "testValue");
+    assertFalse("Single nulled value does match",
+        propertyA.equals(propertyB));
+  }
+
+  public void testHashCodeEquals() {
+    Property propertyA = createProperty("testKey", "testValue");
+    Property propertyB = createProperty("testKey", "testValue");
+    assertEquals("Hash codes do not match", propertyA.hashCode(),
+        propertyB.hashCode());
+  }
+
+  public void testHashCodeNullKey() {
+    Property property = new Property(null, "testValue");
+    property.hashCode();
+  }
+
+  public void testHashCodeNullValue() {
+    Property property = createProperty("testKey", null);
+    property.hashCode();
+  }
+
+  private Property createProperty(String key, String value) {
+    Property.Factory factory = injector.getInstance(Property.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(Property.Factory.class);
+    }
+  }
+}
\ No newline at end of file