Skip to content

Commit

Permalink
Rework table paragraph substitution/removal
Browse files Browse the repository at this point in the history
While debugging replacing custom placeholders
in tables, another issues related to nested
paragraph substitution/removal was uncovered.
This has been fixed at the price of more
complicated code involving accessing the raw
xml stuff.
  • Loading branch information
AntonOellerer committed Aug 8, 2023
1 parent 09bb80f commit 65a2937
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 88 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group 'com.docutools'
version = '1.6.2'
version = '1.6.3-beta.1'

java {
toolchain {
Expand Down
@@ -0,0 +1,8 @@
package com.docutools.jocument.impl.word;

//TODO move to checked expression on major version bump
public class ElementRemovalException extends RuntimeException {
public ElementRemovalException(Exception e) {
super(e);
}
}
Expand Up @@ -17,6 +17,7 @@
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFSDT;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;

class WordGenerator {
private static final Logger logger = LogManager.getLogger();
Expand Down Expand Up @@ -72,9 +73,8 @@ private void transform(XWPFTable table) {
table.getRows()
.stream()
.flatMap(xwpfTableRow -> xwpfTableRow.getTableCells().stream())
.flatMap(xwpfTableCell -> xwpfTableCell.getParagraphs().stream())
.filter(xwpfParagraph -> !xwpfParagraph.isEmpty())
.forEach(this::transform);
.map(XWPFTableCell::getBodyElements)
.forEachOrdered(bodyElements -> new WordGenerator(this.resolver, bodyElements, options).generate());
logger.debug("Transformed table {}", table);
}

Expand Down
209 changes: 125 additions & 84 deletions src/main/java/com/docutools/jocument/impl/word/WordUtilities.java
@@ -1,6 +1,7 @@
package com.docutools.jocument.impl.word;

import com.docutools.jocument.impl.ParsingUtils;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -16,6 +17,7 @@
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.xwpf.usermodel.IBody;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFFooter;
Expand All @@ -26,6 +28,9 @@
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHdrFtr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
Expand Down Expand Up @@ -80,7 +85,7 @@ public static void replaceText(XWPFParagraph paragraph, String newText) {
* @return {@code true} when exists
*/
public static boolean exists(IBodyElement element) {
return findPositionInBody(element).orElseGet(() -> findPositionInHeader(element).orElseGet(() -> findPositionInFooter(element).orElse(-1))) != -1;
return findPositionInBody(element).isPresent() || findInHeader(element) || findInFooter(element) || findNestedInTable(element);
}

/**
Expand All @@ -89,25 +94,9 @@ public static boolean exists(IBodyElement element) {
* @param element the element
* @return the index
*/
public static OptionalInt findPositionInBody(IBodyElement element) {
var document = element.getBody().getXWPFDocument();
if (element instanceof XWPFParagraph xwpfParagraph) {
var position = document.getPosOfParagraph(xwpfParagraph);
if (position >= 0) {
return OptionalInt.of(position);
} else {
return OptionalInt.empty();
}
} else if (element instanceof XWPFTable xwpfTable) {
var position = document.getPosOfTable(xwpfTable);
if (position >= 0) {
return OptionalInt.of(position);
} else {
return OptionalInt.empty();
}
}
logger.warn("Failed to find position of element {}", element);
return OptionalInt.empty();
public static Optional<Integer> findPositionInBody(IBodyElement element) {
int index = element.getBody().getXWPFDocument().getBodyElements().indexOf(element);
return index == -1 ? Optional.empty() : Optional.of(index);
}

/**
Expand Down Expand Up @@ -149,98 +138,150 @@ public static IBodyElement copyBefore(IBodyElement element, IBodyElement destina
* @param element the element to be removed
*/
public static void removeIfExists(IBodyElement element) {
logger.debug("Removing element {}", element);
var document = element.getBody().getXWPFDocument();
OptionalInt position = findPositionInBody(element);
if (position.isPresent()) {
document.removeBodyElement(position.getAsInt());
} else {
if (element instanceof XWPFParagraph xwpfParagraph) {
var positionInHeader = findPositionInHeader(xwpfParagraph, document.getHeaderList());
if (positionInHeader.isPresent()) {
document.getHeaderArray(positionInHeader.getAsInt()).removeParagraph(xwpfParagraph);
} else {
var positionInFooter = findPositionInFooter(xwpfParagraph, document.getFooterList());
positionInFooter.ifPresent(integer -> document.getHeaderArray(integer).removeParagraph(xwpfParagraph));
IBody body = element.getBody();
CTDocument1 document = body.getXWPFDocument().getDocument();
if (element instanceof XWPFParagraph xwpfParagraph) {
try (XmlCursor xmlCursor = xwpfParagraph.getCTP().newCursor()) {
XmlObject object = getParentObject(xmlCursor);
if (object.equals(document.getBody())) {
findPositionInBody(element).ifPresent(pos -> body.getXWPFDocument().removeBodyElement(pos));
} else if (object instanceof CTTc ctTc) {
removeElementFromTable(element, ctTc, xmlCursor, body);
} else if (object instanceof CTHdrFtr ctHdrFtr) {
xmlCursor.toParent();
new XWPFFooter(body.getXWPFDocument(), ctHdrFtr).removeParagraph(xwpfParagraph);
}
} else if (element instanceof XWPFTable xwpfTable) {
var positionInHeader = findPositionInHeader(xwpfTable, document.getHeaderList());
if (positionInHeader.isPresent()) {
document.getHeaderArray(positionInHeader.getAsInt()).removeTable(xwpfTable);
} else {
var positionInFooter = findPositionInFooter(xwpfTable, document.getFooterList());
positionInFooter.ifPresent(integer -> document.getHeaderArray(integer).removeTable(xwpfTable));
} catch (IOException e) {
throw new ElementRemovalException(e);
}
} else if (element instanceof XWPFTable xwpfTable) {
try (XmlCursor xmlCursor = xwpfTable.getCTTbl().newCursor()) {
XmlObject object = getParentObject(xmlCursor);
if (object.equals(document.getBody())) {
findPositionInBody(element).ifPresent(pos -> body.getXWPFDocument().removeBodyElement(pos));
} else if (object instanceof CTTc ctTc) {
removeElementFromTable(element, ctTc, xmlCursor, body);
} else if (object instanceof CTHdrFtr ctHdrFtr) {
xmlCursor.toParent();
new XWPFFooter(body.getXWPFDocument(), ctHdrFtr).removeTable(xwpfTable);
}
} catch (IOException e) {
throw new ElementRemovalException(e);
}
}
}

private static OptionalInt findPositionInHeader(IBodyElement element) {
if (element instanceof XWPFParagraph xwpfParagraph) {
return findPositionInHeader(xwpfParagraph, element.getBody().getXWPFDocument().getHeaderList());
} else if (element instanceof XWPFTable xwpfTable) {
return findPositionInHeader(xwpfTable, element.getBody().getXWPFDocument().getHeaderList());
}
return OptionalInt.empty();
private static XmlObject getParentObject(XmlCursor xmlCursor) {
xmlCursor.toParent();
return xmlCursor.getObject();
}

private static void removeElementFromTable(IBodyElement element, CTTc ctTc, XmlCursor xmlCursor, IBody body) {
XmlObject rowObject = getParentObject(xmlCursor);
XmlObject tableObject = getParentObject(xmlCursor);
XWPFTableCell cell = new XWPFTableCell(ctTc, new XWPFTableRow((CTRow) rowObject, new XWPFTable((CTTbl) tableObject, body)), body);
findPositionInParagraphs(element, cell.getParagraphs()).ifPresent(cell::removeParagraph);
}

private static boolean findInHeader(IBodyElement element) {
return findInHeader(element, element.getBody().getXWPFDocument().getHeaderList());
}

private static OptionalInt findPositionInHeader(XWPFParagraph xwpfParagraph, List<XWPFHeader> headerList) {
var i = 0;
for (XWPFHeader xwpfHeader : headerList) {
for (XWPFParagraph paragraph : xwpfHeader.getParagraphs()) {
if (xwpfParagraph.equals(paragraph)) {
return OptionalInt.of(i);
private static boolean findInHeader(IBodyElement element, List<XWPFHeader> headers) {
for (XWPFHeader header : headers) {
for (IBodyElement bodyElement : header.getBodyElements()) {
if (element.equals(bodyElement)) {
return true;
}
}
if (findInTables(element, header.getTables())) {
return true;
}
}
return OptionalInt.empty();
return false;
}

private static OptionalInt findPositionInHeader(XWPFTable xwpfTable, List<XWPFHeader> headerList) {
var i = 0;
for (XWPFHeader xwpfHeader : headerList) {
for (XWPFTable table : xwpfHeader.getTables()) {
if (xwpfTable.equals(table)) {
return OptionalInt.of(i);
private static boolean findInFooter(IBodyElement element) {
return findInFooter(element, element.getBody().getXWPFDocument().getFooterList());
}

private static boolean findInFooter(IBodyElement element, List<XWPFFooter> footers) {
for (XWPFFooter footer : footers) {
for (IBodyElement bodyElement : footer.getBodyElements()) {
if (element.equals(bodyElement)) {
return true;
}
}
if (findInTables(element, footer.getTables())) {
return true;
}
}
return OptionalInt.empty();
return false;
}

private static OptionalInt findPositionInFooter(IBodyElement element) {
if (element instanceof XWPFParagraph xwpfParagraph) {
return findPositionInFooter(xwpfParagraph, element.getBody().getXWPFDocument().getFooterList());
} else if (element instanceof XWPFTable xwpfTable) {
return findPositionInFooter(xwpfTable, element.getBody().getXWPFDocument().getFooterList());
private static boolean findNestedInTable(IBodyElement element) {
for (XWPFTable table : element.getBody().getXWPFDocument().getTables()) {
if (findInTable(element, table)) {
return true;
}
}
return OptionalInt.empty();
return false;
}

private static OptionalInt findPositionInFooter(XWPFParagraph xwpfParagraph, List<XWPFFooter> footerList) {
var i = 0;
for (XWPFFooter xwpfFooter : footerList) {
for (XWPFParagraph paragraph : xwpfFooter.getParagraphs()) {
if (xwpfParagraph.equals(paragraph)) {
return OptionalInt.of(i);
}
private static boolean findInTable(IBodyElement element, XWPFTable table) {
for (XWPFTableRow row : table.getRows()) {
if (findInRow(element, row)) {
return true;
}
}
return OptionalInt.empty();
return false;
}

private static OptionalInt findPositionInFooter(XWPFTable xwpfTable, List<XWPFFooter> footerList) {
var i = 0;
for (XWPFFooter xwpfFooter : footerList) {
for (XWPFTable table : xwpfFooter.getTables()) {
if (xwpfTable.equals(table)) {
return OptionalInt.of(i);
}
private static boolean findInRow(IBodyElement element, XWPFTableRow row) {
for (XWPFTableCell cell : row.getTableCells()) {
if (findInCell(element, cell)) {
return true;
}
}
return false;
}

private static boolean findInCell(IBodyElement element, XWPFTableCell cell) {
if (element instanceof XWPFParagraph && findInParagraphs(element, cell.getParagraphs())) {
return true;
}
return findInTables(element, cell.getTables());
}

private static boolean findInParagraphs(IBodyElement element, List<XWPFParagraph> paragraphs) {
for (XWPFParagraph paragraph : paragraphs) {
if (element.equals(paragraph)) {
return true;
}
}
return false;
}

private static OptionalInt findPositionInParagraphs(IBodyElement element, List<XWPFParagraph> paragraphs) {
var position = 0;
for (XWPFParagraph paragraph : paragraphs) {
if (element.equals(paragraph)) {
return OptionalInt.of(position);
}
position++;
}
return OptionalInt.empty();
}

private static boolean findInTables(IBodyElement element, List<XWPFTable> tables) {
for (XWPFTable nestedTable : tables) {
if ((element instanceof XWPFTable && element.equals(nestedTable)) || findInTable(element, nestedTable)) {
return true;
}
}
return false;
}

/**
* Opens a {@link org.apache.xmlbeans.XmlCursor} to the given element in its {@link org.apache.poi.xwpf.usermodel.XWPFDocument}.
*
Expand All @@ -250,10 +291,10 @@ private static OptionalInt findPositionInFooter(XWPFTable xwpfTable, List<XWPFFo
public static Optional<XmlCursor> openCursor(IBodyElement element) {
if (element instanceof XWPFParagraph xwpfParagraph) {
logger.debug("Opening cursor to paragraph {}", xwpfParagraph);
return Optional.of((xwpfParagraph).getCTP().newCursor());
return Optional.of(xwpfParagraph.getCTP().newCursor());
} else if (element instanceof XWPFTable xwpfTable) {
logger.debug("Opening cursor to table {}", xwpfTable);
return Optional.of((xwpfTable).getCTTbl().newCursor());
return Optional.of(xwpfTable.getCTTbl().newCursor());
} else {
logger.warn("Failed to open cursor to element {}", element);
return Optional.empty();
Expand Down
Expand Up @@ -26,6 +26,7 @@
import java.util.Locale;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -126,6 +127,27 @@ void shouldReplacePlaceholdersInTables() throws InterruptedException, IOExceptio
assertThat(table.row(2).cell(1).bodyElement(0).asParagraph().text(), equalTo(birthdate));
}

@Test
@Disabled("Pending apache poi 5.2.4 release")
@DisplayName("Replace custom placeholders in tables.")
void shouldReplaceCustomPlaceholderInTable() throws InterruptedException, IOException {
// Arrange
Template template = Template.fromClassPath("/templates/word/CustomPlaceholderInTableTemplate.docx")
.orElseThrow();
PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.PICARD);

// Act
Document document = template.startGeneration(resolver);
document.blockUntilCompletion(60000L); // 1 minute

// Assert
assertThat(document.completed(), is(true));
xwpfDocument = TestUtils.getXWPFDocumentFromDocument(document);
var documentWrapper = new XWPFDocumentWrapper(xwpfDocument);
var table = documentWrapper.bodyElement(0).asTable();
assertThat(table.row(0).cell(1).bodyElement(0).asParagraph().run(0).pictures().size(), equalTo(1));
}

@Test
@DisplayName("Resolve collection placeholders.")
void shouldResolveCollectionPlaceholders() throws InterruptedException, IOException {
Expand Down
Binary file not shown.

0 comments on commit 65a2937

Please sign in to comment.