diff --git a/build.gradle b/build.gradle
index 38c452e9..d222a18e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ plugins {
}
group 'com.docutools'
-version = '4.1.3'
+version = '4.2.0'
java {
toolchain {
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
index 29384e25..95863ed1 100644
--- a/config/checkstyle/checkstyle.xml
+++ b/config/checkstyle/checkstyle.xml
@@ -228,7 +228,7 @@
-
+
diff --git a/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelDocumentImpl.java b/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelDocumentImpl.java
index b4beef60..48883fe6 100644
--- a/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelDocumentImpl.java
+++ b/src/main/java/com/docutools/jocument/impl/excel/implementations/ExcelDocumentImpl.java
@@ -6,12 +6,15 @@
import com.docutools.jocument.impl.DocumentImpl;
import com.docutools.jocument.impl.excel.interfaces.ExcelWriter;
import java.io.IOException;
+import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
+import java.util.stream.StreamSupport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -36,7 +39,7 @@ public ExcelDocumentImpl(Template template, PlaceholderResolver resolver, Genera
}
/**
- * Start generation of a excel report from the template supplied in the constructor, using the also supplied
+ * Start generation of an Excel report from the template supplied in the constructor, using the also supplied
* resolver for resolving placeholders.
*
* @return The path to the generated report
@@ -46,18 +49,40 @@ public ExcelDocumentImpl(Template template, PlaceholderResolver resolver, Genera
protected Path generate() throws IOException {
logger.info("Starting generation");
Path file = Files.createTempFile("jocument-", ".xlsx");
- ExcelWriter excelWriter = new SXSSFWriter(file);
try (XSSFWorkbook workbook = new XSSFWorkbook(template.openStream())) {
+ ExcelWriter excelWriter = new XSSFWriter(workbook);
for (Iterator it = workbook.sheetIterator(); it.hasNext(); ) {
Sheet sheet = it.next();
- logger.info("Starting generation of sheet {}", sheet.getSheetName());
+ sanitizeSheet(sheet);
excelWriter.newSheet(sheet);
- ExcelGenerator.apply(resolver, sheet.rowIterator(), excelWriter, options);
+ logger.info("Starting generation of sheet {}", sheet.getSheetName());
+ ExcelGenerator.apply(resolver, StreamSupport.stream(sheet.spliterator(), false).toList(), excelWriter, options);
+ }
+ XSSFFormulaEvaluator.evaluateAllFormulaCells(workbook);
+ try (OutputStream os = Files.newOutputStream(file)) {
+ logger.info("Writing document to {}", os);
+ workbook.write(os);
}
- } finally {
- excelWriter.recalculateFormulas();
- excelWriter.complete();
}
return file;
}
+
+ /**
+ * Add empty rows to sheet.
+ * To save storage space, Excel files are usually stored in a sparse format, meaning that empty rows are not represented as java objects.
+ * To be able to work with loops which contain empty rows properly, we fill those empty rows up.
+ *
+ * @param sheet The sheet to insert the empty rows into
+ */
+ private void sanitizeSheet(Sheet sheet) {
+ // creates rows where there are none
+ int lastRowNum = sheet.getLastRowNum();
+
+ for (int i = 0; i <= lastRowNum; i++) {
+ var row = sheet.getRow(i);
+ if (row == null) {
+ sheet.createRow(i);
+ }
+ }
+ }
}
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 2b3fd401..2fbeefbd 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
@@ -22,6 +22,7 @@
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.util.LocaleUtil;
+import org.apache.xmlbeans.impl.values.XmlValueDisconnectedException;
/**
@@ -37,14 +38,13 @@ public class ExcelGenerator {
private final ExcelWriter excelWriter;
private final PlaceholderResolver resolver;
- private final Iterator rowIterator;
+ private final List rows;
private final int nestedLoopDepth;
private final GenerationOptions options;
- private int alreadyProcessedLoopsSize = 0;
- private ExcelGenerator(Iterator rowIterator, ExcelWriter excelWriter, PlaceholderResolver resolver, int nestedLoopDepth,
+ private ExcelGenerator(List rows, ExcelWriter excelWriter, PlaceholderResolver resolver, int nestedLoopDepth,
GenerationOptions options) {
- this.rowIterator = rowIterator;
+ this.rows = rows;
this.excelWriter = excelWriter;
this.resolver = resolver;
this.nestedLoopDepth = nestedLoopDepth;
@@ -55,38 +55,49 @@ private ExcelGenerator(Iterator rowIterator, ExcelWriter excelWriter, Place
* This function starts the generating process for the supplied row iterator.
*
* @param resolver The resolver to use for looking up placeholders
- * @param rowIterator An iterator over the template row which should be processed
+ * @param rows The rows which should be processed
* @param excelWriter The writer to write the report out to.
* @param options {@link GenerationOptions}
*/
- static void apply(PlaceholderResolver resolver, Iterator rowIterator, ExcelWriter excelWriter, GenerationOptions options) {
- apply(resolver, rowIterator, excelWriter, 0, options);
+ static void apply(PlaceholderResolver resolver, List rows, ExcelWriter excelWriter, GenerationOptions options) {
+ apply(resolver, rows, excelWriter, 0, options);
}
- private static void apply(PlaceholderResolver resolver, Iterator rowIterator, ExcelWriter excelWriter, int nestedLoopDepth,
+ private static void apply(PlaceholderResolver resolver, List rows, ExcelWriter excelWriter, int nestedLoopDepth,
GenerationOptions options) {
- new ExcelGenerator(rowIterator, excelWriter, resolver, nestedLoopDepth, options).generate();
+ new ExcelGenerator(rows, excelWriter, resolver, nestedLoopDepth, options).generate();
}
private void generate() {
logger.debug("Starting generation by applying resolver {}", resolver);
- for (Iterator iterator = rowIterator; iterator.hasNext(); ) {
- Row row = iterator.next();
- if (isLoopStart(row)) {
- handleLoop(row, iterator);
- } else {
- handleRow(row);
+ List toProcess = new LinkedList<>(rows);
+ while (!toProcess.isEmpty()) {
+ Row row = toProcess.get(0);
+ toProcess = toProcess.subList(1, toProcess.size());
+ try {
+ if (isLoopStart(row)) {
+ toProcess = handleLoop(row, toProcess);
+ } else {
+ handleRow(row);
+ }
+ } catch (XmlValueDisconnectedException e) {
+ logger.warn(e);
}
}
- if (nestedLoopDepth != 0) { //here for clarity, could be removed since generation finishes if nestedLoopDepth == 0
- logger.debug("Adding offset of {}", alreadyProcessedLoopsSize);
- excelWriter.addRowOffset(alreadyProcessedLoopsSize); //we are in nested loop, readd the offset to prevent subtracting it multiple times
- }
logger.debug("Finished generation of elements by resolver {}", resolver);
}
private void handleRow(Row row) {
- excelWriter.newRow(row);
+ if (notInNestedLoop()) {
+ // We can operate on original row
+ excelWriter.setRow(row);
+ } else {
+ // We need to insert a new row
+ excelWriter.shiftRows(row.getRowNum(), 1); // shift rows below insertion point one down, so we do not overwrite an existing one
+ excelWriter.newRow(row);
+ }
+ excelWriter.addRowToIgnore(row.getRowNum());
+ excelWriter.updateRowsWritten(1);
ModificationInformation modificationInformation = new ModificationInformation(Optional.empty(), 0);
for (Cell cell : row) {
Optional skipUntil = modificationInformation.skipUntil();
@@ -94,7 +105,7 @@ private void handleRow(Row row) {
if (ExcelUtils.containsPlaceholder(cell)) {
var newModificationInformation = replacePlaceholder(cell, modificationInformation.offset());
modificationInformation = modificationInformation.merge(newModificationInformation);
- } else if (ExcelUtils.isSimpleCell(cell)) {
+ } else if (nestedLoopDepth != 0) {
excelWriter.addCell(cell);
}
}
@@ -122,38 +133,57 @@ private ModificationInformation replacePlaceholder(Cell cell, int offset) {
return ModificationInformation.empty();
}
- private void handleLoop(Row row, Iterator iterator) {
+ private List handleLoop(Row row, List rows) {
logger.debug("Handling loop at row {}", row.getRowNum());
- var loopBody = getLoopBody(row, iterator);
+ var loopBody = getLoopBody(row, rows);
var loopBodySize = getLoopBodySize(loopBody);
logger.debug("Loop body size: {}", loopBodySize);
- var finalLoopBody = loopBody.subList(1, loopBody.size() - 1);
- var placeholderData = getPlaceholderData(row);
- placeholderData.stream()
- .forEach(placeholderResolver -> {
- excelWriter.addRowOffset(-1); //So we also fill the cell of the loop start placeholder
- ExcelGenerator.apply(placeholderResolver, finalLoopBody.iterator(), excelWriter, nestedLoopDepth + 1, options);
- excelWriter.addRowOffset(1); //To avoid subtracting the placeholder size multiple times
- excelWriter.addRowOffset(loopBodySize);
- });
- var loopPlaceholderSize = getLoopSize(loopBody);
- excelWriter.addRowOffset(-1 * loopPlaceholderSize);
- logger.debug("Subtracting row offset of {}", loopPlaceholderSize);
- alreadyProcessedLoopsSize += loopPlaceholderSize;
+ int loopSize = getLoopSize(loopBody);
+ excelWriter.addRowToIgnore(row.getRowNum()); // ignore opening tag
+ excelWriter.addRowToIgnore(loopBody.get(loopBody.size() - 1).getRowNum()); // ignore closing tag
+ if (notInNestedLoop()) {
+ // Insert all data after the template rows
+ excelWriter.setSectionOffset(loopSize);
+ }
+ var loopBodyWithoutTags = loopBody.subList(1, loopBody.size() - 1); // remove loop opening and closing tag
+ PlaceholderData placeholderData = getPlaceholderData(row);
+ placeholderData.stream().forEach(placeholderResolver ->
+ ExcelGenerator.apply(placeholderResolver, loopBodyWithoutTags, excelWriter, nestedLoopDepth + 1, options));
+ if (notInNestedLoop()) {
+ // Processing of the outermost loop has finished, we can delete the template
+ int rowNum = row.getRowNum();
+ excelWriter.finishLoopProcessing(rowNum, loopSize);
+ rows = rows.stream().filter(row1 -> {
+ try {
+ row1.toString();
+ return true;
+ } catch (XmlValueDisconnectedException ignored) {
+ return false;
+ }
+ }).toList();
+ } else {
+ // We finished a nested loop, remove the template from the working set to continue processing of the current iteration
+ rows = rows.subList(loopBodyWithoutTags.size() + 1, rows.size());
+ }
+ return rows;
+ }
+
+ private boolean notInNestedLoop() {
+ return nestedLoopDepth == 0;
}
- private List getLoopBody(Row row, Iterator iterator) {
+ private List getLoopBody(Row row, List rows) {
var placeholder = ExcelUtils.getPlaceholder(row);
- logger.debug("Unrolling loop of {}", placeholder);
+ logger.debug("Getting loop body of {}", placeholder);
LinkedList rowBuffer = new LinkedList<>();
rowBuffer.add(row);
- var rowInFocus = iterator.next();
+ Iterator rowIterator = rows.iterator();
+ var rowInFocus = rowIterator.next();
while (!ExcelUtils.isMatchingLoopEnd(rowInFocus, placeholder)) {
rowBuffer.addLast(rowInFocus);
- rowInFocus = iterator.next();
+ rowInFocus = rowIterator.next();
}
rowBuffer.addLast(rowInFocus);
- logger.debug("Unrolled loop of {}", placeholder);
return rowBuffer;
}
diff --git a/src/main/java/com/docutools/jocument/impl/excel/implementations/SXSSFWriter.java b/src/main/java/com/docutools/jocument/impl/excel/implementations/XSSFWriter.java
similarity index 51%
rename from src/main/java/com/docutools/jocument/impl/excel/implementations/SXSSFWriter.java
rename to src/main/java/com/docutools/jocument/impl/excel/implementations/XSSFWriter.java
index 3ffe8024..6d567caf 100644
--- a/src/main/java/com/docutools/jocument/impl/excel/implementations/SXSSFWriter.java
+++ b/src/main/java/com/docutools/jocument/impl/excel/implementations/XSSFWriter.java
@@ -1,14 +1,10 @@
package com.docutools.jocument.impl.excel.implementations;
import com.docutools.jocument.impl.excel.interfaces.ExcelWriter;
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
-import java.util.Optional;
+import java.util.SortedSet;
+import java.util.TreeSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.ss.usermodel.Cell;
@@ -18,143 +14,50 @@
import org.apache.poi.ss.usermodel.Hyperlink;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
-import org.apache.poi.ss.util.CellRangeAddress;
-import org.apache.poi.xssf.streaming.SXSSFFormulaEvaluator;
-import org.apache.poi.xssf.streaming.SXSSFSheet;
-import org.apache.poi.xssf.streaming.SXSSFWorkbook;
-import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
-import org.apache.poi.xssf.usermodel.XSSFDrawing;
-import org.apache.poi.xssf.usermodel.XSSFPicture;
-import org.apache.poi.xssf.usermodel.XSSFPictureData;
-import org.apache.poi.xssf.usermodel.XSSFShape;
-import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.ss.usermodel.Workbook;
/**
- * This is a streamed implementation of the {@link com.docutools.jocument.impl.excel.interfaces.ExcelWriter} interface. The streaming is done so
- * memory can be saved. For now, the amount of rows kept in memory is set to the default, 100. SXSSFWriter works by keeping a reference to the current
- * sheet and row being edited, and copying/cloning required values on the creation of new objects. This is why to the `new...`/`add...` methods the
- * original references of the template should be passed. If one would like to use objects created somewhere else directly, a new implementation
- * considering this would have to be created.
+ * This is an implementation of the {@link ExcelWriter} interface.
*
* @author Anton Oellerer
- * @since 2020-04-02
+ * @since 2024-08-21
*/
-public class SXSSFWriter implements ExcelWriter {
+public class XSSFWriter implements ExcelWriter {
private static final Logger logger = LogManager.getLogger();
- private final Path path;
- private final SXSSFWorkbook workbook;
+ private final Workbook workbook;
private final CreationHelper creationHelper;
/**
* Maps the {@link CellStyle} objects of the old workbook to the new ones.
*/
private final Map cellStyleMap = new HashMap<>();
+ private final SortedSet rowsToIgnore = new TreeSet<>();
private Sheet currentSheet;
- private Sheet templateSheet;
private Row currentRow;
- private int rowOffset = 0;
private int leftMostColumn = -1;
private int rightMostColumn = -1;
+ private int rowsWritten = 0;
+ private int sectionOffset = 0;
/**
* Creates a new SXSSFWriter.
- *
- * @param path The path to save the finished report to.
*/
- public SXSSFWriter(Path path) {
- workbook = new SXSSFWorkbook();
+ public XSSFWriter(Workbook workbook) {
this.creationHelper = workbook.getCreationHelper();
- this.path = path;
- }
-
- private static void transferPicture(XSSFShape shape, SXSSFSheet newSheet) {
- XSSFPicture picture = (XSSFPicture) shape;
-
- XSSFPictureData xssfPictureData = picture.getPictureData();
- XSSFClientAnchor anchor = (XSSFClientAnchor) shape.getAnchor();
-
- int col1 = anchor.getCol1();
- int col2 = anchor.getCol2();
- int row1 = anchor.getRow1();
- int row2 = anchor.getRow2();
-
- int x1 = anchor.getDx1();
- int x2 = anchor.getDx2();
- int y1 = anchor.getDy1();
- int y2 = anchor.getDy2();
-
- var newWb = newSheet.getWorkbook();
- var newHelper = newWb.getCreationHelper();
- var newAnchor = newHelper.createClientAnchor();
-
- // Row / Column placement.
- newAnchor.setCol1(col1);
- newAnchor.setCol2(col2);
- newAnchor.setRow1(row1);
- newAnchor.setRow2(row2);
-
- // Fine touch adjustment along the XY coordinate.
- newAnchor.setDx1(x1);
- newAnchor.setDx2(x2);
- newAnchor.setDy1(y1);
- newAnchor.setDy2(y2);
-
- int newPictureIndex = newWb.addPicture(xssfPictureData.getData(), xssfPictureData.getPictureType());
-
- var newDrawing = newSheet.createDrawingPatriarch();
- newDrawing.createPicture(newAnchor, newPictureIndex);
+ this.workbook = workbook;
}
@Override
public void newSheet(Sheet sheet) {
- logger.info("Creating new sheet of {}", sheet.getSheetName());
- templateSheet = sheet;
- currentSheet = workbook.createSheet(sheet.getSheetName());
- Optional.ofNullable(sheet.getActiveCell()).ifPresent(activeCell -> currentSheet.setActiveCell(activeCell));
- currentSheet.setAutobreaks(sheet.getAutobreaks());
- Arrays.stream(sheet.getColumnBreaks()).forEach(column -> currentSheet.setColumnBreak(column));
- currentSheet.setDefaultColumnWidth(sheet.getDefaultColumnWidth());
- currentSheet.setDefaultRowHeight(sheet.getDefaultRowHeight());
- currentSheet.setDisplayFormulas(sheet.isDisplayFormulas());
- currentSheet.setDisplayGridlines(sheet.isDisplayGridlines());
- currentSheet.setDisplayGuts(sheet.getDisplayGuts());
- currentSheet.setDisplayRowColHeadings(sheet.isDisplayRowColHeadings());
- currentSheet.setDisplayZeros(sheet.isDisplayZeros());
- currentSheet.setFitToPage(sheet.getFitToPage());
- currentSheet.setHorizontallyCenter(sheet.getHorizontallyCenter());
- currentSheet.setPrintGridlines(sheet.isPrintGridlines());
- currentSheet.setPrintRowAndColumnHeadings(sheet.isPrintRowAndColumnHeadings());
- currentSheet.setRepeatingColumns(sheet.getRepeatingColumns());
- currentSheet.setRepeatingRows(sheet.getRepeatingRows());
- currentSheet.setRightToLeft(sheet.isRightToLeft());
- Arrays.stream(sheet.getRowBreaks()).forEach(row -> currentSheet.setRowBreak(row));
- currentSheet.setRowSumsBelow(sheet.getRowSumsBelow());
- currentSheet.setRowSumsRight(sheet.getRowSumsRight());
- currentSheet.setSelected(sheet.isSelected());
- currentSheet.setVerticallyCenter(sheet.getVerticallyCenter());
-
- // copy auto filters to new sheet
- if (sheet instanceof XSSFSheet xssfSheet) {
- var autoFilter = xssfSheet.getCTWorksheet().getAutoFilter();
- if (autoFilter != null) {
- var ref = autoFilter.getRef();
- var range = CellRangeAddress.valueOf(ref);
- currentSheet.setAutoFilter(range);
- }
- }
-
- var drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
- for (var shape : drawing.getShapes()) {
- if (shape instanceof XSSFPicture) {
- transferPicture(shape, (SXSSFSheet) currentSheet);
- }
- }
+ currentSheet = sheet;
}
@Override
public void newRow(Row row) {
- logger.debug("Creating new row {}", row.getRowNum());
- currentRow = currentSheet.createRow(row.getRowNum() + rowOffset);
+ logger.debug("Creating new row {}",
+ row.getRowNum() + sectionOffset + rowsWritten - rowsToIgnore.headSet(row.getRowNum()).size()); //row num is 0 based
+ currentRow = currentSheet.createRow(
+ row.getRowNum() + sectionOffset + rowsWritten - rowsToIgnore.headSet(row.getRowNum()).size());
currentRow.setHeight(row.getHeight());
if (row.isFormatted()) {
currentRow.setRowStyle(cellStyleMap.computeIfAbsent((int) row.getRowStyle().getIndex(), i -> copyCellStyle(row.getRowStyle())));
@@ -201,7 +104,7 @@ private void setup(Row row) {
}
private void copyColumnStyle(int rightMostColumn) {
- CellStyle columnStyle = templateSheet.getColumnStyle(rightMostColumn);
+ CellStyle columnStyle = currentSheet.getColumnStyle(rightMostColumn);
if (columnStyle != null) {
currentSheet.setDefaultColumnStyle(rightMostColumn,
cellStyleMap.computeIfAbsent((int) columnStyle.getIndex(), i -> copyCellStyle(columnStyle)));
@@ -245,7 +148,7 @@ public void addCell(Cell templateCell, double newCellValue) {
@Override
public void addCell(Cell templateCell, String newCellText, int columnOffset) {
logger.trace("Creating new cell {} {} with text {}",
- templateCell.getColumnIndex(), templateCell.getRow().getRowNum(), newCellText);
+ templateCell.getColumnIndex() + columnOffset, templateCell.getRow().getRowNum(), newCellText);
var newCell = createNewCell(templateCell, columnOffset);
if (templateCell.getCellType() == CellType.FORMULA) {
newCell.setCellFormula(newCellText);
@@ -272,31 +175,59 @@ private Cell createNewCell(Cell templateCell, int columnOffset) {
}
@Override
- public void complete() throws IOException {
- var outputStream = new BufferedOutputStream(Files.newOutputStream(path));
- workbook.write(outputStream);
- outputStream.close();
- workbook.close();
+ public void setRow(Row row) {
+ this.currentRow = row;
}
@Override
- public void addRowOffset(int size) {
- rowOffset += size;
+ public void deleteRows(int loopStart, int noRows) {
+ for (int i = loopStart; i < loopStart + noRows; i++) {
+ Row row = currentSheet.getRow(i);
+ if (row != null) {
+ currentSheet.removeRow(row);
+ }
+ }
+ currentSheet.shiftRows(loopStart + noRows, currentSheet.getLastRowNum(), -noRows);
}
@Override
- public void recalculateFormulas() {
- try {
- SXSSFFormulaEvaluator.evaluateAllFormulaCells(workbook, true);
- } catch (Exception e) {
- workbook.setForceFormulaRecalculation(true);
- logger.error(e);
+ public void shiftRows(int startingRow, int toShift) {
+ //rows are 1 indexed, row nums 0
+ if (startingRow + sectionOffset + rowsWritten - rowsToIgnore.headSet(startingRow).size()
+ <= currentSheet.getLastRowNum()) {
+ currentSheet.shiftRows(startingRow + sectionOffset + rowsWritten - rowsToIgnore.headSet(startingRow).size(),
+ currentSheet.getLastRowNum(),
+ toShift);
}
}
+ @Override
+ public void updateRowsWritten(int rows) {
+ this.rowsWritten += rows;
+ }
+
+ @Override
+ public void addRowToIgnore(int row) {
+ this.rowsToIgnore.add(row);
+ }
+
+ @Override
+ public void setSectionOffset(int rows) {
+ this.sectionOffset = rows;
+ }
+
+ @Override
+ public void finishLoopProcessing(int rowNum, int loopSize) {
+ this.deleteRows(rowNum, loopSize);
+ this.sectionOffset = 0;
+ this.rowsWritten = 0;
+ rowsToIgnore.clear();
+ }
+
+
private CellStyle copyCellStyle(CellStyle cellStyle) {
var newStyle = workbook.createCellStyle();
newStyle.cloneStyleFrom(cellStyle);
return newStyle;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/docutools/jocument/impl/excel/interfaces/ExcelWriter.java b/src/main/java/com/docutools/jocument/impl/excel/interfaces/ExcelWriter.java
index 7fcee1e9..cafccf5a 100644
--- a/src/main/java/com/docutools/jocument/impl/excel/interfaces/ExcelWriter.java
+++ b/src/main/java/com/docutools/jocument/impl/excel/interfaces/ExcelWriter.java
@@ -1,6 +1,5 @@
package com.docutools.jocument.impl.excel.interfaces;
-import java.io.IOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
@@ -17,22 +16,6 @@ public interface ExcelWriter {
void newRow(Row row);
- /**
- * Complete the creation of the report, writing the workbook to the earlier specified path.
- *
- * @throws IOException If writing out of the workbook fails.
- */
- void complete() throws IOException;
-
- /**
- * Add a row offset to consider when creating new rows.
- * This has to be done for example when unrolling loops, since the original rows still have the old indices,
- * possibly pointing to already written rows
- *
- * @param size The number of rows to add to the row index of rows to clone when creating new rows
- */
- void addRowOffset(int size);
-
/**
* Create a new cell from the templateCell with the specified cell text.
*
@@ -71,5 +54,17 @@ public interface ExcelWriter {
void addCell(Cell cell);
- void recalculateFormulas();
+ void setRow(Row row);
+
+ void deleteRows(int loopStart, int noRows);
+
+ void shiftRows(int startingRow, int loopBodySize);
+
+ void updateRowsWritten(int rows);
+
+ void addRowToIgnore(int row);
+
+ void setSectionOffset(int rows);
+
+ void finishLoopProcessing(int rowNum, int loopSize);
}
diff --git a/src/test/java/com/docutools/jocument/excel/AutomatedXlsxTests.java b/src/test/java/com/docutools/jocument/excel/AutomatedXlsxTests.java
deleted file mode 100644
index d5a348d4..00000000
--- a/src/test/java/com/docutools/jocument/excel/AutomatedXlsxTests.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.docutools.jocument.excel;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-
-import com.docutools.jocument.Document;
-import com.docutools.jocument.PlaceholderResolver;
-import com.docutools.jocument.Template;
-import com.docutools.jocument.TestUtils;
-import com.docutools.jocument.impl.ReflectionResolver;
-import com.docutools.jocument.sample.model.SampleModelData;
-import com.docutools.poipath.xssf.XSSFWorkbookWrapper;
-import java.io.IOException;
-import org.apache.poi.ss.util.CellRangeAddress;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-
-@DisplayName("Excel Generator Tests")
-@Tag("automated")
-@Tag("xssf")
-class AutomatedXlsxTests {
-
- @Test
- void copiesHyperlink() throws InterruptedException, IOException {
- // Arrange
- Template template = Template.fromClassPath("/templates/excel/HyperlinkDocument.xlsx")
- .orElseThrow();
- PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.PICARD);
-
- // Act
- Document document = template.startGeneration(resolver);
- document.blockUntilCompletion(60000L); // 1 minute
-
- // Assert
- assertThat(document.completed(), is(true));
- var xssfWorkbook = TestUtils.getXSSFWorkbookFromDocument(document);
- var documentWrapper = new XSSFWorkbookWrapper(xssfWorkbook);
- assertThat(documentWrapper.sheet(0).row(0).cell(0).text(), equalTo("orf.at"));
- assertThat(documentWrapper.sheet(0).row(0).cell(0).cell().getHyperlink().getAddress(), equalTo("https://orf.at/"));
- }
-
- @Test
- @DisplayName("Keep Auto Filter")
- void keepAutoFilters() throws InterruptedException, IOException {
- // Arrange
- Template template = Template.fromClassPath("/templates/excel/AutoFilters.xlsx")
- .orElseThrow();
- PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.PICARD);
-
- // 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);
- var autoFilter = sheet.sheet().getCTWorksheet().getAutoFilter();
- assertThat(autoFilter, notNullValue());
- var autoFilterRef = autoFilter.getRef();
- var rangeAddress = CellRangeAddress.valueOf(autoFilterRef);
- assertThat(rangeAddress.isInRange(sheet.row(0).cell(0).cell()), is(true));
- }
-}
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 041b3fbf..1d5c3787 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
@@ -5,6 +5,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;
import com.docutools.jocument.CustomPlaceholderRegistry;
@@ -30,8 +31,10 @@
import java.util.Locale;
import java.util.Optional;
import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
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;
@@ -73,6 +76,151 @@ void shouldCloneSimpleExcel() throws InterruptedException, IOException {
assertThat(firstSheet.row(4).cell(4).text(), equalTo("1312.0"));
}
+ @Test
+ void simpleLoop() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/SimpleLoop.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ assertThat(firstSheet.row(0).cell(0).stringValue(), equalTo("USS Enterprise"));
+ assertThat(firstSheet.row(1).cell(0).stringValue(), equalTo("US Defiant"));
+ }
+
+ @Test
+ void simpleLoopWithSpacingAtStart() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/SimpleLoopWithSpacingAtStart.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ assertThat(firstSheet.row(1).cell(0).stringValue(), equalTo("USS Enterprise"));
+ assertThat(firstSheet.row(3).cell(0).stringValue(), equalTo("US Defiant"));
+ }
+
+ @Test
+ void simpleLoopWithSpacingAtEnd() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/SimpleLoopWithSpacingAtEnd.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ assertThat(firstSheet.row(0).cell(0).stringValue(), equalTo("USS Enterprise"));
+ assertThat(firstSheet.row(2).cell(0).stringValue(), equalTo("US Defiant"));
+ }
+
+ @Test
+ void nestedSimpleLoop() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/NestedSimpleLoop.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ var services = SampleModelData.PICARD.getServices();
+ assertThat(firstSheet.row(0).cell(0).stringValue(), equalTo(services.get(0).getVisitedPlanets().get(0).getPlanetName()));
+ assertThat(firstSheet.row(1).cell(0).stringValue(), equalTo(services.get(1).getVisitedPlanets().get(0).getPlanetName()));
+ }
+
+
+ @Test
+ void multiValueLoop() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/MultiValueLoop.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ var services = SampleModelData.PICARD.getServices();
+ assertThat(firstSheet.row(0).cell(0).stringValue(), equalTo(services.get(0).getShipName()));
+ assertThat(firstSheet.row(1).cell(0).stringValue(), equalTo(services.get(0).getShipName()));
+ assertThat(firstSheet.row(2).cell(0).stringValue(), equalTo(services.get(1).getShipName()));
+ assertThat(firstSheet.row(3).cell(0).stringValue(), equalTo(services.get(1).getShipName()));
+ }
+
+
+ @Test
+ void multiValueLoopWithSpacingInBetween() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/MultiValueLoopWithSpacingInBetween.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ var services = SampleModelData.PICARD.getServices();
+ assertThat(firstSheet.row(0).cell(0).stringValue(), equalTo(services.get(0).getShipName()));
+ assertThat(firstSheet.row(2).cell(0).stringValue(), equalTo(services.get(0).getShipName()));
+ assertThat(firstSheet.row(3).cell(0).stringValue(), equalTo(services.get(1).getShipName()));
+ assertThat(firstSheet.row(5).cell(0).stringValue(), equalTo(services.get(1).getShipName()));
+ }
+
+ @Test
+ void nestedMultiValueLoop() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/NestedMultiValueLoop.xlsx")
+ .orElseThrow();
+ PlaceholderResolver 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 firstSheet = PoiPath.xssf(workbook).sheet(0);
+ var services = SampleModelData.PICARD.getServices();
+ assertThat(firstSheet.row(0).cell(0).stringValue(), equalTo(services.get(0).getVisitedPlanets().get(0).getPlanetName()));
+ assertThat(firstSheet.row(1).cell(0).stringValue(), equalTo(services.get(0).getVisitedPlanets().get(0).getPlanetName()));
+ assertThat(firstSheet.row(2).cell(0).stringValue(), equalTo(services.get(1).getVisitedPlanets().get(0).getPlanetName()));
+ assertThat(firstSheet.row(3).cell(0).stringValue(), equalTo(services.get(1).getVisitedPlanets().get(1).getPlanetName()));
+ }
+
@Test
@DisplayName("Should copy constant cell values in a loop.")
void shouldCloneSimpleExcelWithLoop() throws InterruptedException, IOException {
@@ -261,6 +409,7 @@ void xlsxQuotesBlockPlaceholder() throws InterruptedException, IOException {
}
@Test
+ @Disabled("This test is disabled because it takes too long to run.")
void evaluatesLargeDocument() throws InterruptedException {
// Assemble
Template template = Template.fromClassPath("/templates/excel/LargeTemplate.xlsx")
@@ -393,4 +542,65 @@ void rangedRowPlaceholder() throws InterruptedException, IOException {
assertThat(row.cell(1).stringValue(), equalTo(QuotesBlockPlaceholder.quotes.get("engels")));
assertThat(row.cell(2).cell().toString(), is("5.0"));
}
+
+ @Test
+ void copiesHyperlink() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/HyperlinkDocument.xlsx")
+ .orElseThrow();
+ PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.PICARD);
+
+ // Act
+ Document document = template.startGeneration(resolver);
+ document.blockUntilCompletion(60000L); // 1 minute
+
+ // Assert
+ assertThat(document.completed(), is(true));
+ var xssfWorkbook = TestUtils.getXSSFWorkbookFromDocument(document);
+ var documentWrapper = new XSSFWorkbookWrapper(xssfWorkbook);
+ assertThat(documentWrapper.sheet(0).row(0).cell(0).text(), equalTo("orf.at"));
+ assertThat(documentWrapper.sheet(0).row(0).cell(0).cell().getHyperlink().getAddress(), equalTo("https://orf.at/"));
+ }
+
+ @Test
+ @DisplayName("Keep Auto Filter")
+ void keepAutoFilters() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/AutoFilters.xlsx")
+ .orElseThrow();
+ PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.PICARD);
+
+ // 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);
+ var autoFilter = sheet.sheet().getCTWorksheet().getAutoFilter();
+ assertThat(autoFilter, notNullValue());
+ var autoFilterRef = autoFilter.getRef();
+ var rangeAddress = CellRangeAddress.valueOf(autoFilterRef);
+ assertThat(rangeAddress.isInRange(sheet.row(0).cell(0).cell()), is(true));
+ }
+
+ @Test
+ void copiesDiagram() throws InterruptedException, IOException {
+ // Arrange
+ Template template = Template.fromClassPath("/templates/excel/Diagrams.xlsx")
+ .orElseThrow();
+ PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.ARMY);
+
+ // 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);
+ assertThat(xssf.sheet(0).sheet().getDrawingPatriarch().getShapes().size(), equalTo(1));
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/docutools/jocument/sample/model/Army.java b/src/test/java/com/docutools/jocument/sample/model/Army.java
new file mode 100644
index 00000000..769ec87e
--- /dev/null
+++ b/src/test/java/com/docutools/jocument/sample/model/Army.java
@@ -0,0 +1,6 @@
+package com.docutools.jocument.sample.model;
+
+import java.util.List;
+
+public record Army(List captains) {
+}
diff --git a/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java b/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java
index 54d822fd..d3ff84af 100644
--- a/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java
+++ b/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java
@@ -13,6 +13,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.Random;
import java.util.concurrent.CompletableFuture;
public class SampleModelData {
@@ -25,6 +26,8 @@ public class SampleModelData {
public static final Ship ENTERPRISE;
public static final Ship ENTERPRISE_WITHOUT_SERVICES;
public static final Planet PLANET;
+ public static final Army ARMY;
+ private static final Random RANDOM = new Random();
static {
try {
@@ -55,11 +58,17 @@ public PlaceholderData create(CustomPlaceholderRegistry customPlaceholderRegistr
return new IterablePlaceholderData(new ReflectionResolver(PICARD, customPlaceholderRegistry, options, parent));
}
});
+ ARMY = new Army(List.of(createCaptain(), createCaptain(), createCaptain(), createCaptain()));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
+ private static Captain createCaptain() {
+ return new Captain("Captain #%s".formatted(RANDOM.nextInt()), RANDOM.nextInt(), Uniform.values()[RANDOM.nextInt(Uniform.values().length - 1)],
+ null, Collections.emptyList(), null, "");
+ }
+
private SampleModelData() {
}
diff --git a/src/test/java/com/docutools/jocument/sample/placeholders/QuotesBlockPlaceholder.java b/src/test/java/com/docutools/jocument/sample/placeholders/QuotesBlockPlaceholder.java
index 53072500..11bd737b 100644
--- a/src/test/java/com/docutools/jocument/sample/placeholders/QuotesBlockPlaceholder.java
+++ b/src/test/java/com/docutools/jocument/sample/placeholders/QuotesBlockPlaceholder.java
@@ -27,7 +27,6 @@ public ModificationInformation transform(Cell cell, ExcelWriter excelWriter, int
private ModificationInformation transform(Row row, ExcelWriter excelWriter) {
int cellPointer = getPlaceholderStart(row);
- excelWriter.newRow(row);
while (!row.getCell(cellPointer).getStringCellValue().equals("{{/quotes}}")) {
Cell authorCell = row.getCell(cellPointer + 1); //cells get shifted to the left by one since the placeholder is removed
String quote = quotes.get(authorCell.getStringCellValue().toLowerCase());
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml
index 2b169dd2..1698acf1 100644
--- a/src/test/resources/log4j2.xml
+++ b/src/test/resources/log4j2.xml
@@ -6,6 +6,9 @@
+
+
+
diff --git a/src/test/resources/templates/excel/CollectionsTemplate.xlsx b/src/test/resources/templates/excel/CollectionsTemplate.xlsx
index 416339d0..40c2842c 100644
Binary files a/src/test/resources/templates/excel/CollectionsTemplate.xlsx and b/src/test/resources/templates/excel/CollectionsTemplate.xlsx differ
diff --git a/src/test/resources/templates/excel/Diagrams.xlsx b/src/test/resources/templates/excel/Diagrams.xlsx
new file mode 100644
index 00000000..d00cae26
Binary files /dev/null and b/src/test/resources/templates/excel/Diagrams.xlsx differ
diff --git a/src/test/resources/templates/excel/MultiValueLoop.xlsx b/src/test/resources/templates/excel/MultiValueLoop.xlsx
new file mode 100644
index 00000000..29298b7a
Binary files /dev/null and b/src/test/resources/templates/excel/MultiValueLoop.xlsx differ
diff --git a/src/test/resources/templates/excel/MultiValueLoopWithSpacingInBetween.xlsx b/src/test/resources/templates/excel/MultiValueLoopWithSpacingInBetween.xlsx
new file mode 100644
index 00000000..2fc55a8f
Binary files /dev/null and b/src/test/resources/templates/excel/MultiValueLoopWithSpacingInBetween.xlsx differ
diff --git a/src/test/resources/templates/excel/NestedLoopDocument.xlsx b/src/test/resources/templates/excel/NestedLoopDocument.xlsx
index 6defb67c..a5ea987c 100644
Binary files a/src/test/resources/templates/excel/NestedLoopDocument.xlsx and b/src/test/resources/templates/excel/NestedLoopDocument.xlsx differ
diff --git a/src/test/resources/templates/excel/NestedLoopDocumentBackup.xlsx b/src/test/resources/templates/excel/NestedLoopDocumentBackup.xlsx
new file mode 100644
index 00000000..6defb67c
Binary files /dev/null and b/src/test/resources/templates/excel/NestedLoopDocumentBackup.xlsx differ
diff --git a/src/test/resources/templates/excel/NestedMultiValueLoop.xlsx b/src/test/resources/templates/excel/NestedMultiValueLoop.xlsx
new file mode 100644
index 00000000..f932e495
Binary files /dev/null and b/src/test/resources/templates/excel/NestedMultiValueLoop.xlsx differ
diff --git a/src/test/resources/templates/excel/NestedSimpleLoop.xlsx b/src/test/resources/templates/excel/NestedSimpleLoop.xlsx
new file mode 100644
index 00000000..0c6a7bb9
Binary files /dev/null and b/src/test/resources/templates/excel/NestedSimpleLoop.xlsx differ
diff --git a/src/test/resources/templates/excel/SimpleLoop.xlsx b/src/test/resources/templates/excel/SimpleLoop.xlsx
new file mode 100644
index 00000000..b48f89df
Binary files /dev/null and b/src/test/resources/templates/excel/SimpleLoop.xlsx differ
diff --git a/src/test/resources/templates/excel/SimpleLoopWithSpacingAtEnd.xlsx b/src/test/resources/templates/excel/SimpleLoopWithSpacingAtEnd.xlsx
new file mode 100644
index 00000000..bb066028
Binary files /dev/null and b/src/test/resources/templates/excel/SimpleLoopWithSpacingAtEnd.xlsx differ
diff --git a/src/test/resources/templates/excel/SimpleLoopWithSpacingAtStart.xlsx b/src/test/resources/templates/excel/SimpleLoopWithSpacingAtStart.xlsx
new file mode 100644
index 00000000..440d5fd2
Binary files /dev/null and b/src/test/resources/templates/excel/SimpleLoopWithSpacingAtStart.xlsx differ