Merge "Implement extracting properties from events"
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/PropertyAttributeExtractor.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/PropertyAttributeExtractor.java
new file mode 100644
index 0000000..539b646
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/PropertyAttributeExtractor.java
@@ -0,0 +1,101 @@
+//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.common.collect.Sets;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+/**
+ * Extractor to translate the various {@code *Attribute}s to
+ * {@link Property Properties}.
+ */
+public class PropertyAttributeExtractor {
+  private Property.Factory propertyFactory;
+
+  @Inject
+  PropertyAttributeExtractor(Property.Factory propertyFactory) {
+    this.propertyFactory = propertyFactory;
+  }
+
+  public Set<Property> extractFrom(AccountAttribute accountAttribute,
+      String prefix) {
+    Set<Property> properties = Sets.newHashSet();
+    if (accountAttribute != null) {
+      properties.add(propertyFactory.create(prefix + "-email",
+          accountAttribute.email));
+      properties.add(propertyFactory.create(prefix + "-username",
+          accountAttribute.username));
+      properties.add(propertyFactory.create(prefix + "-name",
+          accountAttribute.name));
+    }
+    return properties;
+  }
+
+  public Set<Property> extractFrom(ChangeAttribute changeAttribute) {
+    Set<Property> properties = Sets.newHashSet();
+    properties.add(propertyFactory.create("project", changeAttribute.project));
+    properties.add(propertyFactory.create("branch", changeAttribute.branch));
+    properties.add(propertyFactory.create("topic", changeAttribute.topic));
+    properties.add(propertyFactory.create("subject", changeAttribute.subject));
+    properties.add(propertyFactory.create("change-id", changeAttribute.id));
+    properties.add(propertyFactory.create("change-number", changeAttribute.number));
+    properties.add(propertyFactory.create("change-url", changeAttribute.url));
+    properties.addAll(extractFrom(changeAttribute.owner, "owner"));
+    return properties;
+  }
+
+  public Set<Property>extractFrom(PatchSetAttribute patchSetAttribute) {
+    Set<Property> properties = Sets.newHashSet();
+    properties.add(propertyFactory.create("revision",
+        patchSetAttribute.revision));
+    properties.add(propertyFactory.create("patch-set-number",
+        patchSetAttribute.number));
+    properties.add(propertyFactory.create("ref", patchSetAttribute.ref));
+    properties.add(propertyFactory.create("created-on",
+        patchSetAttribute.createdOn.toString()));
+    properties.add(propertyFactory.create("parents",
+        patchSetAttribute.parents.toString()));
+    properties.add(propertyFactory.create("deletions",
+        Integer.toString(patchSetAttribute.sizeDeletions)));
+    properties.add(propertyFactory.create("insertions",
+        Integer.toString(patchSetAttribute.sizeInsertions)));
+    properties.addAll(extractFrom(patchSetAttribute.uploader,
+        "uploader"));
+    properties.addAll(extractFrom(patchSetAttribute.author,
+        "author"));
+    return properties;
+  }
+
+  public Set<Property>extractFrom(RefUpdateAttribute refUpdateAttribute) {
+    Set<Property> properties = Sets.newHashSet();
+    properties.add(propertyFactory.create("revision",
+        refUpdateAttribute.newRev));
+    properties.add(propertyFactory.create("revision-old",
+        refUpdateAttribute.oldRev));
+    properties.add(propertyFactory.create("project",
+        refUpdateAttribute.project));
+    properties.add(propertyFactory.create("ref",
+        refUpdateAttribute.refName));
+    return properties;
+  }
+}
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
index 9cf514e..895ec33 100644
--- 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
@@ -14,9 +14,18 @@
 
 package com.googlesource.gerrit.plugins.hooks.util;
 
+import java.util.Map;
 import java.util.Set;
 
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.events.ChangeAbandonedEvent;
 import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.inject.Inject;
 
 import com.googlesource.gerrit.plugins.hooks.workflow.Property;
 
@@ -25,14 +34,89 @@
  * {@link Property Properties}.
  */
 public class PropertyExtractor {
+  private IssueExtractor issueExtractor;
+  private Property.Factory propertyFactory;
+  private PropertyAttributeExtractor propertyAttributeExtractor;
+
+  @Inject
+  PropertyExtractor(IssueExtractor issueExtractor,
+      Property.Factory propertyFactory,
+      PropertyAttributeExtractor propertyAttributeExtractor) {
+    this.issueExtractor = issueExtractor;
+    this.propertyFactory = propertyFactory;
+    this.propertyAttributeExtractor = propertyAttributeExtractor;
+  }
+
+  private Map<String,Set<String>> extractFrom(ChangeAbandonedEvent event,
+      Set<Property> common) {
+    common.add(propertyFactory.create("event-type", event.type));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.change));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.abandoner, "abandoner"));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.patchSet));
+    common.add(propertyFactory.create("reason", event.reason));
+    return issueExtractor.getIssueIds(event.change.project,
+        event.patchSet.revision);
+  }
+
+  private Map<String,Set<String>> extractFrom(ChangeMergedEvent event,
+      Set<Property> common) {
+    common.add(propertyFactory.create("event-type", event.type));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.change));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.submitter, "submitter"));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.patchSet));
+    return issueExtractor.getIssueIds(event.change.project,
+        event.patchSet.revision);
+  }
+
+  private Map<String,Set<String>> extractFrom(ChangeRestoredEvent event,
+      Set<Property> common) {
+    common.add(propertyFactory.create("event-type", event.type));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.change));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.restorer, "restorer"));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.patchSet));
+    common.add(propertyFactory.create("reason", event.reason));
+    return issueExtractor.getIssueIds(event.change.project,
+        event.patchSet.revision);
+  }
+
+  private Map<String,Set<String>> extractFrom(RefUpdatedEvent event,
+      Set<Property> common) {
+    common.add(propertyFactory.create("event-type", event.type));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.submitter, "submitter"));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.refUpdate));
+    return issueExtractor.getIssueIds(event.refUpdate.project,
+        event.refUpdate.newRev);
+  }
+
+  private Map<String,Set<String>> extractFrom(PatchSetCreatedEvent event,
+      Set<Property> common) {
+    common.add(propertyFactory.create("event-type", event.type));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.change));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.patchSet));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.uploader, "uploader"));
+    return issueExtractor.getIssueIds(event.change.project,
+        event.patchSet.revision);
+  }
+
+  private Map<String,Set<String>> extractFrom(CommentAddedEvent event,
+      Set<Property> common) {
+    common.add(propertyFactory.create("event-type", event.type));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.change));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.patchSet));
+    common.addAll(propertyAttributeExtractor.extractFrom(event.author, "commenter"));
+    common.add(propertyFactory.create("comment", event.comment));
+    //TODO approvals
+    return issueExtractor.getIssueIds(event.change.project,
+        event.patchSet.revision);
+  }
+
   /**
    * 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:
+   * should be tied to a single issue, returning {@code Set<Property>} is not
+   * sufficient, and we need to return {@code Set<Set<Property>>}. Using this
+   * approach, a PatchSetCreatedEvent for a patch set with commit message:
    *
    * <pre>
    *   (bug 4711) Fix treatment of special characters in title
@@ -47,7 +131,7 @@
    * <pre>
    *   issue: 4711
    *   association: subject
-   *   event: PatchSetCreatedEvent
+   *   event: patchset-created
    * </pre>
    *
    * and
@@ -55,11 +139,11 @@
    * <pre>
    *   issue: 42
    *   association: body
-   *   event: PatchSetCreatedEvent
+   *   event: patchset-created
    * </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
+   * Thereby, sites can choose 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.
    *
@@ -67,7 +151,39 @@
    * @return sets of property sets extracted from the event.
    */
   public Set<Set<Property>> extractFrom(ChangeEvent event) {
-    // TODO implement
-    throw new RuntimeException("unimplemented");
+    Map<String,Set<String>> associations = null;
+    Set<Set<Property>> ret = Sets.newHashSet();
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertyFactory.create("event", event.getClass().getName()));
+
+    if (event instanceof ChangeAbandonedEvent) {
+      associations = extractFrom((ChangeAbandonedEvent) event, common);
+    } else if (event instanceof ChangeMergedEvent) {
+      associations = extractFrom((ChangeMergedEvent) event, common);
+    } else if (event instanceof ChangeRestoredEvent) {
+      associations = extractFrom((ChangeRestoredEvent) event, common);
+    } else if (event instanceof CommentAddedEvent) {
+      associations = extractFrom((CommentAddedEvent) event, common);
+    } else if (event instanceof PatchSetCreatedEvent) {
+      associations = extractFrom((PatchSetCreatedEvent) event, common);
+    } else if (event instanceof RefUpdatedEvent) {
+      associations = extractFrom((RefUpdatedEvent) event, common);
+    }
+
+    if (associations != null) {
+      for (String issue : associations.keySet()) {
+        Set<Property> properties = Sets.newHashSet();
+        Property property = propertyFactory.create("issue", issue);
+        properties.add(property);
+        for (String occurrence: associations.get(issue)) {
+          property = propertyFactory.create("association", occurrence);
+          properties.add(property);
+        }
+        properties.addAll(common);
+        ret.add(properties);
+      }
+    }
+    return ret;
   }
 }
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/PropertyAttributeExtractorTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/PropertyAttributeExtractorTest.java
new file mode 100644
index 0000000..0aa5da5
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/PropertyAttributeExtractorTest.java
@@ -0,0 +1,311 @@
+// 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 static org.easymock.EasyMock.expect;
+
+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.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+public class PropertyAttributeExtractorTest extends LoggingMockingTestCase {
+  private Injector injector;
+
+  private Property.Factory propertyFactory;
+
+  public void testAccountAttributeNull() {
+    replayMocks();
+
+    PropertyAttributeExtractor extractor =
+        injector.getInstance(PropertyAttributeExtractor.class);
+
+    Set<Property> actual = extractor.extractFrom(null, "prefix");
+
+    Set<Property> expected = Sets.newHashSet();
+
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void testAccountAttribute() {
+    AccountAttribute accountAttribute = new AccountAttribute();
+    accountAttribute.email = "testEmail";
+    accountAttribute.name = "testName";
+    accountAttribute.username = "testUsername";
+
+    Property propertyEmail= createMock(Property.class);
+    expect(propertyFactory.create("prefix-email", "testEmail"))
+        .andReturn(propertyEmail);
+
+    Property propertyName = createMock(Property.class);
+    expect(propertyFactory.create("prefix-name", "testName"))
+        .andReturn(propertyName);
+
+    Property propertyUsername = createMock(Property.class);
+    expect(propertyFactory.create("prefix-username", "testUsername"))
+        .andReturn(propertyUsername);
+
+    replayMocks();
+
+    PropertyAttributeExtractor extractor =
+        injector.getInstance(PropertyAttributeExtractor.class);
+
+    Set<Property> actual = extractor.extractFrom(accountAttribute, "prefix");
+
+    Set<Property> expected = Sets.newHashSet();
+    expected.add(propertyEmail);
+    expected.add(propertyName);
+    expected.add(propertyUsername);
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void testChangeAttribute() {
+    AccountAttribute owner = new AccountAttribute();
+    owner.email = "testEmail";
+    owner.name = "testName";
+    owner.username = "testUsername";
+
+    ChangeAttribute changeAttribute = new ChangeAttribute();
+    changeAttribute.project = "testProject";
+    changeAttribute.branch = "testBranch";
+    changeAttribute.topic = "testTopic";
+    changeAttribute.subject = "testSubject";
+    changeAttribute.id = "testId";
+    changeAttribute.number = "4711";
+    changeAttribute.url = "http://www.example.org/test";
+    changeAttribute.owner = owner;
+
+    Property propertyProject = createMock(Property.class);
+    expect(propertyFactory.create("project", "testProject"))
+        .andReturn(propertyProject);
+
+    Property propertyBranch = createMock(Property.class);
+    expect(propertyFactory.create("branch", "testBranch"))
+        .andReturn(propertyBranch);
+
+    Property propertyTopic = createMock(Property.class);
+    expect(propertyFactory.create("topic", "testTopic"))
+        .andReturn(propertyTopic);
+
+    Property propertySubject = createMock(Property.class);
+    expect(propertyFactory.create("subject", "testSubject"))
+        .andReturn(propertySubject);
+
+    Property propertyId = createMock(Property.class);
+    expect(propertyFactory.create("change-id", "testId"))
+        .andReturn(propertyId);
+
+    Property propertyNumber = createMock(Property.class);
+    expect(propertyFactory.create("change-number", "4711"))
+        .andReturn(propertyNumber);
+
+    Property propertyUrl = createMock(Property.class);
+    expect(propertyFactory.create("change-url", "http://www.example.org/test"))
+        .andReturn(propertyUrl);
+
+    Property propertyEmail= createMock(Property.class);
+    expect(propertyFactory.create("owner-email", "testEmail"))
+        .andReturn(propertyEmail);
+
+    Property propertyName = createMock(Property.class);
+    expect(propertyFactory.create("owner-name", "testName"))
+        .andReturn(propertyName);
+
+    Property propertyUsername = createMock(Property.class);
+    expect(propertyFactory.create("owner-username", "testUsername"))
+        .andReturn(propertyUsername);
+
+
+    replayMocks();
+
+    PropertyAttributeExtractor extractor =
+        injector.getInstance(PropertyAttributeExtractor.class);
+
+    Set<Property> actual = extractor.extractFrom(changeAttribute);
+
+    Set<Property> expected = Sets.newHashSet();
+    expected.add(propertyProject);
+    expected.add(propertyBranch);
+    expected.add(propertyTopic);
+    expected.add(propertySubject);
+    expected.add(propertyId);
+    expected.add(propertyNumber);
+    expected.add(propertyUrl);
+    expected.add(propertyEmail);
+    expected.add(propertyName);
+    expected.add(propertyUsername);
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void testPatchSetAttribute() {
+    AccountAttribute uploader = new AccountAttribute();
+    uploader.email = "testEmail1";
+    uploader.name = "testName1";
+    uploader.username = "testUsername1";
+
+    AccountAttribute author = new AccountAttribute();
+    author.email = "testEmail2";
+    author.name = "testName2";
+    author.username = "testUsername2";
+
+    PatchSetAttribute patchSetAttribute = new PatchSetAttribute();
+    patchSetAttribute.revision = "1234567891123456789212345678931234567894";
+    patchSetAttribute.number = "42";
+    patchSetAttribute.ref = "testRef";
+    patchSetAttribute.createdOn = 1234567890L;
+    patchSetAttribute.parents = Lists.newArrayList("parent1", "parent2");
+    patchSetAttribute.sizeDeletions = 7;
+    patchSetAttribute.sizeInsertions = 12;
+    patchSetAttribute.uploader = uploader;
+    patchSetAttribute.author = author;
+
+    Property propertyRevision = createMock(Property.class);
+    expect(propertyFactory.create("revision",
+        "1234567891123456789212345678931234567894"))
+        .andReturn(propertyRevision);
+
+    Property propertyNumber = createMock(Property.class);
+    expect(propertyFactory.create("patch-set-number", "42"))
+        .andReturn(propertyNumber);
+
+    Property propertyRef = createMock(Property.class);
+    expect(propertyFactory.create("ref", "testRef"))
+        .andReturn(propertyRef);
+
+    Property propertyCreatedOn = createMock(Property.class);
+    expect(propertyFactory.create("created-on", "1234567890"))
+        .andReturn(propertyCreatedOn);
+
+    Property propertyParents = createMock(Property.class);
+    expect(propertyFactory.create("parents", "[parent1, parent2]"))
+        .andReturn(propertyParents);
+
+    Property propertyDeletions = createMock(Property.class);
+    expect(propertyFactory.create("deletions", "7"))
+        .andReturn(propertyDeletions);
+
+    Property propertyInsertions = createMock(Property.class);
+    expect(propertyFactory.create("insertions", "12"))
+        .andReturn(propertyInsertions);
+
+    Property propertyEmail1 = createMock(Property.class);
+    expect(propertyFactory.create("uploader-email", "testEmail1"))
+        .andReturn(propertyEmail1);
+
+    Property propertyName1 = createMock(Property.class);
+    expect(propertyFactory.create("uploader-name", "testName1"))
+        .andReturn(propertyName1);
+
+    Property propertyUsername1 = createMock(Property.class);
+    expect(propertyFactory.create("uploader-username", "testUsername1"))
+        .andReturn(propertyUsername1);
+
+    Property propertyEmail2 = createMock(Property.class);
+    expect(propertyFactory.create("author-email", "testEmail2"))
+        .andReturn(propertyEmail2);
+
+    Property propertyName2 = createMock(Property.class);
+    expect(propertyFactory.create("author-name", "testName2"))
+        .andReturn(propertyName2);
+
+    Property propertyUsername2 = createMock(Property.class);
+    expect(propertyFactory.create("author-username", "testUsername2"))
+        .andReturn(propertyUsername2);
+
+    replayMocks();
+
+    PropertyAttributeExtractor extractor =
+        injector.getInstance(PropertyAttributeExtractor.class);
+
+    Set<Property> actual = extractor.extractFrom(patchSetAttribute);
+
+    Set<Property> expected = Sets.newHashSet();
+    expected.add(propertyRevision);
+    expected.add(propertyNumber);
+    expected.add(propertyRef);
+    expected.add(propertyCreatedOn);
+    expected.add(propertyParents);
+    expected.add(propertyDeletions);
+    expected.add(propertyInsertions);
+    expected.add(propertyEmail1);
+    expected.add(propertyName1);
+    expected.add(propertyUsername1);
+    expected.add(propertyEmail2);
+    expected.add(propertyName2);
+    expected.add(propertyUsername2);
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void testRefUpdateAttribute() {
+    RefUpdateAttribute refUpdateAttribute = new RefUpdateAttribute();
+    refUpdateAttribute.newRev = "1234567891123456789212345678931234567894";
+    refUpdateAttribute.oldRev = "9876543211987654321298765432139876543214";
+    refUpdateAttribute.project = "testProject";
+    refUpdateAttribute.refName = "testRef";
+
+    Property propertyRevision = createMock(Property.class);
+    expect(propertyFactory.create("revision",
+        "1234567891123456789212345678931234567894"))
+        .andReturn(propertyRevision);
+
+    Property propertyRevisionOld = createMock(Property.class);
+    expect(propertyFactory.create("revision-old",
+        "9876543211987654321298765432139876543214"))
+        .andReturn(propertyRevisionOld);
+
+    Property propertyProject = createMock(Property.class);
+    expect(propertyFactory.create("project", "testProject"))
+        .andReturn(propertyProject);
+
+    Property propertyRef = createMock(Property.class);
+    expect(propertyFactory.create("ref", "testRef"))
+        .andReturn(propertyRef);
+
+    replayMocks();
+
+    PropertyAttributeExtractor extractor =
+        injector.getInstance(PropertyAttributeExtractor.class);
+
+    Set<Property> actual = extractor.extractFrom(refUpdateAttribute);
+
+    Set<Property> expected = Sets.newHashSet();
+    expected.add(propertyRevision);
+    expected.add(propertyRevisionOld);
+    expected.add(propertyProject);
+    expected.add(propertyRef);
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      propertyFactory = createMock(Property.Factory.class);
+      bind(Property.Factory.class).toInstance(propertyFactory);
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/PropertyExtractorTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/PropertyExtractorTest.java
new file mode 100644
index 0000000..ef9270b
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/PropertyExtractorTest.java
@@ -0,0 +1,358 @@
+// 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 static org.easymock.EasyMock.expect;
+
+import java.util.HashMap;
+import java.util.Set;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.events.ChangeAbandonedEvent;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+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.IssueExtractor;
+import com.googlesource.gerrit.plugins.hooks.util.PropertyExtractor;
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+public class PropertyExtractorTest extends LoggingMockingTestCase {
+  private Injector injector;
+
+  private IssueExtractor issueExtractor;
+  private Property.Factory propertyFactory;
+  private PropertyAttributeExtractor propertyAttributeExtractor;
+
+  public void testDummyChangeEvent() {
+    PropertyExtractor propertyExtractor = injector.getInstance(
+        PropertyExtractor.class);
+
+    Property property1 = createMock(Property.class);
+    expect(propertyFactory.create("event", "com.googlesource.gerrit.plugins." +
+        "hooks.util.PropertyExtractorTest$DummyChangeEvent"))
+        .andReturn(property1);
+
+    replayMocks();
+
+    Set<Set<Property>> actual = propertyExtractor.extractFrom(
+        new DummyChangeEvent());
+
+    Set<Set<Property>> expected = Sets.newHashSet();
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void testChangeAbandonedEvent() {
+    ChangeAbandonedEvent event = new ChangeAbandonedEvent();
+
+    ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
+    event.change = changeAttribute;
+    Property propertyChange = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(changeAttribute))
+        .andReturn(Sets.newHashSet(propertyChange));
+
+    AccountAttribute accountAttribute = createMock(AccountAttribute.class);
+    event.abandoner= accountAttribute;
+    Property propertySubmitter = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(accountAttribute,
+        "abandoner")).andReturn(Sets.newHashSet(propertySubmitter));
+
+    PatchSetAttribute patchSetAttribute = createMock(PatchSetAttribute.class);
+    event.patchSet = patchSetAttribute;
+    Property propertyPatchSet = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(patchSetAttribute))
+        .andReturn(Sets.newHashSet(propertyPatchSet));
+
+    event.reason = "testReason";
+    Property propertyReason = createMock(Property.class);
+    expect(propertyFactory.create("reason", "testReason"))
+        .andReturn(propertyReason);
+
+    changeAttribute.project = "testProject";
+    patchSetAttribute.revision = "testRevision";
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertyChange);
+    common.add(propertySubmitter);
+    common.add(propertyPatchSet);
+    common.add(propertyReason);
+
+    eventHelper(event, "ChangeAbandonedEvent", "change-abandoned", common);
+  }
+
+  public void testChangeMergedEvent() {
+    ChangeMergedEvent event = new ChangeMergedEvent();
+
+    ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
+    event.change = changeAttribute;
+    Property propertyChange = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(changeAttribute))
+        .andReturn(Sets.newHashSet(propertyChange));
+
+    AccountAttribute accountAttribute = createMock(AccountAttribute.class);
+    event.submitter = accountAttribute;
+    Property propertySubmitter = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(accountAttribute,
+        "submitter")).andReturn(Sets.newHashSet(propertySubmitter));
+
+    PatchSetAttribute patchSetAttribute = createMock(PatchSetAttribute.class);
+    event.patchSet = patchSetAttribute;
+    Property propertyPatchSet = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(patchSetAttribute))
+        .andReturn(Sets.newHashSet(propertyPatchSet));
+
+    changeAttribute.project = "testProject";
+    patchSetAttribute.revision = "testRevision";
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertyChange);
+    common.add(propertySubmitter);
+    common.add(propertyPatchSet);
+
+    eventHelper(event, "ChangeMergedEvent", "change-merged", common);
+  }
+
+  public void testChangeRestoredEvent() {
+    ChangeRestoredEvent event = new ChangeRestoredEvent();
+
+    ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
+    event.change = changeAttribute;
+    Property propertyChange = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(changeAttribute))
+        .andReturn(Sets.newHashSet(propertyChange));
+
+    AccountAttribute accountAttribute = createMock(AccountAttribute.class);
+    event.restorer = accountAttribute;
+    Property propertySubmitter = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(accountAttribute,
+        "restorer")).andReturn(Sets.newHashSet(propertySubmitter));
+
+    PatchSetAttribute patchSetAttribute = createMock(PatchSetAttribute.class);
+    event.patchSet = patchSetAttribute;
+    Property propertyPatchSet = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(patchSetAttribute))
+        .andReturn(Sets.newHashSet(propertyPatchSet));
+
+    event.reason = "testReason";
+    Property propertyReason = createMock(Property.class);
+    expect(propertyFactory.create("reason", "testReason"))
+        .andReturn(propertyReason);
+
+    changeAttribute.project = "testProject";
+    patchSetAttribute.revision = "testRevision";
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertyChange);
+    common.add(propertySubmitter);
+    common.add(propertyPatchSet);
+    common.add(propertyReason);
+
+    eventHelper(event, "ChangeRestoredEvent", "change-restored", common);
+  }
+
+  public void testCommentAddedEvent() {
+    CommentAddedEvent event = new CommentAddedEvent();
+
+    ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
+    event.change = changeAttribute;
+    Property propertyChange = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(changeAttribute))
+        .andReturn(Sets.newHashSet(propertyChange));
+
+    AccountAttribute accountAttribute = createMock(AccountAttribute.class);
+    event.author = accountAttribute;
+    Property propertySubmitter = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(accountAttribute,
+        "commenter")).andReturn(Sets.newHashSet(propertySubmitter));
+
+    PatchSetAttribute patchSetAttribute = createMock(PatchSetAttribute.class);
+    event.patchSet = patchSetAttribute;
+    Property propertyPatchSet = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(patchSetAttribute))
+        .andReturn(Sets.newHashSet(propertyPatchSet));
+
+    event.comment = "testComment";
+    Property propertyComment = createMock(Property.class);
+    expect(propertyFactory.create("comment", "testComment"))
+        .andReturn(propertyComment);
+
+    changeAttribute.project = "testProject";
+    patchSetAttribute.revision = "testRevision";
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertyChange);
+    common.add(propertySubmitter);
+    common.add(propertyPatchSet);
+    common.add(propertyComment);
+
+    eventHelper(event, "CommentAddedEvent", "comment-added", common);
+  }
+
+  public void testPatchSetCreatedEvent() {
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+
+    ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
+    event.change = changeAttribute;
+    Property propertyChange = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(changeAttribute))
+        .andReturn(Sets.newHashSet(propertyChange));
+
+    AccountAttribute accountAttribute = createMock(AccountAttribute.class);
+    event.uploader = accountAttribute;
+    Property propertySubmitter = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(accountAttribute,
+        "uploader")).andReturn(Sets.newHashSet(propertySubmitter));
+
+    PatchSetAttribute patchSetAttribute = createMock(PatchSetAttribute.class);
+    event.patchSet = patchSetAttribute;
+    Property propertyPatchSet = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(patchSetAttribute))
+        .andReturn(Sets.newHashSet(propertyPatchSet));
+
+    changeAttribute.project = "testProject";
+    patchSetAttribute.revision = "testRevision";
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertyChange);
+    common.add(propertySubmitter);
+    common.add(propertyPatchSet);
+
+    eventHelper(event, "PatchSetCreatedEvent", "patchset-created", common);
+  }
+
+  public void testRefUpdatedEvent() {
+    RefUpdatedEvent event = new RefUpdatedEvent();
+
+    AccountAttribute accountAttribute = createMock(AccountAttribute.class);
+    event.submitter = accountAttribute;
+    Property propertySubmitter = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(accountAttribute,
+        "submitter")).andReturn(Sets.newHashSet(propertySubmitter));
+
+    RefUpdateAttribute refUpdateAttribute =
+        createMock(RefUpdateAttribute.class);
+    event.refUpdate = refUpdateAttribute;
+    Property propertyRefUpdated = createMock(Property.class);
+    expect(propertyAttributeExtractor.extractFrom(refUpdateAttribute))
+        .andReturn(Sets.newHashSet(propertyRefUpdated));
+
+    refUpdateAttribute.project = "testProject";
+    refUpdateAttribute.newRev = "testRevision";
+
+    Set<Property> common = Sets.newHashSet();
+    common.add(propertySubmitter);
+    common.add(propertyRefUpdated);
+
+    eventHelper(event, "RefUpdatedEvent", "ref-updated", common);
+  }
+
+  private void eventHelper(ChangeEvent event, String className, String type,
+      Set<Property> common) {
+    PropertyExtractor propertyExtractor = injector.getInstance(
+        PropertyExtractor.class);
+
+    Property propertyEvent = createMock(Property.class);
+    expect(propertyFactory.create("event", "com.google.gerrit.server.events." +
+        className)).andReturn(propertyEvent);
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyFactory.create("event-type", type))
+        .andReturn(propertyEventType);
+
+    Property propertyAssociationFooter = createMock(Property.class);
+    expect(propertyFactory.create("association", "footer"))
+        .andReturn(propertyAssociationFooter);
+
+    Property propertyAssociationAnywhere = createMock(Property.class);
+    expect(propertyFactory.create("association", "anywhere"))
+        .andReturn(propertyAssociationAnywhere).times(2);
+
+    Property propertyAssociationBody = createMock(Property.class);
+    expect(propertyFactory.create("association", "body"))
+        .andReturn(propertyAssociationBody);
+
+    Property propertyIssue42 = createMock(Property.class);
+    expect(propertyFactory.create("issue", "42"))
+        .andReturn(propertyIssue42);
+
+    Property propertyIssue4711 = createMock(Property.class);
+    expect(propertyFactory.create("issue", "4711"))
+        .andReturn(propertyIssue4711);
+
+    HashMap<String,Set<String>> issueMap = Maps.newHashMap();
+    issueMap.put("4711", Sets.newHashSet("body", "anywhere"));
+    issueMap.put("42", Sets.newHashSet("footer", "anywhere"));
+    expect(issueExtractor.getIssueIds("testProject", "testRevision"))
+        .andReturn(issueMap);
+
+    replayMocks();
+
+    Set<Set<Property>> actual = propertyExtractor.extractFrom(event);
+
+    Set<Set<Property>> expected = Sets.newHashSet();
+    Set<Property> properties = Sets.newHashSet();
+    properties.add(propertyEvent);
+    properties.add(propertyEventType);
+    properties.add(propertyAssociationAnywhere);
+    properties.add(propertyAssociationFooter);
+    properties.add(propertyIssue42);
+    properties.addAll(common);
+    expected.add(properties);
+
+    properties = Sets.newHashSet();
+    properties.add(propertyEvent);
+    properties.add(propertyEventType);
+    properties.add(propertyAssociationAnywhere);
+    properties.add(propertyAssociationBody);
+    properties.add(propertyIssue4711);
+    properties.addAll(common);
+    expected.add(properties);
+    assertEquals("Properties do not match", expected, actual);
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      issueExtractor = createMock(IssueExtractor.class);
+      bind(IssueExtractor.class).toInstance(issueExtractor);
+
+      propertyAttributeExtractor = createMock(PropertyAttributeExtractor.class);
+      bind(PropertyAttributeExtractor.class).toInstance(
+          propertyAttributeExtractor);
+
+      propertyFactory = createMock(Property.Factory.class);
+      bind(Property.Factory.class).toInstance(propertyFactory);
+      //factory(Property.Factory.class);
+    }
+  }
+
+  private class DummyChangeEvent extends ChangeEvent {
+  }
+}
\ No newline at end of file