|  | // 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.Comment; | 
|  | 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 @param email the message as received from the email service | 
|  | * @param comments list of {@link Comment}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<Comment> 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<Comment> iter = Iterators.peekingIterator(comments.iterator()); | 
|  |  | 
|  | MailComment currentComment = null; | 
|  | String lastEncounteredFileName = null; | 
|  | Comment 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.CHANGE_MESSAGE) { | 
|  | currentComment.message = ParserUtil.trimQuotation(currentComment.message); | 
|  | } | 
|  | if (!Strings.isNullOrEmpty(currentComment.message)) { | 
|  | ParserUtil.appendOrAddNewComment(currentComment, parsedComments); | 
|  | } | 
|  | currentComment = null; | 
|  | } | 
|  |  | 
|  | if (!iter.hasNext()) { | 
|  | continue; | 
|  | } | 
|  | Comment 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.CHANGE_MESSAGE; | 
|  | } 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(); | 
|  | } | 
|  | } |