| // 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(); | |
| } | |
| } |