From b45ca6dc90132fa751114a52078e4b3a4f9fefc4 Mon Sep 17 00:00:00 2001 From: Anton Oellerer <13524304+AntonOellerer@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:51:44 +0200 Subject: [PATCH 1/2] Concatenate `.toString` of elements in iterableplaceholder --- build.gradle | 2 +- .../docutools/jocument/impl/IterablePlaceholderData.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c62c0401..bc179898 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group 'com.docutools' -version = '1.6.3' +version = '1.6.4-beta.1' java { toolchain { diff --git a/src/main/java/com/docutools/jocument/impl/IterablePlaceholderData.java b/src/main/java/com/docutools/jocument/impl/IterablePlaceholderData.java index 71890908..5399144c 100644 --- a/src/main/java/com/docutools/jocument/impl/IterablePlaceholderData.java +++ b/src/main/java/com/docutools/jocument/impl/IterablePlaceholderData.java @@ -5,6 +5,7 @@ import com.docutools.jocument.PlaceholderType; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -45,4 +46,10 @@ public long count() { return count; } + @Override + public String toString() { + return StreamSupport.stream(iterable.spliterator(), false) + .map(Object::toString) + .collect(Collectors.joining(", ")); + } } From 7fb7cf1b74050ebd80f677df6f051f01bf485de6 Mon Sep 17 00:00:00 2001 From: Alexander Partsch Date: Wed, 9 Aug 2023 14:36:55 +0200 Subject: [PATCH 2/2] Fix Resolving IterablePlaceholders when no End-Marker present When you have a placeholder that's being resolved to IterablePlaceholderData and no End-Marker was present, the Word- and ExcelGenerators still tried to unroll them. But the default behaviour should resolve a scalar value to the beans toString method. * Therefore, the WordGenerator#isLoopStart and ExcelGenerator#isLoopStart now probe if a end loop marker is present and only then evaluate to true. * The ReflectionResolver#toString method is returning the Beans toString value. --- build.gradle | 2 +- .../jocument/PlaceholderResolver.java | 9 ++++++ .../docutools/jocument/impl/JsonResolver.java | 5 +++ .../jocument/impl/ReflectionResolver.java | 5 +++ .../excel/implementations/ExcelGenerator.java | 30 ++++++++++++++---- .../jocument/impl/word/WordGenerator.java | 27 ++++++++++------ .../implementations/ExcelGeneratorTest.java | 21 ++++++++++++ .../jocument/impl/word/WordGeneratorTest.java | 19 +++++++++++ .../sample/SamplePlaceholderResolver.java | 6 ++++ .../sample/ServicePlaceholderResolver.java | 5 +++ .../jocument/sample/model/FirstOfficer.java | 5 +++ .../excel/IterablePlaceholderWithoutLoop.xlsx | Bin 0 -> 9067 bytes .../word/IterablePlaceholderWithoutLoop.docx | Bin 0 -> 11968 bytes 13 files changed, 118 insertions(+), 16 deletions(-) create mode 100644 src/test/resources/templates/excel/IterablePlaceholderWithoutLoop.xlsx create mode 100644 src/test/resources/templates/word/IterablePlaceholderWithoutLoop.docx diff --git a/build.gradle b/build.gradle index bc179898..7605b13c 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group 'com.docutools' -version = '1.6.4-beta.1' +version = '1.6.4' java { toolchain { diff --git a/src/main/java/com/docutools/jocument/PlaceholderResolver.java b/src/main/java/com/docutools/jocument/PlaceholderResolver.java index 50e81b02..7d68d614 100644 --- a/src/main/java/com/docutools/jocument/PlaceholderResolver.java +++ b/src/main/java/com/docutools/jocument/PlaceholderResolver.java @@ -56,4 +56,13 @@ private PlaceholderData format(Locale locale, PlaceholderData original) { protected abstract Optional doResolve(String placeholderName, Locale locale); + /** + * Overwrite of the {@link Object#toString()} method, forcing any subclass to implement it, + * since its required for resolving {@link com.docutools.jocument.impl.IterablePlaceholderData} + * when no loop-end is present + * + * @return a {@link String} + */ + @Override + public abstract String toString(); } diff --git a/src/main/java/com/docutools/jocument/impl/JsonResolver.java b/src/main/java/com/docutools/jocument/impl/JsonResolver.java index 3ca991be..d5daad5f 100644 --- a/src/main/java/com/docutools/jocument/impl/JsonResolver.java +++ b/src/main/java/com/docutools/jocument/impl/JsonResolver.java @@ -79,6 +79,11 @@ protected Optional doResolve(String placeholderName, Locale loc return Optional.empty(); } + @Override + public String toString() { + return jsonElement.getAsString(); + } + private Optional fromObject(String placeholderName, JsonObject jsonObject) { if (!jsonObject.has(placeholderName)) { logger.info("Did not find placeholder {} in JSON Object {}", placeholderName, jsonObject); diff --git a/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java b/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java index a9de0969..66cfd36d 100644 --- a/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java +++ b/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java @@ -507,4 +507,9 @@ private Object resolveNonFinalValue(Object property, String placeholderName) } return resolvedProperty; } + + @Override + public String toString() { + return bean.toString(); + } } diff --git a/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelGenerator.java b/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelGenerator.java index 758b2f45..e44db9cc 100644 --- a/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelGenerator.java +++ b/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelGenerator.java @@ -7,17 +7,20 @@ import com.docutools.jocument.impl.ParsingUtils; import com.docutools.jocument.impl.excel.interfaces.ExcelWriter; import com.docutools.jocument.impl.excel.util.ExcelUtils; +import com.docutools.jocument.impl.word.WordUtilities; import com.google.common.collect.Lists; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Optional; +import java.util.stream.StreamSupport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.util.LocaleUtil; +import org.apache.poi.xwpf.usermodel.XWPFParagraph; /** @@ -165,12 +168,27 @@ private boolean isLoopStart(Row row) { if (ExcelUtils.getNumberOfNonEmptyCells(row) == 1) { var cell = row.getCell(row.getFirstCellNum()); if (cell.getCellType() == CellType.STRING) { - return resolver.resolve( - ParsingUtils.stripBrackets( - cell.getStringCellValue() - )).map(PlaceholderData::getType) - .map(type -> type == PlaceholderType.SET) - .orElse(false); + var placeholderName = ParsingUtils.stripBrackets( + cell.getStringCellValue() + ); + return resolver.resolve(placeholderName) + .filter(placeholderData -> placeholderData.getType() == PlaceholderType.SET) + .map(placeholderData -> { + var endLoopMarkers = ParsingUtils.getMatchingLoopEnds(placeholderName); + // Since ExcelGenerator only takes an Iterator of the remaining elements, we can't + // do a lookahead from that. So we access the sheet from the current row and create + // a new iterator dropping everything before the current row. + return StreamSupport.stream(row.getSheet().spliterator(), false) + // since we only get the physical row index we need to manually probe till we + // reach the current iterator state + .dropWhile(nextRow -> !nextRow.equals(row)) + .skip(1) + .filter(nextRow -> ExcelUtils.getNumberOfNonEmptyCells(nextRow) == 1) + .map(nextRow -> nextRow.getCell(nextRow.getFirstCellNum())) + .filter(nextCell -> nextCell.getCellType() == CellType.STRING) + .map(Cell::getStringCellValue) + .anyMatch(text -> endLoopMarkers.contains(text.strip())); + }).orElse(false); } } return false; diff --git a/src/main/java/com/docutools/jocument/impl/word/WordGenerator.java b/src/main/java/com/docutools/jocument/impl/word/WordGenerator.java index 7ef8eba4..a7ef72df 100644 --- a/src/main/java/com/docutools/jocument/impl/word/WordGenerator.java +++ b/src/main/java/com/docutools/jocument/impl/word/WordGenerator.java @@ -58,7 +58,7 @@ private void transform(IBodyElement element, List remaining) { if (isCustomPlaceholder(element)) { resolver.resolve(WordUtilities.extractPlaceholderName((XWPFParagraph) element)) .ifPresent(placeholderData -> placeholderData.transform(element, locale, options)); - } else if (isLoopStart(element)) { + } else if (isLoopStart(element, remaining)) { unrollLoop((XWPFParagraph) element, remaining); } else if (element instanceof XWPFParagraph xwpfParagraph) { transform(xwpfParagraph); @@ -121,14 +121,23 @@ private List getLoopBody(String placeholderName, List type == PlaceholderType.SET) - .orElse(false); + private boolean isLoopStart(IBodyElement element, List remaining) { + if (element instanceof XWPFParagraph xwpfParagraph) { + var placeholderName = ParsingUtils.stripBrackets( + WordUtilities.toString(xwpfParagraph) + ); + return resolver.resolve(placeholderName) + .filter(placeholderData -> placeholderData.getType() == PlaceholderType.SET) + .map(placeholderData -> { + var endLoopMarkers = ParsingUtils.getMatchingLoopEnds(placeholderName); + return remaining.stream() + .filter(bodyElement -> bodyElement instanceof XWPFParagraph) + .map(bodyElement -> (XWPFParagraph)bodyElement) + .map(XWPFParagraph::getText) + .anyMatch(text -> endLoopMarkers.contains(text.strip())); + }).orElse(false); + } + return false; } private boolean isCustomPlaceholder(IBodyElement element) { diff --git a/src/test/java/com/docutools/jocument/impl/excel/implementations/ExcelGeneratorTest.java b/src/test/java/com/docutools/jocument/impl/excel/implementations/ExcelGeneratorTest.java index d54a8d6d..7172988b 100644 --- a/src/test/java/com/docutools/jocument/impl/excel/implementations/ExcelGeneratorTest.java +++ b/src/test/java/com/docutools/jocument/impl/excel/implementations/ExcelGeneratorTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; @@ -17,6 +18,7 @@ import com.docutools.jocument.sample.placeholders.QuotesBlockPlaceholder; import com.docutools.poipath.PoiPath; import com.docutools.poipath.xssf.XSSFWorkbookWrapper; +import com.docutools.poipath.xwpf.XWPFDocumentWrapper; import java.io.IOException; import java.time.LocalDate; import java.time.Period; @@ -268,4 +270,23 @@ void evaluatesLargeDocument() throws InterruptedException { assertThat(document.completed(), is(true)); } + + @Test + @DisplayName("Resolve IterablePlaceholder with toString when there's no closing placeholder.") + void shouldResolveIPWithToStringWhenNoLoop() throws InterruptedException, IOException { + // Assemble + Template template = Template.fromClassPath("/templates/excel/IterablePlaceholderWithoutLoop.xlsx") + .orElseThrow(); + var resolver = new ReflectionResolver(SampleModelData.PICARD); + + // Act + Document document = template.startGeneration(resolver); + document.blockUntilCompletion(60000L); // 1 minute + + // Assert + assertThat(document.completed(), is(true)); + workbook = TestUtils.getXSSFWorkbookFromDocument(document); + var documentWrapper = new XSSFWorkbookWrapper(workbook); + assertThat(documentWrapper.sheet(0).row(0).cell(0).text(), containsString(SampleModelData.PICARD.getOfficer().toString())); + } } \ No newline at end of file diff --git a/src/test/java/com/docutools/jocument/impl/word/WordGeneratorTest.java b/src/test/java/com/docutools/jocument/impl/word/WordGeneratorTest.java index 69e45577..756df8b2 100644 --- a/src/test/java/com/docutools/jocument/impl/word/WordGeneratorTest.java +++ b/src/test/java/com/docutools/jocument/impl/word/WordGeneratorTest.java @@ -451,4 +451,23 @@ void shouldFormatInstant() throws IOException, InterruptedException { var documentWrapper = new XWPFDocumentWrapper(xwpfDocument); assertThat(documentWrapper.bodyElement(0).asParagraph().text(), containsString(SampleModelData.PICARD_PERSON.getEntryDate().toString())); } + + @Test + @DisplayName("Resolve IterablePlaceholder with toString when there's no closing placeholder.") + void shouldResolveIPWithToStringWhenNoLoop() throws InterruptedException, IOException { + // Assemble + Template template = Template.fromClassPath("/templates/word/IterablePlaceholderWithoutLoop.docx") + .orElseThrow(); + var 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); + assertThat(documentWrapper.bodyElement(0).asParagraph().text(), containsString(SampleModelData.PICARD.getOfficer().toString())); + } } \ No newline at end of file diff --git a/src/test/java/com/docutools/jocument/sample/SamplePlaceholderResolver.java b/src/test/java/com/docutools/jocument/sample/SamplePlaceholderResolver.java index 94e4add0..bd8a5804 100644 --- a/src/test/java/com/docutools/jocument/sample/SamplePlaceholderResolver.java +++ b/src/test/java/com/docutools/jocument/sample/SamplePlaceholderResolver.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.stream.Collectors; public class SamplePlaceholderResolver extends PlaceholderResolver { @@ -26,4 +27,9 @@ protected Optional doResolve(String placeholderName, Locale loc } } + @Override + public String toString() { + return services.stream().map(PlaceholderResolver::toString).collect(Collectors.joining(", ")); + } + } diff --git a/src/test/java/com/docutools/jocument/sample/ServicePlaceholderResolver.java b/src/test/java/com/docutools/jocument/sample/ServicePlaceholderResolver.java index 6e5baf5a..cd322930 100644 --- a/src/test/java/com/docutools/jocument/sample/ServicePlaceholderResolver.java +++ b/src/test/java/com/docutools/jocument/sample/ServicePlaceholderResolver.java @@ -24,4 +24,9 @@ protected Optional doResolve(String placeholderName, Locale loc } } + @Override + public String toString() { + return name; + } + } diff --git a/src/test/java/com/docutools/jocument/sample/model/FirstOfficer.java b/src/test/java/com/docutools/jocument/sample/model/FirstOfficer.java index a6583b9d..d3aa98be 100644 --- a/src/test/java/com/docutools/jocument/sample/model/FirstOfficer.java +++ b/src/test/java/com/docutools/jocument/sample/model/FirstOfficer.java @@ -23,4 +23,9 @@ public int getRank() { public Uniform getUniform() { return uniform; } + + @Override + public String toString() { + return name; + } } diff --git a/src/test/resources/templates/excel/IterablePlaceholderWithoutLoop.xlsx b/src/test/resources/templates/excel/IterablePlaceholderWithoutLoop.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2514a115202eaf1abac46516f90c19111afe91ea GIT binary patch literal 9067 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 Cn&V*r literal 0 HcmV?d00001 diff --git a/src/test/resources/templates/word/IterablePlaceholderWithoutLoop.docx b/src/test/resources/templates/word/IterablePlaceholderWithoutLoop.docx new file mode 100644 index 0000000000000000000000000000000000000000..1662a4364f0ad8a88f2c5fbef419b4a61bcf3dd0 GIT binary patch literal 11968 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