// Copyright (C) 2008 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.httpd.rpc; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.AccountInfo; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.data.ReviewerInfo; import com.google.gerrit.common.data.SuggestService; import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountVisibility; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupMembers; import com.google.gerrit.server.change.PostReviewers; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import org.eclipse.jgit.lib.Config; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; class SuggestServiceImpl extends BaseServiceImplementation implements SuggestService { private static final String MAX_SUFFIX = "\u9fa5"; private final Provider<ReviewDb> reviewDbProvider; private final AccountCache accountCache; private final GroupMembers.Factory groupMembersFactory; private final IdentifiedUser.GenericFactory identifiedUserFactory; private final AccountControl.Factory accountControlFactory; private final ChangeControl.Factory changeControlFactory; private final ProjectControl.Factory projectControlFactory; private final Config cfg; private final GroupBackend groupBackend; private final boolean suggestAccounts; @Inject SuggestServiceImpl(final Provider<ReviewDb> schema, final AccountCache accountCache, final GroupMembers.Factory groupMembersFactory, final Provider<CurrentUser> currentUser, final IdentifiedUser.GenericFactory identifiedUserFactory, final AccountControl.Factory accountControlFactory, final ChangeControl.Factory changeControlFactory, final ProjectControl.Factory projectControlFactory, @GerritServerConfig final Config cfg, final GroupBackend groupBackend) { super(schema, currentUser); this.reviewDbProvider = schema; this.accountCache = accountCache; this.groupMembersFactory = groupMembersFactory; this.identifiedUserFactory = identifiedUserFactory; this.accountControlFactory = accountControlFactory; this.changeControlFactory = changeControlFactory; this.projectControlFactory = projectControlFactory; this.cfg = cfg; this.groupBackend = groupBackend; if ("OFF".equals(cfg.getString("suggest", null, "accounts"))) { this.suggestAccounts = false; } else { boolean suggestAccounts; try { AccountVisibility av = cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL); suggestAccounts = (av != AccountVisibility.NONE); } catch (IllegalArgumentException err) { suggestAccounts = cfg.getBoolean("suggest", null, "accounts", true); } this.suggestAccounts = suggestAccounts; } } private interface VisibilityControl { boolean isVisible(Account account) throws OrmException; } public void suggestAccount(final String query, final Boolean active, final int limit, final AsyncCallback<List<AccountInfo>> callback) { run(callback, new Action<List<AccountInfo>>() { public List<AccountInfo> run(final ReviewDb db) throws OrmException { return suggestAccount(db, query, active, limit, new VisibilityControl() { @Override public boolean isVisible(Account account) throws OrmException { return accountControlFactory.get().canSee(account); } }); } }); } private List<AccountInfo> suggestAccount(final ReviewDb db, final String query, final Boolean active, final int limit, VisibilityControl visibilityControl) throws OrmException { if (!suggestAccounts) { return Collections.emptyList(); } final String a = query; final String b = a + MAX_SUFFIX; final int max = 10; final int n = limit <= 0 ? max : Math.min(limit, max); LinkedHashMap<Account.Id, AccountInfo> r = new LinkedHashMap<>(); for (final Account p : db.accounts().suggestByFullName(a, b, n)) { addSuggestion(r, p, new AccountInfo(p), active, visibilityControl); } if (r.size() < n) { for (final Account p : db.accounts().suggestByPreferredEmail(a, b, n - r.size())) { addSuggestion(r, p, new AccountInfo(p), active, visibilityControl); } } if (r.size() < n) { for (final AccountExternalId e : db.accountExternalIds() .suggestByEmailAddress(a, b, n - r.size())) { if (!r.containsKey(e.getAccountId())) { final Account p = accountCache.get(e.getAccountId()).getAccount(); final AccountInfo info = new AccountInfo(p); info.setPreferredEmail(e.getEmailAddress()); addSuggestion(r, p, info, active, visibilityControl); } } } return new ArrayList<>(r.values()); } private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account, AccountInfo info, Boolean active, VisibilityControl visibilityControl) throws OrmException { if (map.containsKey(account.getId())) { return; } if (active != null && active != account.isActive()) { return; } if (visibilityControl.isVisible(account)) { map.put(account.getId(), info); } } public void suggestAccountGroup(final String query, final int limit, final AsyncCallback<List<GroupReference>> callback) { suggestAccountGroupForProject(null, query, limit, callback); } public void suggestAccountGroupForProject(final Project.NameKey project, final String query, final int limit, final AsyncCallback<List<GroupReference>> callback) { run(callback, new Action<List<GroupReference>>() { public List<GroupReference> run(final ReviewDb db) { ProjectControl projectControl = null; if (project != null) { try { projectControl = projectControlFactory.controlFor(project); } catch (NoSuchProjectException e) { return Collections.emptyList(); } } return suggestAccountGroup(projectControl, query, limit); } }); } private List<GroupReference> suggestAccountGroup( @Nullable final ProjectControl projectControl, final String query, final int limit) { return Lists.newArrayList(Iterables.limit( groupBackend.suggest(query, projectControl), limit <= 0 ? 10 : Math.min(limit, 10))); } @Override public void suggestReviewer(Project.NameKey project, String query, int limit, AsyncCallback<List<ReviewerInfo>> callback) { // The RPC is deprecated, but return an empty list for RPC API compatibility. callback.onSuccess(Collections.<ReviewerInfo>emptyList()); } @Override public void suggestChangeReviewer(final Change.Id change, final String query, final int limit, final AsyncCallback<List<ReviewerInfo>> callback) { run(callback, new Action<List<ReviewerInfo>>() { public List<ReviewerInfo> run(final ReviewDb db) throws OrmException, Failure { final ChangeControl changeControl; try { changeControl = changeControlFactory.controlFor(change); } catch (NoSuchChangeException e) { return Collections.emptyList(); } VisibilityControl visibilityControl; if (changeControl.getRefControl().isVisibleByRegisteredUsers()) { visibilityControl = new VisibilityControl() { @Override public boolean isVisible(Account account) throws OrmException { return true; } }; } else { visibilityControl = new VisibilityControl() { @Override public boolean isVisible(Account account) throws OrmException { IdentifiedUser who = identifiedUserFactory.create(reviewDbProvider, account.getId()); // we can't use changeControl directly as it won't suggest reviewers // to drafts return changeControl.forUser(who).isRefVisible(); } }; } final List<AccountInfo> suggestedAccounts = suggestAccount(db, query, Boolean.TRUE, limit, visibilityControl); final List<ReviewerInfo> reviewer = new ArrayList<>(suggestedAccounts.size()); for (final AccountInfo a : suggestedAccounts) { reviewer.add(new ReviewerInfo(a)); } final List<GroupReference> suggestedAccountGroups = suggestAccountGroup(changeControl.getProjectControl(), query, limit); for (final GroupReference g : suggestedAccountGroups) { if (suggestGroupAsReviewer(changeControl.getProject().getNameKey(), g)) { reviewer.add(new ReviewerInfo(g)); } } Collections.sort(reviewer); if (reviewer.size() <= limit) { return reviewer; } else { return reviewer.subList(0, limit); } } }); } private boolean suggestGroupAsReviewer(final Project.NameKey project, final GroupReference group) throws OrmException, Failure { if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) { return false; } try { final Set<Account> members = groupMembersFactory.create(getCurrentUser()) .listAccounts(group.getUUID(), project); if (members.isEmpty()) { return false; } final int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", PostReviewers.DEFAULT_MAX_REVIEWERS); if (maxAllowed > 0 && members.size() > maxAllowed) { return false; } } catch (NoSuchGroupException e) { return false; } catch (NoSuchProjectException e) { return false; } catch (IOException e) { throw new Failure(e); } return true; } }