LCOV - code coverage report
Current view: top level - mail - TextParser.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 51 51 100.0 %
Date: 2022-11-19 15:00:39 Functions: 2 2 100.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750