All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.change.SuggestReviewers Maven / Gradle / Ivy

// Copyright (C) 2013 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.change;

import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
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.AccountInfo;
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.config.GerritServerConfig;
import com.google.gerrit.server.group.GroupJson.GroupBaseInfo;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;

import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class SuggestReviewers implements RestReadView {

  private static final String MAX_SUFFIX = "\u9fa5";
  private static final int MAX = 10;

  private final AccountInfo.Loader.Factory accountLoaderFactory;
  private final AccountControl.Factory accountControlFactory;
  private final GroupMembers.Factory groupMembersFactory;
  private final AccountCache accountCache;
  private final Provider dbProvider;
  private final Provider currentUser;
  private final IdentifiedUser.GenericFactory identifiedUserFactory;
  private final GroupBackend groupBackend;
  private final boolean suggestAccounts;
  private final int suggestFrom;
  private final int maxAllowed;
  private int limit;
  private String query;

  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
      usage = "maximum number of reviewers to list")
  public void setLimit(int l) {
    this.limit = l <= 0 ? MAX : Math.min(l, MAX);
  }

  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY",
      usage = "match reviewers query")
  public void setQuery(String q) {
    this.query = q;
  }

  @Inject
  SuggestReviewers(AccountVisibility av,
      AccountInfo.Loader.Factory accountLoaderFactory,
      AccountControl.Factory accountControlFactory,
      AccountCache accountCache,
      GroupMembers.Factory groupMembersFactory,
      IdentifiedUser.GenericFactory identifiedUserFactory,
      Provider currentUser,
      Provider dbProvider,
      @GerritServerConfig Config cfg,
      GroupBackend groupBackend) {
    this.accountLoaderFactory = accountLoaderFactory;
    this.accountControlFactory = accountControlFactory;
    this.accountCache = accountCache;
    this.groupMembersFactory = groupMembersFactory;
    this.dbProvider = dbProvider;
    this.identifiedUserFactory = identifiedUserFactory;
    this.currentUser = currentUser;
    this.groupBackend = groupBackend;

    String suggest = cfg.getString("suggest", null, "accounts");
    if ("OFF".equalsIgnoreCase(suggest)
        || "false".equalsIgnoreCase(suggest)) {
      this.suggestAccounts = false;
    } else {
      this.suggestAccounts = (av != AccountVisibility.NONE);
    }

    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
    this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
        PostReviewers.DEFAULT_MAX_REVIEWERS);
  }

  private interface VisibilityControl {
    boolean isVisibleTo(Account account) throws OrmException;
  }

  @Override
  public List apply(ChangeResource rsrc)
      throws BadRequestException, OrmException, IOException {
    if (Strings.isNullOrEmpty(query)) {
      throw new BadRequestException("missing query field");
    }

    if (!suggestAccounts || query.length() < suggestFrom) {
      return Collections.emptyList();
    }

    VisibilityControl visibilityControl = getVisibility(rsrc);
    List suggestedAccounts = suggestAccount(visibilityControl);
    accountLoaderFactory.create(true).fill(suggestedAccounts);

    List reviewer = Lists.newArrayList();
    for (AccountInfo a : suggestedAccounts) {
      reviewer.add(new SuggestedReviewerInfo(a));
    }

    Project p = rsrc.getControl().getProject();
    for (GroupReference g : suggestAccountGroup(
        rsrc.getControl().getProjectControl())) {
      if (suggestGroupAsReviewer(p, g, visibilityControl)) {
        GroupBaseInfo info = new GroupBaseInfo();
        info.id = Url.encode(g.getUUID().get());
        info.name = g.getName();
        reviewer.add(new SuggestedReviewerInfo(info));
      }
    }

    Collections.sort(reviewer);
    if (reviewer.size() <= limit) {
      return reviewer;
    } else {
      return reviewer.subList(0, limit);
    }
  }

  private VisibilityControl getVisibility(final ChangeResource rsrc) {
    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
      return new VisibilityControl() {
        @Override
        public boolean isVisibleTo(Account account) throws OrmException {
          return true;
        }
      };
    } else {
      return new VisibilityControl() {
        @Override
        public boolean isVisibleTo(Account account) throws OrmException {
          IdentifiedUser who =
              identifiedUserFactory.create(dbProvider, account.getId());
          // we can't use changeControl directly as it won't suggest reviewers
          // to drafts
          return rsrc.getControl().forUser(who).isRefVisible();
        }
      };
    }
  }

  private List suggestAccountGroup(ProjectControl ctl) {
    return Lists.newArrayList(
        Iterables.limit(groupBackend.suggest(query, ctl), limit));
  }

  private List suggestAccount(VisibilityControl visibilityControl)
      throws OrmException {
    String a = query;
    String b = a + MAX_SUFFIX;

    LinkedHashMap r = Maps.newLinkedHashMap();
    for (Account p : dbProvider.get().accounts()
        .suggestByFullName(a, b, limit)) {
      addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
    }

    if (r.size() < limit) {
      for (Account p : dbProvider.get().accounts()
          .suggestByPreferredEmail(a, b, limit - r.size())) {
        addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
      }
    }

    if (r.size() < limit) {
      for (AccountExternalId e : dbProvider.get().accountExternalIds()
          .suggestByEmailAddress(a, b, limit - r.size())) {
        if (!r.containsKey(e.getAccountId())) {
          Account p = accountCache.get(e.getAccountId()).getAccount();
          AccountInfo info = new AccountInfo(p.getId());
          addSuggestion(r, p, info, visibilityControl);
        }
      }
    }

    return Lists.newArrayList(r.values());
  }

  private void addSuggestion(Map map, Account account,
      AccountInfo info, VisibilityControl visibilityControl)
      throws OrmException {
    if (!map.containsKey(account.getId())
        && account.isActive()
        // Can the suggestion see the change?
        && visibilityControl.isVisibleTo(account)
        // Can the account see the current user?
        && accountControlFactory.get().canSee(account)) {
      map.put(account.getId(), info);
    }
  }

  private boolean suggestGroupAsReviewer(Project project,
      GroupReference group, VisibilityControl visibilityControl)
      throws OrmException, IOException {
    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
      return false;
    }

    try {
      Set members = groupMembersFactory
          .create(currentUser.get())
          .listAccounts(group.getUUID(), project.getNameKey());

      if (members.isEmpty()) {
        return false;
      }

      if (maxAllowed > 0 && members.size() > maxAllowed) {
        return false;
      }

      // require that at least one member in the group can see the change
      for (Account account : members) {
        if (visibilityControl.isVisibleTo(account)) {
          return true;
        }
      }
    } catch (NoSuchGroupException e) {
      return false;
    } catch (NoSuchProjectException e) {
      return false;
    }

    return false;
  }

  public static class SuggestedReviewerInfo implements Comparable {
    public AccountInfo account;
    public GroupBaseInfo group;

    SuggestedReviewerInfo(AccountInfo a) {
      this.account = a;
    }

    SuggestedReviewerInfo(GroupBaseInfo g) {
      this.group = g;
    }

    @Override
    public int compareTo(SuggestedReviewerInfo o) {
      return getSortValue().compareTo(o.getSortValue());
    }

    private String getSortValue() {
      return account != null
          ? Objects.firstNonNull(account.email,
              Strings.nullToEmpty(account.name))
          : Strings.nullToEmpty(group.name);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy