Line data Source code
1 : // Copyright (C) 2016 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.mail.send;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
19 : import static java.util.stream.Collectors.toList;
20 :
21 : import com.google.common.base.Strings;
22 : import com.google.common.base.Supplier;
23 : import com.google.common.base.Suppliers;
24 : import com.google.common.collect.ImmutableList;
25 : import com.google.common.flogger.FluentLogger;
26 : import com.google.gerrit.common.Nullable;
27 : import com.google.gerrit.common.data.FilenameComparator;
28 : import com.google.gerrit.entities.Account;
29 : import com.google.gerrit.entities.Change;
30 : import com.google.gerrit.entities.Comment;
31 : import com.google.gerrit.entities.HumanComment;
32 : import com.google.gerrit.entities.NotifyConfig.NotifyType;
33 : import com.google.gerrit.entities.Patch;
34 : import com.google.gerrit.entities.Project;
35 : import com.google.gerrit.entities.RobotComment;
36 : import com.google.gerrit.entities.SubmitRequirement;
37 : import com.google.gerrit.entities.SubmitRequirementResult;
38 : import com.google.gerrit.exceptions.EmailException;
39 : import com.google.gerrit.exceptions.NoSuchEntityException;
40 : import com.google.gerrit.exceptions.StorageException;
41 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
42 : import com.google.gerrit.mail.MailHeader;
43 : import com.google.gerrit.mail.MailProcessingUtil;
44 : import com.google.gerrit.server.CommentsUtil;
45 : import com.google.gerrit.server.config.GerritServerConfig;
46 : import com.google.gerrit.server.mail.receive.Protocol;
47 : import com.google.gerrit.server.patch.PatchFile;
48 : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
49 : import com.google.gerrit.server.util.LabelVote;
50 : import com.google.inject.Inject;
51 : import com.google.inject.assistedinject.Assisted;
52 : import java.io.IOException;
53 : import java.time.ZoneId;
54 : import java.time.ZonedDateTime;
55 : import java.util.ArrayList;
56 : import java.util.Collections;
57 : import java.util.Comparator;
58 : import java.util.HashMap;
59 : import java.util.HashSet;
60 : import java.util.List;
61 : import java.util.Map;
62 : import java.util.Optional;
63 : import org.apache.james.mime4j.dom.field.FieldName;
64 : import org.eclipse.jgit.lib.Config;
65 : import org.eclipse.jgit.lib.ObjectId;
66 : import org.eclipse.jgit.lib.Repository;
67 :
68 : /** Send comments, after the author of them hit used Publish Comments in the UI. */
69 : public class CommentSender extends ReplyToChangeSender {
70 :
71 66 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
72 :
73 : public interface Factory {
74 :
75 : CommentSender create(
76 : Project.NameKey project,
77 : Change.Id changeId,
78 : ObjectId preUpdateMetaId,
79 : Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults);
80 : }
81 :
82 23 : private class FileCommentGroup {
83 :
84 : public String filename;
85 : public int patchSetId;
86 : public PatchFile fileData;
87 23 : public List<Comment> comments = new ArrayList<>();
88 :
89 : /** Returns a web link to a comment for a change. */
90 : @Nullable
91 : public String getCommentLink(String uuid) {
92 22 : return args.urlFormatter.get().getInlineCommentView(change, uuid).orElse(null);
93 : }
94 :
95 : /** Returns a web link to the comment tab view of a change. */
96 : @Nullable
97 : public String getCommentsTabLink() {
98 4 : return args.urlFormatter.get().getCommentsTabView(change).orElse(null);
99 : }
100 :
101 : /** Returns a web link to the findings tab view of a change. */
102 : @Nullable
103 : public String getFindingsTabLink() {
104 1 : return args.urlFormatter.get().getFindingsTabView(change).orElse(null);
105 : }
106 :
107 : /**
108 : * Returns a title for the group, i.e. "Commit Message", "Merge List", or "File [[filename]]".
109 : */
110 : public String getTitle() {
111 23 : if (Patch.COMMIT_MSG.equals(filename)) {
112 10 : return "Commit Message";
113 17 : } else if (Patch.MERGE_LIST.equals(filename)) {
114 1 : return "Merge List";
115 17 : } else if (Patch.PATCHSET_LEVEL.equals(filename)) {
116 5 : return "Patchset";
117 : } else {
118 16 : return "File " + filename;
119 : }
120 : }
121 : }
122 :
123 65 : private List<? extends Comment> inlineComments = Collections.emptyList();
124 : @Nullable private String patchSetComment;
125 65 : private ImmutableList<LabelVote> labels = ImmutableList.of();
126 : private final CommentsUtil commentsUtil;
127 : private final boolean incomingEmailEnabled;
128 : private final String replyToAddress;
129 : private final Supplier<Map<SubmitRequirement, SubmitRequirementResult>>
130 : preUpdateSubmitRequirementResultsSupplier;
131 : private final Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults;
132 :
133 : @Inject
134 : public CommentSender(
135 : EmailArguments args,
136 : CommentsUtil commentsUtil,
137 : @GerritServerConfig Config cfg,
138 : @Assisted Project.NameKey project,
139 : @Assisted Change.Id changeId,
140 : @Assisted ObjectId preUpdateMetaId,
141 : @Assisted
142 : Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults) {
143 65 : super(args, "comment", newChangeData(args, project, changeId));
144 65 : this.commentsUtil = commentsUtil;
145 65 : this.incomingEmailEnabled =
146 65 : cfg.getEnum("receiveemail", null, "protocol", Protocol.NONE).ordinal()
147 65 : > Protocol.NONE.ordinal();
148 65 : this.replyToAddress = cfg.getString("sendemail", null, "replyToAddress");
149 65 : this.preUpdateSubmitRequirementResultsSupplier =
150 65 : Suppliers.memoize(
151 : () ->
152 : // Triggers an (expensive) evaluation of the submit requirements. This is OK since
153 : // all callers sent this email asynchronously, see EmailReviewComments.
154 65 : newChangeData(args, project, changeId, preUpdateMetaId)
155 65 : .submitRequirementsIncludingLegacy());
156 65 : this.postUpdateSubmitRequirementResults = postUpdateSubmitRequirementResults;
157 65 : }
158 :
159 : public void setComments(List<? extends Comment> comments) {
160 65 : inlineComments = comments;
161 65 : }
162 :
163 : public void setPatchSetComment(@Nullable String comment) {
164 65 : this.patchSetComment = comment;
165 65 : }
166 :
167 : public void setLabels(ImmutableList<LabelVote> labels) {
168 65 : this.labels = labels;
169 65 : }
170 :
171 : @Override
172 : protected void init() throws EmailException {
173 65 : super.init();
174 :
175 65 : if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
176 65 : ccAllApprovals();
177 : }
178 65 : if (notify.handling().compareTo(NotifyHandling.ALL) >= 0) {
179 64 : bccStarredBy();
180 64 : includeWatchers(NotifyType.ALL_COMMENTS, !change.isWorkInProgress() && !change.isPrivate());
181 : }
182 :
183 : // Add header that enables identifying comments on parsed email.
184 : // Grouping is currently done by timestamp.
185 65 : setHeader(MailHeader.COMMENT_DATE.fieldName(), timestamp);
186 :
187 65 : if (incomingEmailEnabled) {
188 1 : if (replyToAddress == null) {
189 : // Remove Reply-To and use outbound SMTP (default) instead.
190 0 : removeHeader(FieldName.REPLY_TO);
191 : } else {
192 1 : setHeader(FieldName.REPLY_TO, replyToAddress);
193 : }
194 : }
195 65 : }
196 :
197 : @Override
198 : public void formatChange() throws EmailException {
199 65 : appendText(textTemplate("Comment"));
200 65 : if (useHtml()) {
201 65 : appendHtml(soyHtmlTemplate("CommentHtml"));
202 : }
203 65 : }
204 :
205 : @Override
206 : public void formatFooter() throws EmailException {
207 65 : appendText(textTemplate("CommentFooter"));
208 65 : if (useHtml()) {
209 65 : appendHtml(soyHtmlTemplate("CommentFooterHtml"));
210 : }
211 65 : }
212 :
213 : /**
214 : * Returns a list of FileCommentGroup objects representing the inline comments grouped by the
215 : * file.
216 : */
217 : private List<CommentSender.FileCommentGroup> getGroupedInlineComments(Repository repo) {
218 65 : List<CommentSender.FileCommentGroup> groups = new ArrayList<>();
219 :
220 : // Loop over the comments and collect them into groups based on the file
221 : // location of the comment.
222 65 : FileCommentGroup currentGroup = null;
223 65 : for (Comment c : inlineComments) {
224 : // If it's a new group:
225 23 : if (currentGroup == null
226 9 : || !c.key.filename.equals(currentGroup.filename)
227 : || c.key.patchSetId != currentGroup.patchSetId) {
228 23 : currentGroup = new FileCommentGroup();
229 23 : currentGroup.filename = c.key.filename;
230 23 : currentGroup.patchSetId = c.key.patchSetId;
231 : // Get the modified files:
232 23 : Map<String, FileDiffOutput> modifiedFiles = listModifiedFiles(c.key.patchSetId);
233 :
234 23 : groups.add(currentGroup);
235 23 : if (modifiedFiles != null && !modifiedFiles.isEmpty()) {
236 : try {
237 23 : currentGroup.fileData = new PatchFile(repo, modifiedFiles, c.key.filename);
238 7 : } catch (IOException e) {
239 7 : logger.atWarning().withCause(e).log(
240 : "Cannot load %s from %s in %s",
241 : c.key.filename,
242 7 : modifiedFiles.values().iterator().next().newCommitId().name(),
243 7 : projectState.getName());
244 7 : currentGroup.fileData = null;
245 23 : }
246 : }
247 : }
248 :
249 23 : if (currentGroup.filename.equals(PATCHSET_LEVEL) || currentGroup.fileData != null) {
250 23 : currentGroup.comments.add(c);
251 : }
252 23 : }
253 :
254 65 : groups.sort(Comparator.comparing(g -> g.filename, FilenameComparator.INSTANCE));
255 65 : return groups;
256 : }
257 :
258 : /** Get the set of accounts whose comments have been replied to in this email. */
259 : private HashSet<Account.Id> getReplyAccounts() {
260 65 : HashSet<Account.Id> replyAccounts = new HashSet<>();
261 : // Track visited parent UUIDs to avoid cycles.
262 65 : HashSet<String> visitedUuids = new HashSet<>();
263 :
264 65 : for (Comment comment : inlineComments) {
265 23 : visitedUuids.add(comment.key.uuid);
266 : // Traverse the parent relation to the top of the comment thread.
267 23 : Comment current = comment;
268 23 : while (current.parentUuid != null && !visitedUuids.contains(current.parentUuid)) {
269 5 : Optional<HumanComment> optParent = getParent(current);
270 5 : if (!optParent.isPresent()) {
271 : // There is a parent UUID, but it cannot be loaded, break from the comment thread.
272 3 : break;
273 : }
274 :
275 4 : HumanComment parent = optParent.get();
276 4 : replyAccounts.add(parent.author.getId());
277 4 : visitedUuids.add(current.parentUuid);
278 4 : current = parent;
279 4 : }
280 23 : }
281 65 : return replyAccounts;
282 : }
283 :
284 : private String getCommentLinePrefix(Comment comment) {
285 23 : int lineNbr = comment.range == null ? comment.lineNbr : comment.range.startLine;
286 23 : StringBuilder sb = new StringBuilder();
287 23 : sb.append("PS").append(comment.key.patchSetId);
288 23 : if (lineNbr != 0) {
289 21 : sb.append(", Line ").append(lineNbr);
290 : }
291 23 : sb.append(": ");
292 23 : return sb.toString();
293 : }
294 :
295 : /**
296 : * Returns the lines of file content in fileData that are encompassed by range on the given side.
297 : */
298 : private List<String> getLinesByRange(Comment.Range range, PatchFile fileData, short side) {
299 7 : List<String> lines = new ArrayList<>();
300 :
301 7 : for (int n = range.startLine; n <= range.endLine; n++) {
302 7 : String s = getLine(fileData, side, n);
303 7 : if (n == range.startLine && n == range.endLine && range.startChar < range.endChar) {
304 6 : s = s.substring(Math.min(range.startChar, s.length()), Math.min(range.endChar, s.length()));
305 2 : } else if (n == range.startLine) {
306 2 : s = s.substring(Math.min(range.startChar, s.length()));
307 2 : } else if (n == range.endLine) {
308 2 : s = s.substring(0, Math.min(range.endChar, s.length()));
309 : }
310 7 : lines.add(s);
311 : }
312 7 : return lines;
313 : }
314 :
315 : /**
316 : * Get the parent comment of a given comment.
317 : *
318 : * @param child the comment with a potential parent comment.
319 : * @return an optional comment that will be present if the given comment has a parent, and is
320 : * empty if it does not.
321 : */
322 : private Optional<HumanComment> getParent(Comment child) {
323 23 : if (child.parentUuid == null) {
324 23 : return Optional.empty();
325 : }
326 5 : Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename, child.key.patchSetId);
327 : try {
328 5 : return commentsUtil.getPublishedHumanComment(changeData.notes(), key);
329 0 : } catch (StorageException e) {
330 0 : logger.atWarning().log("Could not find the parent of this comment: %s", child);
331 0 : return Optional.empty();
332 : }
333 : }
334 :
335 : /**
336 : * Retrieve the file lines referred to by a comment.
337 : *
338 : * @param comment The comment that refers to some file contents. The comment may be a line comment
339 : * or a ranged comment.
340 : * @param fileData The file on which the comment appears.
341 : * @return file contents referred to by the comment. If the comment is a line comment, the result
342 : * will be a list of one string. Otherwise it will be a list of one or more strings.
343 : */
344 : private List<String> getLinesOfComment(Comment comment, PatchFile fileData) {
345 23 : List<String> lines = new ArrayList<>();
346 23 : if (comment.lineNbr == 0) {
347 : // file level comment has no line
348 12 : return lines;
349 : }
350 21 : if (comment.range == null) {
351 17 : lines.add(getLine(fileData, comment.side, comment.lineNbr));
352 : } else {
353 7 : lines.addAll(getLinesByRange(comment.range, fileData, comment.side));
354 : }
355 21 : return lines;
356 : }
357 :
358 : /**
359 : * Returns a shortened version of the given comment's message. Will be shortened to 100 characters
360 : * or the first line, or following the last period within the first 100 characters, whichever is
361 : * shorter. If the message is shortened, an ellipsis is appended.
362 : */
363 : protected static String getShortenedCommentMessage(String message) {
364 4 : int threshold = 100;
365 4 : String fullMessage = message.trim();
366 4 : String msg = fullMessage;
367 :
368 4 : if (msg.length() > threshold) {
369 1 : msg = msg.substring(0, threshold);
370 : }
371 :
372 4 : int lf = msg.indexOf('\n');
373 4 : int period = msg.lastIndexOf('.');
374 :
375 4 : if (lf > 0) {
376 : // Truncate if a line feed appears within the threshold.
377 1 : msg = msg.substring(0, lf);
378 :
379 4 : } else if (period > 0) {
380 : // Otherwise truncate if there is a period within the threshold.
381 1 : msg = msg.substring(0, period + 1);
382 : }
383 :
384 : // Append an ellipsis if the message has been truncated.
385 4 : if (!msg.equals(fullMessage)) {
386 1 : msg += " […]";
387 : }
388 :
389 4 : return msg;
390 : }
391 :
392 : protected static String getShortenedCommentMessage(Comment comment) {
393 3 : return getShortenedCommentMessage(comment.message);
394 : }
395 :
396 : /**
397 : * Returns grouped inline comment data mapped to data structures that are suitable for passing
398 : * into Soy.
399 : */
400 : private List<Map<String, Object>> getCommentGroupsTemplateData(Repository repo) {
401 65 : List<Map<String, Object>> commentGroups = new ArrayList<>();
402 :
403 65 : for (CommentSender.FileCommentGroup group : getGroupedInlineComments(repo)) {
404 23 : Map<String, Object> groupData = new HashMap<>();
405 23 : groupData.put("title", group.getTitle());
406 23 : groupData.put("patchSetId", group.patchSetId);
407 :
408 23 : List<Map<String, Object>> commentsList = new ArrayList<>();
409 23 : for (Comment comment : group.comments) {
410 23 : Map<String, Object> commentData = new HashMap<>();
411 23 : if (group.fileData != null) {
412 23 : commentData.put("lines", getLinesOfComment(comment, group.fileData));
413 : }
414 23 : commentData.put("message", comment.message.trim());
415 23 : List<CommentFormatter.Block> blocks = CommentFormatter.parse(comment.message);
416 23 : commentData.put("messageBlocks", commentBlocksToSoyData(blocks));
417 :
418 : // Set the prefix.
419 23 : String prefix = getCommentLinePrefix(comment);
420 23 : commentData.put("linePrefix", prefix);
421 23 : commentData.put("linePrefixEmpty", Strings.padStart(": ", prefix.length(), ' '));
422 :
423 : // Set line numbers.
424 : int startLine;
425 23 : if (comment.range == null) {
426 23 : startLine = comment.lineNbr;
427 : } else {
428 7 : startLine = comment.range.startLine;
429 7 : commentData.put("endLine", comment.range.endLine);
430 : }
431 23 : commentData.put("startLine", startLine);
432 :
433 : // Set the comment link.
434 :
435 23 : if (comment.key.filename.equals(Patch.PATCHSET_LEVEL)) {
436 5 : if (comment instanceof RobotComment) {
437 1 : commentData.put("link", group.getFindingsTabLink());
438 : } else {
439 4 : commentData.put("link", group.getCommentsTabLink());
440 : }
441 : } else {
442 22 : commentData.put("link", group.getCommentLink(comment.key.uuid));
443 : }
444 :
445 : // Set robot comment data.
446 23 : if (comment instanceof RobotComment) {
447 7 : RobotComment robotComment = (RobotComment) comment;
448 7 : commentData.put("isRobotComment", true);
449 7 : commentData.put("robotId", robotComment.robotId);
450 7 : commentData.put("robotRunId", robotComment.robotRunId);
451 7 : commentData.put("robotUrl", robotComment.url);
452 7 : } else {
453 22 : commentData.put("isRobotComment", false);
454 : }
455 :
456 : // If the comment has a quote, don't bother loading the parent message.
457 23 : if (!hasQuote(blocks)) {
458 : // Set parent comment info.
459 23 : Optional<HumanComment> parent = getParent(comment);
460 23 : if (parent.isPresent()) {
461 3 : commentData.put("parentMessage", getShortenedCommentMessage(parent.get()));
462 : }
463 : }
464 :
465 23 : commentsList.add(commentData);
466 23 : }
467 23 : groupData.put("comments", commentsList);
468 :
469 23 : commentGroups.add(groupData);
470 23 : }
471 65 : return commentGroups;
472 : }
473 :
474 : private List<Map<String, Object>> commentBlocksToSoyData(List<CommentFormatter.Block> blocks) {
475 65 : return blocks.stream()
476 65 : .map(
477 : b -> {
478 65 : Map<String, Object> map = new HashMap<>();
479 65 : switch (b.type) {
480 : case PARAGRAPH:
481 65 : map.put("type", "paragraph");
482 65 : map.put("text", b.text);
483 65 : break;
484 : case PRE_FORMATTED:
485 0 : map.put("type", "pre");
486 0 : map.put("text", b.text);
487 0 : break;
488 : case QUOTE:
489 0 : map.put("type", "quote");
490 0 : map.put("quotedBlocks", commentBlocksToSoyData(b.quotedBlocks));
491 0 : break;
492 : case LIST:
493 0 : map.put("type", "list");
494 0 : map.put("items", b.items);
495 : break;
496 : }
497 65 : return map;
498 : })
499 65 : .collect(toList());
500 : }
501 :
502 : private boolean hasQuote(List<CommentFormatter.Block> blocks) {
503 23 : for (CommentFormatter.Block block : blocks) {
504 23 : if (block.type == CommentFormatter.BlockType.QUOTE) {
505 0 : return true;
506 : }
507 23 : }
508 23 : return false;
509 : }
510 :
511 : @Nullable
512 : private Repository getRepository() {
513 : try {
514 65 : return args.server.openRepository(projectState.getNameKey());
515 0 : } catch (IOException e) {
516 0 : return null;
517 : }
518 : }
519 :
520 : @Override
521 : protected void setupSoyContext() {
522 65 : super.setupSoyContext();
523 : boolean hasComments;
524 65 : try (Repository repo = getRepository()) {
525 65 : List<Map<String, Object>> files = getCommentGroupsTemplateData(repo);
526 65 : soyContext.put("commentFiles", files);
527 65 : hasComments = !files.isEmpty();
528 : }
529 :
530 65 : soyContext.put(
531 65 : "patchSetCommentBlocks", commentBlocksToSoyData(CommentFormatter.parse(patchSetComment)));
532 65 : soyContext.put("labels", getLabelVoteSoyData(labels));
533 65 : soyContext.put("commentCount", inlineComments.size());
534 65 : soyContext.put("commentTimestamp", getCommentTimestamp());
535 65 : soyContext.put(
536 65 : "coverLetterBlocks", commentBlocksToSoyData(CommentFormatter.parse(getCoverLetter())));
537 :
538 65 : if (isChangeNoLongerSubmittable()) {
539 7 : soyContext.put("unsatisfiedSubmitRequirements", formatUnsatisfiedSubmitRequirements());
540 7 : soyContext.put(
541 : "oldSubmitRequirements",
542 7 : formatSubmitRequirments(preUpdateSubmitRequirementResultsSupplier.get()));
543 7 : soyContext.put(
544 7 : "newSubmitRequirements", formatSubmitRequirments(postUpdateSubmitRequirementResults));
545 : }
546 :
547 65 : footers.add(MailHeader.COMMENT_DATE.withDelimiter() + getCommentTimestamp());
548 65 : footers.add(MailHeader.HAS_COMMENTS.withDelimiter() + (hasComments ? "Yes" : "No"));
549 65 : footers.add(MailHeader.HAS_LABELS.withDelimiter() + (labels.isEmpty() ? "No" : "Yes"));
550 :
551 65 : for (Account.Id account : getReplyAccounts()) {
552 4 : footers.add(MailHeader.COMMENT_IN_REPLY_TO.withDelimiter() + getNameEmailFor(account));
553 4 : }
554 65 : }
555 :
556 : /**
557 : * Checks whether the change is no longer submittable.
558 : *
559 : * @return {@code true} if the change has been submittable before the update and is no longer
560 : * submittable after the update has been applied, otherwise {@code false}
561 : */
562 : private boolean isChangeNoLongerSubmittable() {
563 65 : boolean isSubmittablePreUpdate =
564 65 : preUpdateSubmitRequirementResultsSupplier.get().values().stream()
565 65 : .allMatch(SubmitRequirementResult::fulfilled);
566 65 : logger.atFine().log(
567 : "the submitability of change %s before the update is %s",
568 65 : change.getId(), isSubmittablePreUpdate);
569 65 : if (!isSubmittablePreUpdate) {
570 65 : return false;
571 : }
572 :
573 18 : boolean isSubmittablePostUpdate =
574 18 : postUpdateSubmitRequirementResults.values().stream()
575 18 : .allMatch(SubmitRequirementResult::fulfilled);
576 18 : logger.atFine().log(
577 : "the submitability of change %s after the update is %s",
578 18 : change.getId(), isSubmittablePostUpdate);
579 18 : return !isSubmittablePostUpdate;
580 : }
581 :
582 : private ImmutableList<String> formatUnsatisfiedSubmitRequirements() {
583 7 : return postUpdateSubmitRequirementResults.entrySet().stream()
584 7 : .filter(e -> SubmitRequirementResult.Status.UNSATISFIED.equals(e.getValue().status()))
585 7 : .map(Map.Entry::getKey)
586 7 : .map(SubmitRequirement::name)
587 7 : .sorted()
588 7 : .collect(toImmutableList());
589 : }
590 :
591 : private static ImmutableList<String> formatSubmitRequirments(
592 : Map<SubmitRequirement, SubmitRequirementResult> submitRequirementResults) {
593 7 : return submitRequirementResults.entrySet().stream()
594 7 : .map(
595 : e -> {
596 7 : if (e.getValue().errorMessage().isPresent()) {
597 0 : return String.format(
598 : "%s: %s (%s)",
599 0 : e.getKey().name(),
600 0 : e.getValue().status().name(),
601 0 : e.getValue().errorMessage().get());
602 : }
603 7 : return String.format("%s: %s", e.getKey().name(), e.getValue().status().name());
604 : })
605 7 : .sorted()
606 7 : .collect(toImmutableList());
607 : }
608 :
609 : private String getLine(PatchFile fileInfo, short side, int lineNbr) {
610 : try {
611 21 : return fileInfo.getLine(side, lineNbr);
612 0 : } catch (IOException err) {
613 : // Default to the empty string if the file cannot be safely read.
614 0 : logger.atWarning().withCause(err).log("Failed to read file on side %d", side);
615 0 : return "";
616 1 : } catch (IndexOutOfBoundsException err) {
617 : // Default to the empty string if the given line number does not appear
618 : // in the file.
619 1 : logger.atFine().withCause(err).log(
620 : "Failed to get line number %d of file on side %d", lineNbr, side);
621 1 : return "";
622 1 : } catch (NoSuchEntityException err) {
623 : // Default to the empty string if the side cannot be found.
624 1 : logger.atWarning().withCause(err).log("Side %d of file didn't exist", side);
625 1 : return "";
626 : }
627 : }
628 :
629 : private ImmutableList<Map<String, Object>> getLabelVoteSoyData(ImmutableList<LabelVote> votes) {
630 65 : ImmutableList.Builder<Map<String, Object>> result = ImmutableList.builder();
631 65 : for (LabelVote vote : votes) {
632 58 : Map<String, Object> data = new HashMap<>();
633 58 : data.put("label", vote.label());
634 :
635 : // Soy needs the short to be cast as an int for it to get converted to the
636 : // correct tamplate type.
637 58 : data.put("value", (int) vote.value());
638 58 : result.add(data);
639 58 : }
640 65 : return result.build();
641 : }
642 :
643 : private String getCommentTimestamp() {
644 : // Grouping is currently done by timestamp.
645 65 : return MailProcessingUtil.rfcDateformatter.format(
646 65 : ZonedDateTime.ofInstant(timestamp, ZoneId.of("UTC")));
647 : }
648 : }
|