| // 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 java.util.stream.Collectors.toList; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Multimap; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.UsedAt; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.index.IndexConfig; |
| import com.google.gerrit.index.query.InternalQuery; |
| import com.google.gerrit.index.query.Predicate; |
| import com.google.gerrit.server.account.AccountState; |
| import com.google.gerrit.server.account.externalids.ExternalId; |
| import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory; |
| import com.google.gerrit.server.config.AccountConfig; |
| import com.google.gerrit.server.index.account.AccountIndexCollection; |
| import com.google.inject.Inject; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| |
| /** |
| * Query wrapper for the account index. |
| * |
| * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than |
| * holding on to a single instance. |
| */ |
| public class InternalAccountQuery extends InternalQuery<AccountState, InternalAccountQuery> { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final AccountConfig accountConfig; |
| private final ExternalIdKeyFactory externalIdKeyFactory; |
| |
| @Inject |
| InternalAccountQuery( |
| AccountQueryProcessor queryProcessor, |
| AccountIndexCollection indexes, |
| IndexConfig indexConfig, |
| ExternalIdKeyFactory externalIdKeyFactory, |
| AccountConfig accountConfig) { |
| |
| super(queryProcessor, indexes, indexConfig); |
| this.accountConfig = accountConfig; |
| this.externalIdKeyFactory = externalIdKeyFactory; |
| } |
| |
| public List<AccountState> byDefault(String query, boolean canSeeSecondaryEmails) { |
| return query(AccountPredicates.defaultPredicate(schema(), canSeeSecondaryEmails, query)); |
| } |
| |
| public List<AccountState> byExternalId(String scheme, String id) { |
| return byExternalId(externalIdKeyFactory.create(scheme, id)); |
| } |
| |
| public List<AccountState> byExternalId(ExternalId.Key externalId) { |
| return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString())); |
| } |
| |
| @Nullable |
| @UsedAt(UsedAt.Project.COLLABNET) |
| public AccountState oneByExternalId(ExternalId.Key externalId) { |
| List<AccountState> accountStates = byExternalId(externalId); |
| if (accountStates.size() == 1) { |
| return accountStates.get(0); |
| } else if (!accountStates.isEmpty()) { |
| StringBuilder msg = new StringBuilder(); |
| msg.append("Ambiguous external ID ").append(externalId).append(" for accounts: "); |
| Joiner.on(", ") |
| .appendTo( |
| msg, accountStates.stream().map(a -> a.account().id().toString()).collect(toList())); |
| logger.atWarning().log("%s", msg); |
| } |
| return null; |
| } |
| |
| public List<AccountState> byFullName(String fullName) { |
| return query(AccountPredicates.fullName(fullName)); |
| } |
| |
| /** |
| * Queries for accounts that have a preferred email that matches the given email. |
| * |
| * <p>The local part of the email is compared either in a case-insensitive or case-sensitive |
| * manner, depending on the configuration parameter {@code accounts.caseInsensitiveLocalPart}. |
| * Check the configuration documentation for more details. |
| * |
| * @param email preferred email by which accounts should be found |
| * @return list of accounts that have a preferred email that exactly matches the given email |
| */ |
| public List<AccountState> byPreferredEmail(String email) { |
| return query(getPreferredEmailPredicate(email)).stream() |
| .filter(a -> normalizeEmail(a.account().preferredEmail()).equals(normalizeEmail(email))) |
| .collect(toList()); |
| } |
| |
| /** |
| * Makes multiple queries for accounts by preferred email. |
| * |
| * <p>The local part of the email is compared either in a case-insensitive or case-sensitive |
| * manner, depending on the configuration parameter {@code accounts.caseInsensitiveLocalPart}. |
| * Check the configuration documentation for more details. |
| * |
| * @param emails preferred emails by which accounts should be found |
| * @return multimap of the given emails to accounts that have a preferred email that exactly |
| * matches this email |
| */ |
| public Multimap<String, AccountState> byPreferredEmail(List<String> emails) { |
| List<List<AccountState>> r = |
| query(emails.stream().map(email -> getPreferredEmailPredicate(email)).collect(toList())); |
| ListMultimap<String, AccountState> accountsByEmail = ArrayListMultimap.create(); |
| for (int i = 0; i < emails.size(); i++) { |
| String email = emails.get(i); |
| Set<AccountState> matchingAccounts = |
| r.get(i).stream() |
| .filter( |
| a -> normalizeEmail(a.account().preferredEmail()).equals(normalizeEmail(email))) |
| .collect(toSet()); |
| accountsByEmail.putAll(email, matchingAccounts); |
| } |
| return accountsByEmail; |
| } |
| |
| public List<AccountState> byWatchedProject(Project.NameKey project) { |
| return query(AccountPredicates.watchedProject(project)); |
| } |
| |
| private Predicate<AccountState> getPreferredEmailPredicate(String email) { |
| return useCaseInsensitiveLocalParts(email) |
| ? AccountPredicates.preferredEmail(normalizeEmail(email)) |
| : AccountPredicates.preferredEmailExact(email); |
| } |
| |
| private String normalizeEmail(String email) { |
| return useCaseInsensitiveLocalParts(email) ? email.toLowerCase(Locale.US) : email; |
| } |
| |
| private boolean useCaseInsensitiveLocalParts(String email) { |
| return Arrays.asList(accountConfig.getCaseInsensitiveLocalParts()) |
| .contains(getLowerCaseEmailDomain(email)); |
| } |
| |
| private String getLowerCaseEmailDomain(String email) { |
| String[] parts = email.split("@", 2); |
| // The caller method byPreferredEmail can be invoked with the local part |
| // of the email only. Handle this case by just returning it. |
| if (parts.length != 2) { |
| return email; |
| } |
| return parts[1].toLowerCase(Locale.US); |
| } |
| } |