Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for replacing placeholder expressions with top-level docx4j elements #40

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j</artifactId>
<version>3.3.0</version>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ public void replace(ParagraphWrapper p, String placeholder, Object replacementOb
if (replacementObject instanceof R) {
RunUtil.applyParagraphStyle(p.getParagraph(), (R) replacementObject);
}
p.replace(placeholder, replacementObject);
try {
p.replace(placeholder, replacementObject);
} catch (DocxStamperException e) {
throw new DocxStamperException("Failed to replace expression " + placeholder + ".", e);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.wickedsource.docxstamper.replace;
package org.wickedsource.docxstamper.util;

import org.docx4j.wml.R;
import org.wickedsource.docxstamper.util.RunUtil;

public class IndexedRun {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.wickedsource.docxstamper.util;

import org.docx4j.jaxb.Context;
import org.docx4j.wml.Body;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
Expand All @@ -20,11 +21,14 @@ private ParagraphUtil() {
* @return a new paragraph containing the given text.
*/
public static P create(String... texts) {
Body b = objectFactory.createBody();
P p = objectFactory.createP();
for (String text : texts) {
R r = RunUtil.create(text, p);
p.getContent().add(r);
}
b.getContent().add(p);
p.setParent(b);
return p;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package org.wickedsource.docxstamper.util;

import java.io.StringWriter;
import org.docx4j.TextUtils;
import org.docx4j.wml.ContentAccessor;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.wickedsource.docxstamper.replace.IndexedRun;
import org.wickedsource.docxstamper.util.RunUtil;
import org.wickedsource.docxstamper.api.DocxStamperException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.google.common.collect.Lists;

/**
* A "Run" defines a region of text within a docx document with a common set of properties. Word processors are
Expand Down Expand Up @@ -84,27 +88,21 @@ public void replace(String placeholder, Object replacement) {

if (placeholderSpansCompleteRun) {
this.paragraph.getContent().remove(run.getRun());
this.paragraph.getContent().add(run.getIndexInParent(), replacement);
recalculateRuns();
addReplacement(run, replacement);
} else if (placeholderAtStartOfRun) {
run.replace(matchStartIndex, matchEndIndex, "");
this.paragraph.getContent().add(run.getIndexInParent(), replacement);
recalculateRuns();
addReplacement(run, replacement);
} else if (placeholderAtEndOfRun) {
run.replace(matchStartIndex, matchEndIndex, "");
this.paragraph.getContent().add(run.getIndexInParent() + 1, replacement);
recalculateRuns();
addReplacement(run, 1, replacement);
} else if (placeholderWithinRun) {
String runText = RunUtil.getText(run.getRun());
int startIndex = runText.indexOf(placeholder);
int endIndex = startIndex + placeholder.length();
R run1 = RunUtil.create(runText.substring(0, startIndex), this.paragraph);
R run2 = RunUtil.create(runText.substring(endIndex), this.paragraph);
this.paragraph.getContent().add(run.getIndexInParent(), run2);
this.paragraph.getContent().add(run.getIndexInParent(), replacement);
this.paragraph.getContent().add(run.getIndexInParent(), run1);
addReplacement(run, Lists.newArrayList(run1, replacement, run2));
this.paragraph.getContent().remove(run.getRun());
recalculateRuns();
}

} else {
Expand All @@ -123,9 +121,75 @@ public void replace(String placeholder, Object replacement) {
}

// add replacement run between first and last run
this.paragraph.getContent().add(firstRun.getIndexInParent() + 1, replacement);
addReplacement(firstRun, 1, replacement);

recalculateRuns();
}
recalculateRuns();
}

/**
* @param run the (first) affected run
* @param replacement the docx4j element(s) to replace the expression
*/
private void addReplacement(IndexedRun run, Object replacement) {
addReplacement(run, 0, replacement);
}

/**
* @param run the (first) affected run
* @param offset offset relative to the given run to add the replacement
* @param replacement the docx4j element(s) to replace the expression
*/
private void addReplacement(IndexedRun run, int offset, Object replacement) {
if (replacement instanceof Collection) {
addReplacement(run, offset, (Collection<?>) replacement);
} else {
addReplacement(run, offset, Lists.newArrayList(replacement));
}
}

/**
* @param run the (first) affected run
* @param replacement the docx4j element(s) to replace the expression
*/
private void addReplacement(IndexedRun run, Collection<?> replacement) {
addReplacement(run, 0, replacement);
}
/**
* @param run the (first) affected run
* @param offset offset relative to the given run to add the replacement
* @param replacement docx4j element(s) to replace the expression
*/
private void addReplacement(IndexedRun run, int offset, Collection<?> replacement) {
// multiple docx4j elements (multiple runs and/or possibly content from a separate document)
ContentAccessor parent = (ContentAccessor) this.paragraph.getParent();
int thisParagraphIndex = parent.getContent().indexOf(this.paragraph);
int i = offset, j = thisParagraphIndex;
for (Object part : replacement) {
if (part instanceof R) {
// we're adding a collection of runs; add them to this paragraph
this.paragraph.getContent().add(run.getIndexInParent() + i++, part);
} else {
// element such as table or another paragraph - add as a sibling of this paragraph
parent.getContent().add(j++, part);
}
}
if (j > thisParagraphIndex) {
String text;
try {
StringWriter sw = new StringWriter();
TextUtils.extractText(paragraph, sw);
text = sw.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
if (!text.trim().isEmpty()) {
throw new DocxStamperException("Template placeholder expressions to be replaced"
+ " with anything other than a simple text run should be in their own"
+ " paragraph.");
}
// replacement added as sibling(s) and this (placeholder) paragraph is now empty
parent.getContent().remove(paragraph);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ public void walk() {
walkContent(headerPart.getContent());
}

// walk through elements in main document part
walkContent(document.getMainDocumentPart().getContent());
// walk through elements in main document part.
//
// walk a copy of the document content list to avoid a comod exception in the event that a
// custom ITypeResolver wants to insert top-level element(s) like a paragraph or table
// (e.g. content from another document)
walkContent(new ArrayList<>(document.getMainDocumentPart().getContent()));

// walk through elements in headers
List<Relationship> footerRelationships = getRelationshipsOfType(document, Namespaces.FOOTER);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.wickedsource.docxstamper;

import static org.junit.Assert.assertEquals;

import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.docx4j.TextUtils;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Br;
import org.docx4j.wml.ContentAccessor;
import org.junit.Test;
import org.wickedsource.docxstamper.api.DocxStamperException;
import org.wickedsource.docxstamper.api.typeresolver.ITypeResolver;

import com.google.common.collect.Lists;

public class ExpressionReplacementWithDocumentElementsTest extends AbstractDocx4jTest {

private static final String TEST_FILE = "ExpressionReplacementWithDocumentElements.docx";
private static final String BAD_TEST_FILE =
"ExpressionReplacementWithDocumentElements-Invalid.docx";

@Test
public void test() throws Exception {
test(TEST_FILE);
}

@Test(expected = DocxStamperException.class)
public void testInvalidFile() throws Exception {
test(BAD_TEST_FILE);
}

private void test(String testFile) throws Exception {
InputStream template = getClass().getResourceAsStream(testFile);
DocxStamperConfiguration config = new DocxStamperConfiguration();
config.addTypeResolver(MainDocumentPart.class, new DocumentContentResolver());
WordprocessingMLPackage stamped = stampAndLoad(template, new TestContext(), config);

assertPagesMatch(stamped);
}

/**
* tests that the content on the first page of the test document was copied to the
* placeholder location on the second page
*
* @param stamped
* @throws Exception
*/
private void assertPagesMatch(WordprocessingMLPackage stamped) throws Exception {
List<Object> content = stamped.getMainDocumentPart().getContent();
boolean inFirstPage = true;
ArrayList<Object> page1 = Lists.newArrayList();
ArrayList<Object> page2 = Lists.newArrayList();
for (Object o : content) {
if (hasPageBreak(o)) {
inFirstPage = false;
continue;
}
if (inFirstPage) {
page1.add(o);
} else {
page2.add(o);
}
}
assertEquals(13, page1.size());
List<Object> expected = loadDocument(TEST_FILE).getMainDocumentPart().getContent();
for (int i = 0; i < page1.size(); i++) {
assertEquals(extractText(expected.get(i)), extractText(page1.get(i)));
assertEquals(extractText(expected.get(i)), extractText(page2.get(i)));
}
return;
}

private String extractText(Object e) throws Exception {
StringWriter w = new StringWriter();
TextUtils.extractText(e, w);
return w.toString();
}

private boolean hasPageBreak(Object o) {
if (o instanceof Br) {
return true;
}
if (!(o instanceof ContentAccessor)) {
return false;
}
for (Object c : ((ContentAccessor) o).getContent()) {
if (hasPageBreak(c)) {
return true;
}
}
return false;
}

private final class DocumentContentResolver
implements ITypeResolver<MainDocumentPart, Collection<?>> {
public Collection<?> resolve(WordprocessingMLPackage document,
MainDocumentPart expressionResult) {
List<Object> content = expressionResult.getContent();
// leave the placeholder paragraph out of the replacement content
content.remove(content.size() - 1);
return content;
}
}

public class TestContext {

private WordprocessingMLPackage doc;

TestContext() throws Docx4JException {
doc = loadDocument(TEST_FILE);
}

public MainDocumentPart getCopiedElements() {
return doc.getMainDocumentPart();
}
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package org.wickedsource.docxstamper.replace;
package org.wickedsource.docxstamper.util;

import org.docx4j.jaxb.Context;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.R;
import org.junit.Assert;
import org.junit.Test;
import org.wickedsource.docxstamper.util.RunUtil;

public class IndexedRunTest {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.wickedsource.docxstamper.replace;
package org.wickedsource.docxstamper.util;

import org.junit.Assert;
import org.junit.Test;
Expand Down
Binary file not shown.
Binary file not shown.