Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.change;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static com.google.common.collect.ImmutableList.toImmutableList;
20 : import static com.google.gerrit.extensions.client.ReviewerState.CC;
21 : import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
22 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
23 : import static java.util.Objects.requireNonNull;
24 : import static java.util.stream.Collectors.toList;
25 :
26 : import com.google.common.collect.ImmutableList;
27 : import com.google.common.collect.ImmutableSet;
28 : import com.google.common.collect.Lists;
29 : import com.google.common.collect.Streams;
30 : import com.google.gerrit.entities.Account;
31 : import com.google.gerrit.entities.Address;
32 : import com.google.gerrit.entities.Change;
33 : import com.google.gerrit.entities.PatchSetApproval;
34 : import com.google.gerrit.extensions.client.ReviewerState;
35 : import com.google.gerrit.extensions.restapi.RestApiException;
36 : import com.google.gerrit.server.PatchSetUtil;
37 : import com.google.gerrit.server.account.AccountCache;
38 : import com.google.gerrit.server.account.AccountState;
39 : import com.google.gerrit.server.approval.ApprovalsUtil;
40 : import com.google.gerrit.server.extensions.events.ReviewerAdded;
41 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
42 : import com.google.gerrit.server.project.ProjectCache;
43 : import com.google.gerrit.server.update.ChangeContext;
44 : import com.google.gerrit.server.update.PostUpdateContext;
45 : import com.google.inject.Inject;
46 : import com.google.inject.assistedinject.Assisted;
47 : import java.io.IOException;
48 : import java.util.Collection;
49 : import java.util.List;
50 : import java.util.Set;
51 :
52 : public class AddReviewersOp extends ReviewerOp {
53 : public interface Factory {
54 :
55 : /**
56 : * Create a new op.
57 : *
58 : * <p>Users may be added by account or by email addresses, as determined by {@code accountIds}
59 : * and {@code addresses}. The reviewer state for both accounts and email addresses is determined
60 : * by {@code state}.
61 : *
62 : * @param accountIds account IDs to add.
63 : * @param addresses email addresses to add.
64 : * @param state resulting reviewer state.
65 : * @param forGroup whether this reviewer addition adds accounts for a group
66 : * @return batch update operation.
67 : */
68 : AddReviewersOp create(
69 : Set<Account.Id> accountIds,
70 : Collection<Address> addresses,
71 : ReviewerState state,
72 : boolean forGroup);
73 : }
74 :
75 : private final ApprovalsUtil approvalsUtil;
76 : private final PatchSetUtil psUtil;
77 : private final ReviewerAdded reviewerAdded;
78 : private final AccountCache accountCache;
79 : private final ProjectCache projectCache;
80 : private final ModifyReviewersEmail modifyReviewersEmail;
81 : private final Set<Account.Id> accountIds;
82 : private final Collection<Address> addresses;
83 : private final ReviewerState state;
84 : private final boolean forGroup;
85 :
86 : // Unlike addedCCs, addedReviewers is a PatchSetApproval because the ReviewerResult returned
87 : // via the REST API is supposed to include vote information.
88 47 : private List<PatchSetApproval> addedReviewers = ImmutableList.of();
89 47 : private Collection<Address> addedReviewersByEmail = ImmutableList.of();
90 47 : private Collection<Account.Id> addedCCs = ImmutableList.of();
91 47 : private Collection<Address> addedCCsByEmail = ImmutableList.of();
92 :
93 : private Change change;
94 :
95 : @Inject
96 : AddReviewersOp(
97 : ApprovalsUtil approvalsUtil,
98 : PatchSetUtil psUtil,
99 : ReviewerAdded reviewerAdded,
100 : AccountCache accountCache,
101 : ProjectCache projectCache,
102 : ModifyReviewersEmail modifyReviewersEmail,
103 : @Assisted Set<Account.Id> accountIds,
104 : @Assisted Collection<Address> addresses,
105 : @Assisted ReviewerState state,
106 47 : @Assisted boolean forGroup) {
107 47 : checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
108 47 : this.approvalsUtil = approvalsUtil;
109 47 : this.psUtil = psUtil;
110 47 : this.reviewerAdded = reviewerAdded;
111 47 : this.accountCache = accountCache;
112 47 : this.projectCache = projectCache;
113 47 : this.modifyReviewersEmail = modifyReviewersEmail;
114 :
115 47 : this.accountIds = accountIds;
116 47 : this.addresses = addresses;
117 47 : this.state = state;
118 47 : this.forGroup = forGroup;
119 47 : }
120 :
121 : @Override
122 : public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
123 47 : change = ctx.getChange();
124 47 : if (!accountIds.isEmpty()) {
125 42 : if (state == CC) {
126 29 : addedCCs =
127 29 : approvalsUtil.addCcs(
128 29 : ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds, forGroup);
129 : } else {
130 33 : addedReviewers =
131 33 : approvalsUtil.addReviewers(
132 33 : ctx.getNotes(),
133 33 : ctx.getUpdate(change.currentPatchSetId()),
134 : projectCache
135 33 : .get(change.getProject())
136 33 : .orElseThrow(illegalState(change.getProject()))
137 33 : .getLabelTypes(change.getDest()),
138 : change,
139 : accountIds);
140 : }
141 : }
142 :
143 47 : ReviewerStateInternal internalState = ReviewerStateInternal.fromReviewerState(state);
144 :
145 : // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does.
146 47 : ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState);
147 47 : ImmutableList<Address> addressesToAdd =
148 47 : addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList());
149 :
150 47 : if (state == CC) {
151 39 : addedCCsByEmail = addressesToAdd;
152 : } else {
153 33 : addedReviewersByEmail = addressesToAdd;
154 : }
155 47 : for (Address a : addressesToAdd) {
156 10 : ctx.getUpdate(change.currentPatchSetId()).putReviewerByEmail(a, internalState);
157 10 : }
158 :
159 47 : if (addedCCs.isEmpty() && addedReviewers.isEmpty() && addressesToAdd.isEmpty()) {
160 33 : return false;
161 : }
162 :
163 42 : checkAdded();
164 :
165 42 : if (patchSet == null) {
166 33 : patchSet = requireNonNull(psUtil.current(ctx.getNotes()));
167 : }
168 42 : return true;
169 : }
170 :
171 : private void checkAdded() {
172 : // Should only affect either reviewers or CCs, not both. But the logic in updateChange is
173 : // complex, so programmer error is conceivable.
174 42 : boolean addedAnyReviewers = !addedReviewers.isEmpty() || !addedReviewersByEmail.isEmpty();
175 42 : boolean addedAnyCCs = !addedCCs.isEmpty() || !addedCCsByEmail.isEmpty();
176 42 : checkState(
177 : !(addedAnyReviewers && addedAnyCCs),
178 : "should not have added both reviewers and CCs:\n"
179 : + "Arguments:\n"
180 : + " accountIds=%s\n"
181 : + " addresses=%s\n"
182 : + "Results:\n"
183 : + " addedReviewers=%s\n"
184 : + " addedReviewersByEmail=%s\n"
185 : + " addedCCs=%s\n"
186 : + " addedCCsByEmail=%s",
187 : accountIds,
188 : addresses,
189 : addedReviewers,
190 : addedReviewersByEmail,
191 : addedCCs,
192 : addedCCsByEmail);
193 42 : }
194 :
195 : @Override
196 : public void postUpdate(PostUpdateContext ctx) throws Exception {
197 47 : opResult =
198 47 : Result.builder()
199 47 : .setAddedReviewers(addedReviewers)
200 47 : .setAddedReviewersByEmail(addedReviewersByEmail)
201 47 : .setAddedCCs(addedCCs)
202 47 : .setAddedCCsByEmail(addedCCsByEmail)
203 47 : .build();
204 47 : if (sendEmail) {
205 31 : modifyReviewersEmail.emailReviewersAsync(
206 31 : ctx.getUser().asIdentifiedUser(),
207 : change,
208 31 : Lists.transform(addedReviewers, PatchSetApproval::accountId),
209 : addedCCs,
210 31 : ImmutableSet.of(),
211 : addedReviewersByEmail,
212 : addedCCsByEmail,
213 31 : ImmutableSet.of(),
214 31 : ctx.getNotify(change.getId()));
215 : }
216 47 : if (!addedReviewers.isEmpty()) {
217 33 : List<AccountState> reviewers =
218 33 : addedReviewers.stream()
219 33 : .map(r -> accountCache.get(r.accountId()))
220 33 : .flatMap(Streams::stream)
221 33 : .collect(toList());
222 33 : eventSender =
223 : () ->
224 33 : reviewerAdded.fire(
225 33 : ctx.getChangeData(change), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
226 33 : if (sendEvent) {
227 33 : sendEvent();
228 : }
229 : }
230 47 : }
231 : }
|