Changes API: Add methods to suggest reviewers

Change-Id: I8bdc52eeaa8336558fff2e3a0682f861bc9008b8
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 15ada4a..ec1e299 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -26,10 +26,10 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.CreateGroupArgs;
 import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.server.change.SuggestReviewers.SuggestedReviewerInfo;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gson.reflect.TypeToken;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 78c95b9..bcf78b3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -16,10 +16,12 @@
 
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Set;
 
 public interface ChangeApi {
@@ -79,6 +81,9 @@
   void addReviewer(AddReviewerInput in) throws RestApiException;
   void addReviewer(String in) throws RestApiException;
 
+  SuggestedReviewersRequest suggestReviewers() throws RestApiException;
+  SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException;
+
   ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
 
   /** {@code get} with {@link ListChangesOption} set to all except CHECK. */
@@ -101,6 +106,31 @@
   ChangeInfo check() throws RestApiException;
   ChangeInfo check(FixInput fix) throws RestApiException;
 
+  public abstract class SuggestedReviewersRequest {
+    private String query;
+    private int limit;
+
+    public abstract List<SuggestedReviewerInfo> get() throws RestApiException;
+
+    public SuggestedReviewersRequest withQuery(String query) {
+      this.query = query;
+      return this;
+    }
+
+    public SuggestedReviewersRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public String getQuery() {
+      return query;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+  }
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -177,6 +207,16 @@
     }
 
     @Override
+    public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupBaseInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupBaseInfo.java
new file mode 100644
index 0000000..288adb6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupBaseInfo.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2014 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.google.gerrit.extensions.common;
+
+public class GroupBaseInfo {
+  public String id;
+  public String name;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
new file mode 100644
index 0000000..d371f35
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2014 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.google.gerrit.extensions.common;
+
+public class SuggestedReviewerInfo {
+  public AccountInfo account;
+  public GroupBaseInfo group;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 9dcf97b..e2d4a92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.Abandon;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
 import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.change.SuggestReviewers;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,6 +49,7 @@
 
 import java.io.IOException;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Set;
 
 class ChangeApiImpl extends ChangeApi.NotImplemented implements ChangeApi {
@@ -57,6 +60,7 @@
   private final Changes changeApi;
   private final Revisions revisions;
   private final RevisionApiImpl.Factory revisionApi;
+  private final Provider<SuggestReviewers> suggestReviewers;
   private final ChangeResource change;
   private final Abandon abandon;
   private final Revert revert;
@@ -73,6 +77,7 @@
   ChangeApiImpl(Changes changeApi,
       Revisions revisions,
       RevisionApiImpl.Factory revisionApi,
+      Provider<SuggestReviewers> suggestReviewers,
       Abandon abandon,
       Revert revert,
       Restore restore,
@@ -88,6 +93,7 @@
     this.revert = revert;
     this.revisions = revisions;
     this.revisionApi = revisionApi;
+    this.suggestReviewers = suggestReviewers;
     this.abandon = abandon;
     this.restore = restore;
     this.getTopic = getTopic;
@@ -200,6 +206,34 @@
   }
 
   @Override
+  public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
+    return new SuggestedReviewersRequest() {
+      @Override
+      public List<SuggestedReviewerInfo> get() throws RestApiException {
+        return ChangeApiImpl.this.suggestReviewers(this);
+      }
+    };
+  }
+
+  @Override
+  public SuggestedReviewersRequest suggestReviewers(String query)
+      throws RestApiException {
+    return suggestReviewers().withQuery(query);
+  }
+
+  private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
+      throws RestApiException {
+    try {
+      SuggestReviewers mySuggestReviewers = suggestReviewers.get();
+      mySuggestReviewers.setQuery(r.getQuery());
+      mySuggestReviewers.setLimit(r.getLimit());
+      return mySuggestReviewers.apply(change);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve suggested reviewers", e);
+    }
+  }
+
+  @Override
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index ce22034..5998eb1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -14,12 +14,17 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.GroupBaseInfo;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -37,7 +42,6 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.group.GroupJson.GroupBaseInfo;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
@@ -60,6 +64,20 @@
   private static final String MAX_SUFFIX = "\u9fa5";
   private static final int DEFAULT_MAX_SUGGESTED = 10;
   private static final int DEFAULT_MAX_MATCHES = 100;
+  private static final Ordering<SuggestedReviewerInfo> ORDERING =
+      Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
+        @Nullable
+        @Override
+        public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
+          if (suggestedReviewerInfo == null) {
+            return null;
+          }
+          return suggestedReviewerInfo.account != null
+              ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
+              Strings.nullToEmpty(suggestedReviewerInfo.account.name))
+              : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
+        }
+      });
 
   private final AccountLoader accountLoader;
   private final AccountControl accountControl;
@@ -159,7 +177,9 @@
 
     List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
     for (AccountInfo a : suggestedAccounts) {
-      reviewer.add(new SuggestedReviewerInfo(a));
+      SuggestedReviewerInfo info = new SuggestedReviewerInfo();
+      info.account = a;
+      reviewer.add(info);
     }
 
     Project p = rsrc.getControl().getProject();
@@ -169,11 +189,13 @@
         GroupBaseInfo info = new GroupBaseInfo();
         info.id = Url.encode(g.getUUID().get());
         info.name = g.getName();
-        reviewer.add(new SuggestedReviewerInfo(info));
+        SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
+        suggestedReviewerInfo.group = info;
+        reviewer.add(suggestedReviewerInfo);
       }
     }
 
-    Collections.sort(reviewer);
+    reviewer = ORDERING.immutableSortedCopy(reviewer);
     if (reviewer.size() <= limit) {
       return reviewer;
     } else {
@@ -342,29 +364,4 @@
 
     return false;
   }
-
-  public static class SuggestedReviewerInfo implements Comparable<SuggestedReviewerInfo> {
-    public AccountInfo account;
-    public GroupBaseInfo group;
-
-    SuggestedReviewerInfo(AccountInfo a) {
-      this.account = a;
-    }
-
-    SuggestedReviewerInfo(GroupBaseInfo g) {
-      this.group = g;
-    }
-
-    @Override
-    public int compareTo(SuggestedReviewerInfo o) {
-      return getSortValue().compareTo(o.getSortValue());
-    }
-
-    private String getSortValue() {
-      return account != null
-          ? MoreObjects.firstNonNull(account.email,
-              Strings.nullToEmpty(account.name))
-          : Strings.nullToEmpty(group.name);
-    }
-  }
 }