Add REST endpoint to rename operation

Implement Rest API equivalent to the ssh rename command with usage of
projects API. New project name is getting passed in through json request
body.

With this change the plugin will be able to receive rename requests not
only through ssh but also through its new endpoint, making it more
testable with frameworks that utilise http, such as Gatling.

DatabaseRenameHandler has no support for reviewDB in versions 3.0 and up
and that created conflict during cherry-pick. This resolution is making
the rename method to use renameInReviewDb and renameInNoteDb again.

Change-Id: I0ce0ce68a4d5e65569e8b14a580f7890ef70075b
(cherry picked from commit 94dd6934e9ab5fe03961ae431b5d50f6610f4e65)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
index 093609e..f35c55e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
@@ -14,12 +14,14 @@
 
 package com.googlesource.gerrit.plugins.renameproject;
 
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 import static com.googlesource.gerrit.plugins.renameproject.RenameOwnProjectCapability.RENAME_OWN_PROJECT;
 import static com.googlesource.gerrit.plugins.renameproject.RenameProjectCapability.RENAME_PROJECT;
 
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.internal.UniqueAnnotations;
 import com.googlesource.gerrit.plugins.renameproject.cache.CacheRenameHandler;
@@ -47,5 +49,13 @@
     bind(IndexUpdateHandler.class);
     bind(RevertRenameProject.class);
     bind(SshSessionFactory.class).toProvider(RenameReplicationSshSessionFactoryProvider.class);
+
+    install(
+        new RestApiModule() {
+          @Override
+          protected void configure() {
+            post(PROJECT_KIND, "rename").to(RenameProject.class);
+          }
+        });
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java
index 2dfb975..50f86d7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameCommand.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.renameproject;
 
+import static com.googlesource.gerrit.plugins.renameproject.RenameProject.CANCELLATION_MSG;
 import static com.googlesource.gerrit.plugins.renameproject.RenameProject.WARNING_LIMIT;
 
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -88,14 +89,13 @@
         }
       } else {
         try (CommandProgressMonitor monitor = new CommandProgressMonitor(stdout)) {
-          renameProject.assertCanRename(rsrc, input, monitor);
-          List<Change.Id> changeIds = renameProject.getChanges(rsrc, monitor);
+          renameProject.assertCanRename(rsrc, input, Optional.of(monitor));
+          List<Change.Id> changeIds = renameProject.getChanges(rsrc, Optional.of(monitor));
           if (continueRename(changeIds, monitor)) {
-            renameProject.doRename(changeIds, rsrc, input, monitor);
+            renameProject.doRename(changeIds, rsrc, input, Optional.of(monitor));
           } else {
-            String cancellationMsg = "Rename operation was cancelled by user.";
-            log.debug(cancellationMsg);
-            stdout.println(cancellationMsg);
+            log.debug(CANCELLATION_MSG);
+            stdout.println(CANCELLATION_MSG);
             stdout.flush();
           }
         }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
index 98b365e..2cbe0e4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
@@ -25,6 +25,8 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
@@ -40,6 +42,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.renameproject.RenameProject.Input;
 import com.googlesource.gerrit.plugins.renameproject.cache.CacheRenameHandler;
 import com.googlesource.gerrit.plugins.renameproject.conditions.RenamePreconditions;
 import com.googlesource.gerrit.plugins.renameproject.database.DatabaseRenameHandler;
@@ -58,13 +61,34 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class RenameProject {
+public class RenameProject implements RestModifyView<ProjectResource, Input> {
+
+  @Override
+  public Object apply(ProjectResource resource, Input input)
+      throws IOException, AuthException, BadRequestException, ResourceConflictException,
+          InterruptedException, ConfigInvalidException, OrmException {
+    assertCanRename(resource, input, Optional.empty());
+    List<Change.Id> changeIds = getChanges(resource, Optional.empty());
+
+    if (changeIds == null || changeIds.size() <= WARNING_LIMIT || input.continueWithRename) {
+      doRename(changeIds, resource, input, Optional.empty());
+    } else {
+      log.debug(CANCELLATION_MSG);
+      return Response.none();
+    }
+    return Response.ok("");
+  }
 
   static class Input {
+
     String name;
+    boolean continueWithRename;
   }
 
   static final int WARNING_LIMIT = 5000;
+  static final String CANCELLATION_MSG =
+      "Rename cancelled due to number of changes exceeding warning limit and user's will to not continue";
+
   private static final Logger log = LoggerFactory.getLogger(RenameProject.class);
   private static final String CACHE_NAME = "changeid_project";
 
@@ -165,10 +189,11 @@
     return true;
   }
 
-  void assertCanRename(ProjectResource rsrc, Input input, ProgressMonitor pm)
+  void assertCanRename(ProjectResource rsrc, Input input, Optional<ProgressMonitor> pm)
       throws ResourceConflictException, BadRequestException, AuthException {
     try {
-      pm.beginTask("Checking preconditions");
+      pm.ifPresent(progressMonitor -> progressMonitor.beginTask("Checking preconditions"));
+
       assertNewNameNotNull(input);
       assertRenamePermission(rsrc);
       renamePreconditions.assertCanRename(rsrc, new Project.NameKey(input.name));
@@ -178,14 +203,15 @@
     }
   }
 
-  void doRename(List<Change.Id> changeIds, ProjectResource rsrc, Input input, ProgressMonitor pm)
+  void doRename(
+      List<Change.Id> changeIds, ProjectResource rsrc, Input input, Optional<ProgressMonitor> pm)
       throws InterruptedException, OrmException, ConfigInvalidException, IOException {
     Project.NameKey oldProjectKey = rsrc.getNameKey();
     Project.NameKey newProjectKey = new Project.NameKey(input.name);
     Exception ex = null;
     stepsPerformed.clear();
     try {
-      fsRenameStep(oldProjectKey, newProjectKey, Optional.of(pm));
+      fsRenameStep(oldProjectKey, newProjectKey, pm);
 
       cacheRenameStep(rsrc.getNameKey(), newProjectKey);
 
@@ -233,9 +259,9 @@
   }
 
   void fsRenameStep(
-      Project.NameKey oldProjectKey, Project.NameKey newProjectKey, Optional<ProgressMonitor> opm)
+      Project.NameKey oldProjectKey, Project.NameKey newProjectKey, Optional<ProgressMonitor> pm)
       throws IOException {
-    fsHandler.rename(oldProjectKey, newProjectKey, opm);
+    fsHandler.rename(oldProjectKey, newProjectKey, pm);
     logPerformedStep(Step.FILESYSTEM, newProjectKey, oldProjectKey);
   }
 
@@ -249,7 +275,7 @@
       List<Change.Id> changeIds,
       Project.NameKey oldProjectKey,
       Project.NameKey newProjectKey,
-      ProgressMonitor pm)
+      Optional<ProgressMonitor> pm)
       throws OrmException {
     List<Change.Id> updatedChangeIds =
         dbHandler.rename(changeIds, oldProjectKey, newProjectKey, pm);
@@ -261,7 +287,7 @@
       List<Change.Id> updatedChangeIds,
       Project.NameKey oldProjectKey,
       Project.NameKey newProjectKey,
-      ProgressMonitor pm)
+      Optional<ProgressMonitor> pm)
       throws InterruptedException {
     indexHandler.updateIndex(updatedChangeIds, newProjectKey, pm);
     logPerformedStep(Step.INDEX, newProjectKey, oldProjectKey);
@@ -297,9 +323,9 @@
     return stepsPerformed;
   }
 
-  List<Change.Id> getChanges(ProjectResource rsrc, ProgressMonitor pm)
+  List<Change.Id> getChanges(ProjectResource rsrc, Optional<ProgressMonitor> opm)
       throws OrmException, IOException {
-    pm.beginTask("Retrieving the list of changes from DB");
+    opm.ifPresent(pm -> pm.beginTask("Retrieving the list of changes from DB"));
     Project.NameKey oldProjectKey = rsrc.getNameKey();
     return dbHandler.getChangeIds(oldProjectKey);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java
index 6d46552..4d61a49 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProject.java
@@ -57,13 +57,13 @@
       List<Id> changeIds,
       Project.NameKey oldProjectKey,
       Project.NameKey newProjectKey,
-      ProgressMonitor pm)
+      Optional<ProgressMonitor> opm)
       throws IOException, OrmException {
-    pm.beginTask("Reverting the rename procedure.");
+    opm.ifPresent(pm -> pm.beginTask("Reverting the rename procedure."));
     List<Change.Id> updatedChangeIds = Collections.emptyList();
     if (stepsPerformed.contains(Step.FILESYSTEM)) {
       try {
-        fsHandler.rename(newProjectKey, oldProjectKey, Optional.of(pm));
+        fsHandler.rename(newProjectKey, oldProjectKey, opm);
         log.debug("Reverted the git repo name to {} successfully.", oldProjectKey.get());
       } catch (IOException e) {
         log.error(
@@ -77,7 +77,7 @@
     }
     if (stepsPerformed.contains(Step.DATABASE)) {
       try {
-        updatedChangeIds = dbHandler.rename(changeIds, newProjectKey, oldProjectKey, pm);
+        updatedChangeIds = dbHandler.rename(changeIds, newProjectKey, oldProjectKey, opm);
         log.debug(
             "Reverted the changes in DB successfully from project {} to project {}.",
             newProjectKey.get(),
@@ -92,7 +92,7 @@
     }
     if (stepsPerformed.contains(Step.INDEX)) {
       try {
-        indexHandler.updateIndex(updatedChangeIds, oldProjectKey, pm);
+        indexHandler.updateIndex(updatedChangeIds, oldProjectKey, opm);
         log.debug(
             "Reverted the secondary index successfully from project {} to project {}.",
             newProjectKey.get(),
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java
index a837267..57623ba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/DatabaseRenameHandler.java
@@ -48,6 +48,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -137,9 +138,9 @@
       List<Change.Id> changes,
       Project.NameKey oldProjectKey,
       Project.NameKey newProjectKey,
-      ProgressMonitor pm)
+      Optional<ProgressMonitor> opm)
       throws OrmException, RenameRevertException {
-    pm.beginTask("Updating changes in the database");
+    opm.ifPresent(pm -> pm.beginTask("Updating changes in the database"));
     ReviewDb db = schemaFactory.open();
     return (isNoteDb())
         ? renameInNoteDb(changes, oldProjectKey, newProjectKey)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/IndexUpdateHandler.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/IndexUpdateHandler.java
index ed12dc3..37745ff 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/IndexUpdateHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/database/IndexUpdateHandler.java
@@ -28,6 +28,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -53,17 +54,17 @@
   }
 
   public void updateIndex(
-      List<Change.Id> changeIds, Project.NameKey newProjectKey, ProgressMonitor pm)
+      List<Change.Id> changeIds, Project.NameKey newProjectKey, Optional<ProgressMonitor> opm)
       throws InterruptedException {
     log.debug("Starting to index {} change(s).", changeIds.size());
     ExecutorService executor =
         Executors.newFixedThreadPool(
             config.getIndexThreads(),
             new ThreadFactoryBuilder().setNameFormat("Rename-Index-%d").build());
-    pm.beginTask("Indexing changes", changeIds.size());
+    opm.ifPresent(pm -> pm.beginTask("Indexing changes", changeIds.size()));
     List<Callable<Boolean>> callableTasks = new ArrayList<>(changeIds.size());
     for (Change.Id id : changeIds) {
-      callableTasks.add(new IndexTask(id, newProjectKey, pm));
+      callableTasks.add(new IndexTask(id, newProjectKey, opm));
     }
     List<Future<Boolean>> tasksCompleted = executor.invokeAll(callableTasks);
     executor.shutdown();
@@ -90,9 +91,10 @@
 
     private Change.Id changeId;
     private Project.NameKey newProjectKey;
-    private ProgressMonitor monitor;
+    private Optional<ProgressMonitor> monitor;
 
-    IndexTask(Change.Id changeId, Project.NameKey newProjectKey, ProgressMonitor monitor) {
+    IndexTask(
+        Change.Id changeId, Project.NameKey newProjectKey, Optional<ProgressMonitor> monitor) {
       this.changeId = changeId;
       this.newProjectKey = newProjectKey;
       this.monitor = monitor;
@@ -102,7 +104,7 @@
     public Boolean call() throws Exception {
       try (ReviewDb db = schemaFactory.open()) {
         indexer.index(db, newProjectKey, changeId);
-        monitor.update(1);
+        monitor.ifPresent(monitor -> monitor.update(1));
         return Boolean.TRUE;
       } catch (OrmException | IOException e) {
         log.error("Failed to reindex change {} from index, Cause: {}", changeId, e);
diff --git a/src/main/resources/Documentation/rest-api-rename.md b/src/main/resources/Documentation/rest-api-rename.md
new file mode 100644
index 0000000..70ecf15
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-rename.md
@@ -0,0 +1,55 @@
+@PLUGIN@ - /@PLUGIN@/ REST API
+===================================
+
+This page describes the REST endpoint that is added by the @PLUGIN@
+plugin.
+
+Please also take note of the general information on the
+[REST API](../../../Documentation/rest-api.html).
+
+This API implements a REST equivalent of the Ssh rename-project command.
+For more information, refer to:
+* [Ssh rename-project command](cmd-rename.md)
+------------------------------------------
+
+REQUEST
+-------
+```
+POST /projects/project-1/@PLUGIN@~rename HTTP/1.1
+  {
+    "name" : "project-2",
+  }
+```
+to rename project-1 to project-2.
+
+By default, if project-1 has more than 5000 changes, the rename procedure will be cancelled as it
+can take longer time and can degrade in performance in that time frame.
+
+To rename a project with more than 5000 changes, the following request is needed:
+```
+POST /projects/project-1/@PLUGIN@~rename HTTP/1.1
+  {
+    "name" : "project-2",
+    "continueWithRename" : "true"
+  }
+```
+
+RESPONSE
+--------
+If rename succeeded:
+
+```
+HTTP/1.1 200 OK
+```
+
+If rename was cancelled due to user's intent to not proceed when the number of changes exceeds the
+warning limit of 5000 changes:
+
+```
+HTTP/1.1 204 No Content
+```
+
+ACCESS
+------
+Same as ssh version of the command, caller must be a member of a group that is granted the
+'Rename Project' (provided by this plugin) or 'Administrate Server' capabilities.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
index a552a4c..a9bea02 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.UseSsh;
@@ -222,4 +223,39 @@
     verify(sshHelper, atLeastOnce())
         .executeRemoteSsh(eq(new URIish(URL)), eq(expectedCommand), eq(errStream));
   }
+
+  @Test
+  @UseLocalDisk
+  public void testRenameViaHttpSuccessful() throws Exception {
+    createChange();
+    RestResponse r = renameProjectTo(NEW_PROJECT_NAME);
+    r.assertOK();
+
+    ProjectState projectState = projectCache.get(new Project.NameKey(NEW_PROJECT_NAME));
+    assertThat(projectState).isNotNull();
+    assertThat(queryProvider.get().byProject(project)).isEmpty();
+    assertThat(queryProvider.get().byProject(new Project.NameKey(NEW_PROJECT_NAME))).isNotEmpty();
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testRenameViaHttpWithEmptyNewName() throws Exception {
+    createChange();
+    String newProjectName = "";
+    RestResponse r = renameProjectTo(newProjectName);
+    r.assertBadRequest();
+
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNull();
+  }
+
+  private RestResponse renameProjectTo(String newName) throws Exception {
+    setApiUser(user);
+    sender.clear();
+    String endPoint = "/projects/" + project.get() + "/" + PLUGIN_NAME + "~rename";
+    Input i = new Input();
+    i.name = newName;
+    i.continueWithRename = true;
+    return adminRestSession.post(endPoint, i);
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java
index a027c39..f77d80e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RevertRenameProjectTest.java
@@ -46,7 +46,7 @@
   private RevertRenameProject revertRenameProject;
   private Project.NameKey oldProjectKey;
   private Project.NameKey newProjectKey;
-  private ProgressMonitor pm;
+  private Optional<ProgressMonitor> pm;
   private ProjectResource oldRsrc;
 
   @Before
@@ -57,7 +57,7 @@
     oldProjectKey = project;
     newProjectKey = new Project.NameKey(NEW_PROJECT_NAME);
 
-    pm = Mockito.mock(ProgressMonitor.class);
+    pm = Optional.of(Mockito.mock(ProgressMonitor.class));
 
     oldRsrc = Mockito.mock(ProjectResource.class);
     when(oldRsrc.getNameKey()).thenReturn(oldProjectKey);
@@ -69,7 +69,7 @@
     Result result = createChange();
     List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
 
-    renameProject.fsRenameStep(oldProjectKey, newProjectKey, Optional.of(pm));
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
     assertRenamed(result);
 
     revertRenameProject.performRevert(
@@ -83,7 +83,7 @@
     Result result = createChange();
     List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
 
-    renameProject.fsRenameStep(oldProjectKey, newProjectKey, Optional.of(pm));
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
     renameProject.cacheRenameStep(oldProjectKey, newProjectKey);
     assertRenamed(result);
 
@@ -98,7 +98,7 @@
     Result result = createChange();
     List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
 
-    renameProject.fsRenameStep(oldProjectKey, newProjectKey, Optional.of(pm));
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
     renameProject.cacheRenameStep(oldProjectKey, newProjectKey);
     renameProject.dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
     assertRenamed(result);
@@ -114,7 +114,7 @@
     Result result = createChange();
     List<Change.Id> changeIds = renameProject.getChanges(oldRsrc, pm);
 
-    renameProject.fsRenameStep(oldProjectKey, newProjectKey, Optional.of(pm));
+    renameProject.fsRenameStep(oldProjectKey, newProjectKey, pm);
     renameProject.cacheRenameStep(oldProjectKey, newProjectKey);
     renameProject.dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
     renameProject.indexRenameStep(changeIds, oldProjectKey, newProjectKey, pm);