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.mail; 16 : 17 : import com.google.common.base.Splitter; 18 : import com.google.common.base.Strings; 19 : import com.google.common.collect.Iterators; 20 : import com.google.common.collect.PeekingIterator; 21 : import com.google.gerrit.entities.HumanComment; 22 : import java.util.ArrayList; 23 : import java.util.Collection; 24 : import java.util.List; 25 : 26 : /** Provides parsing functionality for plaintext email. */ 27 : public class TextParser { 28 : private TextParser() {} 29 : 30 : /** 31 : * Parses comments from plaintext email. 32 : * 33 : * @param email the message as received from the email service 34 : * @param comments list of {@link HumanComment}s previously persisted on the change that caused 35 : * the original notification email to be sent out. Ordering must be the same as in the 36 : * outbound email 37 : * @param changeUrl canonical change url that points to the change on this Gerrit instance. 38 : * Example: https://go-review.googlesource.com/#/c/91570 39 : * @return list of MailComments parsed from the plaintext part of the email 40 : */ 41 : public static List<MailComment> parse( 42 : MailMessage email, Collection<HumanComment> comments, String changeUrl) { 43 3 : String body = email.textContent(); 44 : // Replace CR-LF by \n 45 3 : body = body.replace("\r\n", "\n"); 46 : 47 3 : List<MailComment> parsedComments = new ArrayList<>(); 48 : 49 : // Some email clients (like GMail) use >> for enquoting text when there are 50 : // inline comments that the users typed. These will then be enquoted by a 51 : // single >. We sanitize this by unifying it into >. Inline comments typed 52 : // by the user will not be enquoted. 53 : // 54 : // Example: 55 : // Some comment 56 : // >> Quoted Text 57 : // >> Quoted Text 58 : // > A comment typed in the email directly 59 3 : String singleQuotePattern = "\n> "; 60 3 : String doubleQuotePattern = "\n>> "; 61 3 : if (countOccurrences(body, doubleQuotePattern) > countOccurrences(body, singleQuotePattern)) { 62 1 : body = body.replace(doubleQuotePattern, singleQuotePattern); 63 : } 64 : 65 3 : PeekingIterator<HumanComment> iter = Iterators.peekingIterator(comments.iterator()); 66 : 67 3 : MailComment currentComment = null; 68 3 : String lastEncounteredFileName = null; 69 3 : HumanComment lastEncounteredComment = null; 70 3 : for (String line : Splitter.on('\n').split(body)) { 71 3 : if (line.equals(">")) { 72 : // Skip empty lines 73 1 : continue; 74 : } 75 3 : if (line.startsWith("> ")) { 76 3 : line = line.substring("> ".length()).trim(); 77 : // This is not a comment, try to advance the file/comment pointers and 78 : // add previous comment to list if applicable 79 3 : if (currentComment != null) { 80 3 : if (currentComment.type == MailComment.CommentType.PATCHSET_LEVEL) { 81 3 : currentComment.message = ParserUtil.trimQuotation(currentComment.message); 82 : } 83 3 : if (!Strings.isNullOrEmpty(currentComment.message)) { 84 3 : ParserUtil.appendOrAddNewComment(currentComment, parsedComments); 85 : } 86 3 : currentComment = null; 87 : } 88 : 89 3 : if (!iter.hasNext()) { 90 1 : continue; 91 : } 92 3 : HumanComment perspectiveComment = iter.peek(); 93 3 : if (line.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) { 94 2 : if (lastEncounteredFileName == null 95 2 : || !lastEncounteredFileName.equals(perspectiveComment.key.filename)) { 96 : // This is the annotation of a file 97 2 : lastEncounteredFileName = perspectiveComment.key.filename; 98 2 : lastEncounteredComment = null; 99 2 : } else if (perspectiveComment.lineNbr == 0) { 100 : // This was originally a file-level comment 101 2 : lastEncounteredComment = perspectiveComment; 102 2 : iter.next(); 103 : } 104 3 : } else if (ParserUtil.isCommentUrl(line, changeUrl, perspectiveComment)) { 105 2 : lastEncounteredComment = perspectiveComment; 106 2 : iter.next(); 107 : } 108 3 : } else { 109 : // This is a comment. Try to append to previous comment if applicable or 110 : // create a new comment. 111 3 : if (currentComment == null) { 112 : // Start new comment 113 3 : currentComment = new MailComment(); 114 3 : currentComment.message = line; 115 3 : if (lastEncounteredComment == null) { 116 3 : if (lastEncounteredFileName == null) { 117 : // Change message 118 3 : currentComment.type = MailComment.CommentType.PATCHSET_LEVEL; 119 : } else { 120 : // File comment not sent in reply to another comment 121 2 : currentComment.type = MailComment.CommentType.FILE_COMMENT; 122 2 : currentComment.fileName = lastEncounteredFileName; 123 : } 124 : } else { 125 : // Comment sent in reply to another comment 126 2 : currentComment.inReplyTo = lastEncounteredComment; 127 2 : currentComment.type = MailComment.CommentType.INLINE_COMMENT; 128 : } 129 : } else { 130 : // Attach to previous comment 131 3 : currentComment.message += "\n" + line; 132 : } 133 : } 134 3 : } 135 : // There is no need to attach the currentComment after this loop as all 136 : // emails have footers and other enquoted text after the last comment 137 : // appeared and the last comment will have already been added to the list 138 : // at this point. 139 : 140 3 : return parsedComments; 141 : } 142 : 143 : /** Counts the occurrences of pattern in s */ 144 : private static int countOccurrences(String s, String pattern) { 145 3 : return (s.length() - s.replace(pattern, "").length()) / pattern.length(); 146 : } 147 : }