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:
Edwin Kempin
2016-06-30 11:33:47 +02:00
parent 4c8a1340a1
commit c236140227
6 changed files with 188 additions and 41 deletions

View File

@@ -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

View 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
---------

View File

@@ -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()

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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);
}