Apache POI docx file content control parse

Viewed 285

I'm trying to parse docx file that contains content control fields (that are added using window like this, reference image, mine is on another language)

enter image description here

I'm using library APACHE POI. I found this question on how to do it. I used the same code:

import java.io.FileInputStream;

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

import java.util.List;
import java.util.ArrayList;

import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.apache.xmlbeans.XmlCursor;
import javax.xml.namespace.QName;

public class ReadWordForm {

 private static List<XWPFSDT> extractSDTsFromBody(XWPFDocument document) {
  XWPFSDT sdt;
  XmlCursor xmlcursor = document.getDocument().getBody().newCursor();
  QName qnameSdt = new QName("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "sdt", "w");
  List<XWPFSDT> allsdts = new ArrayList<XWPFSDT>();
  while (xmlcursor.hasNextToken()) {
   XmlCursor.TokenType tokentype = xmlcursor.toNextToken();
   if (tokentype.isStart()) {
    if (qnameSdt.equals(xmlcursor.getName())) {
     if (xmlcursor.getObject() instanceof CTSdtRun) {
      sdt = new XWPFSDT((CTSdtRun)xmlcursor.getObject(), document); 
//System.out.println("block: " + sdt);
      allsdts.add(sdt);
     } else if (xmlcursor.getObject() instanceof CTSdtBlock) {
      sdt = new XWPFSDT((CTSdtBlock)xmlcursor.getObject(), document); 
//System.out.println("inline: " + sdt);
      allsdts.add(sdt);
     }
    } 
   }
  }
  return allsdts;
 }

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

  XWPFDocument document = new XWPFDocument(new FileInputStream("WordDataCollectingForm.docx"));

  List<XWPFSDT> allsdts = extractSDTsFromBody(document);

  for (XWPFSDT sdt : allsdts) {
//System.out.println(sdt);
   String title = sdt.getTitle();
   String content = sdt.getContent().getText();
   if (!(title == null) && !(title.isEmpty())) {
    System.out.println(title + ": " + content);
   } else {
    System.out.println("====sdt without title====");
   }
  }

  document.close();
 }
}

The problem is that this code doesn't see these fields in the my docx file until I open it in LibreOffice and re-save it. So if the file is from Windows being put into this code it doesn't see these content control fields. But if I re-save the file in the LibreOffice (using the same format) it starts to see these fields, even tho it loses some of the data (titles and tags of some fields). Can someone tell me what might be the reason of it, how do I fix that so it will see these fields? Or there's an easier way using docx4j maybe? Unfortunately there's not much info about how to do it using these 2 libs in the internet, at least I didn't find it.

Examle files are located on google disk. The first one doesn't work, the second one works (after it was opened in Libre and field was changed to one of the options).

1 Answers

According to your uploaded sample files your content controls are in a table. The code you had found only gets content controls from document body directly.

Tables are beastly things in Word as table cells may contain whole document bodies each. That's why content controls in table cells are strictly separated from content controls in main document body. Their ooxml class is CTSdtCell instead of CTSdtRun or CTSdtBlock and in apache poi their class is XWPFSDTCell instead of XWPFSDT.

If it is only about reading the content, then one could fall back to XWPFAbstractSDT which is the abstract parent class of XWPFSDTCell as well as of XWPFSDT. So following code should work:

 private static List<XWPFAbstractSDT> extractSDTsFromBody(XWPFDocument document) {
  XWPFAbstractSDT sdt;
  XmlCursor xmlcursor = document.getDocument().getBody().newCursor();
  QName qnameSdt = new QName("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "sdt", "w");
  List<XWPFAbstractSDT> allsdts = new ArrayList<XWPFAbstractSDT>();
  while (xmlcursor.hasNextToken()) {
   XmlCursor.TokenType tokentype = xmlcursor.toNextToken();
   if (tokentype.isStart()) {
    if (qnameSdt.equals(xmlcursor.getName())) {
//System.out.println(xmlcursor.getObject().getClass().getName());
     if (xmlcursor.getObject() instanceof CTSdtRun) {
      sdt = new XWPFSDT((CTSdtRun)xmlcursor.getObject(), document); 
//System.out.println("block: " + sdt);
      allsdts.add(sdt);
     } else if (xmlcursor.getObject() instanceof CTSdtBlock) {
      sdt = new XWPFSDT((CTSdtBlock)xmlcursor.getObject(), document); 
//System.out.println("inline: " + sdt);
      allsdts.add(sdt);
     } else if (xmlcursor.getObject() instanceof CTSdtCell) {
      sdt = new XWPFSDTCell((CTSdtCell)xmlcursor.getObject(), null, null); 
//System.out.println("cell: " + sdt);
      allsdts.add(sdt);
     }
    } 
   }
  }
  return allsdts;
 }

But as you see in code line sdt = new XWPFSDTCell((CTSdtCell)xmlcursor.getObject(), null, null), the XWPFSDTCell totaly lost its connection to table and tablerow.

There is not a proper method to get the XWPFSDTCell directly from a XWPFTable. So If one would need to get XWPFSDTCell connected to its table, then also parsing the XML is needed. This could look like so:

 private static List<XWPFSDTCell> extractSDTsFromTableRow(XWPFTableRow row) {
  XWPFSDTCell sdt;
  XmlCursor xmlcursor = row.getCtRow().newCursor();
  QName qnameSdt = new QName("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "sdt", "w");
  QName qnameTr = new QName("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "tr", "w");
  List<XWPFSDTCell> allsdts = new ArrayList<XWPFSDTCell>();
  while (xmlcursor.hasNextToken()) {
   XmlCursor.TokenType tokentype = xmlcursor.toNextToken();
   if (tokentype.isStart()) {
    if (qnameSdt.equals(xmlcursor.getName())) {
//System.out.println(xmlcursor.getObject().getClass().getName());
     if (xmlcursor.getObject() instanceof CTSdtCell) {
      sdt = new XWPFSDTCell((CTSdtCell)xmlcursor.getObject(), row, row.getTable().getBody()); 
//System.out.println("cell: " + sdt);
      allsdts.add(sdt);
     }
    } 
   } else if (tokentype.isEnd()) {
    //we have to check whether we are at the end of the table row
    xmlcursor.push();
    xmlcursor.toParent();  
    if (qnameTr.equals(xmlcursor.getName())) {
     break;
    }
    xmlcursor.pop();
   }
  }
  return allsdts;
 }

And called from document like so:

...
  for (XWPFTable table : document.getTables()) {
   for (XWPFTableRow row : table.getRows()) {
    List<XWPFSDTCell> allTrsdts = extractSDTsFromTableRow(row);
    for (XWPFSDTCell sdt : allTrsdts) {
//System.out.println(sdt);
     String title = sdt.getTitle();
     String content = sdt.getContent().getText();
     if (!(title == null) && !(title.isEmpty())) {
      System.out.println(title + ": " + content);
     } else {
      System.out.println("====sdt without title====");
      System.out.println(content);
     }
    }
   }
  }
...

Using curren apache poi 5.2.0 it is possible getting the XWPFSDTCell from XWPFTableRow via XWPFTableRow.getTableICells. This gets al List of ICells which is an interface which XWPFSDTCell also implemets.

So following code will get all XWPFSDTCell from tables without the need of low level XML parsing:

...
  for (XWPFTable table : document.getTables()) {
   for (XWPFTableRow row : table.getRows()) {
    for (ICell iCell : row.getTableICells()) {
     if (iCell instanceof XWPFSDTCell) {
      XWPFSDTCell sdt = (XWPFSDTCell)iCell;
//System.out.println(sdt);
      String title = sdt.getTitle();
      String content = sdt.getContent().getText();
      if (!(title == null) && !(title.isEmpty())) {
       System.out.println(title + ": " + content);
      } else {
       System.out.println("====sdt without title====");
       System.out.println(content);
      }
     }
    }
   }
  }
...
Related