blob: c43d200b7534daab6e89fa79a741a9750072e641 [file] [log] [blame]
// 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();
}
}