| // Copyright (C) 2016 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.mail; |
| |
| import com.google.common.base.Splitter; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Iterators; |
| import com.google.common.collect.PeekingIterator; |
| import com.google.gerrit.entities.HumanComment; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| |
| /** Provides parsing functionality for plaintext email. */ |
| public class TextParser { |
| private TextParser() {} |
| |
| /** |
| * Parses comments from plaintext email. |
| * |
| * @param email the message as received from the email service |
| * @param comments list of {@link HumanComment}s previously persisted on the change that caused |
| * the original notification email to be sent out. Ordering must be the same as in the |
| * outbound email |
| * @param changeUrl canonical change url that points to the change on this Gerrit instance. |
| * Example: https://go-review.googlesource.com/#/c/91570 |
| * @return list of MailComments parsed from the plaintext part of the email |
| */ |
| public static List<MailComment> parse( |
| MailMessage email, Collection<HumanComment> comments, String changeUrl) { |
| String body = email.textContent(); |
| // Replace CR-LF by \n |
| body = body.replace("\r\n", "\n"); |
| |
| List<MailComment> parsedComments = new ArrayList<>(); |
| |
| // Some email clients (like GMail) use >> for enquoting text when there are |
| // inline comments that the users typed. These will then be enquoted by a |
| // single >. We sanitize this by unifying it into >. Inline comments typed |
| // by the user will not be enquoted. |
| // |
| // Example: |
| // Some comment |
| // >> Quoted Text |
| // >> Quoted Text |
| // > A comment typed in the email directly |
| String singleQuotePattern = "\n> "; |
| String doubleQuotePattern = "\n>> "; |
| if (countOccurrences(body, doubleQuotePattern) > countOccurrences(body, singleQuotePattern)) { |
| body = body.replace(doubleQuotePattern, singleQuotePattern); |
| } |
| |
| PeekingIterator<HumanComment> iter = Iterators.peekingIterator(comments.iterator()); |
| |
| MailComment currentComment = null; |
| String lastEncounteredFileName = null; |
| HumanComment lastEncounteredComment = null; |
| for (String line : Splitter.on('\n').split(body)) { |
| if (line.equals(">")) { |
| // Skip empty lines |
| continue; |
| } |
| if (line.startsWith("> ")) { |
| line = line.substring("> ".length()).trim(); |
| // This is not a comment, try to advance the file/comment pointers and |
| // add previous comment to list if applicable |
| if (currentComment != null) { |
| if (currentComment.type == MailComment.CommentType.PATCHSET_LEVEL) { |
| currentComment.message = ParserUtil.trimQuotation(currentComment.message); |
| } |
| if (!Strings.isNullOrEmpty(currentComment.message)) { |
| ParserUtil.appendOrAddNewComment(currentComment, parsedComments); |
| } |
| currentComment = null; |
| } |
| |
| if (!iter.hasNext()) { |
| continue; |
| } |
| HumanComment perspectiveComment = iter.peek(); |
| if (line.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) { |
| if (lastEncounteredFileName == null |
| || !lastEncounteredFileName.equals(perspectiveComment.key.filename)) { |
| // This is the annotation of a file |
| lastEncounteredFileName = perspectiveComment.key.filename; |
| lastEncounteredComment = null; |
| } else if (perspectiveComment.lineNbr == 0) { |
| // This was originally a file-level comment |
| lastEncounteredComment = perspectiveComment; |
| iter.next(); |
| } |
| } else if (ParserUtil.isCommentUrl(line, changeUrl, perspectiveComment)) { |
| lastEncounteredComment = perspectiveComment; |
| iter.next(); |
| } |
| } else { |
| // This is a comment. Try to append to previous comment if applicable or |
| // create a new comment. |
| if (currentComment == null) { |
| // Start new comment |
| currentComment = new MailComment(); |
| currentComment.message = line; |
| if (lastEncounteredComment == null) { |
| if (lastEncounteredFileName == null) { |
| // Change message |
| currentComment.type = MailComment.CommentType.PATCHSET_LEVEL; |
| } else { |
| // File comment not sent in reply to another comment |
| currentComment.type = MailComment.CommentType.FILE_COMMENT; |
| currentComment.fileName = lastEncounteredFileName; |
| } |
| } else { |
| // Comment sent in reply to another comment |
| currentComment.inReplyTo = lastEncounteredComment; |
| currentComment.type = MailComment.CommentType.INLINE_COMMENT; |
| } |
| } else { |
| // Attach to previous comment |
| currentComment.message += "\n" + line; |
| } |
| } |
| } |
| // There is no need to attach the currentComment after this loop as all |
| // emails have footers and other enquoted text after the last comment |
| // appeared and the last comment will have already been added to the list |
| // at this point. |
| |
| return parsedComments; |
| } |
| |
| /** Counts the occurrences of pattern in s */ |
| private static int countOccurrences(String s, String pattern) { |
| return (s.length() - s.replace(pattern, "").length()) / pattern.length(); |
| } |
| } |