Search code examples
javaapache-poitableofcontentsxwpf

How to insert a sdt before a given paragraph by using POI?


I want to generate a TOC before main body in a existed word file. I have redefined a custom XWPFDocument.createTOC function to generate a TOC whose styles accord with my needs. But in the createTOC function, "this.getDocument().getBody().addNewSdt()" only can insert a sdt at the last of body. I spend lots time to find a method to change the position of sdt element. I find this method can work eventually.

Node body_node = doc.getDocument().getDomNode().item(0);
NodeList nodeList = body_node.getChildNodes();
Node sdt_xxx = nodeList.item(8);
body_node.removeChild(sdt_xxx);
body_node.insertBefore(sdt_xxx ,nodeList.item(0));

This method can modify the position of sdt. But this method works with the CT class, so the variable in XWPFDocument (such as List<XWPFSDT>, List<XWPFParagraph>) do not change after I modify the CT class. And I don't know how to modify the bodyelements in XWPFDocument, which is a unmodifiable variable. I don't know how to reload the XWPFDocument object after I modify CT variable.

(When reopen file after write the modified object to a new file, the XWPFDocument object can update. But I don't want to do this, this method seems too stupid.)

So, who knows how to add a sdt (TOC) into a given position using POI? Thanks a lot.


Solution

  • You say you have extended XWPFDocument with your own createTOC method. So you also could provide a createTOC(org.apache.xmlbeans.XmlCursor cursor) along the lines of insertNewParagraph(org.apache.xmlbeans.XmlCursor cursor). There the cursor determines where the TOC is inserted.

    And because of updating the bodyelements lists, the extended XWPFDocument could provide a recreateBodyElementLists method. This method then recreates all necessary bodyelements lists when called.

    Complete example to show the principle:

    import java.io.FileOutputStream;
    import java.io.FileInputStream;
    
    import org.apache.poi.xwpf.usermodel.*;
    import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyles;
    import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle;
    import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSimpleField;
    
    import org.apache.xmlbeans.XmlCursor;
    
    public class CreateWordTOC {
        
     static String cTStyleTOC1 =
      "<w:style xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" w:type=\"paragraph\" w:styleId=\"TOC1\">"
    + "<w:name w:val=\"toc 1\"/>"
    + "<w:basedOn w:val=\"Normal\"/>"
    + "<w:next w:val=\"Normal\"/>"
    + "<w:autoRedefine/><w:unhideWhenUsed/>"
    + "<w:rPr><w:b/><w:bCs/><w:caps/><w:color w:val=\"C00000\"/><w:sz w:val=\"32\"/><w:szCs w:val=\"32\"/></w:rPr>"
    + "</w:style>";
    
     static String cTStyleTOC2 =
      "<w:style xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" w:type=\"paragraph\" w:styleId=\"TOC2\">"
    + "<w:name w:val=\"toc 2\"/>"
    + "<w:basedOn w:val=\"Normal\"/>"
    + "<w:next w:val=\"Normal\"/>"
    + "<w:autoRedefine/><w:unhideWhenUsed/>"
    + "<w:rPr><w:i/><w:iCs/><w:sz w:val=\"28\"/><w:szCs w:val=\"28\"/></w:rPr>"
    + "</w:style>";
    
     public static void main(String[] args) throws Exception {
    
      //XWPFDocument document = new XWPFDocument(new FileInputStream("./WordDocument.docx"));
      MyXWPFDocument document = new MyXWPFDocument(new FileInputStream("./WordDocument.docx"));
    
      XWPFStyles styles = document.createStyles(); 
      CTStyles cTStyles = CTStyles.Factory.parse(cTStyleTOC1);
      CTStyle cTStyle = cTStyles.getStyleArray(0);
      styles.addStyle(new XWPFStyle(cTStyle));
      cTStyles = CTStyles.Factory.parse(cTStyleTOC2);
      cTStyle = cTStyles.getStyleArray(0);
      styles.addStyle(new XWPFStyle(cTStyle));
      
      System.out.println(document.getBodyElements().size());
      System.out.println(document.getContentControls().size());
     
      //TOC before first parahraph
      XWPFParagraph paragraph = document.getParagraphs().get(0);
      XmlCursor cursor = paragraph.getCTP().newCursor();
      //document.createTOC(cursor, "Heading");
      document.createTOC(cursor, "berschrift"); // German style Ids are "berschrift1", "berschrift2", ... (from "Überschrift" umlaut is not used in Id)
      cursor.dispose();
      //TOC as last element in body
      //document.createTOC(null, "Heading");
      document.createTOC(null, "berschrift");
      
      System.out.println(document.getBodyElements().size());
      System.out.println(document.getContentControls().size());
      if (document.getContentControls().size() > 0 )
       System.out.println(document.getContentControls().get(0).getContent());
    
    
      FileOutputStream out = new FileOutputStream("./WordDocumentNew.docx");   
      document.write(out);
      out.close();
      document.close();
    
     }
    
    }
    

    MyXWPFDocument.java:

    import java.io.*; 
    import java.math.BigInteger;
    import org.apache.poi.util.POILogFactory;
    import org.apache.poi.util.POILogger;
    import org.apache.poi.xwpf.usermodel.*; 
    import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
    
    import org.apache.xmlbeans.XmlCursor;
    import org.apache.xmlbeans.XmlObject;
    
    import java.util.List;
    import java.util.ArrayList;
    import java.util.Collections;
    
    public class MyXWPFDocument extends XWPFDocument {
        
     private static final POILogger LOG = POILogFactory.getLogger(XWPFDocument.class);
        
     public MyXWPFDocument(InputStream is) throws IOException {
      super(is);   
     }
        
     public void createTOC(XmlCursor cursor, String headingStyleIdPrefix) {
      CTSdtBlock block = null;
      if (cursor != null && isCursorInBody(cursor)) {
       String uri = CTSdtBlock.type.getName().getNamespaceURI();
       String localPart = "sdt";
       cursor.beginElement(localPart, uri);
       cursor.toParent();
       block = (CTSdtBlock) cursor.getObject(); 
      } else {
       block = this.getDocument().getBody().addNewSdt();
      }
      TOC toc = new TOC(block);
      int bookmarkId = 0;
      String tocIdPrefix = "123456";
      for (XWPFParagraph par : paragraphs) {
       String parStyleId = par.getStyle();
       if (parStyleId != null && parStyleId.toLowerCase().startsWith(headingStyleIdPrefix.toLowerCase())) {
        try {
         int level = Integer.parseInt(parStyleId.substring(headingStyleIdPrefix.length()));
         par.getCTP().addNewBookmarkStart();
         par.getCTP().getBookmarkStartArray(0).setId(BigInteger.valueOf(bookmarkId));
         par.getCTP().getBookmarkStartArray(0).setName("_Toc" + tocIdPrefix + bookmarkId);
         par.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(bookmarkId));
         toc.addRow(level, par.getText(), 1, "" + tocIdPrefix + bookmarkId);
         bookmarkId++;
        } catch (Exception e) {
         LOG.log(POILogger.ERROR, "can't create TOC item", e);
        }
       }
      }
      recreateBodyElementLists();
     }
    
     protected void recreateBodyElementLists() {
      bodyElements = new ArrayList<>();
      paragraphs = new ArrayList<>();
      tables = new ArrayList<>();
      contentControls = new ArrayList<>();
      // parse the document with cursor and add
      // the XmlObject to its lists
      XmlCursor docCursor = this.getDocument().newCursor();
      docCursor.selectPath("./*");
      while (docCursor.toNextSelection()) {
       XmlObject o = docCursor.getObject();
       if (o instanceof CTBody) {
        XmlCursor bodyCursor = o.newCursor();
        bodyCursor.selectPath("./*");
        while (bodyCursor.toNextSelection()) {
         XmlObject bodyObj = bodyCursor.getObject();
         if (bodyObj instanceof CTP) {
          XWPFParagraph p = new XWPFParagraph((CTP) bodyObj, this);
          bodyElements.add(p);
          paragraphs.add(p);
         } else if (bodyObj instanceof CTTbl) {
          XWPFTable t = new XWPFTable((CTTbl) bodyObj, this);
          bodyElements.add(t);
          tables.add(t);
         } else if (bodyObj instanceof CTSdtBlock) {
          XWPFSDT c = new XWPFSDT((CTSdtBlock) bodyObj, this);
          bodyElements.add(c);
          contentControls.add(c);
         }
        }
        bodyCursor.dispose();
       }
      }
      docCursor.dispose();
     }
     
     public List<XWPFSDT> getContentControls() {
      return Collections.unmodifiableList(contentControls);
     }
                
     private boolean isCursorInBody(XmlCursor cursor) {
      XmlCursor verify = cursor.newCursor();
      verify.toParent();
      boolean result = (verify.getObject() == getDocument().getBody());
      verify.dispose();
      return result;
     }
    }
    

    In my opinion org.apache.poi.xwpf.usermodel.TOC also is incomplete and needs to be extended. But this is another thing then.