0

My goal is to search for a word or a phrase in a Word .docx document, and add a comment to it. I have been referring to the sample code found here, here, and here with regards to adding comments using Apache POI. However, all three examples add comments to a whole paragraph (or even a whole table) rather than to a specific word, or run.

I have tried creating an XML cursor at the run level, but cannot cast it to the necessary CTMarkupRange to apply the start and end of the comment.

        // Create comment
                        BigInteger cId = getCommentId(comments);
                        ctComment = comments.addNewComment();
                        ctComment.setAuthor("John Smith");
                        ctComment.setInitials("JS");
                        ctComment.setDate(new GregorianCalendar(Locale.getDefault()));
                        ctComment.addNewP().addNewR().addNewT().setStringValue("Test Comment");
                        ctComment.setId(cId);
                        
        // Set CommentRangeStart
                        String uri = CTMarkupRange.type.getName().getNamespaceURI();
                        String localPart = "commentRangeStart";

                        // XmlCursor cursor = p.getCTP().newCursor();
                        XmlCursor cursor = r.getCTR().newCursor();  
                        cursor.toFirstChild();
                        cursor.beginElement(localPart, uri);
                        cursor.toParent();
                        CTMarkupRange commentRangeStart =  (CTMarkupRange) cursor.getObject(); // This line throws a ClassCastException error
                        cursor.dispose();

                        commentRangeStart.setId(cId);

        // Set CommentRangeEnd and CommentReference

                        p.getCTP().addNewCommentRangeEnd().setId(cId);
                        // p.getCTP().addNewR().addNewCommentReference().setId(cId);
                        r.getCTR().addNewCommentReference().setId(cId);

EDIT1: Snippet showing the logic for looping through the runs

for(XWPFParagraph p:paragraphs){
    List<XWPFRun> runs = p.getRuns();
    if (runs.size() > 0) {
        for (XWPFRun r : runs) {
            String text = r.getText(0);
            for (Map.Entry<String, List<String>> entry : rules.entrySet()) {
                String key = entry.getKey();
                List<String> value = entry.getValue();

                for (int i = 0; i < value.size(); i++) {
                    if (text != null && regexContains(text, value.get(i))) {
                        // Create comment
                        BigInteger cId = getCommentId(comments);
                        ctComment = comments.addNewComment();
                        ctComment.setAuthor("John Smith");
                        ctComment.setInitials("JS");
                        ctComment.setDate(new GregorianCalendar(Locale.getDefault()));
                        ctComment.addNewP().addNewR().addNewT().setStringValue(key);
                        ctComment.setId(cId);

                        // New snippet from Axel Richter
                        p.getCTP().addNewCommentRangeStart().setId(cId);
                        

                        p.getCTP().addNewCommentRangeEnd().setId(cId);
                        p.getCTP().addNewR().addNewCommentReference().setId(cId);
                    }
                }
            }
        }
    }

}
Kel196
  • 150
  • 10

1 Answers1

2

This is not as difficult as you might think.

To comment a run inside a paragraph, the comment range start needs to be set before text run starts in paragraph. The comment range end needs to be set after text run ends in paragraph. This is exactly what my code examples had done already. Of course all paragraphs in my code examples have had only one text run.

In following complete example the second comment comments the word "second" only. To do so the paragraph has three text runs. First having text "Paragraph with the ", second having text "second" and has comment and third having text " comment.".

import java.io.*;

import org.apache.poi.*;
import org.apache.poi.ooxml.*;
import org.apache.poi.openxml4j.opc.*;
import org.apache.xmlbeans.*;

import org.apache.poi.xwpf.usermodel.*;

import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS;

import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

import javax.xml.namespace.QName;

import java.math.BigInteger;
import java.util.GregorianCalendar;
import java.util.Locale;


public class CreateWordWithComments {

//a method for creating the CommentsDocument /word/comments.xml in the *.docx ZIP archive  
 private static MyXWPFCommentsDocument createCommentsDocument(XWPFDocument document) throws Exception {
  OPCPackage oPCPackage = document.getPackage();
  PackagePartName partName = PackagingURIHelper.createPartName("/word/comments.xml");
  PackagePart part = oPCPackage.createPart(partName, "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml");
  MyXWPFCommentsDocument myXWPFCommentsDocument = new MyXWPFCommentsDocument(part);

  String rId = document.addRelation(null, XWPFRelation.COMMENT, myXWPFCommentsDocument).getRelationship().getId();

  return myXWPFCommentsDocument;
 }

 public static void main(String[] args) throws Exception {

  XWPFDocument document = new XWPFDocument();

  MyXWPFCommentsDocument myXWPFCommentsDocument = createCommentsDocument(document);

  CTComments comments = myXWPFCommentsDocument.getComments();
  CTComment ctComment;
  XWPFParagraph paragraph;
  XWPFRun run;

//first comment
  BigInteger cId = BigInteger.ZERO;

  ctComment = comments.addNewComment();
  ctComment.setAuthor("Axel Ríchter");
  ctComment.setInitials("AR");
  ctComment.setDate(new GregorianCalendar(Locale.US));
  ctComment.addNewP().addNewR().addNewT().setStringValue("The first comment.");
  ctComment.setId(cId);

  paragraph = document.createParagraph();

  paragraph.getCTP().addNewCommentRangeStart().setId(cId); //comment range start is set before text run
  run = paragraph.createRun();
  run.setText("Paragraph with the first comment.");
  paragraph.getCTP().addNewCommentRangeEnd().setId(cId); //comment range end is set after text run

  paragraph.getCTP().addNewR().addNewCommentReference().setId(cId); 

//paragraph without comment
  paragraph = document.createParagraph();
  run = paragraph.createRun();
  run.setText("Paragraph without comment.");

//second comment
  cId = cId.add(BigInteger.ONE);

  ctComment = comments.addNewComment();
  ctComment.setAuthor("Axel Ríchter");
  ctComment.setInitials("AR");
  ctComment.setDate(new GregorianCalendar(Locale.US));
  ctComment.addNewP().addNewR().addNewT().setStringValue("The second comment. Comments the word \"second\".");
  ctComment.setId(cId);

  paragraph = document.createParagraph();
  run = paragraph.createRun();
  run.setText("Paragraph with the ");

  paragraph.getCTP().addNewCommentRangeStart().setId(cId); //comment range start is set before text run
  run = paragraph.createRun();
  run.setText("second");
  paragraph.getCTP().addNewCommentRangeEnd().setId(cId); //comment range end is set after text run

  run = paragraph.createRun();
  run.setText(" comment.");

  paragraph.getCTP().addNewR().addNewCommentReference().setId(cId);

//write document
  FileOutputStream out = new FileOutputStream("CreateWordWithComments.docx");
  document.write(out);
  out.close();
  document.close();

 }

//a wrapper class for the CommentsDocument /word/comments.xml in the *.docx ZIP archive
 private static class MyXWPFCommentsDocument extends POIXMLDocumentPart {

  private CTComments comments;

  private MyXWPFCommentsDocument(PackagePart part) throws Exception {
   super(part);
   comments = CommentsDocument.Factory.newInstance().addNewComments();
  }

  private CTComments getComments() {
   return comments;
  }

  @Override
  protected void commit() throws IOException {
   XmlOptions xmlOptions = new XmlOptions(DEFAULT_XML_OPTIONS);
   xmlOptions.setSaveSyntheticDocumentElement(new QName(CTComments.type.getName().getNamespaceURI(), "comments"));
   PackagePart part = getPackagePart();
   OutputStream out = part.getOutputStream();
   comments.save(out, xmlOptions);
   out.close();
  }

 }

}
Axel Richter
  • 56,077
  • 6
  • 60
  • 87
  • Hi Axel, Thank you for your detailed reply and sample code. I understand that '.addNewCommentRangeStart()' has to be set before the text run, but am still struggling a little with the logic for my 'search and comment' tool. At the moment, I loop through each paragraph and through each run, and do a Pattern/Matcher to find a match to a String. When I find a run with a match, I then want to add a comment to that specific run. However I assume I would have needed to have add the comment range starter marker _before_ the run? Any points to correct my logic would be much appreciated. Thank you. – Kel196 Sep 18 '20 at 06:35
  • For reference, I've added a snippet of the logic in the original post – Kel196 Sep 18 '20 at 06:45
  • My code is only to create comments in a new `XWPFDocument` while creating the new paragraphs and text runs. To add comments at special positions into an existing document is a very complex task. This is nothing what can be answered in a question here because there are multiple questions: Insert at a position? Then: How to find the start and end position in document? If found, how to insert the comment range markers at thoes positions? Comment an existing word? Then: How to get that single word into it's own text run **and** insert the comment range markers before and after that text run? – Axel Richter Sep 18 '20 at 10:28