// 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.server.account.AccountDirectory.FillOptions; import com.google.gerrit.server.api.accounts.AccountInfoComparator; import com.google.gerrit.server.config.GerritServerConfig; 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 java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jgit.lib.Config; import org.kohsuke.args4j.Option; public class QueryAccounts implements RestReadView<TopLevelResource> { private static final int MAX_SUGGEST_RESULTS = 100; private final AccountLoader.Factory accountLoaderFactory; private final AccountQueryBuilder queryBuilder; private final AccountQueryProcessor queryProcessor; 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( AccountLoader.Factory accountLoaderFactory, AccountQueryBuilder queryBuilder, AccountQueryProcessor queryProcessor, @GerritServerConfig Config cfg) { this.accountLoaderFactory = accountLoaderFactory; this.queryBuilder = queryBuilder; this.queryProcessor = queryProcessor; 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(); } Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID); 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); 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.defaultQuery(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()); } } }