Skip to content

Commit

Permalink
Allow per-cell custom placeholders (#253)
Browse files Browse the repository at this point in the history
* Allow per-cell custom placeholders & ranged placeholders in same row as custom placeholders

Up until now, it was not possible to have a ranged placeholder
(e.g. {{quotes}}) in the same row as another placeholder.
To make this possible, custom excel placeholders now return an
object containing information about any offsets and/or column
skips resulting from their application.
Additionally, the custom excel placeholders now only get passed the cell of the first relevant placeholder, instead of
the full row.
  • Loading branch information
AntonOellerer authored Jun 10, 2024
1 parent ec1b351 commit 34dc258
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 109 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group 'com.docutools'
version = '2.0.3'
version = '3.0.0'

java {
toolchain {
Expand Down
14 changes: 0 additions & 14 deletions src/main/java/com/docutools/jocument/PlaceholderData.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.docutools.jocument;

import com.docutools.jocument.impl.excel.interfaces.ExcelWriter;
import java.util.Locale;
import java.util.stream.Stream;

Expand Down Expand Up @@ -58,19 +57,6 @@ default void transform(Object placeholder, Locale locale, GenerationOptions opti
throw new UnsupportedOperationException();
}

/**
* Transformation method for {@link PlaceholderData} needing to insert into excel. Since xlsx generation generates the document newly, as opposed to
* working on a copy (like word generation) we need the possibility to write to the document for some custom placeholders.
*
* @param placeholder the placeholder
* @param excelWriter The {@link ExcelWriter} to write to the excel document
* @param locale the {@link Locale}
* @param options the {@link GenerationOptions}
*/
default void transform(Object placeholder, ExcelWriter excelWriter, Locale locale, GenerationOptions options) {
throw new UnsupportedOperationException();
}

/**
* Tries to return the raw value behind the {@link PlaceholderData}. May not be implemented in all sepcifications.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ public boolean governs(String placeholderName, Object unused) {

@Override
public boolean governs(String placeholderName, Object bean, Optional<MimeType> mimeType) {
if (placeholderName.equals("crew") && mimeType.isPresent()) {
return mimeType.get().equals(MimeType.XLSX);
} else {
return governs(placeholderName, bean);
}
return governs(placeholderName, bean);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import com.docutools.jocument.PlaceholderResolver;
import com.docutools.jocument.PlaceholderType;
import com.docutools.jocument.impl.ParsingUtils;
import com.docutools.jocument.impl.ScalarPlaceholderData;
import com.docutools.jocument.impl.excel.interfaces.ExcelPlaceholderData;
import com.docutools.jocument.impl.excel.interfaces.ExcelWriter;
import com.docutools.jocument.impl.excel.util.ExcelUtils;
import com.docutools.jocument.impl.excel.util.ModificationInformation;
import com.google.common.collect.Lists;
import java.util.Iterator;
import java.util.LinkedList;
Expand Down Expand Up @@ -69,24 +72,10 @@ private void generate() {
logger.debug("Starting generation by applying resolver {}", resolver);
for (Iterator<Row> iterator = rowIterator; iterator.hasNext(); ) {
Row row = iterator.next();
if (isCustomPlaceholder(row)) {
var cell = row.getCell(row.getFirstCellNum());
resolver.resolve(ParsingUtils.stripBrackets(
cell.getStringCellValue()
))
.ifPresent(placeholderData -> placeholderData.transform(row, excelWriter, LocaleUtil.getUserLocale(), options));
} else if (isLoopStart(row)) {
if (isLoopStart(row)) {
handleLoop(row, iterator);
} else {
excelWriter.newRow(row);
for (Cell cell : row) {
if (ExcelUtils.containsPlaceholder(cell)) {
var newCellValue = ExcelUtils.replacePlaceholders(cell, resolver);
newCellValue.ifLeftOrElse(stringValue -> excelWriter.addCell(cell, stringValue), doubleValue -> excelWriter.addCell(cell, doubleValue));
} else if (ExcelUtils.isSimpleCell(cell)) {
excelWriter.addCell(cell);
}
}
handleRow(row);
}
}
if (nestedLoopDepth != 0) { //here for clarity, could be removed since generation finishes if nestedLoopDepth == 0
Expand All @@ -96,6 +85,43 @@ private void generate() {
logger.debug("Finished generation of elements by resolver {}", resolver);
}

private void handleRow(Row row) {
excelWriter.newRow(row);
ModificationInformation modificationInformation = new ModificationInformation(Optional.empty(), 0);
for (Cell cell : row) {
Optional<Integer> skipUntil = modificationInformation.skipUntil();
if (skipUntil.isEmpty() || cell.getColumnIndex() > skipUntil.get()) {
if (ExcelUtils.containsPlaceholder(cell)) {
var newModificationInformation = replacePlaceholder(cell, modificationInformation.offset());
modificationInformation = modificationInformation.merge(newModificationInformation);
} else if (ExcelUtils.isSimpleCell(cell)) {
excelWriter.addCell(cell);
}
}
}
}

private ModificationInformation replacePlaceholder(Cell cell, int offset) {
String cellValue = ExcelUtils.getCellContentAsString(cell);
Optional<PlaceholderData> placeholderDataOptional = ExcelUtils.resolveCell(cellValue, resolver);
if (placeholderDataOptional.isPresent()) {
PlaceholderData placeholderData = placeholderDataOptional.get();
if (placeholderData instanceof ScalarPlaceholderData<?> scalarPlaceholderData
&& scalarPlaceholderData.getRawValue() instanceof Number number) {
excelWriter.addCell(cell, number.doubleValue());
return ModificationInformation.empty();
} else if (placeholderData.getType().equals(PlaceholderType.CUSTOM) && placeholderData instanceof ExcelPlaceholderData excelPlaceholderData) {
return excelPlaceholderData.transform(cell, excelWriter, offset, LocaleUtil.getUserLocale(), options);
}
}
// to resolve cell content such as "{{name}} {{crew}}", we match against the full string and resolve per match
var matcher = ParsingUtils.matchPlaceholders(cellValue);
excelWriter.addCell(cell, matcher.replaceAll(matchResult -> resolver.resolve(matchResult.group(1))
.orElse(new ScalarPlaceholderData<>("-"))
.toString()));
return ModificationInformation.empty();
}

private void handleLoop(Row row, Iterator<Row> iterator) {
logger.debug("Handling loop at row {}", row.getRowNum());
var loopBody = getLoopBody(row, iterator);
Expand Down Expand Up @@ -191,21 +217,4 @@ private boolean isLoopStart(Row row) {
}
return false;
}

private boolean isCustomPlaceholder(Row row) {
var firstCell = row.getFirstCellNum();
if (firstCell < 0) {
return false;
}
var cell = row.getCell(firstCell);
if (cell.getCellType() == CellType.STRING) {
return resolver.resolve(
ParsingUtils.stripBrackets(
cell.getStringCellValue()
)).map(PlaceholderData::getType)
.map(type -> type == PlaceholderType.CUSTOM)
.orElse(false);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,7 @@ public void addCell(Cell templateCell, String newCellText) {

@Override
public void addCell(Cell templateCell, double newCellValue) {
logger.trace("Creating new cell {} {} with double value {}",
templateCell.getColumnIndex(), templateCell.getRow().getRowNum(), newCellValue);
var newCell = createNewCell(templateCell, 0);
newCell.setCellValue(newCellValue);
addCell(templateCell, newCellValue, 0);
}

@Override
Expand All @@ -257,6 +254,14 @@ public void addCell(Cell templateCell, String newCellText, int columnOffset) {
}
}

@Override
public void addCell(Cell templateCell, double newCellValue, int columnOffset) {
logger.trace("Creating new cell {} {} with double value {} and offset {}",
templateCell.getColumnIndex(), templateCell.getRow().getRowNum(), newCellValue, columnOffset);
var newCell = createNewCell(templateCell, columnOffset);
newCell.setCellValue(newCellValue);
}

private Cell createNewCell(Cell templateCell, int columnOffset) {
var newCell = currentRow.createCell(templateCell.getColumnIndex() + columnOffset, templateCell.getCellType());
newCell.setCellComment(templateCell.getCellComment());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.docutools.jocument.impl.excel.interfaces;

import com.docutools.jocument.GenerationOptions;
import com.docutools.jocument.PlaceholderData;
import com.docutools.jocument.impl.excel.util.ModificationInformation;
import java.util.Locale;
import java.util.Optional;
import org.apache.poi.ss.usermodel.Cell;

/**
* An interface which is used by custom placeholders for Excel reports resolving single cells.
*/
public interface ExcelPlaceholderData extends PlaceholderData {
/**
* Transformation method for {@link PlaceholderData} needing to insert into excel. Since xlsx generation generates the document newly, as opposed to
* working on a copy (like word generation) we need the possibility to write to the document for some custom placeholders.
*
* @param cell The cell containing the placeholder
* @param excelWriter The {@link ExcelWriter} to write to the excel document
* @param offset any offset that should be regarded when inserting a new cell (caused by e.g. ranged placeholders in the row)
* @param locale the {@link Locale}
* @param options the {@link GenerationOptions}
* @return Modification information specifying the last column number which has been modified by this placeholder (or {@link Optional#empty()} if it
* just modified the passed cell) and whether the custom placeholder application resulted in any offsets in the rows columns.
*/
ModificationInformation transform(Cell cell, ExcelWriter excelWriter, int offset, Locale locale, GenerationOptions options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public interface ExcelWriter {
* Create a new cell from the templateCell with the specified cell value.
*
* @param templateCell The template cell to create the new cell from
* @param newCellValue The text to insert into the cell
* @param newCellValue The numeric value to insert into the cell
*/
void addCell(Cell templateCell, double newCellValue);

Expand All @@ -59,6 +59,16 @@ public interface ExcelWriter {
*/
void addCell(Cell templateCell, String newCellText, int columnOffset);

/**
* Create a new cell from the templateCell with the specified cell text and the specified offset. This has to be done e.g. for template cells in
* loops which have to be used multiple times.
*
* @param templateCell The template cell to create the new cell from
* @param newCellValue The numeric value to insert into the cell
* @param columnOffset The column offset to apply to the new cell
*/
void addCell(Cell templateCell, double newCellValue, int columnOffset);

void addCell(Cell cell);

void recalculateFormulas();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import com.docutools.jocument.PlaceholderResolver;
import com.docutools.jocument.impl.DocumentImpl;
import com.docutools.jocument.impl.ParsingUtils;
import com.docutools.jocument.impl.ScalarPlaceholderData;
import io.jbock.util.Either;
import java.util.Locale;
import java.util.Optional;
import java.util.Spliterator;
Expand All @@ -22,28 +20,13 @@ private ExcelUtils() {
}

/**
* Find and replace the placeholders found in value using given resolver.
* Resolve a cell to a placeholderData.
*
* @param cell the cell with placeholders to be replaced.
* @param resolver {@link PlaceholderResolver} used to resolve the placeholders found in value.
* @return String with replaced placeholders.
* @param cellValue The cell content as a string
* @param resolver The resolver to use for resolving
* @return An empty {@link Optional} if the cellValue did not resolve to anything, otherwise an {@link Optional} containing the resolved content.
*/
public static Either<String, Double> replacePlaceholders(Cell cell, PlaceholderResolver resolver) {
String cellValue = getCellContentAsString(cell);
Optional<PlaceholderData> firstPlaceholderData = resolveCell(cellValue, resolver);
if (firstPlaceholderData.isPresent()
&& firstPlaceholderData.get() instanceof ScalarPlaceholderData<?> scalarPlaceholderData
&& scalarPlaceholderData.getRawValue() instanceof Number number) {
return Either.right(number.doubleValue());
}
var matcher = ParsingUtils.matchPlaceholders(cellValue);
return Either.left(matcher.replaceAll(matchResult -> resolver.resolve(matchResult.group(1))
.orElse(new ScalarPlaceholderData<>("-"))
.toString())
);
}

private static Optional<PlaceholderData> resolveCell(String cellValue, PlaceholderResolver resolver) {
public static Optional<PlaceholderData> resolveCell(String cellValue, PlaceholderResolver resolver) {
if (cellValue == null || !cellValue.startsWith("{{") && !cellValue.endsWith("}}")) {
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.docutools.jocument.impl.excel.util;

import java.util.Optional;

public record ModificationInformation(Optional<Integer> skipUntil, int offset) {

public static ModificationInformation empty() {
return new ModificationInformation(Optional.empty(), 0);
}

public ModificationInformation merge(ModificationInformation newModificationInformation) {
return new ModificationInformation(newModificationInformation.skipUntil(), offset + newModificationInformation.offset());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
import com.docutools.jocument.sample.placeholders.CrewPlaceholder;
import com.docutools.jocument.sample.placeholders.QuotesBlockPlaceholder;
import com.docutools.poipath.PoiPath;
import com.docutools.poipath.xssf.RowWrapper;
import com.docutools.poipath.xssf.XSSFWorkbookWrapper;
import java.io.IOException;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Optional;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.AfterEach;
Expand Down Expand Up @@ -319,7 +321,16 @@ void insertNumericValueFromCustomPlaceholder() throws InterruptedException, IOEx
// Arrange
Template template = Template.fromClassPath("/templates/excel/NumericValues.xlsx")
.orElseThrow();
CustomPlaceholderRegistry customPlaceholderRegistry = new CustomPlaceholderRegistryImpl();
CustomPlaceholderRegistry customPlaceholderRegistry = new CustomPlaceholderRegistryImpl() {
@Override
public boolean governs(String placeholderName, Object bean, Optional<MimeType> mimeType) {
if (placeholderName.equals("crew") && mimeType.isPresent()) {
return mimeType.get().equals(MimeType.XLSX);
} else {
return governs(placeholderName, bean);
}
}
};
customPlaceholderRegistry.addHandler("crew", CrewPlaceholder.class);
GenerationOptions generationOptions = new GenerationOptionsBuilder().withMimeType(MimeType.XLSX).build();
PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.ENTERPRISE, customPlaceholderRegistry, generationOptions);
Expand All @@ -335,4 +346,51 @@ void insertNumericValueFromCustomPlaceholder() throws InterruptedException, IOEx
var sheet = xssf.sheet(0);
assertThat(sheet.row(0).cell(0).doubleValue(), is(5.0));
}

@Test
void multiplePlaceholdersPerRow() throws InterruptedException, IOException {
// Arrange
Template template = Template.fromClassPath("/templates/excel/MultiplePlaceholdersPerRow.xlsx").orElseThrow();
CustomPlaceholderRegistry customPlaceholderRegistry = new CustomPlaceholderRegistryImpl();
customPlaceholderRegistry.addHandler("crew", CrewPlaceholder.class);
GenerationOptions generationOptions = new GenerationOptionsBuilder().withMimeType(MimeType.XLSX).build();
PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.ENTERPRISE, customPlaceholderRegistry, generationOptions);

// Act
Document document = template.startGeneration(resolver);
document.blockUntilCompletion(5_000L); // 5 seconds

// Assert
assertThat(document.completed(), is(true));
var xssfWorkbook = TestUtils.getXSSFWorkbookFromDocument(document);
var xssf = new XSSFWorkbookWrapper(xssfWorkbook);
var sheet = xssf.sheet(0);
assertThat(sheet.row(0).cell(0).stringValue(), is(SampleModelData.ENTERPRISE.name()));
assertThat(sheet.row(0).cell(1).cell().toString(), is("5.0"));
}

@Test
void rangedRowPlaceholder() throws InterruptedException, IOException {
// Arrange
Template template = Template.fromClassPath("/templates/excel/RangedRowPlaceholder.xlsx").orElseThrow();
CustomPlaceholderRegistry customPlaceholderRegistry = new CustomPlaceholderRegistryImpl();
customPlaceholderRegistry.addHandler("crew", CrewPlaceholder.class);
customPlaceholderRegistry.addHandler("quotes", QuotesBlockPlaceholder.class);
GenerationOptions generationOptions = new GenerationOptionsBuilder().withMimeType(MimeType.XLSX).build();
PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.ENTERPRISE, customPlaceholderRegistry, generationOptions);

// Act
Document document = template.startGeneration(resolver);
document.blockUntilCompletion(50_000L); // 5 seconds

// Assert
assertThat(document.completed(), is(true));
var xssfWorkbook = TestUtils.getXSSFWorkbookFromDocument(document);
var xssf = new XSSFWorkbookWrapper(xssfWorkbook);
var sheet = xssf.sheet(0);
RowWrapper row = sheet.row(0);
assertThat(row.cell(0).stringValue(), equalTo(QuotesBlockPlaceholder.quotes.get("marx")));
assertThat(row.cell(1).stringValue(), equalTo(QuotesBlockPlaceholder.quotes.get("engels")));
assertThat(row.cell(2).cell().toString(), is("5.0"));
}
}
Loading

0 comments on commit 34dc258

Please sign in to comment.