// Copyright (C) 2009 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.patch; import com.google.gerrit.common.ChangeHookRunner; import com.google.gerrit.common.data.ApprovalType; import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.ApprovalCategoryValue; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.ChangeMessage; import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.PatchSetApproval; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.mail.CommentSender; import com.google.gerrit.server.mail.EmailException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.workflow.FunctionState; import com.google.gwtjsonrpc.client.VoidResult; import com.google.gwtorm.client.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; public class PublishComments implements Callable<VoidResult> { private static final Logger log = LoggerFactory.getLogger(PublishComments.class); public interface Factory { PublishComments create(PatchSet.Id patchSetId, String messageText, Set<ApprovalCategoryValue.Id> approvals); } private final ReviewDb db; private final IdentifiedUser user; private final ApprovalTypes types; private final CommentSender.Factory commentSenderFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final ChangeControl.Factory changeControlFactory; private final FunctionState.Factory functionStateFactory; private final ChangeHookRunner hooks; private final PatchSet.Id patchSetId; private final String messageText; private final Set<ApprovalCategoryValue.Id> approvals; private Change change; private PatchSet patchSet; private ChangeMessage message; private List<PatchLineComment> drafts; @Inject PublishComments(final ReviewDb db, final IdentifiedUser user, final ApprovalTypes approvalTypes, final CommentSender.Factory commentSenderFactory, final PatchSetInfoFactory patchSetInfoFactory, final ChangeControl.Factory changeControlFactory, final FunctionState.Factory functionStateFactory, final ChangeHookRunner hooks, @Assisted final PatchSet.Id patchSetId, @Assisted final String messageText, @Assisted final Set<ApprovalCategoryValue.Id> approvals) { this.db = db; this.user = user; this.types = approvalTypes; this.patchSetInfoFactory = patchSetInfoFactory; this.commentSenderFactory = commentSenderFactory; this.changeControlFactory = changeControlFactory; this.functionStateFactory = functionStateFactory; this.hooks = hooks; this.patchSetId = patchSetId; this.messageText = messageText; this.approvals = approvals; } @Override public VoidResult call() throws NoSuchChangeException, OrmException { final Change.Id changeId = patchSetId.getParentKey(); final ChangeControl ctl = changeControlFactory.validateFor(changeId); change = ctl.getChange(); patchSet = db.patchSets().get(patchSetId); if (patchSet == null) { throw new NoSuchChangeException(changeId); } drafts = drafts(); publishDrafts(); final boolean isCurrent = patchSetId.equals(change.currentPatchSetId()); if (isCurrent && change.getStatus().isOpen()) { publishApprovals(); } else { publishMessageOnly(); } touchChange(); email(); fireHook(); return VoidResult.INSTANCE; } private void publishDrafts() throws OrmException { for (final PatchLineComment c : drafts) { c.setStatus(PatchLineComment.Status.PUBLISHED); c.updated(); } db.patchComments().update(drafts); } private void publishApprovals() throws OrmException { ChangeUtil.updated(change); final Set<ApprovalCategory.Id> dirty = new HashSet<ApprovalCategory.Id>(); final List<PatchSetApproval> ins = new ArrayList<PatchSetApproval>(); final List<PatchSetApproval> upd = new ArrayList<PatchSetApproval>(); final Collection<PatchSetApproval> all = db.patchSetApprovals().byPatchSet(patchSetId).toList(); final Map<ApprovalCategory.Id, PatchSetApproval> mine = mine(all); // Ensure any new approvals are stored properly. // for (final ApprovalCategoryValue.Id want : approvals) { PatchSetApproval a = mine.get(want.getParentKey()); if (a == null) { a = new PatchSetApproval(new PatchSetApproval.Key(// patchSetId, user.getAccountId(), want.getParentKey()), want.get()); a.cache(change); ins.add(a); all.add(a); mine.put(a.getCategoryId(), a); dirty.add(a.getCategoryId()); } } // Normalize all of the items the user is changing. // final FunctionState functionState = functionStateFactory.create(change, patchSetId, all); for (final ApprovalCategoryValue.Id want : approvals) { final PatchSetApproval a = mine.get(want.getParentKey()); final short o = a.getValue(); a.setValue(want.get()); a.cache(change); functionState.normalize(types.getApprovalType(a.getCategoryId()), a); if (o != a.getValue()) { // Value changed, ensure we update the database. // a.setGranted(); dirty.add(a.getCategoryId()); } if (!ins.contains(a)) { upd.add(a); } } // Format a message explaining the actions taken. // final StringBuilder msgbuf = new StringBuilder(); for (final ApprovalType at : types.getApprovalTypes()) { if (dirty.contains(at.getCategory().getId())) { final PatchSetApproval a = mine.get(at.getCategory().getId()); if (a.getValue() == 0 && ins.contains(a)) { // Don't say "no score" for an initial entry. continue; } final ApprovalCategoryValue val = at.getValue(a); if (msgbuf.length() > 0) { msgbuf.append("; "); } if (val != null && val.getName() != null && !val.getName().isEmpty()) { msgbuf.append(val.getName()); } else { msgbuf.append(at.getCategory().getName()); msgbuf.append(" "); if (a.getValue() > 0) msgbuf.append('+'); msgbuf.append(a.getValue()); } } } // Update dashboards for everyone else. // for (PatchSetApproval a : all) { if (!user.getAccountId().equals(a.getAccountId())) { a.cache(change); upd.add(a); } } db.patchSetApprovals().update(upd); db.patchSetApprovals().insert(ins); summarizeInlineComments(msgbuf); message(msgbuf.toString()); } private void publishMessageOnly() throws OrmException { StringBuilder msgbuf = new StringBuilder(); summarizeInlineComments(msgbuf); message(msgbuf.toString()); } private void message(String actions) throws OrmException { if ((actions == null || actions.isEmpty()) && (messageText == null || messageText.isEmpty())) { // They had nothing to say? // return; } final StringBuilder msgbuf = new StringBuilder(); msgbuf.append("Patch Set " + patchSetId.get() + ":"); if (actions != null && !actions.isEmpty()) { msgbuf.append(" "); msgbuf.append(actions); } msgbuf.append("\n\n"); msgbuf.append(messageText != null ? messageText : ""); message = new ChangeMessage(new ChangeMessage.Key(change.getId(),// ChangeUtil.messageUUID(db)), user.getAccountId()); message.setMessage(msgbuf.toString()); db.changeMessages().insert(Collections.singleton(message)); } private Map<ApprovalCategory.Id, PatchSetApproval> mine( Collection<PatchSetApproval> all) { Map<ApprovalCategory.Id, PatchSetApproval> r = new HashMap<ApprovalCategory.Id, PatchSetApproval>(); for (PatchSetApproval a : all) { if (user.getAccountId().equals(a.getAccountId())) { r.put(a.getCategoryId(), a); } } return r; } private void touchChange() { try { ChangeUtil.touch(change, db); } catch (OrmException e) { } } private List<PatchLineComment> drafts() throws OrmException { return db.patchComments().draft(patchSetId, user.getAccountId()).toList(); } private void email() { try { final CommentSender cm = commentSenderFactory.create(change); cm.setFrom(user.getAccountId()); cm.setPatchSet(patchSet, patchSetInfoFactory.get(patchSetId)); cm.setChangeMessage(message); cm.setPatchLineComments(drafts); cm.send(); } catch (EmailException e) { log.error("Cannot send comments by email for patch set " + patchSetId, e); } catch (PatchSetInfoNotAvailableException e) { log.error("Failed to obtain PatchSetInfo for patch set " + patchSetId, e); } } private void fireHook() { final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> changed = new HashMap<ApprovalCategory.Id, ApprovalCategoryValue.Id>(); for (ApprovalCategoryValue.Id v : approvals) { changed.put(v.getParentKey(), v); } hooks.doCommentAddedHook(change, user.getAccount(), patchSet, messageText, changed); } private void summarizeInlineComments(StringBuilder in) { if (!drafts.isEmpty()) { if (in.length() != 0) { in.append("\n\n"); } if (drafts.size() == 1) { in.append("(1 inline comment)"); } else { in.append("(" + drafts.size() + " inline comments)"); } } } }