Merge changes I49a9aa11,Icc8c34c3,I31497d0d,I193a77d1,I5b18c6ea, ...

* changes:
  Account query: Add option to control if details should be returned
  Set '_more_accounts' on last account of query result
  QueryAccounts: Support parameter 'start'
  Test account query with limit
  Account index: Make query matches case insensitive
  Add basic account query tests
  Accounts API: Add query methods
  Support arbitrary account queries via REST
  Use account index for suggesting accounts if available
This commit is contained in:
Edwin Kempin
2016-07-01 12:45:46 +00:00
committed by Gerrit Code Review
16 changed files with 1106 additions and 193 deletions

View File

@@ -7,19 +7,71 @@ 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&n=2 HTTP/1.0
----
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
[
{
"_account_id": 1000096,
},
{
"_account_id": 1001439,
"_more_accounts": true
}
]
----
If the number of accounts matching the query exceeds either the
internal limit or a supplied `n` query parameter, the last account
object has a `_more_accounts: true` JSON field set.
The `S` or `start` query parameter can be supplied to skip a number
of accounts from the list.
Additional fields can be obtained by adding `o` parameters, each
option slows down the query response time to the client so they are
generally disabled by default. Optional fields are:
[[details]]
--
* `DETAILS`: Includes full name, preferred email, username and avatars
for each account.
--
[[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.
For account suggestions link:#details[account details] are always
returned.
.Request
----
GET /accounts/?suggest&q=John HTTP/1.0
----
.Response
@@ -1898,20 +1950,29 @@ registered.
The `AccountInfo` entity contains information about an account.
[options="header",cols="1,^1,5"]
|===========================
|Field Name ||Description
|`_account_id` ||The numeric ID of the account.
|`name` |optional|The full name of the user. +
Only set if link:rest-api-changes.html#detailed-accounts[detailed
account information] is requested.
|`email` |optional|
|=============================
|Field Name ||Description
|`_account_id` ||The numeric ID of the account.
|`name` |optional|The full name of the user. +
Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries +
and option link:#detaileds[DETAILS] for account queries.
|`email` |optional|
The email address the user prefers to be contacted through. +
Only set if link:rest-api-changes.html#detailed-accounts[detailed
account information] is requested.
|`username` |optional|The username of the user. +
Only set if link:rest-api-changes.html#detailed-accounts[detailed
account information] is requested.
|===========================
Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries +
and option link:#detaileds[DETAILS] for account queries.
|`username` |optional|The username of the user. +
Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries +
and option link:#detaileds[DETAILS] for account queries.
|`_more_accounts`|optional, not set if `false`|
Whether the query would deliver more results if not limited. +
Only set on the last account that is returned.
|=============================
[[account-input]]
=== AccountInput

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

@@ -14,10 +14,13 @@
package com.google.gerrit.extensions.api.accounts;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
public interface Accounts {
@@ -69,6 +72,25 @@ public interface Accounts {
SuggestAccountsRequest suggestAccounts(String query)
throws RestApiException;
/**
* Queries users.
* <p>
* Example code:
* {@code query().withQuery("name:John email:example.com").withLimit(5).get()}
*
* @return API for setting parameters and getting result.
*/
QueryRequest query() throws RestApiException;
/**
* Queries users.
* <p>
* Shortcut API for {@code query().withQuery(String)}.
*
* @see #query()
*/
QueryRequest query(String query) throws RestApiException;
/**
* API for setting parameters and getting result.
* Used for {@code suggestAccounts()}.
@@ -112,6 +134,84 @@ public interface Accounts {
}
}
/**
* API for setting parameters and getting result.
* Used for {@code query()}.
*
* @see #query()
*/
abstract class QueryRequest {
private String query;
private int limit;
private int start;
private EnumSet<ListAccountsOption> options =
EnumSet.noneOf(ListAccountsOption.class);
/**
* Executes query and returns a list of accounts.
*/
public abstract List<AccountInfo> get() throws RestApiException;
/**
* Set query.
*
* @param query needs to be in human-readable form.
*/
public QueryRequest withQuery(String query) {
this.query = query;
return this;
}
/**
* Set limit for returned list of accounts.
* Optional; server-default is used when not provided.
*/
public QueryRequest withLimit(int limit) {
this.limit = limit;
return this;
}
/**
* Set number of accounts to skip.
* Optional; no accounts are skipped when not provided.
*/
public QueryRequest withStart(int start) {
this.start = start;
return this;
}
public QueryRequest withOption(ListAccountsOption options) {
this.options.add(options);
return this;
}
public QueryRequest withOptions(ListAccountsOption... options) {
this.options.addAll(Arrays.asList(options));
return this;
}
public QueryRequest withOptions(EnumSet<ListAccountsOption> options) {
this.options = options;
return this;
}
public String getQuery() {
return query;
}
public int getLimit() {
return limit;
}
public int getStart() {
return start;
}
public EnumSet<ListAccountsOption> getOptions() {
return options;
}
}
/**
* A default implementation which allows source compatibility
* when adding new methods to the interface.
@@ -142,5 +242,15 @@ public interface Accounts {
throws RestApiException {
throw new NotImplementedException();
}
@Override
public QueryRequest query() throws RestApiException {
throw new NotImplementedException();
}
@Override
public QueryRequest query(String query) throws RestApiException {
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (C) 2016 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.client;
import java.util.EnumSet;
import java.util.Set;
/** Output options available for retrieval of account details. */
public enum ListAccountsOption {
/** Return detailed account properties. */
DETAILS(0);
private final int value;
ListAccountsOption(int v) {
this.value = v;
}
public int getValue() {
return value;
}
public static EnumSet<ListAccountsOption> fromBits(int v) {
EnumSet<ListAccountsOption> r = EnumSet.noneOf(ListAccountsOption.class);
for (ListAccountsOption o : ListAccountsOption.values()) {
if ((v & (1 << o.value)) != 0) {
r.add(o);
v &= ~(1 << o.value);
}
if (v == 0) {
return r;
}
}
if (v != 0) {
throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
}
return r;
}
public static int toBits(Set<ListAccountsOption> set) {
int r = 0;
for (ListAccountsOption o : set) {
r |= 1 << o.value;
}
return r;
}
}

View File

@@ -22,6 +22,7 @@ public class AccountInfo {
public String email;
public String username;
public List<AvatarInfo> avatars;
public Boolean _moreAccounts;
public AccountInfo(Integer id) {
this._accountId = id;

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

@@ -0,0 +1,265 @@
// 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.ImmutableList;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
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.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;
import com.google.gerrit.server.query.account.AccountQueryProcessor;
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.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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;
private final AccountLoader.Factory accountLoaderFactory;
private final AccountCache accountCache;
private final AccountIndexCollection indexes;
private final AccountQueryBuilder queryBuilder;
private final AccountQueryProcessor queryProcessor;
private final ReviewDb db;
private final boolean suggestConfig;
private final int suggestFrom;
private AccountLoader accountLoader;
private boolean suggest;
private int suggestLimit = 10;
private String query;
private Integer start;
private EnumSet<ListAccountsOption> options;
@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) {
suggestLimit = 10;
} else if (n == 0) {
suggestLimit = MAX_SUGGEST_RESULTS;
} else {
suggestLimit = Math.min(n, MAX_SUGGEST_RESULTS);
}
}
@Option(name = "-o", usage = "Output options per account")
public void addOption(ListAccountsOption o) {
options.add(o);
}
@Option(name = "-O", usage = "Output option flags, in hex")
void setOptionFlagsHex(String hex) {
options.addAll(ListAccountsOption.fromBits(Integer.parseInt(hex, 16)));
}
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
public void setQuery(String query) {
this.query = query;
}
@Option(name = "--start", aliases = {"-S"}, metaVar = "CNT",
usage = "Number of accounts to skip")
public void setStart(int start) {
this.start = start;
}
@Inject
QueryAccounts(AccountControl.Factory accountControlFactory,
AccountLoader.Factory accountLoaderFactory,
AccountCache accountCache,
AccountIndexCollection indexes,
AccountQueryBuilder queryBuilder,
AccountQueryProcessor queryProcessor,
ReviewDb db,
@GerritServerConfig Config cfg) {
this.accountControl = accountControlFactory.get();
this.accountLoaderFactory = accountLoaderFactory;
this.accountCache = accountCache;
this.indexes = indexes;
this.queryBuilder = queryBuilder;
this.queryProcessor = queryProcessor;
this.db = db;
this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
this.options = EnumSet.noneOf(ListAccountsOption.class);
if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
suggestConfig = 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.suggestConfig = suggest;
}
}
@Override
public List<AccountInfo> apply(TopLevelResource rsrc)
throws OrmException, BadRequestException, MethodNotAllowedException {
if (Strings.isNullOrEmpty(query)) {
throw new BadRequestException("missing query field");
}
if (suggest && (!suggestConfig || query.length() < suggestFrom)) {
return Collections.emptyList();
}
accountLoader = accountLoaderFactory
.create(suggest || options.contains(ListAccountsOption.DETAILS));
AccountIndex searchIndex = indexes.getSearchIndex();
if (searchIndex != null) {
return queryFromIndex();
}
if (!suggest) {
throw new MethodNotAllowedException();
}
if (start != null) {
throw new MethodNotAllowedException("option start not allowed");
}
return queryFromDb();
}
public List<AccountInfo> queryFromIndex()
throws BadRequestException, MethodNotAllowedException, OrmException {
if (queryProcessor.isDisabled()) {
throw new MethodNotAllowedException("query disabled");
}
if (start != null) {
queryProcessor.setStart(start);
}
Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
try {
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));
}
accountLoader.fill();
List<AccountInfo> sorted =
AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values());
if (!sorted.isEmpty() && result.more()) {
sorted.get(sorted.size() - 1)._moreAccounts = true;
}
return sorted;
} catch (QueryParseException e) {
if (suggest) {
return ImmutableList.of();
}
throw new BadRequestException(e.getMessage());
}
}
public List<AccountInfo> queryFromDb() throws OrmException {
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, suggestLimit)) {
addSuggestion(matches, p);
}
if (matches.size() < suggestLimit) {
for (Account p : db.accounts()
.suggestByPreferredEmail(a, b, suggestLimit - matches.size())) {
addSuggestion(matches, p);
}
}
if (matches.size() < suggestLimit) {
for (AccountExternalId e : db.accountExternalIds()
.suggestByEmailAddress(a, b, suggestLimit - 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();
}
}
return AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values());
}
private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account a) {
if (!a.isActive()) {
return false;
}
Account.Id id = a.getId();
if (!map.containsKey(id) && accountControl.canSee(id)) {
map.put(id, accountLoader.get(id));
return true;
}
return false;
}
private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) {
Account a = accountCache.get(id).getAccount();
return addSuggestion(map, a);
}
}

View File

@@ -1,158 +0,0 @@
// 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.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.api.accounts.AccountInfoComparator;
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.Collections;
import java.util.HashMap;
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;
private static final String MAX_SUFFIX = "\u9fa5";
private final AccountControl accountControl;
private final AccountLoader accountLoader;
private final AccountCache accountCache;
private final ReviewDb db;
private final boolean suggest;
private final int suggestFrom;
private int limit = 10;
private String query;
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
public 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")
public void setQuery(String query) {
this.query = query;
}
@Inject
SuggestAccounts(AccountControl.Factory accountControlFactory,
AccountLoader.Factory accountLoaderFactory,
AccountCache accountCache,
ReviewDb db,
@GerritServerConfig Config cfg) {
accountControl = accountControlFactory.get();
accountLoader = accountLoaderFactory.create(true);
this.accountCache = accountCache;
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);
}
if (matches.size() < limit) {
for (Account p : db.accounts()
.suggestByPreferredEmail(a, b, limit - matches.size())) {
addSuggestion(matches, p);
}
}
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();
}
}
return AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values());
}
private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account a) {
if (!a.isActive()) {
return false;
}
Account.Id id = a.getId();
if (!map.containsKey(id) && accountControl.canSee(id)) {
map.put(id, accountLoader.get(id));
return true;
}
return false;
}
private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) {
Account a = accountCache.get(id).getAccount();
return addSuggestion(map, a);
}
}

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.api.accounts;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.accounts.Accounts;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
@@ -24,7 +25,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 +38,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 +93,42 @@ 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);
}
}
@Override
public QueryRequest query() throws RestApiException {
return new QueryRequest() {
@Override
public List<AccountInfo> get() throws RestApiException {
return AccountsImpl.this.query(this);
}
};
}
@Override
public QueryRequest query(String query) throws RestApiException {
return query().withQuery(query);
}
private List<AccountInfo> query(QueryRequest r)
throws RestApiException {
try {
QueryAccounts myQueryAccounts = queryAccountsProvider.get();
myQueryAccounts.setQuery(r.getQuery());
myQueryAccounts.setLimit(r.getLimit());
myQueryAccounts.setStart(r.getStart());
for (ListAccountsOption option : r.getOptions()) {
myQueryAccounts.addOption(option);
}
return myQueryAccounts.apply(TopLevelResource.INSTANCE);
} catch (OrmException e) {
throw new RestApiException("Cannot retrieve suggested accounts", e);
}

View File

@@ -77,7 +77,7 @@ public class AccountField {
// Additional values not currently added by getPersonParts.
// TODO(dborowitz): Move to getPersonParts and remove this hack.
if (fullName != null) {
parts.add(fullName);
parts.add(fullName.toLowerCase());
}
return parts;
}

View File

@@ -35,12 +35,12 @@ public class AccountPredicates {
static Predicate<AccountState> email(String email) {
return new AccountPredicate(AccountField.EMAIL,
AccountQueryBuilder.FIELD_EMAIL, email);
AccountQueryBuilder.FIELD_EMAIL, email.toLowerCase());
}
static Predicate<AccountState> equalsName(String name) {
return new AccountPredicate(AccountField.NAME_PART,
AccountQueryBuilder.FIELD_NAME, name);
AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
}
public static Predicate<AccountState> isActive() {
@@ -53,7 +53,7 @@ public class AccountPredicates {
static Predicate<AccountState> username(String username) {
return new AccountPredicate(AccountField.USERNAME,
AccountQueryBuilder.FIELD_USERNAME, username);
AccountQueryBuilder.FIELD_USERNAME, username.toLowerCase());
}
static class AccountPredicate extends IndexPredicate<AccountState> {

View File

@@ -123,7 +123,7 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
}
@Override
protected Predicate<AccountState> defaultField(String query)
public Predicate<AccountState> defaultField(String query)
throws QueryParseException {
if ("self".equalsIgnoreCase(query)) {
return AccountPredicates.id(self());

View File

@@ -57,6 +57,6 @@ public class AccountQueryProcessor extends QueryProcessor<AccountState> {
protected Predicate<AccountState> enforceVisibility(
Predicate<AccountState> pred) {
return new AndSource<>(pred,
new AccountIsVisibleToPredicate(accountControlFactory.get()));
new AccountIsVisibleToPredicate(accountControlFactory.get()), start);
}
}

View File

@@ -0,0 +1,427 @@
// Copyright (C) 2016 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.query.account;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
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.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.GerritServerTests;
import com.google.gerrit.testutil.InMemoryDatabase;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
import org.eclipse.jgit.lib.Config;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@Ignore
public abstract class AbstractQueryAccountsTest extends GerritServerTests {
@ConfigSuite.Default
public static Config defaultConfig() {
Config cfg = new Config();
cfg.setInt("index", null, "maxPages", 10);
return cfg;
}
@Rule
public final TestName testName = new TestName();
@Inject
protected AccountCache accountCache;
@Inject
protected AccountManager accountManager;
@Inject
protected GerritApi gApi;
@Inject
protected IdentifiedUser.GenericFactory userFactory;
@Inject
protected InMemoryDatabase schemaFactory;
@Inject
protected InternalChangeQuery internalChangeQuery;
@Inject
protected SchemaCreator schemaCreator;
@Inject
protected ThreadLocalRequestContext requestContext;
protected LifecycleManager lifecycle;
protected ReviewDb db;
protected AccountInfo currentUserInfo;
protected CurrentUser user;
protected abstract Injector createInjector();
@Before
public void setUpInjector() throws Exception {
lifecycle = new LifecycleManager();
Injector injector = createInjector();
lifecycle.add(injector);
injector.injectMembers(this);
lifecycle.start();
db = schemaFactory.open();
schemaCreator.create(db);
Account.Id userId = createAccount("user", "User", "user@example.com", true);
user = userFactory.create(userId);
requestContext.setContext(newRequestContext(userId));
currentUserInfo = gApi.accounts().id(userId.get()).get();
}
protected RequestContext newRequestContext(Account.Id requestUserId) {
final CurrentUser requestUser =
userFactory.create(requestUserId);
return new RequestContext() {
@Override
public CurrentUser getUser() {
return requestUser;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return Providers.of(db);
}
};
}
@After
public void tearDownInjector() {
if (lifecycle != null) {
lifecycle.stop();
}
requestContext.setContext(null);
if (db != null) {
db.close();
}
InMemoryDatabase.drop(schemaFactory);
}
@Test
public void byId() throws Exception {
AccountInfo user = newAccount("user");
assertQuery("9999999");
assertQuery(currentUserInfo._accountId, currentUserInfo);
assertQuery(user._accountId, user);
}
@Test
public void bySelf() throws Exception {
assertQuery("self", currentUserInfo);
}
@Test
public void byEmail() throws Exception {
AccountInfo user1 = newAccountWithEmail("user1", name("user1@example.com"));
String domain = name("test.com");
AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain);
String prefix = name("prefix");
AccountInfo user4 =
newAccountWithEmail("user4", prefix + "user4@example.com");
AccountInfo user5 =
newAccountWithEmail("user5", name("user5MixedCase@example.com"));
assertQuery("notexisting@test.com");
assertQuery(currentUserInfo.email, currentUserInfo);
assertQuery("email:" + currentUserInfo.email, currentUserInfo);
assertQuery(user1.email, user1);
assertQuery("email:" + user1.email, user1);
assertQuery(domain, user2, user3);
assertQuery("email:" + prefix, user4);
assertQuery(user5.email, user5);
assertQuery("email:" + user5.email, user5);
assertQuery("email:" + user5.email.toUpperCase(), user5);
}
@Test
public void byUsername() throws Exception {
AccountInfo user1 = newAccount("myuser");
assertQuery("notexisting");
assertQuery("Not Existing");
assertQuery(user1.username, user1);
assertQuery("username:" + user1.username, user1);
assertQuery("username:" + user1.username.toUpperCase(), user1);
}
@Test
public void isActive() throws Exception {
String domain = name("test.com");
AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain);
AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
AccountInfo user3 = newAccount("user3", "user3@" + domain, false);
AccountInfo user4 = newAccount("user4", "user4@" + domain, false);
// by default only active accounts are returned
assertQuery(domain, user1, user2);
assertQuery("name:" + domain, user1, user2);
assertQuery("is:active name:" + domain, user1, user2);
assertQuery("is:inactive name:" + domain, user3, user4);
}
@Test
public void byName() throws Exception {
AccountInfo user1 = newAccountWithFullName("jdoe", "John Doe");
AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
assertQuery("notexisting");
assertQuery("Not Existing");
assertQuery(quote(user1.name), user1);
assertQuery("name:" + quote(user1.name), user1);
assertQuery("John", user1);
assertQuery("john", user1);
assertQuery("Doe", user1);
assertQuery("doe", user1);
assertQuery("DOE", user1);
assertQuery("name:John", user1);
assertQuery("name:john", user1);
assertQuery("name:Doe", user1);
assertQuery("name:doe", user1);
assertQuery("name:DOE", user1);
assertQuery(quote(user2.name), user2);
assertQuery("name:" + quote(user2.name), user2);
}
@Test
public void withLimit() throws Exception {
String domain = name("test.com");
AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain);
AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain);
List<AccountInfo> result = assertQuery(domain, user1, user2, user3);
assertThat(result.get(result.size() - 1)._moreAccounts).isNull();
result = assertQuery(newQuery(domain).withLimit(2), user1, user2);
assertThat(result.get(result.size() - 1)._moreAccounts).isTrue();
}
@Test
public void withStart() throws Exception {
String domain = name("test.com");
AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain);
AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain);
assertQuery(domain, user1, user2, user3);
assertQuery(newQuery(domain).withStart(1), user2, user3);
}
@Test
public void withDetails() throws Exception {
AccountInfo user1 =
newAccount("myuser", "My User", "my.user@example.com", true);
List<AccountInfo> result = assertQuery(user1.username, user1);
AccountInfo ai = result.get(0);
assertThat(ai._accountId).isEqualTo(user1._accountId);
assertThat(ai.name).isNull();
assertThat(ai.username).isNull();
assertThat(ai.email).isNull();
assertThat(ai.avatars).isNull();
result = assertQuery(
newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
ai = result.get(0);
assertThat(ai._accountId).isEqualTo(user1._accountId);
assertThat(ai.name).isEqualTo(user1.name);
assertThat(ai.username).isEqualTo(user1.username);
assertThat(ai.email).isEqualTo(user1.email);
assertThat(ai.avatars).isNull();
}
protected AccountInfo newAccount(String username) throws Exception {
return newAccountWithEmail(username, null);
}
protected AccountInfo newAccountWithEmail(String username, String email)
throws Exception {
return newAccount(username, email, true);
}
protected AccountInfo newAccountWithFullName(String username, String fullName)
throws Exception {
return newAccount(username, fullName, null, true);
}
protected AccountInfo newAccount(String username, String email,
boolean active) throws Exception {
return newAccount(username, null, email, active);
}
protected AccountInfo newAccount(String username, String fullName,
String email, boolean active) throws Exception {
String uniqueName = name(username);
try {
gApi.accounts().id(uniqueName).get();
fail("user " + uniqueName + " already exists");
} catch (ResourceNotFoundException e) {
// expected: user does not exist yet
}
Account.Id id = createAccount(uniqueName, fullName, email, active);
return gApi.accounts().id(id.get()).get();
}
protected String quote(String s) {
return "\"" + s + "\"";
}
protected String name(String name) {
if (name == null) {
return null;
}
String suffix = testName.getMethodName().toLowerCase();
if (name.contains("@")) {
return name + "." + suffix;
}
return name + "_" + suffix;
}
private Account.Id createAccount(String username, String fullName,
String email, boolean active) throws Exception {
Account.Id id =
accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email));
}
Account a = db.accounts().get(id);
a.setFullName(fullName);
a.setPreferredEmail(email);
a.setActive(active);
db.accounts().update(ImmutableList.of(a));
accountCache.evict(id);
return id;
}
protected QueryRequest newQuery(Object query) throws RestApiException {
return gApi.accounts().query(query.toString());
}
protected List<AccountInfo> assertQuery(Object query, AccountInfo... accounts)
throws Exception {
return assertQuery(newQuery(query), accounts);
}
protected List<AccountInfo> assertQuery(QueryRequest query, AccountInfo... accounts)
throws Exception {
List<AccountInfo> result = query.get();
Iterable<Integer> ids = ids(result);
assertThat(ids).named(format(query, result, accounts))
.containsExactlyElementsIn(ids(accounts)).inOrder();
return result;
}
private String format(QueryRequest query, Iterable<AccountInfo> actualIds,
AccountInfo... expectedAccounts) {
StringBuilder b = new StringBuilder();
b.append("query '").append(query.getQuery())
.append("' with expected accounts ");
b.append(format(Arrays.asList(expectedAccounts)));
b.append(" and result ");
b.append(format(actualIds));
return b.toString();
}
private String format(Iterable<AccountInfo> accounts) {
StringBuilder b = new StringBuilder();
b.append("[");
Iterator<AccountInfo> it = accounts.iterator();
while (it.hasNext()) {
AccountInfo a = it.next();
b.append("{").append(a._accountId).append(", ").append("name=")
.append(a.name).append(", ").append("email=").append(a.email)
.append(", ").append("username=").append(a.username).append("}");
if (it.hasNext()) {
b.append(", ");
}
}
b.append("]");
return b.toString();
}
protected static Iterable<Integer> ids(AccountInfo... accounts) {
return FluentIterable.from(Arrays.asList(accounts)).transform(
new Function<AccountInfo, Integer>() {
@Override
public Integer apply(AccountInfo in) {
return in._accountId;
}
});
}
protected static Iterable<Integer> ids(Iterable<AccountInfo> accounts) {
return FluentIterable.from(accounts).transform(
new Function<AccountInfo, Integer>() {
@Override
public Integer apply(AccountInfo in) {
return in._accountId;
}
});
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (C) 2016 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.query.account;
import com.google.gerrit.testutil.InMemoryModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.eclipse.jgit.lib.Config;
public class LuceneQueryAccountsTest extends AbstractQueryAccountsTest {
@Override
protected Injector createInjector() {
Config luceneConfig = new Config(config);
InMemoryModule.setDefaults(luceneConfig);
return Guice.createInjector(
new InMemoryModule(luceneConfig, notesMigration));
}
}