Add an event property value to an ITS field

Change-Id: Ief770151ad38fb2f61f1ca93c0730c80f9814da8
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
index cfb6168..de80229 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
@@ -31,6 +31,7 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.ActionController;
 import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest;
 import com.googlesource.gerrit.plugins.its.base.workflow.AddComment;
+import com.googlesource.gerrit.plugins.its.base.workflow.AddPropertyToField;
 import com.googlesource.gerrit.plugins.its.base.workflow.AddSoyComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.Condition;
@@ -70,6 +71,7 @@
     factory(AddSoyComment.Factory.class);
     factory(AddStandardComment.Factory.class);
     factory(LogEvent.Factory.class);
+    factory(AddPropertyToField.Factory.class);
     install(ItsRulesProjectCacheImpl.module());
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java
index 9d7b3fa..4cd7bc2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java
@@ -31,6 +31,11 @@
 
   public void addComment(String issueId, String comment) throws IOException;
 
+  default void addValueToField(String issueId, String value, String fieldId) throws IOException {
+    throw new UnsupportedOperationException(
+        "add-value-to-field is not currently implemented by " + getClass());
+  }
+
   public void performAction(String issueId, String actionName) throws IOException;
 
   public boolean exists(final String issueId) throws IOException;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java
index de4acd9..85d806e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java
@@ -32,6 +32,13 @@
   }
 
   @Override
+  public void addValueToField(String issueId, String value, String fieldId) throws IOException {
+    if (log.isDebugEnabled()) {
+      log.debug("addValueToField({},{},{})", issueId, fieldId, value);
+    }
+  }
+
+  @Override
   public void addRelatedLink(String issueId, URL relatedUrl, String description)
       throws IOException {
     if (log.isDebugEnabled()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
index 26ae4e7..e209daf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
@@ -32,6 +32,7 @@
   private final AddStandardComment.Factory addStandardCommentFactory;
   private final AddSoyComment.Factory addSoyCommentFactory;
   private final LogEvent.Factory logEventFactory;
+  private final AddPropertyToField.Factory addPropertyToFieldFactory;
 
   @Inject
   public ActionExecutor(
@@ -39,12 +40,14 @@
       AddComment.Factory addCommentFactory,
       AddStandardComment.Factory addStandardCommentFactory,
       AddSoyComment.Factory addSoyCommentFactory,
-      LogEvent.Factory logEventFactory) {
+      LogEvent.Factory logEventFactory,
+      AddPropertyToField.Factory addPropertyToFieldFactory) {
     this.itsFactory = itsFactory;
     this.addCommentFactory = addCommentFactory;
     this.addStandardCommentFactory = addStandardCommentFactory;
     this.addSoyCommentFactory = addSoyCommentFactory;
     this.logEventFactory = logEventFactory;
+    this.addPropertyToFieldFactory = addPropertyToFieldFactory;
   }
 
   private Action getAction(String actionName) {
@@ -57,6 +60,8 @@
         return addSoyCommentFactory.create();
       case "log-event":
         return logEventFactory.create();
+      case "add-property-to-field":
+        return addPropertyToFieldFactory.create();
       default:
         return null;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java
new file mode 100644
index 0000000..1979183
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+
+public class AddPropertyToField implements Action {
+
+  public interface Factory {
+    AddPropertyToField create();
+  }
+
+  private final AddPropertyToFieldParametersExtractor parametersExtractor;
+
+  @Inject
+  public AddPropertyToField(AddPropertyToFieldParametersExtractor parametersExtractor) {
+    this.parametersExtractor = parametersExtractor;
+  }
+
+  @Override
+  public void execute(
+      ItsFacade its, String issue, ActionRequest actionRequest, Map<String, String> properties)
+      throws IOException {
+    Optional<AddPropertyToFieldParameters> parameters =
+        parametersExtractor.extract(actionRequest, properties);
+    if (!parameters.isPresent()) {
+      return;
+    }
+    its.addValueToField(issue, parameters.get().getPropertyValue(), parameters.get().getFieldId());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParameters.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParameters.java
new file mode 100644
index 0000000..55b029d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParameters.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+/** Parameters needed by {@link AddPropertyToField} action */
+public class AddPropertyToFieldParameters {
+
+  private final String propertyValue;
+  private final String fieldId;
+
+  public AddPropertyToFieldParameters(String propertyValue, String fieldId) {
+    this.propertyValue = propertyValue;
+    this.fieldId = fieldId;
+  }
+
+  /** @return The event property's value to add to the ITS field */
+  public String getPropertyValue() {
+    return propertyValue;
+  }
+
+  /** @return The id of the ITS field to which the property value is added */
+  public String getFieldId() {
+    return fieldId;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParametersExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParametersExtractor.java
new file mode 100644
index 0000000..7a259f1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParametersExtractor.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AddPropertyToFieldParametersExtractor {
+
+  private static final Logger log =
+      LoggerFactory.getLogger(AddPropertyToFieldParametersExtractor.class);
+
+  @Inject
+  public AddPropertyToFieldParametersExtractor() {}
+
+  /**
+   * @return The parameters needed to perform an AddPropertyToField action. Empty if the parameters
+   *     could not be extracted.
+   */
+  public Optional<AddPropertyToFieldParameters> extract(
+      ActionRequest actionRequest, Map<String, String> properties) {
+    String[] parameters = actionRequest.getParameters();
+    if (parameters.length != 2) {
+      log.error(
+          "Wrong number of received parameters. Received parameters are {}. Exactly two parameters are expected. The first one is the ITS field id, the second one is the event property id",
+          Arrays.toString(parameters));
+      return Optional.empty();
+    }
+
+    String propertyId = parameters[0];
+    if (Strings.isNullOrEmpty(propertyId)) {
+      log.error("Received property id is blank");
+      return Optional.empty();
+    }
+
+    String fieldId = parameters[1];
+    if (Strings.isNullOrEmpty(fieldId)) {
+      log.error("Received field id is blank");
+      return Optional.empty();
+    }
+
+    if (!properties.containsKey(propertyId)) {
+      log.error("No event property found for id {}", propertyId);
+      return Optional.empty();
+    }
+
+    String propertyValue = properties.get(propertyId);
+    return Optional.of(new AddPropertyToFieldParameters(propertyValue, fieldId));
+  }
+}
diff --git a/src/main/resources/Documentation/config-rulebase-common.md b/src/main/resources/Documentation/config-rulebase-common.md
index 128dac5..65c18b1 100644
--- a/src/main/resources/Documentation/config-rulebase-common.md
+++ b/src/main/resources/Documentation/config-rulebase-common.md
@@ -634,6 +634,20 @@
 the event's subject property, and `$changeNumber` would refer to the
 change's number.
 
+[action-add-property-to-field]: #action-add-property-to-field
+### <a name="action-add-property-to-field">Action: add-property-to-field</a>
+
+The `add-property-to-field` action adds an event property value to an ITS designated field.
+
+The field is expected to be able to hold multiple values.
+The ITS field value deduplication depends on the its implementation.
+
+Example with the event property `branch` and a field identified as `labels`:
+
+```
+  action = add-property-to-field branch labels
+```
+
 [action-log-event]: #action-log-event
 ### <a name="action-log-event">Action: log-event</a>
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
index 69c9be9..3864cdf 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
@@ -38,6 +38,7 @@
   private AddStandardComment.Factory addStandardCommentFactory;
   private AddSoyComment.Factory addSoyCommentFactory;
   private LogEvent.Factory logEventFactory;
+  private AddPropertyToField.Factory addPropertyToFieldFactory;
 
   private Map<String, String> properties =
       ImmutableMap.of("issue", "4711", "project", "testProject");
@@ -212,6 +213,25 @@
     actionExecutor.execute(actionRequests, properties);
   }
 
+  public void testAddPropertyToFieldDelegation() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn("add-property-to-field");
+
+    Set<ActionRequest> actionRequests = ImmutableSet.of(actionRequest);
+
+    AddPropertyToField addPropertyToField = createMock(AddPropertyToField.class);
+    expect(addPropertyToFieldFactory.create()).andReturn(addPropertyToField);
+    expect(itsFacadeFactory.getFacade(new Project.NameKey(properties.get("project"))))
+        .andReturn(its);
+
+    addPropertyToField.execute(its, "4711", actionRequest, properties);
+
+    replayMocks();
+
+    ActionExecutor actionExecutor = createActionExecutor();
+    actionExecutor.execute(actionRequests, properties);
+  }
+
   private ActionExecutor createActionExecutor() {
     return injector.getInstance(ActionExecutor.class);
   }
@@ -242,6 +262,9 @@
 
       itsFacadeFactory = createMock(ItsFacadeFactory.class);
       bind(ItsFacadeFactory.class).toInstance(itsFacadeFactory);
+
+      addPropertyToFieldFactory = createMock(AddPropertyToField.Factory.class);
+      bind(AddPropertyToField.Factory.class).toInstance(addPropertyToFieldFactory);
     }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParametersExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParametersExtractorTest.java
new file mode 100644
index 0000000..fb047f7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldParametersExtractorTest.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.testutil.MockingTestCase;
+import java.util.Collections;
+import java.util.Optional;
+
+public class AddPropertyToFieldParametersExtractorTest extends MockingTestCase {
+
+  private static final String FIELD_ID = "fieldId";
+  private static final String PROPERTY_ID = "propertyId";
+  private static final String PROPERTY_VALUE = "propertyValue";
+
+  private AddPropertyToFieldParametersExtractor extractor;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    Injector injector = Guice.createInjector(new TestModule());
+    extractor = injector.getInstance(AddPropertyToFieldParametersExtractor.class);
+  }
+
+  private class TestModule extends FactoryModule {}
+
+  public void testNoParameter() {
+    testWrongNumberOfReceivedParameters(new String[] {});
+  }
+
+  public void testOneParameter() {
+    testWrongNumberOfReceivedParameters(new String[] {PROPERTY_ID});
+  }
+
+  public void testThreeParameters() {
+    testWrongNumberOfReceivedParameters(new String[] {PROPERTY_ID, PROPERTY_ID, PROPERTY_ID});
+  }
+
+  private void testWrongNumberOfReceivedParameters(String[] parameters) {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(parameters);
+
+    replayMocks();
+
+    assertFalse(extractor.extract(actionRequest, Collections.emptyMap()).isPresent());
+  }
+
+  public void testBlankFieldId() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {PROPERTY_ID, ""});
+
+    replayMocks();
+
+    assertFalse(extractor.extract(actionRequest, Collections.emptyMap()).isPresent());
+  }
+
+  public void testBlankPropertyId() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {"", FIELD_ID});
+
+    replayMocks();
+
+    assertFalse(extractor.extract(actionRequest, Collections.emptyMap()).isPresent());
+  }
+
+  public void testUnknownPropertyId() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {FIELD_ID, PROPERTY_ID});
+
+    replayMocks();
+
+    assertFalse(extractor.extract(actionRequest, Collections.emptyMap()).isPresent());
+  }
+
+  public void testHappyPath() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {PROPERTY_ID, FIELD_ID});
+
+    replayMocks();
+
+    Optional<AddPropertyToFieldParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.singletonMap(PROPERTY_ID, PROPERTY_VALUE));
+    if (!extractedParameters.isPresent()) {
+      fail();
+    }
+    assertEquals(PROPERTY_VALUE, extractedParameters.get().getPropertyValue());
+    assertEquals(FIELD_ID, extractedParameters.get().getFieldId());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldTest.java
new file mode 100644
index 0000000..a601b04
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToFieldTest.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.testutil.MockingTestCase;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import org.easymock.EasyMock;
+
+public class AddPropertyToFieldTest extends MockingTestCase {
+
+  private static final String ISSUE_ID = "4711";
+  private static final String FIELD_ID = "fieldId";
+  private static final String PROPERTY_ID = "propertyId";
+  private static final String PROPERTY_VALUE = "propertyValue";
+
+  private Injector injector;
+  private AddPropertyToFieldParametersExtractor parametersExtractor;
+  private ItsFacade its;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      parametersExtractor = createMock(AddPropertyToFieldParametersExtractor.class);
+      bind(AddPropertyToFieldParametersExtractor.class).toInstance(parametersExtractor);
+
+      its = createMock(ItsFacade.class);
+      bind(ItsFacade.class).toInstance(its);
+    }
+  }
+
+  public void testHappyPath() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Map<String, String> properties = Collections.singletonMap(PROPERTY_ID, PROPERTY_VALUE);
+    expect(parametersExtractor.extract(actionRequest, properties))
+        .andReturn(Optional.of(new AddPropertyToFieldParameters(PROPERTY_VALUE, FIELD_ID)));
+
+    its.addValueToField(ISSUE_ID, PROPERTY_VALUE, FIELD_ID);
+    EasyMock.expectLastCall().once();
+
+    replayMocks();
+
+    AddPropertyToField addPropertyToField = createAddPropertyToField();
+    addPropertyToField.execute(its, ISSUE_ID, actionRequest, properties);
+  }
+
+  private AddPropertyToField createAddPropertyToField() {
+    return injector.getInstance(AddPropertyToField.class);
+  }
+}