Allow its-* plugins to register actions of their own

More and more actions are created in the project. Each one requires
hundred of lines of modifications in its-base. Also most actions will
only be implemented in a unique its-* plugin implementation.

This change allows its-* plugin to expose new actions without requiring
the modification of its-base.

Change-Id: Ib9993cc0643f1524ce31905ebf71e1fe4994c9fa
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 6c68777..dc6f4a5 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
@@ -36,6 +37,7 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.Condition;
 import com.googlesource.gerrit.plugins.its.base.workflow.CreateVersionFromProperty;
+import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction;
 import com.googlesource.gerrit.plugins.its.base.workflow.ItsRulesProjectCacheImpl;
 import com.googlesource.gerrit.plugins.its.base.workflow.LogEvent;
 import com.googlesource.gerrit.plugins.its.base.workflow.Rule;
@@ -74,6 +76,7 @@
     factory(CreateVersionFromProperty.Factory.class);
     factory(LogEvent.Factory.class);
     factory(AddPropertyToField.Factory.class);
+    DynamicMap.mapOf(binder(), CustomAction.class);
     install(ItsRulesProjectCacheImpl.module());
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java
index 338ad81..4198271 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java
@@ -19,8 +19,7 @@
 import java.util.Map;
 
 /** Interface for actions on an issue tracking system */
-interface Action {
-
+public interface Action {
   /** @return The type of this action */
   ActionType getType();
 
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 bd42552..355d1f3 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
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
@@ -34,6 +36,7 @@
   private final LogEvent.Factory logEventFactory;
   private final AddPropertyToField.Factory addPropertyToFieldFactory;
   private final CreateVersionFromProperty.Factory createVersionFromPropertyFactory;
+  private final DynamicMap<CustomAction> customActions;
 
   @Inject
   public ActionExecutor(
@@ -43,7 +46,8 @@
       AddSoyComment.Factory addSoyCommentFactory,
       LogEvent.Factory logEventFactory,
       AddPropertyToField.Factory addPropertyToFieldFactory,
-      CreateVersionFromProperty.Factory createVersionFromPropertyFactory) {
+      CreateVersionFromProperty.Factory createVersionFromPropertyFactory,
+      DynamicMap<CustomAction> customActions) {
     this.itsFactory = itsFactory;
     this.addCommentFactory = addCommentFactory;
     this.addStandardCommentFactory = addStandardCommentFactory;
@@ -51,6 +55,7 @@
     this.logEventFactory = logEventFactory;
     this.addPropertyToFieldFactory = addPropertyToFieldFactory;
     this.createVersionFromPropertyFactory = createVersionFromPropertyFactory;
+    this.customActions = customActions;
   }
 
   private Action getAction(String actionName) {
@@ -68,19 +73,26 @@
       case "create-version-from-property":
         return createVersionFromPropertyFactory.create();
       default:
-        return null;
+        return customActions.get(PluginName.GERRIT, actionName);
     }
   }
 
+  private void execute(
+      Action action, String target, ActionRequest actionRequest, Map<String, String> properties)
+      throws IOException {
+    ItsFacade its = itsFactory.getFacade(new Project.NameKey(properties.get("project")));
+    action.execute(its, target, actionRequest, properties);
+  }
+
   private void executeOnIssue(
       String issue, ActionRequest actionRequest, Map<String, String> properties) {
-    ItsFacade its = itsFactory.getFacade(new Project.NameKey(properties.get("project")));
     try {
       Action action = getAction(actionRequest.getName());
       if (action == null) {
+        ItsFacade its = itsFactory.getFacade(new Project.NameKey(properties.get("project")));
         its.performAction(issue, actionRequest.getUnparsed());
       } else if (action.getType() == ActionType.ISSUE) {
-        action.execute(its, issue, actionRequest, properties);
+        execute(action, issue, actionRequest, properties);
       }
     } catch (IOException e) {
       log.error("Error while executing action " + actionRequest, e);
@@ -105,8 +117,7 @@
       if (action.getType() != ActionType.PROJECT) {
         return;
       }
-      ItsFacade its = itsFactory.getFacade(new Project.NameKey(properties.get("project")));
-      action.execute(its, itsProject, actionRequest, properties);
+      execute(action, itsProject, actionRequest, properties);
     } catch (IOException e) {
       log.error("Error while executing action " + actionRequest, e);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CustomAction.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CustomAction.java
new file mode 100644
index 0000000..5a79650
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CustomAction.java
@@ -0,0 +1,21 @@
+// 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.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Interface for actions specific to its-* plugins * */
+@ExtensionPoint
+public interface CustomAction extends Action {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java
index 230517f..37707ad 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java
@@ -15,7 +15,7 @@
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
 /** Abstraction for actions on ITS issues */
-public abstract class IssueAction implements Action {
+public abstract class IssueAction implements StandardAction {
 
   @Override
   public final ActionType getType() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java
index 4c3c55b..5867377 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java
@@ -15,7 +15,7 @@
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
 /** Abstraction for actions on ITS projects */
-public abstract class ProjectAction implements Action {
+public abstract class ProjectAction implements StandardAction {
 
   @Override
   public final ActionType getType() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/StandardAction.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/StandardAction.java
new file mode 100644
index 0000000..eb23693
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/StandardAction.java
@@ -0,0 +1,18 @@
+// 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.its.base.workflow;
+
+/** Interface for actions defined by base module */
+interface StandardAction extends Action {}
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 88cfd1a..e82ffd1 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
@@ -18,7 +18,9 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -31,6 +33,9 @@
 import java.util.Set;
 
 public class ActionExecutorTest extends LoggingMockingTestCase {
+
+  private static final String CUSTOM_ACTION_NAME = "custom-action-name";
+
   private Injector injector;
 
   private ItsFacade its;
@@ -41,6 +46,7 @@
   private LogEvent.Factory logEventFactory;
   private AddPropertyToField.Factory addPropertyToFieldFactory;
   private CreateVersionFromProperty.Factory createVersionFromPropertyFactory;
+  private CustomAction customAction;
 
   private Map<String, String> properties =
       ImmutableMap.of("issue", "4711", "project", "testProject");
@@ -257,6 +263,42 @@
     actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
+  public void testExecuteIssueCustomAction() throws IOException {
+    expect(customAction.getType()).andReturn(ActionType.ISSUE);
+
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn(CUSTOM_ACTION_NAME);
+    expect(itsFacadeFactory.getFacade(new Project.NameKey(properties.get("project"))))
+        .andReturn(its);
+
+    Set<ActionRequest> actionRequests = ImmutableSet.of(actionRequest);
+
+    customAction.execute(its, "4711", actionRequest, properties);
+
+    replayMocks();
+
+    ActionExecutor actionExecutor = createActionExecutor();
+    actionExecutor.executeOnIssue(actionRequests, properties);
+  }
+
+  public void testExecuteProjectCustomAction() throws IOException {
+    expect(customAction.getType()).andReturn(ActionType.PROJECT);
+
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn(CUSTOM_ACTION_NAME);
+    expect(itsFacadeFactory.getFacade(new Project.NameKey(properties.get("project"))))
+        .andReturn(its);
+
+    Set<ActionRequest> actionRequests = ImmutableSet.of(actionRequest);
+
+    customAction.execute(its, "itsTestProject", actionRequest, projectProperties);
+
+    replayMocks();
+
+    ActionExecutor actionExecutor = createActionExecutor();
+    actionExecutor.executeOnProject(actionRequests, projectProperties);
+  }
+
   private ActionExecutor createActionExecutor() {
     return injector.getInstance(ActionExecutor.class);
   }
@@ -293,6 +335,13 @@
 
       createVersionFromPropertyFactory = createMock(CreateVersionFromProperty.Factory.class);
       bind(CreateVersionFromProperty.Factory.class).toInstance(createVersionFromPropertyFactory);
+
+      DynamicMap.mapOf(binder(), CustomAction.class);
+      customAction = createMock(CustomAction.class);
+
+      bind(CustomAction.class)
+          .annotatedWith(Exports.named(CUSTOM_ACTION_NAME))
+          .toInstance(customAction);
     }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardCommentTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardCommentTest.java
index 044e6a1..d695bd1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardCommentTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardCommentTest.java
@@ -35,7 +35,7 @@
     its.addComment("42", "Change merged");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "42", actionRequest, properties);
   }
 
@@ -59,7 +59,7 @@
             + "HtTp://ExAmPlE.OrG/ChAnGe");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "176", actionRequest, properties);
   }
 
@@ -71,7 +71,7 @@
     its.addComment("42", "Change abandoned");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "42", actionRequest, properties);
   }
 
@@ -99,7 +99,7 @@
             + "HtTp://ExAmPlE.OrG/ChAnGe");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "176", actionRequest, properties);
   }
 
@@ -111,7 +111,7 @@
     its.addComment("42", "Change restored");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "42", actionRequest, properties);
   }
 
@@ -139,7 +139,7 @@
             + "HtTp://ExAmPlE.OrG/ChAnGe");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "176", actionRequest, properties);
   }
 
@@ -151,7 +151,7 @@
     its.addComment("42", "Change had a related patch set uploaded");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "42", actionRequest, properties);
   }
 
@@ -176,7 +176,7 @@
             + "HtTp://ExAmPlE.OrG/ChAnGe");
     replayMocks();
 
-    Action action = injector.getInstance(AddStandardComment.class);
+    StandardAction action = injector.getInstance(AddStandardComment.class);
     action.execute(its, "176", actionRequest, properties);
   }