Line data Source code
1 : // Copyright (C) 2019 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.account;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.collect.ImmutableList.toImmutableList;
19 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
20 : import static java.util.Comparator.comparing;
21 : import static java.util.Objects.requireNonNull;
22 : import static java.util.stream.Collectors.joining;
23 :
24 : import com.google.common.annotations.VisibleForTesting;
25 : import com.google.common.base.Suppliers;
26 : import com.google.common.collect.ImmutableList;
27 : import com.google.common.collect.ImmutableSet;
28 : import com.google.common.collect.Streams;
29 : import com.google.gerrit.entities.Account;
30 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
31 : import com.google.gerrit.index.Schema;
32 : import com.google.gerrit.server.CurrentUser;
33 : import com.google.gerrit.server.IdentifiedUser;
34 : import com.google.gerrit.server.account.externalids.ExternalId;
35 : import com.google.gerrit.server.config.AnonymousCowardName;
36 : import com.google.gerrit.server.permissions.GlobalPermission;
37 : import com.google.gerrit.server.permissions.PermissionBackend;
38 : import com.google.gerrit.server.permissions.PermissionBackendException;
39 : import com.google.gerrit.server.query.account.InternalAccountQuery;
40 : import com.google.inject.Inject;
41 : import com.google.inject.Provider;
42 : import com.google.inject.Singleton;
43 : import java.io.IOException;
44 : import java.util.ArrayList;
45 : import java.util.List;
46 : import java.util.Optional;
47 : import java.util.Set;
48 : import java.util.TreeSet;
49 : import java.util.function.Predicate;
50 : import java.util.function.Supplier;
51 : import java.util.regex.Matcher;
52 : import java.util.regex.Pattern;
53 : import java.util.stream.Stream;
54 : import org.eclipse.jgit.errors.ConfigInvalidException;
55 :
56 : /**
57 : * Helper for resolving accounts given arbitrary user-provided input.
58 : *
59 : * <p>The {@code resolve*} methods each define a list of accepted formats for account resolution.
60 : * The algorithm for resolving accounts from a list of formats is as follows:
61 : *
62 : * <ol>
63 : * <li>For each recognized format in the order listed in the method Javadoc, check whether the
64 : * input matches that format.
65 : * <li>If so, resolve accounts according to that format.
66 : * <li>Filter out invisible and inactive accounts.
67 : * <li>If the result list is non-empty, return.
68 : * <li>If the format is listed above as being short-circuiting, return.
69 : * <li>Otherwise, return to step 1 with the next format.
70 : * </ol>
71 : *
72 : * <p>The result never includes accounts that are not visible to the calling user. It also never
73 : * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc.
74 : */
75 : @Singleton
76 : public class AccountResolver {
77 : public static class UnresolvableAccountException extends UnprocessableEntityException {
78 : private static final long serialVersionUID = 1L;
79 : private final Result result;
80 :
81 : @VisibleForTesting
82 : UnresolvableAccountException(Result result) {
83 24 : super(exceptionMessage(result));
84 24 : this.result = result;
85 24 : }
86 :
87 : public boolean isSelf() {
88 22 : return result.isSelf();
89 : }
90 : }
91 :
92 : public static String exceptionMessage(Result result) {
93 24 : checkArgument(result.asList().size() != 1);
94 24 : if (result.asList().isEmpty()) {
95 24 : if (result.isSelf()) {
96 7 : return "Resolving account '" + result.input() + "' requires login";
97 : }
98 24 : if (result.filteredInactive().isEmpty()) {
99 23 : return "Account '" + result.input() + "' not found";
100 : }
101 4 : return result.filteredInactive().stream()
102 4 : .map(a -> formatForException(result, a))
103 4 : .collect(
104 4 : joining(
105 : "\n",
106 : "Account '"
107 4 : + result.input()
108 : + "' only matches inactive accounts. To use an inactive account, retry with"
109 : + " one of the following exact account IDs:\n",
110 : ""));
111 : }
112 :
113 2 : return result.asList().stream()
114 2 : .map(a -> formatForException(result, a))
115 2 : .limit(3)
116 2 : .collect(
117 2 : joining(
118 2 : "\n", "Account '" + result.input() + "' is ambiguous (at most 3 shown):\n", ""));
119 : }
120 :
121 : private static String formatForException(Result result, AccountState state) {
122 4 : return state.account().id()
123 : + ": "
124 4 : + state.account().getNameEmail(result.accountResolver().anonymousCowardName);
125 : }
126 :
127 : public static boolean isSelf(String input) {
128 61 : return "self".equals(input) || "me".equals(input);
129 : }
130 :
131 : public class Result {
132 : private final String input;
133 : private final ImmutableList<AccountState> list;
134 : private final ImmutableList<AccountState> filteredInactive;
135 :
136 : @VisibleForTesting
137 71 : Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
138 71 : this.input = requireNonNull(input);
139 71 : this.list = canonicalize(list);
140 71 : this.filteredInactive = canonicalize(filteredInactive);
141 71 : }
142 :
143 : private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
144 71 : TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.account().id().get()));
145 71 : set.addAll(requireNonNull(list));
146 71 : return ImmutableList.copyOf(set);
147 : }
148 :
149 : public String input() {
150 24 : return input;
151 : }
152 :
153 : public boolean isSelf() {
154 61 : return AccountResolver.isSelf(input);
155 : }
156 :
157 : public ImmutableList<AccountState> asList() {
158 24 : return list;
159 : }
160 :
161 : public ImmutableSet<Account.Id> asNonEmptyIdSet() throws UnresolvableAccountException {
162 17 : if (list.isEmpty()) {
163 10 : throw new UnresolvableAccountException(this);
164 : }
165 13 : return asIdSet();
166 : }
167 :
168 : public ImmutableSet<Account.Id> asIdSet() {
169 27 : return list.stream().map(a -> a.account().id()).collect(toImmutableSet());
170 : }
171 :
172 : public AccountState asUnique() throws UnresolvableAccountException {
173 61 : ensureUnique();
174 61 : return list.get(0);
175 : }
176 :
177 : private void ensureUnique() throws UnresolvableAccountException {
178 64 : if (list.size() != 1) {
179 22 : throw new UnresolvableAccountException(this);
180 : }
181 63 : }
182 :
183 : public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
184 56 : ensureUnique();
185 56 : if (isSelf()) {
186 : // In the special case of "self", use the exact IdentifiedUser from the request context, to
187 : // preserve the peer address and any other per-request state.
188 7 : return self.get().asIdentifiedUser();
189 : }
190 54 : return userFactory.create(asUnique());
191 : }
192 :
193 : public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller)
194 : throws UnresolvableAccountException {
195 3 : ensureUnique();
196 3 : if (isSelf()) {
197 : // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
198 0 : return self.get().asIdentifiedUser();
199 : }
200 3 : return userFactory.runAs(
201 3 : null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
202 : }
203 :
204 : @VisibleForTesting
205 : ImmutableList<AccountState> filteredInactive() {
206 24 : return filteredInactive;
207 : }
208 :
209 : private AccountResolver accountResolver() {
210 4 : return AccountResolver.this;
211 : }
212 : }
213 :
214 : @VisibleForTesting
215 : interface Searcher<I> {
216 : default boolean callerShouldFilterOutInactiveCandidates() {
217 57 : return true;
218 : }
219 :
220 : default boolean callerMayAssumeCandidatesAreVisible() {
221 62 : return false;
222 : }
223 :
224 : Optional<I> tryParse(String input) throws IOException;
225 :
226 : Stream<AccountState> search(I input) throws IOException, ConfigInvalidException;
227 :
228 : boolean shortCircuitIfNoResults();
229 :
230 : default Optional<Stream<AccountState>> trySearch(String input)
231 : throws IOException, ConfigInvalidException {
232 71 : Optional<I> parsed = tryParse(input);
233 71 : return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
234 : }
235 : }
236 :
237 : @VisibleForTesting
238 150 : abstract static class StringSearcher implements Searcher<String> {
239 : @Override
240 : public final Optional<String> tryParse(String input) {
241 71 : return matches(input) ? Optional.of(input) : Optional.empty();
242 : }
243 :
244 : protected abstract boolean matches(String input);
245 : }
246 :
247 150 : private abstract class AccountIdSearcher implements Searcher<Account.Id> {
248 : @Override
249 : public final Stream<AccountState> search(Account.Id input) {
250 44 : return Streams.stream(accountCache.get(input));
251 : }
252 : }
253 :
254 150 : private class BySelf extends StringSearcher {
255 : @Override
256 : public boolean callerShouldFilterOutInactiveCandidates() {
257 13 : return false;
258 : }
259 :
260 : @Override
261 : public boolean callerMayAssumeCandidatesAreVisible() {
262 13 : return true;
263 : }
264 :
265 : @Override
266 : protected boolean matches(String input) {
267 70 : return "self".equals(input) || "me".equals(input);
268 : }
269 :
270 : @Override
271 : public Stream<AccountState> search(String input) {
272 13 : CurrentUser user = self.get();
273 13 : if (!user.isIdentifiedUser()) {
274 6 : return Stream.empty();
275 : }
276 13 : return Stream.of(user.asIdentifiedUser().state());
277 : }
278 :
279 : @Override
280 : public boolean shortCircuitIfNoResults() {
281 6 : return true;
282 : }
283 : }
284 :
285 150 : private class ByExactAccountId extends AccountIdSearcher {
286 : @Override
287 : public boolean callerShouldFilterOutInactiveCandidates() {
288 44 : return false;
289 : }
290 :
291 : @Override
292 : public Optional<Account.Id> tryParse(String input) {
293 68 : return Account.Id.tryParse(input);
294 : }
295 :
296 : @Override
297 : public boolean shortCircuitIfNoResults() {
298 11 : return true;
299 : }
300 : }
301 :
302 150 : private class ByParenthesizedAccountId extends AccountIdSearcher {
303 150 : private final Pattern pattern = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$");
304 :
305 : @Override
306 : public Optional<Account.Id> tryParse(String input) {
307 57 : Matcher m = pattern.matcher(input);
308 57 : return m.matches() ? Account.Id.tryParse(m.group(1)) : Optional.empty();
309 : }
310 :
311 : @Override
312 : public boolean shortCircuitIfNoResults() {
313 1 : return true;
314 : }
315 : }
316 :
317 150 : private class ByUsername extends StringSearcher {
318 : @Override
319 : public boolean matches(String input) {
320 57 : return ExternalId.isValidUsername(input);
321 : }
322 :
323 : @Override
324 : public Stream<AccountState> search(String input) {
325 51 : return Streams.stream(accountCache.getByUsername(input));
326 : }
327 :
328 : @Override
329 : public boolean shortCircuitIfNoResults() {
330 45 : return false;
331 : }
332 : }
333 :
334 150 : private class ByNameAndEmail extends StringSearcher {
335 : @Override
336 : protected boolean matches(String input) {
337 51 : int lt = input.indexOf('<');
338 51 : int gt = input.indexOf('>');
339 51 : return lt >= 0 && gt > lt && input.contains("@");
340 : }
341 :
342 : @Override
343 : public Stream<AccountState> search(String nameOrEmail) throws IOException {
344 : // TODO(dborowitz): This would probably work as a Searcher<Address>
345 12 : int lt = nameOrEmail.indexOf('<');
346 12 : int gt = nameOrEmail.indexOf('>');
347 12 : Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
348 12 : ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList());
349 12 : if (allMatches.isEmpty() || allMatches.size() == 1) {
350 11 : return allMatches.stream();
351 : }
352 :
353 : // More than one match. If there are any that match the full name as well, return only that
354 : // subset. Otherwise, all are equally non-matching, so return the full set.
355 2 : if (lt == 0) {
356 : // No name was specified in the input string.
357 1 : return allMatches.stream();
358 : }
359 1 : String name = nameOrEmail.substring(0, lt - 1);
360 1 : ImmutableList<AccountState> nameMatches =
361 1 : allMatches.stream()
362 1 : .filter(a -> name.equals(a.account().fullName()))
363 1 : .collect(toImmutableList());
364 1 : return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream();
365 : }
366 :
367 : @Override
368 : public boolean shortCircuitIfNoResults() {
369 9 : return true;
370 : }
371 : }
372 :
373 150 : private class ByEmail extends StringSearcher {
374 : @Override
375 : protected boolean matches(String input) {
376 51 : return input.contains("@");
377 : }
378 :
379 : @Override
380 : public Stream<AccountState> search(String input) throws IOException {
381 42 : return toAccountStates(emails.getAccountFor(input));
382 : }
383 :
384 : @Override
385 : public boolean shortCircuitIfNoResults() {
386 13 : return true;
387 : }
388 : }
389 :
390 150 : private class FromRealm extends AccountIdSearcher {
391 : @Override
392 : public Optional<Account.Id> tryParse(String input) throws IOException {
393 33 : return Optional.ofNullable(realm.lookup(input));
394 : }
395 :
396 : @Override
397 : public boolean shortCircuitIfNoResults() {
398 0 : return false;
399 : }
400 : }
401 :
402 150 : private class ByFullName implements Searcher<AccountState> {
403 : @Override
404 : public boolean callerMayAssumeCandidatesAreVisible() {
405 9 : return true; // Rely on enforceVisibility from the index.
406 : }
407 :
408 : @Override
409 : public Optional<AccountState> tryParse(String input) {
410 33 : List<AccountState> results =
411 33 : accountQueryProvider.get().enforceVisibility(true).byFullName(input);
412 33 : return results.size() == 1 ? Optional.of(results.get(0)) : Optional.empty();
413 : }
414 :
415 : @Override
416 : public Stream<AccountState> search(AccountState input) {
417 9 : return Stream.of(input);
418 : }
419 :
420 : @Override
421 : public boolean shortCircuitIfNoResults() {
422 1 : return false;
423 : }
424 : }
425 :
426 150 : private class ByDefaultSearch extends StringSearcher {
427 : @Override
428 : public boolean callerMayAssumeCandidatesAreVisible() {
429 31 : return true; // Rely on enforceVisibility from the index.
430 : }
431 :
432 : @Override
433 : protected boolean matches(String input) {
434 31 : return true;
435 : }
436 :
437 : @Override
438 : public Stream<AccountState> search(String input) {
439 : // At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
440 : // up with a reasonable result list.
441 : // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
442 : // more strict here.
443 31 : boolean canSeeSecondaryEmails = false;
444 : try {
445 31 : if (permissionBackend.user(self.get()).test(GlobalPermission.MODIFY_ACCOUNT)) {
446 30 : canSeeSecondaryEmails = true;
447 : }
448 0 : } catch (PermissionBackendException e) {
449 : // remains false
450 31 : }
451 31 : return accountQueryProvider.get().enforceVisibility(true)
452 31 : .byDefault(input, canSeeSecondaryEmails).stream();
453 : }
454 :
455 : @Override
456 : public boolean shortCircuitIfNoResults() {
457 : // In practice this doesn't matter since this is the last searcher in the list, but considered
458 : // on its own, it doesn't necessarily need to be terminal.
459 19 : return false;
460 : }
461 : }
462 :
463 150 : private final ImmutableList<Searcher<?>> nameOrEmailSearchers =
464 150 : ImmutableList.of(
465 : new ByNameAndEmail(),
466 : new ByEmail(),
467 : new FromRealm(),
468 : new ByFullName(),
469 : new ByDefaultSearch());
470 :
471 150 : private final ImmutableList<Searcher<?>> searchers =
472 150 : ImmutableList.<Searcher<?>>builder()
473 150 : .add(new BySelf())
474 150 : .add(new ByExactAccountId())
475 150 : .add(new ByParenthesizedAccountId())
476 150 : .add(new ByUsername())
477 150 : .addAll(nameOrEmailSearchers)
478 150 : .build();
479 :
480 : private final AccountCache accountCache;
481 : private final AccountControl.Factory accountControlFactory;
482 : private final Emails emails;
483 : private final IdentifiedUser.GenericFactory userFactory;
484 : private final Provider<CurrentUser> self;
485 : private final Provider<InternalAccountQuery> accountQueryProvider;
486 : private final Realm realm;
487 : private final String anonymousCowardName;
488 : private final PermissionBackend permissionBackend;
489 :
490 : @Inject
491 : AccountResolver(
492 : AccountCache accountCache,
493 : Emails emails,
494 : AccountControl.Factory accountControlFactory,
495 : IdentifiedUser.GenericFactory userFactory,
496 : Provider<CurrentUser> self,
497 : Provider<InternalAccountQuery> accountQueryProvider,
498 : PermissionBackend permissionBackend,
499 : Realm realm,
500 150 : @AnonymousCowardName String anonymousCowardName) {
501 150 : this.accountCache = accountCache;
502 150 : this.emails = emails;
503 150 : this.accountControlFactory = accountControlFactory;
504 150 : this.userFactory = userFactory;
505 150 : this.self = self;
506 150 : this.accountQueryProvider = accountQueryProvider;
507 150 : this.permissionBackend = permissionBackend;
508 150 : this.realm = realm;
509 150 : this.anonymousCowardName = anonymousCowardName;
510 150 : }
511 :
512 : /**
513 : * Resolves all accounts matching the input string, visible to the current user.
514 : *
515 : * <p>The following input formats are recognized:
516 : *
517 : * <ul>
518 : * <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link
519 : * IdentifiedUser}. In this case, may return exactly one inactive account.
520 : * <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
521 : * account. This case short-circuits if the input matches.
522 : * <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
523 : * case short-circuits if the input matches.
524 : * <li>A username ({@code "username"}).
525 : * <li>A full name and email address ({@code "Full Name <email@example>"}). This case
526 : * short-circuits if the input matches.
527 : * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
528 : * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
529 : * <li>A full name ({@code "Full Name"}).
530 : * <li>As a fallback, a {@link
531 : * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
532 : * boolean, String) default search} against the account index.
533 : * </ul>
534 : *
535 : * @param input input string.
536 : * @return a result describing matching accounts. Never null even if the result set is empty.
537 : * @throws ConfigInvalidException if an error occurs.
538 : * @throws IOException if an error occurs.
539 : */
540 : public Result resolve(String input) throws ConfigInvalidException, IOException {
541 51 : return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::isActive);
542 : }
543 :
544 : public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
545 : throws ConfigInvalidException, IOException {
546 10 : return searchImpl(input, searchers, this::canSeePredicate, accountActivityPredicate);
547 : }
548 :
549 : /**
550 : * As opposed to {@link #resolve}, the returned result includes all inactive accounts for the
551 : * input search.
552 : *
553 : * <p>This can be used to resolve Gerrit Account from email to its {@link
554 : * com.google.gerrit.entities.Account.Id}, to make sure that if {@link Account} with such email
555 : * exists in Gerrit (even inactive), user data (email address) won't be recorded as it is, but
556 : * instead will be stored as a link to the corresponding Gerrit Account.
557 : */
558 : public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException {
559 39 : return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::allVisible);
560 : }
561 :
562 : public Result resolveIncludeInactiveIgnoreVisibility(String input)
563 : throws ConfigInvalidException, IOException {
564 4 : return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::allVisible);
565 : }
566 :
567 : public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
568 35 : return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::isActive);
569 : }
570 :
571 : public Result resolveIgnoreVisibility(
572 : String input, Predicate<AccountState> accountActivityPredicate)
573 : throws ConfigInvalidException, IOException {
574 0 : return searchImpl(input, searchers, this::allVisiblePredicate, accountActivityPredicate);
575 : }
576 :
577 : /**
578 : * Resolves all accounts matching the input string by name or email.
579 : *
580 : * <p>The following input formats are recognized:
581 : *
582 : * <ul>
583 : * <li>A full name and email address ({@code "Full Name <email@example>"}). This case
584 : * short-circuits if the input matches.
585 : * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
586 : * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
587 : * <li>A full name ({@code "Full Name"}).
588 : * <li>As a fallback, a {@link
589 : * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
590 : * boolean, String) default search} against the account index.
591 : * </ul>
592 : *
593 : * @param input input string.
594 : * @return a result describing matching accounts. Never null even if the result set is empty.
595 : * @throws ConfigInvalidException if an error occurs.
596 : * @throws IOException if an error occurs.
597 : * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
598 : * reevaluated.
599 : */
600 : @Deprecated
601 : public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
602 1 : return searchImpl(
603 : input, nameOrEmailSearchers, this::canSeePredicate, AccountResolver::isActive);
604 : }
605 :
606 : /**
607 : * Same as {@link #resolveByNameOrEmail(String)}, but with exact matching for the full name, email
608 : * and full name.
609 : *
610 : * @param input input string.
611 : * @return a result describing matching accounts. Never null even if the result set is empty.
612 : * @throws ConfigInvalidException if an error occurs.
613 : * @throws IOException if an error occurs.
614 : * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
615 : * reevaluated.
616 : */
617 : @Deprecated
618 : public Result resolveByExactNameOrEmail(String input) throws ConfigInvalidException, IOException {
619 5 : return searchImpl(
620 : input,
621 5 : ImmutableList.of(new ByNameAndEmail(), new ByEmail(), new ByFullName(), new ByUsername()),
622 : this::canSeePredicate,
623 : AccountResolver::isActive);
624 : }
625 :
626 : private Predicate<AccountState> canSeePredicate() {
627 61 : return this::canSee;
628 : }
629 :
630 : private boolean canSee(AccountState accountState) {
631 60 : return accountControlFactory.get().canSee(accountState);
632 : }
633 :
634 : private Predicate<AccountState> allVisiblePredicate() {
635 25 : return AccountResolver::allVisible;
636 : }
637 :
638 : /** @param accountState account state for which the visibility should be checked */
639 : private static boolean allVisible(AccountState accountState) {
640 39 : return true;
641 : }
642 :
643 : private static boolean isActive(AccountState accountState) {
644 51 : return accountState.account().isActive();
645 : }
646 :
647 : @VisibleForTesting
648 : Result searchImpl(
649 : String input,
650 : ImmutableList<Searcher<?>> searchers,
651 : Supplier<Predicate<AccountState>> visibilitySupplier,
652 : Predicate<AccountState> accountActivityPredicate)
653 : throws ConfigInvalidException, IOException {
654 71 : visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
655 71 : List<AccountState> inactive = new ArrayList<>();
656 :
657 71 : for (Searcher<?> searcher : searchers) {
658 71 : Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
659 71 : if (!maybeResults.isPresent()) {
660 69 : continue;
661 : }
662 71 : Stream<AccountState> results = maybeResults.get();
663 :
664 71 : if (!searcher.callerMayAssumeCandidatesAreVisible()) {
665 63 : results = results.filter(visibilitySupplier.get());
666 : }
667 :
668 : List<AccountState> list;
669 71 : if (searcher.callerShouldFilterOutInactiveCandidates()) {
670 : // Keep track of all inactive candidates discovered by any searchers. If we end up short-
671 : // circuiting, the inactive list will be discarded.
672 58 : List<AccountState> active = new ArrayList<>();
673 58 : results.forEach(a -> (accountActivityPredicate.test(a) ? active : inactive).add(a));
674 58 : list = active;
675 58 : } else {
676 47 : list = results.collect(toImmutableList());
677 : }
678 :
679 71 : if (!list.isEmpty()) {
680 70 : return createResult(input, list);
681 : }
682 46 : if (searcher.shortCircuitIfNoResults()) {
683 : // For a short-circuiting searcher, return results even if empty.
684 17 : return !inactive.isEmpty() ? emptyResult(input, inactive) : createResult(input, list);
685 : }
686 46 : }
687 20 : return emptyResult(input, inactive);
688 : }
689 :
690 : private Result createResult(String input, List<AccountState> list) {
691 70 : return new Result(input, list, ImmutableList.of());
692 : }
693 :
694 : private Result emptyResult(String input, List<AccountState> inactive) {
695 20 : return new Result(input, ImmutableList.of(), inactive);
696 : }
697 :
698 : private Stream<AccountState> toAccountStates(Set<Account.Id> ids) {
699 43 : return accountCache.get(ids).values().stream();
700 : }
701 : }
|