Account queries: Add option to include secondary emails

If an account is suggested due to a match on a secondary email address
the UI needs to have the secondary email address available to show it
in the suggestion (see Ife5e3 for more details).

Change-Id: I23c72630f54250e7ac0a7ff26d4b28e8a6af0df3
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2016-07-01 13:33:32 +02:00
parent aedc6626ec
commit 2eadde4d9f
9 changed files with 140 additions and 25 deletions

View File

@@ -61,13 +61,18 @@ generally disabled by default. Optional fields are:
for each account. for each account.
-- --
[[all-emails]]
--
* `ALL_EMAILS`: Includes all registered emails.
--
[[suggest-account]] [[suggest-account]]
To get account suggestions set the parameter `suggest` and provide the To get account suggestions set the parameter `suggest` and provide the
typed substring as query `q`. If a result limit `n` is not specified, typed substring as query `q`. If a result limit `n` is not specified,
then the default 10 is used. then the default 10 is used.
For account suggestions link:#details[account details] are always For account suggestions link:#details[account details] and
returned. link:#all-emails[all emails] are always returned.
.Request .Request
---- ----
@@ -1950,29 +1955,34 @@ registered.
The `AccountInfo` entity contains information about an account. The `AccountInfo` entity contains information about an account.
[options="header",cols="1,^1,5"] [options="header",cols="1,^1,5"]
|============================= |===============================
|Field Name ||Description |Field Name ||Description
|`_account_id` ||The numeric ID of the account. |`_account_id` ||The numeric ID of the account.
|`name` |optional|The full name of the user. + |`name` |optional|The full name of the user. +
Only set if detailed account information is requested. + Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[ See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries + DETAILED_ACCOUNTS] for change queries +
and option link:#details[DETAILS] for account queries. and option link:#details[DETAILS] for account queries.
|`email` |optional| |`email` |optional|
The email address the user prefers to be contacted through. + The email address the user prefers to be contacted through. +
Only set if detailed account information is requested. + Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[ See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries + DETAILED_ACCOUNTS] for change queries +
and option link:#details[DETAILS] for account queries. and options link:#details[DETAILS] and link:#all-emails[
|`username` |optional|The username of the user. + ALL_EMAILS] for account queries.
|`secondary_emails`|optional|
A list of the secondary email addresses of the user. +
Only set for account queries when the link:#all-emails[ALL_EMAILS]
option is set.
|`username` |optional|The username of the user. +
Only set if detailed account information is requested. + Only set if detailed account information is requested. +
See option link:rest-api-changes.html#detailed-accounts[ See option link:rest-api-changes.html#detailed-accounts[
DETAILED_ACCOUNTS] for change queries + DETAILED_ACCOUNTS] for change queries +
and option link:#details[DETAILS] for account queries. and option link:#details[DETAILS] for account queries.
|`_more_accounts`|optional, not set if `false`| |`_more_accounts` |optional, not set if `false`|
Whether the query would deliver more results if not limited. + Whether the query would deliver more results if not limited. +
Only set on the last account that is returned. Only set on the last account that is returned.
|============================= |===============================
[[account-input]] [[account-input]]
=== AccountInput === AccountInput

View File

@@ -20,7 +20,10 @@ import java.util.Set;
/** Output options available for retrieval of account details. */ /** Output options available for retrieval of account details. */
public enum ListAccountsOption { public enum ListAccountsOption {
/** Return detailed account properties. */ /** Return detailed account properties. */
DETAILS(0); DETAILS(0),
/** Return all secondary emails. */
ALL_EMAILS(1);
private final int value; private final int value;

View File

@@ -20,6 +20,7 @@ public class AccountInfo {
public Integer _accountId; public Integer _accountId;
public String name; public String name;
public String email; public String email;
public List<String> secondaryEmails;
public String username; public String username;
public List<AvatarInfo> avatars; public List<AvatarInfo> avatars;
public Boolean _moreAccounts; public Boolean _moreAccounts;

View File

@@ -32,6 +32,9 @@ public abstract class AccountDirectory {
/** Preferred email address to contact the user at. */ /** Preferred email address to contact the user at. */
EMAIL, EMAIL,
/** All secondary email addresses of the user. */
SECONDARY_EMAILS,
/** User profile images. */ /** User profile images. */
AVATARS, AVATARS,

View File

@@ -23,8 +23,8 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountDirectory.DirectoryException; import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
import com.google.gerrit.server.account.AccountDirectory.FillOptions; import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -36,7 +36,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
public class AccountLoader { public class AccountLoader {
private static final Set<FillOptions> DETAILED_OPTIONS = public static final Set<FillOptions> DETAILED_OPTIONS =
Collections.unmodifiableSet(EnumSet.of( Collections.unmodifiableSet(EnumSet.of(
FillOptions.ID, FillOptions.ID,
FillOptions.NAME, FillOptions.NAME,
@@ -46,6 +46,7 @@ public class AccountLoader {
public interface Factory { public interface Factory {
AccountLoader create(boolean detailed); AccountLoader create(boolean detailed);
AccountLoader create(Set<FillOptions> options);
} }
private final InternalAccountDirectory directory; private final InternalAccountDirectory directory;
@@ -53,10 +54,20 @@ public class AccountLoader {
private final Map<Account.Id, AccountInfo> created; private final Map<Account.Id, AccountInfo> created;
private final List<AccountInfo> provided; private final List<AccountInfo> provided;
@Inject @AssistedInject
AccountLoader(InternalAccountDirectory directory, @Assisted boolean detailed) { AccountLoader(InternalAccountDirectory directory,
@Assisted boolean detailed) {
this(directory,
detailed
? DETAILED_OPTIONS
: InternalAccountDirectory.ID_ONLY);
}
@AssistedInject
AccountLoader(InternalAccountDirectory directory,
@Assisted Set<FillOptions> options) {
this.directory = directory; this.directory = directory;
options = detailed ? DETAILED_OPTIONS : InternalAccountDirectory.ID_ONLY; this.options = options;
created = new HashMap<>(); created = new HashMap<>();
provided = new ArrayList<>(); provided = new ArrayList<>();
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
@@ -26,6 +27,7 @@ import com.google.gerrit.server.CurrentUser.PropertyKey;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
public class AccountState { public class AccountState {
@@ -88,6 +90,16 @@ public class AccountState {
return null; return null;
} }
public static Set<String> getEmails(Collection<AccountExternalId> ids) {
Set<String> emails = new HashSet<>();
for (AccountExternalId id : ids) {
if (id.isScheme(SCHEME_MAILTO)) {
emails.add(id.getSchemeRest());
}
}
return emails;
}
/** /**
* Lookup a previously stored property. * Lookup a previously stored property.
* <p> * <p>

View File

@@ -17,10 +17,12 @@ package com.google.gerrit.server.account;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AvatarInfo; import com.google.gerrit.extensions.common.AvatarInfo;
import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.reviewdb.client.Account; 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.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.avatar.AvatarProvider; import com.google.gerrit.server.avatar.AvatarProvider;
@@ -31,8 +33,10 @@ import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
import java.util.Set; import java.util.Set;
@Singleton @Singleton
@@ -76,7 +80,7 @@ public class InternalAccountDirectory extends AccountDirectory {
Account.Id id = new Account.Id(info._accountId); Account.Id id = new Account.Id(info._accountId);
AccountState state = accountCache.getIfPresent(id); AccountState state = accountCache.getIfPresent(id);
if (state != null) { if (state != null) {
fill(info, state.getAccount(), options); fill(info, state.getAccount(), state.getExternalIds(), options);
} else { } else {
missing.put(id, info); missing.put(id, info);
} }
@@ -84,12 +88,14 @@ public class InternalAccountDirectory extends AccountDirectory {
if (!missing.isEmpty()) { if (!missing.isEmpty()) {
try { try {
for (Account account : db.get().accounts().get(missing.keySet())) { for (Account account : db.get().accounts().get(missing.keySet())) {
if (options.contains(FillOptions.USERNAME)) { Collection<AccountExternalId> externalIds = null;
account.setUserName(AccountState.getUserName( if (options.contains(FillOptions.USERNAME)
db.get().accountExternalIds().byAccount(account.getId()).toList())); || options.contains(FillOptions.SECONDARY_EMAILS)) {
externalIds = db.get().accountExternalIds()
.byAccount(account.getId()).toList();
} }
for (AccountInfo info : missing.get(account.getId())) { for (AccountInfo info : missing.get(account.getId())) {
fill(info, account, options); fill(info, account, externalIds, options);
} }
} }
} catch (OrmException e) { } catch (OrmException e) {
@@ -100,6 +106,7 @@ public class InternalAccountDirectory extends AccountDirectory {
private void fill(AccountInfo info, private void fill(AccountInfo info,
Account account, Account account,
@Nullable Collection<AccountExternalId> externalIds,
Set<FillOptions> options) { Set<FillOptions> options) {
if (options.contains(FillOptions.ID)) { if (options.contains(FillOptions.ID)) {
info._accountId = account.getId().get(); info._accountId = account.getId().get();
@@ -116,8 +123,15 @@ public class InternalAccountDirectory extends AccountDirectory {
if (options.contains(FillOptions.EMAIL)) { if (options.contains(FillOptions.EMAIL)) {
info.email = account.getPreferredEmail(); info.email = account.getPreferredEmail();
} }
if (options.contains(FillOptions.SECONDARY_EMAILS)) {
info.secondaryEmails = externalIds != null
? getSecondaryEmails(account, externalIds)
: null;
}
if (options.contains(FillOptions.USERNAME)) { if (options.contains(FillOptions.USERNAME)) {
info.username = account.getUserName(); info.username = externalIds != null
? AccountState.getUserName(externalIds)
: null;
} }
if (options.contains(FillOptions.AVATARS)) { if (options.contains(FillOptions.AVATARS)) {
AvatarProvider ap = avatar.get(); AvatarProvider ap = avatar.get();
@@ -141,6 +155,16 @@ public class InternalAccountDirectory extends AccountDirectory {
} }
} }
public List<String> getSecondaryEmails(Account account,
Collection<AccountExternalId> externalIds) {
List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
if (account.getPreferredEmail() != null) {
emails.remove(account.getPreferredEmail());
}
Collections.sort(emails);
return emails;
}
private static void addAvatar( private static void addAvatar(
AvatarProvider provider, AvatarProvider provider,
AccountInfo account, AccountInfo account,

View File

@@ -25,6 +25,7 @@ import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.api.accounts.AccountInfoComparator; import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.account.AccountIndex; import com.google.gerrit.server.index.account.AccountIndex;
@@ -46,6 +47,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
public class QueryAccounts implements RestReadView<TopLevelResource> { public class QueryAccounts implements RestReadView<TopLevelResource> {
private static final int MAX_SUGGEST_RESULTS = 100; private static final int MAX_SUGGEST_RESULTS = 100;
@@ -152,8 +154,20 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
return Collections.emptyList(); return Collections.emptyList();
} }
accountLoader = accountLoaderFactory Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID);
.create(suggest || options.contains(ListAccountsOption.DETAILS)); if (options.contains(ListAccountsOption.DETAILS)) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
}
if (options.contains(ListAccountsOption.ALL_EMAILS)) {
fillOptions.add(FillOptions.EMAIL);
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
if (suggest) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
fillOptions.add(FillOptions.EMAIL);
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
accountLoader = accountLoaderFactory.create(fillOptions);
AccountIndex searchIndex = indexes.getSearchIndex(); AccountIndex searchIndex = indexes.getSearchIndex();
if (searchIndex != null) { if (searchIndex != null) {

View File

@@ -294,6 +294,34 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
assertThat(ai.avatars).isNull(); assertThat(ai.avatars).isNull();
} }
@Test
public void withSecondaryEmails() throws Exception {
AccountInfo user1 =
newAccount("myuser", "My User", "my.user@example.com", true);
String[] secondaryEmails =
new String[] {"bar@example.com", "foo@example.com"};
addEmails(user1, secondaryEmails);
List<AccountInfo> result = assertQuery(user1.username, user1);
assertThat(result.get(0).secondaryEmails).isNull();
result = assertQuery(
newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
assertThat(result.get(0).secondaryEmails).isNull();
result = assertQuery(
newQuery(user1.username).withOption(ListAccountsOption.ALL_EMAILS),
user1);
assertThat(result.get(0).secondaryEmails)
.containsExactlyElementsIn(Arrays.asList(secondaryEmails)).inOrder();
result = assertQuery(newQuery(user1.username).withOptions(
ListAccountsOption.DETAILS, ListAccountsOption.ALL_EMAILS), user1);
assertThat(result.get(0).secondaryEmails)
.containsExactlyElementsIn(Arrays.asList(secondaryEmails)).inOrder();
}
protected AccountInfo newAccount(String username) throws Exception { protected AccountInfo newAccount(String username) throws Exception {
return newAccountWithEmail(username, null); return newAccountWithEmail(username, null);
} }
@@ -359,6 +387,15 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
return id; return id;
} }
private void addEmails(AccountInfo account, String... emails)
throws Exception {
Account.Id id = new Account.Id(account._accountId);
for (String email : emails) {
accountManager.link(id, AuthRequest.forEmail(email));
}
accountCache.evict(id);
}
protected QueryRequest newQuery(Object query) throws RestApiException { protected QueryRequest newQuery(Object query) throws RestApiException {
return gApi.accounts().query(query.toString()); return gApi.accounts().query(query.toString());
} }