Support arbitrary account queries via REST
GET /accounts/?q=<query> supports arbitrary account queries now. So far this REST endpoint was used for account suggestions, suggesting accounts for a substring that the user has typed. This will continue to work since this is handled by the default field for account queries. However if the provided query contains query operators a full-fledged account query is executed. This is undesired for account suggestions. This is why the old behaviour for account suggestions is preserved when the parameter 'suggest' is provided. The web UI and the java extension API are adapted to set this new parameter for account suggestions. For old clients the new REST endpoint provides enough backward compatibility so that they are not breaking as it is rather unlikely that a substring that was typed by a user contains a query operator. Using /accounts/?q=<query> for arbitrary account queries has the advantage that this is consistent with the REST endpoint for querying changes which is /changes/?q=<query>. Rename SuggestAccounts to QueryAccounts to reflect the new functionality and to have a consistent name with QueryChanges. Since the account query predicates are now exposed to external users this change also adds documentation for them. Change-Id: Iabe3e52893a17a21d4bfdec6dab737e0cebaea23 Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
@@ -7,19 +7,56 @@ link:rest-api.html[REST API].
|
||||
[[account-endpoints]]
|
||||
== Account Endpoints
|
||||
|
||||
[[suggest-account]]
|
||||
=== Suggest Account
|
||||
[[query-account]]
|
||||
=== Query 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.
|
||||
Queries accounts visible to the caller. The
|
||||
link:user-search-accounts.html#_search_operators[query string] must be
|
||||
provided by the `q` parameter. The `n` parameter can be used to limit
|
||||
the returned results.
|
||||
|
||||
As result a list of link:#account-info[AccountInfo] entities is
|
||||
returned.
|
||||
|
||||
.Request
|
||||
----
|
||||
GET /accounts/?q=John HTTP/1.0
|
||||
GET /accounts/?q=name:John+email:example.com 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"
|
||||
}
|
||||
]
|
||||
----
|
||||
|
||||
[[suggest-account]]
|
||||
To get account suggestions set the parameter `suggest` and provide the
|
||||
typed substring as query `q`. If a result limit `n` is not specified,
|
||||
then the default 10 is used.
|
||||
|
||||
.Request
|
||||
----
|
||||
GET /accounts/?suggest&q=John HTTP/1.0
|
||||
----
|
||||
|
||||
.Response
|
||||
|
83
Documentation/user-search-accounts.txt
Normal file
83
Documentation/user-search-accounts.txt
Normal file
@@ -0,0 +1,83 @@
|
||||
= Gerrit Code Review - Searching Accounts
|
||||
|
||||
== Basic Change Search
|
||||
|
||||
Similar to many popular search engines on the web, just enter some
|
||||
text and let Gerrit figure out the meaning:
|
||||
|
||||
[options="header"]
|
||||
|=============================================================
|
||||
|Description | Examples
|
||||
|Name | John
|
||||
|Email address | jdoe@example.com
|
||||
|Username | jdoe
|
||||
|Account-Id | 1000096
|
||||
|Own account | self
|
||||
|=============================================================
|
||||
|
||||
[[search-operators]]
|
||||
== Search Operators
|
||||
|
||||
Operators act as restrictions on the search. As more operators
|
||||
are added to the same query string, they further restrict the
|
||||
returned results. Search can also be performed by typing only a
|
||||
text with no operator, which will match against a variety of fields.
|
||||
|
||||
[[email]]
|
||||
email:'EMAIL'::
|
||||
+
|
||||
Matches accounts that have the email address 'EMAIL' or an email
|
||||
address that starts with 'EMAIL'.
|
||||
|
||||
[[is]]
|
||||
[[is-active]]
|
||||
is:active::
|
||||
+
|
||||
Matches accounts that are active.
|
||||
|
||||
[[is-inactive]]
|
||||
is:inactive::
|
||||
+
|
||||
Matches accounts that are inactive.
|
||||
|
||||
[[name]]
|
||||
name:'NAME'::
|
||||
+
|
||||
Matches accounts that have any name part 'NAME'. The name parts consist
|
||||
of any part of the full name and the email addresses.
|
||||
|
||||
[[username]]
|
||||
username:'USERNAME'::
|
||||
+
|
||||
Matches accounts that have the username 'USERNAME'.
|
||||
|
||||
== Magical Operators
|
||||
|
||||
[[is-visible]]
|
||||
is:visible::
|
||||
+
|
||||
Magical internal flag to prove the current user has access to read
|
||||
the change. This flag is always added to any query.
|
||||
|
||||
[[is-active-magic]]
|
||||
is:active::
|
||||
+
|
||||
Matches accounts that are active. If neither link:#is-active[is:active]
|
||||
nor link:#is-inactive[is:inactive] is contained in a query, `is:active`
|
||||
is automatically added so that by default only active accounts are
|
||||
matched.
|
||||
|
||||
[[limit]]
|
||||
limit:'CNT'::
|
||||
+
|
||||
Limit the returned results to no more than 'CNT' records. This is
|
||||
automatically set to the page size configured in the current user's
|
||||
preferences. Including it in a web query may lead to unpredictable
|
||||
results with regards to pagination.
|
||||
|
||||
GERRIT
|
||||
------
|
||||
Part of link:index.html[Gerrit Code Review]
|
||||
|
||||
SEARCHBOX
|
||||
---------
|
@@ -53,6 +53,7 @@ public class AccountApi {
|
||||
public static void suggest(String query, int limit,
|
||||
AsyncCallback<JsArray<AccountInfo>> cb) {
|
||||
new RestApi("/accounts/")
|
||||
.addParameterTrue("suggest")
|
||||
.addParameter("q", query)
|
||||
.addParameter("n", limit)
|
||||
.background()
|
||||
|
@@ -40,7 +40,7 @@ public class AccountsCollection implements
|
||||
private final AccountResolver resolver;
|
||||
private final AccountControl.Factory accountControlFactory;
|
||||
private final IdentifiedUser.GenericFactory userFactory;
|
||||
private final Provider<SuggestAccounts> list;
|
||||
private final Provider<QueryAccounts> list;
|
||||
private final DynamicMap<RestView<AccountResource>> views;
|
||||
private final CreateAccount.Factory createAccountFactory;
|
||||
|
||||
@@ -49,7 +49,7 @@ public class AccountsCollection implements
|
||||
AccountResolver resolver,
|
||||
AccountControl.Factory accountControlFactory,
|
||||
IdentifiedUser.GenericFactory userFactory,
|
||||
Provider<SuggestAccounts> list,
|
||||
Provider<QueryAccounts> list,
|
||||
DynamicMap<RestView<AccountResource>> views,
|
||||
CreateAccount.Factory createAccountFactory) {
|
||||
this.self = self;
|
||||
|
@@ -26,7 +26,9 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.api.accounts.AccountInfoComparator;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.index.account.AccountIndex;
|
||||
import com.google.gerrit.server.index.account.AccountIndexCollection;
|
||||
import com.google.gerrit.server.query.Predicate;
|
||||
import com.google.gerrit.server.query.QueryParseException;
|
||||
import com.google.gerrit.server.query.QueryResult;
|
||||
import com.google.gerrit.server.query.account.AccountQueryBuilder;
|
||||
@@ -44,8 +46,8 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
private static final int MAX_RESULTS = 100;
|
||||
public class QueryAccounts implements RestReadView<TopLevelResource> {
|
||||
private static final int MAX_SUGGEST_RESULTS = 100;
|
||||
private static final String MAX_SUFFIX = "\u9fa5";
|
||||
|
||||
private final AccountControl accountControl;
|
||||
@@ -55,22 +57,29 @@ public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
private final AccountQueryBuilder queryBuilder;
|
||||
private final AccountQueryProcessor queryProcessor;
|
||||
private final ReviewDb db;
|
||||
private final boolean suggest;
|
||||
private final boolean suggestConfig;
|
||||
private final int suggestFrom;
|
||||
|
||||
private int limit = 10;
|
||||
private boolean suggest;
|
||||
private int suggestLimit = 10;
|
||||
private String query;
|
||||
|
||||
@Option(name = "--suggest", metaVar = "SUGGEST", usage = "suggest users")
|
||||
public void setSuggest(boolean suggest) {
|
||||
this.suggest = suggest;
|
||||
}
|
||||
|
||||
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
|
||||
public void setLimit(int n) {
|
||||
queryProcessor.setLimit(n);
|
||||
|
||||
if (n < 0) {
|
||||
limit = 10;
|
||||
suggestLimit = 10;
|
||||
} else if (n == 0) {
|
||||
limit = MAX_RESULTS;
|
||||
suggestLimit = MAX_SUGGEST_RESULTS;
|
||||
} else {
|
||||
limit = Math.min(n, MAX_RESULTS);
|
||||
suggestLimit = Math.min(n, MAX_SUGGEST_RESULTS);
|
||||
}
|
||||
queryProcessor.setLimit(limit);
|
||||
}
|
||||
|
||||
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
|
||||
@@ -79,7 +88,7 @@ public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
}
|
||||
|
||||
@Inject
|
||||
SuggestAccounts(AccountControl.Factory accountControlFactory,
|
||||
QueryAccounts(AccountControl.Factory accountControlFactory,
|
||||
AccountLoader.Factory accountLoaderFactory,
|
||||
AccountCache accountCache,
|
||||
AccountIndexCollection indexes,
|
||||
@@ -97,7 +106,7 @@ public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
|
||||
|
||||
if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
|
||||
suggest = false;
|
||||
suggestConfig = false;
|
||||
} else {
|
||||
boolean suggest;
|
||||
try {
|
||||
@@ -107,7 +116,7 @@ public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
} catch (IllegalArgumentException err) {
|
||||
suggest = cfg.getBoolean("suggest", null, "accounts", true);
|
||||
}
|
||||
this.suggest = suggest;
|
||||
this.suggestConfig = suggest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,33 +127,49 @@ public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
throw new BadRequestException("missing query field");
|
||||
}
|
||||
|
||||
if (!suggest || query.length() < suggestFrom) {
|
||||
if (suggest && (!suggestConfig || query.length() < suggestFrom)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Collection<AccountInfo> matches =
|
||||
indexes.getSearchIndex() != null
|
||||
? queryFromIndex()
|
||||
: queryFromDb();
|
||||
AccountIndex searchIndex = indexes.getSearchIndex();
|
||||
Collection<AccountInfo> matches;
|
||||
if (searchIndex != null) {
|
||||
matches = queryFromIndex();
|
||||
} else {
|
||||
if (!suggest) {
|
||||
throw new MethodNotAllowedException();
|
||||
}
|
||||
matches = queryFromDb();
|
||||
}
|
||||
|
||||
return AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches);
|
||||
}
|
||||
|
||||
public Collection<AccountInfo> queryFromIndex()
|
||||
throws MethodNotAllowedException, OrmException {
|
||||
throws BadRequestException, MethodNotAllowedException, OrmException {
|
||||
if (queryProcessor.isDisabled()) {
|
||||
throw new MethodNotAllowedException("query disabled");
|
||||
}
|
||||
|
||||
Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
|
||||
try {
|
||||
QueryResult<AccountState> result =
|
||||
queryProcessor.query(queryBuilder.defaultField(query));
|
||||
Predicate<AccountState> queryPred;
|
||||
if (suggest) {
|
||||
queryPred = queryBuilder.defaultField(query);
|
||||
queryProcessor.setLimit(suggestLimit);
|
||||
} else {
|
||||
queryPred = queryBuilder.parse(query);
|
||||
}
|
||||
QueryResult<AccountState> result = queryProcessor.query(queryPred);
|
||||
for (AccountState accountState : result.entities()) {
|
||||
Account.Id id = accountState.getAccount().getId();
|
||||
matches.put(id, accountLoader.get(id));
|
||||
}
|
||||
} catch (QueryParseException e) {
|
||||
return ImmutableSet.of();
|
||||
if (suggest) {
|
||||
return ImmutableSet.of();
|
||||
}
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
|
||||
accountLoader.fill();
|
||||
@@ -158,18 +183,18 @@ public class SuggestAccounts implements RestReadView<TopLevelResource> {
|
||||
Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
|
||||
Map<Account.Id, String> queryEmail = new HashMap<>();
|
||||
|
||||
for (Account p : db.accounts().suggestByFullName(a, b, limit)) {
|
||||
for (Account p : db.accounts().suggestByFullName(a, b, suggestLimit)) {
|
||||
addSuggestion(matches, p);
|
||||
}
|
||||
if (matches.size() < limit) {
|
||||
if (matches.size() < suggestLimit) {
|
||||
for (Account p : db.accounts()
|
||||
.suggestByPreferredEmail(a, b, limit - matches.size())) {
|
||||
.suggestByPreferredEmail(a, b, suggestLimit - matches.size())) {
|
||||
addSuggestion(matches, p);
|
||||
}
|
||||
}
|
||||
if (matches.size() < limit) {
|
||||
if (matches.size() < suggestLimit) {
|
||||
for (AccountExternalId e : db.accountExternalIds()
|
||||
.suggestByEmailAddress(a, b, limit - matches.size())) {
|
||||
.suggestByEmailAddress(a, b, suggestLimit - matches.size())) {
|
||||
if (addSuggestion(matches, e.getAccountId())) {
|
||||
queryEmail.put(e.getAccountId(), e.getEmailAddress());
|
||||
}
|
@@ -24,7 +24,7 @@ import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.account.AccountResource;
|
||||
import com.google.gerrit.server.account.AccountsCollection;
|
||||
import com.google.gerrit.server.account.SuggestAccounts;
|
||||
import com.google.gerrit.server.account.QueryAccounts;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
@@ -37,17 +37,17 @@ public class AccountsImpl implements Accounts {
|
||||
private final AccountsCollection accounts;
|
||||
private final AccountApiImpl.Factory api;
|
||||
private final Provider<CurrentUser> self;
|
||||
private final Provider<SuggestAccounts> suggestAccountsProvider;
|
||||
private final Provider<QueryAccounts> queryAccountsProvider;
|
||||
|
||||
@Inject
|
||||
AccountsImpl(AccountsCollection accounts,
|
||||
AccountApiImpl.Factory api,
|
||||
Provider<CurrentUser> self,
|
||||
Provider<SuggestAccounts> suggestAccountsProvider) {
|
||||
Provider<QueryAccounts> queryAccountsProvider) {
|
||||
this.accounts = accounts;
|
||||
this.api = api;
|
||||
this.self = self;
|
||||
this.suggestAccountsProvider = suggestAccountsProvider;
|
||||
this.queryAccountsProvider = queryAccountsProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,10 +92,11 @@ public class AccountsImpl implements Accounts {
|
||||
private List<AccountInfo> suggestAccounts(SuggestAccountsRequest r)
|
||||
throws RestApiException {
|
||||
try {
|
||||
SuggestAccounts mySuggestAccounts = suggestAccountsProvider.get();
|
||||
mySuggestAccounts.setQuery(r.getQuery());
|
||||
mySuggestAccounts.setLimit(r.getLimit());
|
||||
return mySuggestAccounts.apply(TopLevelResource.INSTANCE);
|
||||
QueryAccounts myQueryAccounts = queryAccountsProvider.get();
|
||||
myQueryAccounts.setSuggest(true);
|
||||
myQueryAccounts.setQuery(r.getQuery());
|
||||
myQueryAccounts.setLimit(r.getLimit());
|
||||
return myQueryAccounts.apply(TopLevelResource.INSTANCE);
|
||||
} catch (OrmException e) {
|
||||
throw new RestApiException("Cannot retrieve suggested accounts", e);
|
||||
}
|
||||
|
Reference in New Issue
Block a user