4

I am actually generating a Word Document with Apache POI, and I need to automatically create a Table of Contents (TOC) that references the paragraphs, with their page's indication.

This is the code I am using (I omit preconditions and internal methods' body):

XWPFDocument doc = new XWPFDocument(OPCPackage.openOrCreate(new File(document)));

String strStyleId = "Index Style";
addCustomHeadingStyle(doc, strStyleId, 1);

XWPFParagraph documentControlHeading = doc.createParagraph();
changeText(documentControlHeading, "First try");
documentControlHeading.setAlignment(ParagraphAlignment.LEFT);
documentControlHeading.setPageBreak(true);
documentControlHeading.setStyle(strStyleId);

XWPFParagraph documentControlHeading1 = doc.createParagraph();
changeText(documentControlHeading1, "Second try");
documentControlHeading1.setAlignment(ParagraphAlignment.LEFT);
documentControlHeading1.setPageBreak(true);
documentControlHeading1.setStyle(strStyleId);

doc.createTOC();

When I open the resulting document, I am getting this result (see blue squares):

enter image description here

In the left part, I can see the generated TOC. So far, so good. In the document's body, however, I can just see a static text "Table of Contents", with no indications at all (neither paragraphs nor pages). I cannot even interact with it.

If I'd click on the menu entry "Table of Contents" (red square on the upper-left corner), the "real" Table of Content that I want is being generated (follow the arrow, of course...).

My question is: how can I achieve the second result (red TOC) from code?

Thank you so much.

Side note: I even tried putting doc.enforceUpdateFields(); after doc.createTOC();, but every reference of the TOC disappears, this way.

@Sucy, I add the methods that you requested. Don't know if you can find them useful, though:

/*
 * Adds a custom style with the given indentation level at the given document.
 */
private static void addCustomHeadingStyle(XWPFDocument docxDocument, String strStyleId, int headingLevel) {

    CTStyle ctStyle = CTStyle.Factory.newInstance();
    ctStyle.setStyleId(strStyleId);

    CTString styleName = CTString.Factory.newInstance();
    styleName.setVal(strStyleId);
    ctStyle.setName(styleName);

    CTDecimalNumber indentNumber = CTDecimalNumber.Factory.newInstance();
    indentNumber.setVal(BigInteger.valueOf(headingLevel));

    // lower number > style is more prominent in the formats bar
    ctStyle.setUiPriority(indentNumber);

    CTOnOff onoffnull = CTOnOff.Factory.newInstance();
    ctStyle.setUnhideWhenUsed(onoffnull);

    // style shows up in the formats bar
    ctStyle.setQFormat(onoffnull);

    // style defines a heading of the given level
    CTPPr ppr = CTPPr.Factory.newInstance();
    ppr.setOutlineLvl(indentNumber);
    ctStyle.setPPr(ppr);

    XWPFStyle style = new XWPFStyle(ctStyle);

    // is a null op if already defined
    XWPFStyles styles = docxDocument.createStyles();

    style.setType(STStyleType.PARAGRAPH);
    styles.addStyle(style);

}

/*
 * Changes the text of a given paragraph.
 */
public static void changeText(XWPFParagraph p, String newText) {
    if (p != null) {
        List<XWPFRun> runs = p.getRuns();
        for (int i = runs.size() - 1; i >= 0; i--) {
            p.removeRun(i);
        }

        if (runs.size() == 0) {
            p.createRun();
        }

        XWPFRun run = runs.get(0);
        run.setText(newText, 0);
    }
}
Andrea
  • 6,032
  • 2
  • 28
  • 55
  • Can you give me show your changeText() and addCustomHeadingStyle() method, thanks! – Sucy Apr 12 '17 at 05:32
  • @Sucy. I have added them. Hope that you can find them useful. – Andrea Apr 12 '17 at 14:00
  • Do you have any ideas about [my question](http://stackoverflow.com/questions/43335464/how-to-set-table-of-contents-font-style-including-font-color-font-size-and-bol) thanks! – Sucy Apr 13 '17 at 01:49
  • Maybe I should reproduce style.xml, but I don't know the relationship connects document.xml and styles.xml, and how to set styles ids inside document.xml to connect concrete text part with concrete style. I should expend huge effort to do this. Do your have any ideas? – Sucy Apr 13 '17 at 08:32
  • @Sucy No, I am sorry. I imported the addCustomHeadingStyle() method as is from an online discussion in order to achieve a style, but I have never explored styles in a deeper way. Plus, I don't use xml files as you say...sorry but I can't be of further help. – Andrea Apr 13 '17 at 10:44
  • It is really a tricky problem for changing TOC's style.Thank you all the same! – Sucy Apr 14 '17 at 00:51

5 Answers5

11

The XWPF classes as you have seen are a work in progress, with no real overarching architecture. That will change over time as we work on it, but in the mean time you can try to add a simple TOC field to a paragraph in this way.

XWPFParagraph p;
...
// get or create your paragraph
....
CTP ctP = p.getCTP();
CTSimpleField toc = ctP.addNewFldSimple();
toc.setInstr("TOC \\h");
toc.setDirty(STOnOff.TRUE);

This will create a Table of contents with hyperlinks to the pages, it should be recalculated when Word opens it, and the table of contents will be based on predefined HeaderX styles.

jmarkmurphy
  • 11,030
  • 31
  • 59
  • Thank you so much! This really is a valuable answer, I've seen many posts with similar doubts that still unsolved. I just had to do `toc.setDirty(STOnOff.TRUE);`, instead than `toc.setDirty(STOnOff.Enum.TRUE);`. – Andrea Oct 27 '16 at 12:19
  • 1
    Thanks, I adjusted my answer based on your comment. – jmarkmurphy Oct 27 '16 at 13:39
  • Hey, when I try this I have 2 problems: * The field is not included into the document if I do not set a default text (`toc.addNewR().addNewT().setStringValue("x");`) * Neither `toc.setDirty(STOnOff.TRUE);` nor `document.enforceUpdateFields();` seem to force any word processing program to update the fields, do you know why? – Anton May 15 '20 at 09:46
4

I've solved the mystery and, unfortunately (for people having the same problem), there's no good news. Apache POI's createTOC() is bugged (to be honest, it seems a method whose implementation has been started but never completed in a proper way) (please, consider jmarkmurphy's accepted answer).

The Documentation doesn't explain anything about the method itself (it just reports the signature, and nothing more), and that's suspect.

Watching at the XWPFDocument's class code:

public void createTOC() {
    CTSdtBlock block = getDocument().getBody().addNewSdt();
    TOC toc = new TOC(block);
    for (XWPFParagraph par : this.paragraphs) {
        String parStyle = par.getStyle();
        if ((parStyle != null) && (parStyle.startsWith("Heading"))) try {
            int level = Integer.valueOf(parStyle.substring("Heading".length())).intValue();
            toc.addRow(level, par.getText(), 1, "112723803");
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }
}

Apache POI searches for paragraphs having style named "HeadingX", with X being a number. So, my variable strStyleId should have been valorized as Heading1, as an example. But this doesn't solved the problem. In fact, createTOC() always passes 1 as the page number to the addRow() method, that always sets the page as 1, this way. It does absolutely nothing in order to get that dinamically.

That's the final, unuseful result (as you can see, it's also a "fake" TOC, and not the one that you can create via Microsoft Word using the red-squared button in the question):

enter image description here

So, page numbers for a Word document cannot be retrieved dinamically (as I read in other posts), and even Apache POI seems unable to do that, sadly.

Andrea
  • 6,032
  • 2
  • 28
  • 55
0
toc.setInstr("TOC \\h");

h switch has to be used with '\', not '/', because it works correctly only with '\'. More details about using TOC switches: Use Word's TOC field to fine-tune your table of contents

zncoder
  • 61
  • 1
  • 3
0

Recently I had had the same problem, but I wanted to add a ToC with depth=2 (exclude heading 3). I've created a copy and added the ToC manually in the Word document and rename both from .docx to .zip.

Inside each zip document, there's an XML file located in document.zip/word/document.xml. This document is the document content.

I've compared both XML files and found that Word added the following value when you add a ToC:

<w:fldSimple w:instr="TOC \o "1-2" \h \z \u"/>

I've updated my code using this:

CTP ctP = paragraph.getCTP();
CTSimpleField toc = ctP.addNewFldSimple();
toc.setInstr("TOC \\o \"1-2\" \\h \\z \\u");
toc.setDirty(STOnOff.ON);

When you open the document, Word will ask you to update the references and the ToC becomes perfect. Since I want the generation process fully automated I'm still working on that part.

0

If someone still looking for the answer, i followed the suggestion provided by @jmarkmurphy.

Below is the working code

import java.io.FileOutputStream;
import java.math.BigInteger;

import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFStyle;
import org.apache.poi.xwpf.usermodel.XWPFStyles;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTOnOff;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSettings;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSimpleField;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTString;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STStyleType;

public class ApachePOIWordTOCDemo {

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

    XWPFDocument doc = new XWPFDocument();

    doc.createTOC();
    addCustomHeadingStyle(doc, "heading 1", 1);
    addCustomHeadingStyle(doc, "heading 2", 2);
    addCustomHeadingStyle(doc, "heading 3", 3);

    // the body content
    XWPFParagraph paragraph = doc.createParagraph();

    CTP ctP = paragraph.getCTP();
    CTSimpleField toc = ctP.addNewFldSimple();
    toc.setInstr("TOC \\h");
    toc.setDirty(STOnOff.TRUE);
    

    XWPFRun run = paragraph.createRun();

    paragraph = doc.createParagraph();
    run = paragraph.createRun();
    run.setText("Heading 1");
    paragraph.setStyle("heading 1");

    paragraph = doc.createParagraph();
    run = paragraph.createRun();
    run.addBreak(BreakType.PAGE);
    run.setText("Heading 2");
    paragraph.setStyle("heading 2");
    
    paragraph = doc.createParagraph();
    run = paragraph.createRun();
    run.addBreak(BreakType.PAGE);
    run.setText("Heading 3");
    paragraph.setStyle("heading 3");
    
    FileOutputStream fos = new FileOutputStream("createTOC.docx");
    doc.write(fos);
    }

    private static void addCustomHeadingStyle(XWPFDocument docxDocument, String strStyleId, int headingLevel) {

    CTStyle ctStyle = CTStyle.Factory.newInstance();
    ctStyle.setStyleId(strStyleId);

    CTString styleName = CTString.Factory.newInstance();
    styleName.setVal(strStyleId);
    ctStyle.setName(styleName);

    CTDecimalNumber indentNumber = CTDecimalNumber.Factory.newInstance();
    indentNumber.setVal(BigInteger.valueOf(headingLevel));

    // lower number > style is more prominent in the formats bar
    ctStyle.setUiPriority(indentNumber);

    CTOnOff onoffnull = CTOnOff.Factory.newInstance();
    ctStyle.setUnhideWhenUsed(onoffnull);

    // style shows up in the formats bar
    ctStyle.setQFormat(onoffnull);

    // style defines a heading of the given level
    CTPPr ppr = CTPPr.Factory.newInstance();
    ppr.setOutlineLvl(indentNumber);
    ctStyle.setPPr(ppr);

    XWPFStyle style = new XWPFStyle(ctStyle);

    // is a null op if already defined
    XWPFStyles styles = docxDocument.createStyles();

    style.setType(STStyleType.PARAGRAPH);
    styles.addStyle(style);

    }
}
Srikanth Josyula
  • 790
  • 7
  • 15