// 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.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.util.concurrent.CheckedFuture; import com.google.gerrit.common.ChangeHooks; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ChangeUtil; 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.AccountInfo; import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.account.GroupMembers; import com.google.gerrit.server.change.ReviewerJson.PostResult; import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.group.GroupsCollection; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.index.ChangeIndexer; import com.google.gerrit.server.mail.AddReviewerSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.Set; @Singleton public class PostReviewers implements RestModifyView<ChangeResource, AddReviewerInput> { private static final Logger log = LoggerFactory .getLogger(PostReviewers.class); public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10; public static final int DEFAULT_MAX_REVIEWERS = 20; private final AccountsCollection accounts; private final ReviewerResource.Factory reviewerFactory; private final ApprovalsUtil approvalsUtil; private final AddReviewerSender.Factory addReviewerSenderFactory; private final GroupsCollection groupsCollection; private final GroupMembers.Factory groupMembersFactory; private final AccountInfo.Loader.Factory accountLoaderFactory; private final Provider<ReviewDb> dbProvider; private final ChangeUpdate.Factory updateFactory; private final Provider<CurrentUser> currentUser; private final IdentifiedUser.GenericFactory identifiedUserFactory; private final Config cfg; private final ChangeHooks hooks; private final AccountCache accountCache; private final ReviewerJson json; private final ChangeIndexer indexer; @Inject PostReviewers(AccountsCollection accounts, ReviewerResource.Factory reviewerFactory, ApprovalsUtil approvalsUtil, AddReviewerSender.Factory addReviewerSenderFactory, GroupsCollection groupsCollection, GroupMembers.Factory groupMembersFactory, AccountInfo.Loader.Factory accountLoaderFactory, Provider<ReviewDb> db, ChangeUpdate.Factory updateFactory, Provider<CurrentUser> currentUser, IdentifiedUser.GenericFactory identifiedUserFactory, @GerritServerConfig Config cfg, ChangeHooks hooks, AccountCache accountCache, ReviewerJson json, ChangeIndexer indexer) { this.accounts = accounts; this.reviewerFactory = reviewerFactory; this.approvalsUtil = approvalsUtil; this.addReviewerSenderFactory = addReviewerSenderFactory; this.groupsCollection = groupsCollection; this.groupMembersFactory = groupMembersFactory; this.accountLoaderFactory = accountLoaderFactory; this.dbProvider = db; this.updateFactory = updateFactory; this.currentUser = currentUser; this.identifiedUserFactory = identifiedUserFactory; this.cfg = cfg; this.hooks = hooks; this.accountCache = accountCache; this.json = json; this.indexer = indexer; } @Override public PostResult apply(ChangeResource rsrc, AddReviewerInput input) throws AuthException, BadRequestException, UnprocessableEntityException, OrmException, EmailException, IOException { if (input.reviewer == null) { throw new BadRequestException("missing reviewer field"); } try { Account.Id accountId = accounts.parse(input.reviewer).getAccountId(); return putAccount(reviewerFactory.create(rsrc, accountId)); } catch (UnprocessableEntityException e) { try { return putGroup(rsrc, input); } catch (UnprocessableEntityException e2) { throw new UnprocessableEntityException(MessageFormat.format( ChangeMessages.get().reviewerNotFound, input.reviewer)); } } } private PostResult putAccount(ReviewerResource rsrc) throws OrmException, EmailException, IOException { Account.Id id = rsrc.getUser().getAccountId(); ChangeControl control = rsrc.getControl().forUser( identifiedUserFactory.create(id)); PostResult result = new PostResult(); addReviewers(rsrc, result, ImmutableMap.of(id, control)); return result; } private PostResult putGroup(ChangeResource rsrc, AddReviewerInput input) throws BadRequestException, UnprocessableEntityException, OrmException, EmailException, IOException { GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer); PostResult result = new PostResult(); if (!isLegalReviewerGroup(group.getGroupUUID())) { result.error = MessageFormat.format( ChangeMessages.get().groupIsNotAllowed, group.getName()); return result; } Map<Account.Id, ChangeControl> reviewers = Maps.newHashMap(); ChangeControl control = rsrc.getControl(); Set<Account> members; try { members = groupMembersFactory.create(control.getCurrentUser()).listAccounts( group.getGroupUUID(), control.getProject().getNameKey()); } catch (NoSuchGroupException e) { throw new UnprocessableEntityException(e.getMessage()); } catch (NoSuchProjectException e) { throw new BadRequestException(e.getMessage()); } // if maxAllowed is set to 0, it is allowed to add any number of // reviewers int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS); if (maxAllowed > 0 && members.size() > maxAllowed) { result.error = MessageFormat.format( ChangeMessages.get().groupHasTooManyMembers, group.getName()); return result; } // if maxWithoutCheck is set to 0, we never ask for confirmation int maxWithoutConfirmation = cfg.getInt("addreviewer", "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK); if (!input.confirmed() && maxWithoutConfirmation > 0 && members.size() > maxWithoutConfirmation) { result.confirm = true; result.error = MessageFormat.format( ChangeMessages.get().groupManyMembersConfirmation, group.getName(), members.size()); return result; } for (Account member : members) { if (member.isActive()) { IdentifiedUser user = identifiedUserFactory.create(member.getId()); // Does not account for draft status as a user might want to let a // reviewer see a draft. if (control.forUser(user).isRefVisible()) { reviewers.put(user.getAccountId(), control); } } } addReviewers(rsrc, result, reviewers); return result; } private void addReviewers(ChangeResource rsrc, PostResult result, Map<Account.Id, ChangeControl> reviewers) throws OrmException, EmailException, IOException { ReviewDb db = dbProvider.get(); ChangeUpdate update = updateFactory.create(rsrc.getControl()); List<PatchSetApproval> added; db.changes().beginTransaction(rsrc.getChange().getId()); try { ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db); added = approvalsUtil.addReviewers(db, rsrc.getNotes(), update, rsrc.getControl().getLabelTypes(), rsrc.getChange(), reviewers.keySet()); db.commit(); } finally { db.rollback(); } update.commit(); CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(rsrc.getChange().getId()); result.reviewers = Lists.newArrayListWithCapacity(added.size()); for (PatchSetApproval psa : added) { // New reviewers have value 0, don't bother normalizing. result.reviewers.add(json.format( new ReviewerInfo(psa.getAccountId()), reviewers.get(psa.getAccountId()), ImmutableList.of(psa))); } accountLoaderFactory.create(true).fill(result.reviewers); postAdd(rsrc.getChange(), added); indexFuture.checkedGet(); } private void postAdd(Change change, List<PatchSetApproval> added) throws OrmException, EmailException { if (added.isEmpty()) { return; } // Execute hook for added reviewers // PatchSet patchSet = dbProvider.get().patchSets().get(change.currentPatchSetId()); for (PatchSetApproval psa : added) { Account account = accountCache.get(psa.getAccountId()).getAccount(); hooks.doReviewerAddedHook(change, account, patchSet, dbProvider.get()); } // Email the reviewers // // The user knows they added themselves, don't bother emailing them. List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size()); IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get(); for (PatchSetApproval psa : added) { if (!psa.getAccountId().equals(identifiedUser.getAccountId())) { toMail.add(psa.getAccountId()); } } if (!toMail.isEmpty()) { try { AddReviewerSender cm = addReviewerSenderFactory.create(change); cm.setFrom(identifiedUser.getAccountId()); cm.addReviewers(toMail); cm.send(); } catch (Exception err) { log.error("Cannot send email to new reviewers of change " + change.getId(), err); } } } public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) { return !SystemGroupBackend.isSystemGroup(groupUUID); } }