Add new CHANGE_ACTIONS option to /changes/X/detail REST API call

This will move action related JSON processing into the ActionJson class
as well.

Change-Id: I291727762d2f08d00484ca8ce8805624042ab4e8
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 115cee5..284cc83 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -281,8 +281,15 @@
 [[actions]]
 --
 * `CURRENT_ACTIONS`: include information on available actions
-  for the change and its current revision. The caller must be
-  authenticated to obtain the available actions.
+  for the change and its current revision. Ignored if the caller
+  is not authenticated.
+--
+
+[[change-actions]]
+--
+* `CHANGE_ACTIONS`: include information on available
+  change actions for the change. Ignored if the caller
+  is not authenticated.
 --
 
 [[reviewed]]
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 1049d6a..54617a7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -55,7 +55,10 @@
   WEB_LINKS(14),
 
   /** Include consistency check results. */
-  CHECK(15);
+  CHECK(15),
+
+  /** Include allowed change actions client could perform. */
+  CHANGE_ACTIONS(16);
 
   private final int value;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index 24837aab..023173e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -15,9 +15,15 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -29,16 +35,55 @@
 @Singleton
 public class ActionJson {
   private final Revisions revisions;
+  private final DynamicMap<RestView<ChangeResource>> changeViews;
 
   @Inject
-  ActionJson(Revisions revisions) {
+  ActionJson(
+      Revisions revisions,
+      DynamicMap<RestView<ChangeResource>> changeViews) {
     this.revisions = revisions;
+    this.changeViews = changeViews;
   }
 
   public Map<String, ActionInfo> format(RevisionResource rsrc) {
     return toActionMap(rsrc);
   }
 
+  public ChangeInfo addChangeActions(ChangeInfo to, ChangeControl ctl) {
+    to.actions = toActionMap(ctl);
+    return to;
+  }
+
+  public RevisionInfo addRevisionActions(RevisionInfo to,
+      RevisionResource rsrc) {
+    to.actions = toActionMap(rsrc);
+    return to;
+  }
+
+  private Map<String, ActionInfo> toActionMap(ChangeControl ctl) {
+    Map<String, ActionInfo> out = new LinkedHashMap<>();
+    if (!ctl.getCurrentUser().isIdentifiedUser()) {
+      return out;
+    }
+
+    Provider<CurrentUser> userProvider = Providers.of(ctl.getCurrentUser());
+    for (UiAction.Description d : UiActions.from(
+        changeViews,
+        new ChangeResource(ctl),
+        userProvider)) {
+      out.put(d.getId(), new ActionInfo(d));
+    }
+    // TODO(sbeller): why do we need to treat followup specially here?
+    if (ctl.getChange().getStatus().isOpen()) {
+      UiAction.Description descr = new UiAction.Description();
+      PrivateInternals_UiActionDescription.setId(descr, "followup");
+      PrivateInternals_UiActionDescription.setMethod(descr, "POST");
+      descr.setTitle("Create follow-up change");
+      out.put(descr.getId(), new ActionInfo(descr));
+    }
+    return out;
+  }
+
   private Map<String, ActionInfo> toActionMap(RevisionResource rsrc) {
     Map<String, ActionInfo> out = new LinkedHashMap<>();
     if (rsrc.getControl().getCurrentUser().isIdentifiedUser()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 662406a..86a50a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
@@ -59,7 +60,6 @@
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -73,10 +73,7 @@
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
-import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -95,7 +92,6 @@
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -140,13 +136,12 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final DynamicMap<DownloadScheme> downloadSchemes;
   private final DynamicMap<DownloadCommand> downloadCommands;
-  private final DynamicMap<RestView<ChangeResource>> changeViews;
-  private final Revisions revisions;
   private final WebLinks webLinks;
   private final EnumSet<ListChangesOption> options;
   private final ChangeMessagesUtil cmUtil;
   private final PatchLineCommentsUtil plcUtil;
   private final Provider<ConsistencyChecker> checkerProvider;
+  private final ActionJson actionJson;
 
   private AccountLoader accountLoader;
   private FixInput fix;
@@ -164,12 +159,11 @@
       AccountLoader.Factory ailf,
       DynamicMap<DownloadScheme> downloadSchemes,
       DynamicMap<DownloadCommand> downloadCommands,
-      DynamicMap<RestView<ChangeResource>> changeViews,
-      Revisions revisions,
       WebLinks webLinks,
       ChangeMessagesUtil cmUtil,
       PatchLineCommentsUtil plcUtil,
-      Provider<ConsistencyChecker> checkerProvider) {
+      Provider<ConsistencyChecker> checkerProvider,
+      ActionJson actionJson) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
@@ -181,12 +175,11 @@
     this.accountLoaderFactory = ailf;
     this.downloadSchemes = downloadSchemes;
     this.downloadCommands = downloadCommands;
-    this.changeViews = changeViews;
-    this.revisions = revisions;
     this.webLinks = webLinks;
     this.cmUtil = cmUtil;
     this.plcUtil = plcUtil;
     this.checkerProvider = checkerProvider;
+    this.actionJson = actionJson;
     options = EnumSet.noneOf(ListChangesOption.class);
   }
 
@@ -419,22 +412,8 @@
       }
     }
 
-    if (has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
-      out.actions = Maps.newTreeMap();
-      for (UiAction.Description d : UiActions.from(
-          changeViews,
-          new ChangeResource(ctl),
-          userProvider)) {
-        out.actions.put(d.getId(), new ActionInfo(d));
-      }
-      if (userProvider.get().isIdentifiedUser()
-          && in.getStatus().isOpen()) {
-        UiAction.Description descr = new UiAction.Description();
-        PrivateInternals_UiActionDescription.setId(descr, "followup");
-        PrivateInternals_UiActionDescription.setMethod(descr, "POST");
-        descr.setTitle("Create follow-up change");
-        out.actions.put(descr.getId(), new ActionInfo(descr));
-      }
+    if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) {
+      actionJson.addChangeActions(out, ctl);
     }
 
     return out;
@@ -907,13 +886,9 @@
     if ((out.isCurrent || (out.draft != null && out.draft))
         && has(CURRENT_ACTIONS)
         && userProvider.get().isIdentifiedUser()) {
-      out.actions = Maps.newTreeMap();
-      for (UiAction.Description d : UiActions.from(
-          revisions,
-          new RevisionResource(new ChangeResource(ctl), in),
-          userProvider)) {
-        out.actions.put(d.getId(), new ActionInfo(d));
-      }
+
+      actionJson.addRevisionActions(out,
+          new RevisionResource(new ChangeResource(ctl), in));
     }
 
     if (has(DRAFT_COMMENTS)