Move account suggestion to REST API
Accounts can now be found for internal groups and search operators
using GET /accounts/?q=... to locate candidate users that are visible
to the client.
Change-Id: Id1868ca654518a978fcb9783169f33072d3ecb15
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index f5c113f..11e4831 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -7,6 +7,44 @@
[[account-endpoints]]
== Account Endpoints
+[[suggest-account]]
+=== Suggest Account
+--
+'GET /accounts/'
+--
+
+Suggest users for a given query `q` and result limit `n`. If result
+limit is not passed, then the default 10 is used. Returns a list of
+matching link:#account-info[AccountInfo] entities.
+
+.Request
+----
+ GET /accounts/?q=John HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "john"
+ },
+ {
+ "_account_id": 1001439,
+ "name": "John Smith",
+ "email": "john.smith@example.com",
+ "username": "jsmith"
+ },
+ ]
+----
+
[[get-account]]
=== Get Account
--
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index d05dfc2..1d252fd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -25,27 +25,10 @@
@RpcImpl(version = Version.V2_0)
public interface SuggestService extends RemoteJsonService {
- void suggestAccount(String query, Boolean enabled, int limit,
- AsyncCallback<List<AccountInfo>> callback);
-
- /**
- * @see #suggestAccountGroupForProject(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
- */
- @Deprecated
- void suggestAccountGroup(String query, int limit,
- AsyncCallback<List<GroupReference>> callback);
-
void suggestAccountGroupForProject(Project.NameKey project, String query,
int limit, AsyncCallback<List<GroupReference>> callback);
/**
- * @see #suggestChangeReviewer(com.google.gerrit.reviewdb.client.Change.Id, String, int, AsyncCallback)
- */
- @Deprecated
- void suggestReviewer(Project.NameKey project, String query, int limit,
- AsyncCallback<List<ReviewerInfo>> callback);
-
- /**
* Suggests reviewers. A reviewer can be a user or a group. Inactive users,
* the system groups {@code SystemGroupBackend#ANONYMOUS_USERS} and
* {@code SystemGroupBackend#REGISTERED_USERS} and groups that have more than
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 8906da4..59d65f6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -33,6 +33,15 @@
return new RestApi("/accounts/").view("self");
}
+ public static void suggest(String query, int limit,
+ AsyncCallback<JsArray<AccountInfo>> cb) {
+ new RestApi("/accounts/")
+ .addParameter("q", query)
+ .addParameter("n", limit)
+ .background()
+ .get(cb);
+ }
+
public static void putDiffPreferences(DiffPreferences in,
AsyncCallback<DiffPreferences> cb) {
self().view("preferences.diff").put(in, cb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 0562689..cf3e940 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -24,6 +24,7 @@
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
import com.google.gerrit.client.ui.AccountLinkPanel;
+import com.google.gerrit.client.ui.AccountSuggestOracle;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Hyperlink;
@@ -80,7 +81,10 @@
private void initMemberList() {
- addMemberBox = new AddMemberBox();
+ addMemberBox = new AddMemberBox(
+ Util.C.buttonAddGroupMember(),
+ Util.C.defaultAccountName(),
+ new AccountSuggestOracle());
addMemberBox.addClickHandler(new ClickHandler() {
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index ac73dd2..78350db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -15,9 +15,11 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.ui.SuggestOracle;
import java.util.ArrayList;
@@ -26,25 +28,18 @@
/** Suggestion Oracle for Account entities. */
public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle {
@Override
- public void _onRequestSuggestions(final Request req, final Callback callback) {
- RpcStatus.hide(new Runnable() {
- @Override
- public void run() {
- SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE,
- req.getLimit(),
- new GerritCallback<List<AccountInfo>>() {
- @Override
- public void onSuccess(final List<AccountInfo> result) {
- final ArrayList<AccountSuggestion> r =
- new ArrayList<>(result.size());
- for (final AccountInfo p : result) {
- r.add(new AccountSuggestion(p));
- }
- callback.onSuggestionsReady(req, new Response(r));
- }
- });
- }
- });
+ public void _onRequestSuggestions(final Request req, final Callback cb) {
+ AccountApi.suggest(req.getQuery(), req.getLimit(),
+ new GerritCallback<JsArray<AccountInfo>>() {
+ @Override
+ public void onSuccess(JsArray<AccountInfo> in) {
+ List<AccountSuggestion> r = new ArrayList<>(in.length());
+ for (AccountInfo p : Natives.asList(in)) {
+ r.add(new AccountSuggestion(p));
+ }
+ cb.onSuggestionsReady(req, new Response(r));
+ }
+ });
}
private static class AccountSuggestion implements SuggestOracle.Suggestion {
@@ -56,12 +51,12 @@
@Override
public String getDisplayString() {
- return FormatUtil.nameEmail(FormatUtil.asInfo(info));
+ return FormatUtil.nameEmail(info);
}
@Override
public String getReplacementString() {
- return FormatUtil.nameEmail(FormatUtil.asInfo(info));
+ return FormatUtil.nameEmail(info);
}
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
index d4aaa4c..80d4a7b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
@@ -15,7 +15,6 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.admin.Util;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
@@ -38,11 +37,6 @@
private final SuggestBox nameTxt;
private boolean submitOnSelection;
- public AddMemberBox() {
- this(Util.C.buttonAddGroupMember(), Util.C.defaultAccountName(),
- new AccountSuggestOracle());
- }
-
public AddMemberBox(final String buttonLabel, final String hint,
final SuggestOracle suggestOracle) {
addPanel = new FlowPanel();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index ff64c3c..7de6f4e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -30,7 +30,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountVisibility;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers;
@@ -63,7 +62,6 @@
private final AccountCache accountCache;
private final GroupMembers.Factory groupMembersFactory;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
- private final AccountControl.Factory accountControlFactory;
private final ChangeControl.Factory changeControlFactory;
private final ProjectControl.Factory projectControlFactory;
private final Config cfg;
@@ -76,7 +74,6 @@
final GroupMembers.Factory groupMembersFactory,
final Provider<CurrentUser> currentUser,
final IdentifiedUser.GenericFactory identifiedUserFactory,
- final AccountControl.Factory accountControlFactory,
final ChangeControl.Factory changeControlFactory,
final ProjectControl.Factory projectControlFactory,
@GerritServerConfig final Config cfg, final GroupBackend groupBackend) {
@@ -85,7 +82,6 @@
this.accountCache = accountCache;
this.groupMembersFactory = groupMembersFactory;
this.identifiedUserFactory = identifiedUserFactory;
- this.accountControlFactory = accountControlFactory;
this.changeControlFactory = changeControlFactory;
this.projectControlFactory = projectControlFactory;
this.cfg = cfg;
@@ -110,22 +106,6 @@
boolean isVisible(Account account) throws OrmException;
}
- @Override
- public void suggestAccount(final String query, final Boolean active,
- final int limit, final AsyncCallback<List<AccountInfo>> callback) {
- run(callback, new Action<List<AccountInfo>>() {
- @Override
- public List<AccountInfo> run(final ReviewDb db) throws OrmException {
- return suggestAccount(db, query, active, limit, new VisibilityControl() {
- @Override
- public boolean isVisible(Account account) throws OrmException {
- return accountControlFactory.get().canSee(account);
- }
- });
- }
- });
- }
-
private List<AccountInfo> suggestAccount(final ReviewDb db,
final String query, final Boolean active, final int limit,
VisibilityControl visibilityControl)
@@ -178,12 +158,6 @@
}
@Override
- public void suggestAccountGroup(final String query, final int limit,
- final AsyncCallback<List<GroupReference>> callback) {
- suggestAccountGroupForProject(null, query, limit, callback);
- }
-
- @Override
public void suggestAccountGroupForProject(final Project.NameKey project,
final String query, final int limit,
final AsyncCallback<List<GroupReference>> callback) {
@@ -211,13 +185,6 @@
}
@Override
- public void suggestReviewer(Project.NameKey project, String query, int limit,
- AsyncCallback<List<ReviewerInfo>> callback) {
- // The RPC is deprecated, but return an empty list for RPC API compatibility.
- callback.onSuccess(Collections.<ReviewerInfo>emptyList());
- }
-
- @Override
public void suggestChangeReviewer(final Change.Id change,
final String query, final int limit,
final AsyncCallback<List<ReviewerInfo>> callback) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 5ef745b..efe7322 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -40,6 +40,7 @@
private final AccountResolver resolver;
private final AccountControl.Factory accountControlFactory;
private final IdentifiedUser.GenericFactory userFactory;
+ private final Provider<SuggestAccounts> list;
private final DynamicMap<RestView<AccountResource>> views;
private final CreateAccount.Factory createAccountFactory;
@@ -48,12 +49,14 @@
AccountResolver resolver,
AccountControl.Factory accountControlFactory,
IdentifiedUser.GenericFactory userFactory,
+ Provider<SuggestAccounts> list,
DynamicMap<RestView<AccountResource>> views,
CreateAccount.Factory createAccountFactory) {
this.self = self;
this.resolver = resolver;
this.accountControlFactory = accountControlFactory;
this.userFactory = userFactory;
+ this.list = list;
this.views = views;
this.createAccountFactory = createAccountFactory;
}
@@ -128,7 +131,7 @@
@Override
public RestView<TopLevelResource> list() throws ResourceNotFoundException {
- throw new ResourceNotFoundException();
+ return list.get();
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
new file mode 100644
index 0000000..07936d9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
@@ -0,0 +1,156 @@
+// 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.server.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+class SuggestAccounts implements RestReadView<TopLevelResource> {
+ private static final int MAX_RESULTS = 100;
+ private static final String MAX_SUFFIX = "\u9fa5";
+
+ private final AccountControl accountControl;
+ private final AccountLoader accountLoader;
+ private final ReviewDb db;
+ private final boolean suggest;
+ private final int suggestFrom;
+
+ private int limit = 10;
+
+ @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
+ void setLimit(int n) {
+ if (n < 0) {
+ limit = 10;
+ } else if (n == 0) {
+ limit = MAX_RESULTS;
+ } else {
+ limit = Math.min(n, MAX_RESULTS);
+ }
+ }
+
+ @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
+ private String query;
+
+ @Inject
+ SuggestAccounts(AccountControl.Factory accountControlFactory,
+ AccountLoader.Factory accountLoaderFactory,
+ ReviewDb db,
+ @GerritServerConfig Config cfg) {
+ accountControl = accountControlFactory.get();
+ accountLoader = accountLoaderFactory.create(true);
+ this.db = db;
+ this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
+
+ if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
+ suggest = false;
+ } else {
+ boolean suggest;
+ try {
+ AccountVisibility av =
+ cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
+ suggest = (av != AccountVisibility.NONE);
+ } catch (IllegalArgumentException err) {
+ suggest = cfg.getBoolean("suggest", null, "accounts", true);
+ }
+ this.suggest = suggest;
+ }
+ }
+
+ @Override
+ public List<AccountInfo> apply(TopLevelResource rsrc)
+ throws OrmException, BadRequestException {
+ if (Strings.isNullOrEmpty(query)) {
+ throw new BadRequestException("missing query field");
+ }
+
+ if (!suggest || query.length() < suggestFrom) {
+ return Collections.emptyList();
+ }
+
+ String a = query;
+ String b = a + MAX_SUFFIX;
+
+ Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
+ Map<Account.Id, String> queryEmail = new HashMap<>();
+
+ for (Account p : db.accounts().suggestByFullName(a, b, limit)) {
+ addSuggestion(matches, p.getId());
+ }
+ if (matches.size() < limit) {
+ for (Account p : db.accounts()
+ .suggestByPreferredEmail(a, b, limit - matches.size())) {
+ addSuggestion(matches, p.getId());
+ }
+ }
+ if (matches.size() < limit) {
+ for (AccountExternalId e : db.accountExternalIds()
+ .suggestByEmailAddress(a, b, limit - matches.size())) {
+ if (addSuggestion(matches, e.getAccountId())) {
+ queryEmail.put(e.getAccountId(), e.getEmailAddress());
+ }
+ }
+ }
+
+ accountLoader.fill();
+ for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
+ AccountInfo info = matches.get(p.getKey());
+ if (info != null) {
+ info.email = p.getValue();
+ }
+ }
+
+ List<AccountInfo> m = new ArrayList<>(matches.values());
+ Collections.sort(m, new Comparator<AccountInfo>() {
+ @Override
+ public int compare(AccountInfo a, AccountInfo b) {
+ return ComparisonChain.start()
+ .compare(a.name, b.name, Ordering.natural().nullsLast())
+ .compare(a.email, b.email, Ordering.natural().nullsLast())
+ .result();
+ }
+ });
+ return m;
+ }
+
+ private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) {
+ if (!map.containsKey(id) && accountControl.canSee(id)) {
+ map.put(id, accountLoader.get(id));
+ return true;
+ }
+ return false;
+ }
+}