blob: 30534b5add0a0e2a85f70004ab995677602e0757 [file] [log] [blame]
// 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.restapi.account;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.account.AccountPredicates;
import com.google.gerrit.server.query.account.AccountQueryBuilder;
import com.google.gerrit.server.query.account.AccountQueryProcessor;
import com.google.inject.Inject;
import com.google.inject.Provider;
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;
/**
* REST endpoint to query accounts.
*
* <p>This REST endpoint handles {@code GET /accounts/} requests.
*
* <p>The account queries are parsed by {@link AccountQueryBuilder} and executed by {@link
* AccountQueryProcessor}.
*/
public class QueryAccounts implements RestReadView<TopLevelResource> {
private static final int MAX_SUGGEST_RESULTS = 100;
private final PermissionBackend permissionBackend;
private final AccountLoader.Factory accountLoaderFactory;
private final AccountQueryBuilder queryBuilder;
private final Provider<AccountQueryProcessor> queryProcessorProvider;
private final boolean suggestConfig;
private final int suggestFrom;
private AccountLoader accountLoader;
private boolean suggest;
private Integer limit;
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) {
this.limit = 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) throws BadRequestException {
options.addAll(ListOption.fromHexString(ListAccountsOption.class, hex));
}
@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(
PermissionBackend permissionBackend,
AccountLoader.Factory accountLoaderFactory,
AccountQueryBuilder queryBuilder,
Provider<AccountQueryProcessor> queryProcessorProvider,
@GerritServerConfig Config cfg) {
this.permissionBackend = permissionBackend;
this.accountLoaderFactory = accountLoaderFactory;
this.queryBuilder = queryBuilder;
this.queryProcessorProvider = queryProcessorProvider;
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 Response<List<AccountInfo>> apply(TopLevelResource rsrc)
throws RestApiException, PermissionBackendException {
if (Strings.isNullOrEmpty(query)) {
throw new BadRequestException("missing query field");
}
if (suggest && (!suggestConfig || query.length() < suggestFrom)) {
return Response.ok(Collections.emptyList());
}
Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID);
if (options.contains(ListAccountsOption.DETAILS)) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
}
boolean modifyAccountCapabilityChecked = false;
if (options.contains(ListAccountsOption.ALL_EMAILS)) {
permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
modifyAccountCapabilityChecked = true;
fillOptions.add(FillOptions.EMAIL);
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
if (suggest) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
fillOptions.add(FillOptions.EMAIL);
if (modifyAccountCapabilityChecked) {
fillOptions.add(FillOptions.SECONDARY_EMAILS);
} else {
if (permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
}
}
accountLoader = accountLoaderFactory.create(fillOptions);
AccountQueryProcessor queryProcessor = queryProcessorProvider.get();
if (queryProcessor.isDisabled()) {
throw new MethodNotAllowedException("query disabled");
}
if (limit != null) {
queryProcessor.setUserProvidedLimit(limit);
}
if (start != null) {
if (start < 0) {
throw new BadRequestException("'start' parameter cannot be less than zero");
}
queryProcessor.setStart(start);
}
Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
try {
Predicate<AccountState> queryPred;
if (suggest) {
queryPred = queryBuilder.defaultQuery(query);
queryProcessor.setUserProvidedLimit(suggestLimit);
} else {
queryPred = queryBuilder.parse(query);
}
if (!AccountPredicates.hasActive(queryPred)) {
// if neither 'is:active' nor 'is:inactive' appears in the query only
// active accounts should be queried
queryPred = AccountPredicates.andActive(queryPred);
}
QueryResult<AccountState> result = queryProcessor.query(queryPred);
for (AccountState accountState : result.entities()) {
Account.Id id = accountState.account().id();
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 Response.ok(sorted);
} catch (QueryParseException e) {
if (suggest) {
return Response.ok(ImmutableList.of());
}
throw new BadRequestException(e.getMessage());
}
}
}