diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java index fb743c8f13..f3fafd2a81 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -66,6 +67,7 @@ import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellAddress; /** * Fill the data into excel @@ -98,9 +100,10 @@ public class ExcelWriteFillExecutor extends AbstractExcelWriteExecutor { */ private final Map collectionRowHeightCache = MapUtils.newHashMap(); /** - * Last index cache for collection fields + * Last filled coordinates for collection fields. Identity keys remain valid when template rows move. */ - private final Map> collectionLastIndexCache = MapUtils.newHashMap(); + private final Map> collectionLastCellCache = + MapUtils.newHashMap(); private final Map relativeRowIndexMap = MapUtils.newHashMap(); /** @@ -159,13 +162,13 @@ private void shiftRows(int size, List analysisCellList) { return; } int maxRowIndex = 0; - Map collectionLastIndexMap = collectionLastIndexCache.get(currentUniqueDataFlag); + Map collectionLastCellMap = collectionLastCellCache.get(currentUniqueDataFlag); for (AnalysisCell analysisCell : analysisCellList) { - if (collectionLastIndexMap != null) { - Integer lastRowIndex = collectionLastIndexMap.get(analysisCell); - if (lastRowIndex != null) { - if (lastRowIndex > maxRowIndex) { - maxRowIndex = lastRowIndex; + if (collectionLastCellMap != null) { + CellAddress lastCell = collectionLastCellMap.get(analysisCell); + if (lastCell != null) { + if (lastCell.getRow() > maxRowIndex) { + maxRowIndex = lastCell.getRow(); } continue; } @@ -181,7 +184,7 @@ private void shiftRows(int size, List analysisCellList) { } Sheet sheet = writeContext.writeSheetHolder().getCachedSheet(); int number = size; - if (collectionLastIndexMap == null) { + if (collectionLastCellMap == null) { number--; } if (number <= 0) { @@ -192,14 +195,24 @@ private void shiftRows(int size, List analysisCellList) { // The current data is greater than unity rowindex increase increaseRowIndex(templateAnalysisCache, number, maxRowIndex); increaseRowIndex(templateCollectionAnalysisCache, number, maxRowIndex); + for (Map.Entry> entry : collectionLastCellCache.entrySet()) { + if (!sameSheet(entry.getKey())) { + continue; + } + for (Map.Entry lastCellEntry : + entry.getValue().entrySet()) { + CellAddress lastCell = lastCellEntry.getValue(); + if (lastCell.getRow() > maxRowIndex) { + lastCellEntry.setValue(new CellAddress(lastCell.getRow() + number, lastCell.getColumn())); + } + } + } } private void increaseRowIndex( Map> templateAnalysisCache, int number, int maxRowIndex) { for (Map.Entry> entry : templateAnalysisCache.entrySet()) { - UniqueDataFlagKey uniqueDataFlagKey = entry.getKey(); - if (!Objects.equals(currentUniqueDataFlag.getSheetNo(), uniqueDataFlagKey.getSheetNo()) - || !Objects.equals(currentUniqueDataFlag.getSheetName(), uniqueDataFlagKey.getSheetName())) { + if (!sameSheet(entry.getKey())) { continue; } for (AnalysisCell analysisCell : entry.getValue()) { @@ -210,6 +223,11 @@ private void increaseRowIndex( } } + private boolean sameSheet(UniqueDataFlagKey uniqueDataFlagKey) { + return Objects.equals(currentUniqueDataFlag.getSheetNo(), uniqueDataFlagKey.getSheetNo()) + && Objects.equals(currentUniqueDataFlag.getSheetName(), uniqueDataFlagKey.getSheetName()); + } + private void doFill( List analysisCellList, Object oneRowData, FillConfig fillConfig, Integer relativeRowIndex) { if (CollectionUtils.isEmpty(analysisCellList) || oneRowData == null) { @@ -367,38 +385,26 @@ private void createCell( } Sheet sheet = writeContext.writeSheetHolder().getSheet(); - Map collectionLastIndexMap = - collectionLastIndexCache.computeIfAbsent(currentUniqueDataFlag, key -> MapUtils.newHashMap()); + Map collectionLastCellMap = + collectionLastCellCache.computeIfAbsent(currentUniqueDataFlag, key -> new IdentityHashMap<>()); - boolean isOriginalCell = false; + CellAddress lastCell = collectionLastCellMap.get(analysisCell); + boolean isOriginalCell = lastCell == null; Integer lastRowIndex; Integer lastColumnIndex; switch (fillConfig.getDirection()) { case VERTICAL: - lastRowIndex = collectionLastIndexMap.get(analysisCell); - if (lastRowIndex == null) { - lastRowIndex = analysisCell.getRowIndex(); - collectionLastIndexMap.put(analysisCell, lastRowIndex); - isOriginalCell = true; - } else { - collectionLastIndexMap.put(analysisCell, ++lastRowIndex); - } + lastRowIndex = isOriginalCell ? analysisCell.getRowIndex() : lastCell.getRow() + 1; lastColumnIndex = analysisCell.getColumnIndex(); break; case HORIZONTAL: lastRowIndex = analysisCell.getRowIndex(); - lastColumnIndex = collectionLastIndexMap.get(analysisCell); - if (lastColumnIndex == null) { - lastColumnIndex = analysisCell.getColumnIndex(); - collectionLastIndexMap.put(analysisCell, lastColumnIndex); - isOriginalCell = true; - } else { - collectionLastIndexMap.put(analysisCell, ++lastColumnIndex); - } + lastColumnIndex = isOriginalCell ? analysisCell.getColumnIndex() : lastCell.getColumn() + 1; break; default: throw new ExcelGenerateException("The wrong direction."); } + collectionLastCellMap.put(analysisCell, new CellAddress(lastRowIndex, lastColumnIndex)); Row row = createRowIfNecessary( sheet, cachedSheet, lastRowIndex, fillConfig, analysisCell, isOriginalCell, rowWriteHandlerContext); @@ -411,7 +417,7 @@ private void createCell( if (isOriginalCell) { Map collectionFieldStyleMap = - collectionFieldStyleCache.computeIfAbsent(currentUniqueDataFlag, key -> MapUtils.newHashMap()); + collectionFieldStyleCache.computeIfAbsent(currentUniqueDataFlag, key -> new IdentityHashMap<>()); collectionFieldStyleMap.put(analysisCell, cell.getCellStyle()); } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/InterleavedFillTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/InterleavedFillTest.java new file mode 100644 index 0000000000..eae3d96b70 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/InterleavedFillTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.style; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.fesod.sheet.ExcelWriter; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.enums.WriteDirectionEnum; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.write.metadata.WriteSheet; +import org.apache.fesod.sheet.write.metadata.fill.FillConfig; +import org.apache.fesod.sheet.write.metadata.fill.FillWrapper; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@Tag(Tags.ROUND_TRIP) +class InterleavedFillTest extends AbstractExcelTest { + + private static final String[] GROUPS = {"upper", "middle", "lower"}; + private static final short[] COLORS = { + IndexedColors.YELLOW.getIndex(), IndexedColors.LIGHT_GREEN.getIndex(), IndexedColors.LIGHT_BLUE.getIndex() + }; + + private Stream workbooks() { + return Stream.of(ExcelFormat.XLS, ExcelFormat.XLSX) + .flatMap(format -> Stream.of(false, true).map(inMemory -> Arguments.of(format, inMemory))); + } + + @ParameterizedTest + @MethodSource("workbooks") + void appendInterleavedPages(ExcelFormat format, boolean inMemory) throws Exception { + assertPages(format, inMemory, Arrays.asList("upper", "middle", "lower", "upper", "middle", "lower")); + } + + @ParameterizedTest + @MethodSource("workbooks") + void appendAfterFillingLowerGroupsFirst(ExcelFormat format, boolean inMemory) throws Exception { + assertPages(format, inMemory, Arrays.asList("lower", "middle", "upper", "lower", "middle", "upper")); + } + + @ParameterizedTest + @MethodSource("workbooks") + void appendOrderedPages(ExcelFormat format, boolean inMemory) throws Exception { + assertPages(format, inMemory, Arrays.asList("upper", "upper", "middle", "middle", "lower", "lower")); + } + + private void assertPages(ExcelFormat format, boolean inMemory, List order) throws Exception { + File result = createTempFile("interleaved", format); + FillConfig config = FillConfig.builder().forceNewRow(true).build(); + WriteSheet writeSheet = FesodSheet.writerSheet("Report").build(); + Map nextRecord = new HashMap<>(); + try (ExcelWriter writer = FesodSheet.write(result) + .inMemory(inMemory) + .withTemplate(new ByteArrayInputStream(template(format, false))) + .build()) { + for (String group : order) { + int firstRecord = nextRecord.getOrDefault(group, 1); + writer.fill(new FillWrapper(group, records(group, firstRecord, 2)), config, writeSheet); + nextRecord.put(group, firstRecord + 2); + } + writer.fill(Collections.singletonMap("total", "12"), writeSheet); + } + try (Workbook workbook = WorkbookFactory.create(result)) { + Sheet sheet = workbook.getSheet("Report"); + for (int group = 0; group < GROUPS.length; group++) { + for (int record = 0; record < 4; record++) { + Row row = sheet.getRow(group * 6 + record); + String expected = GROUPS[group] + "-" + (record + 1); + assertEquals(expected, row.getCell(0).getStringCellValue()); + assertEquals("Value: " + expected, row.getCell(1).getStringCellValue()); + assertEquals((short) (400 + group * 100), row.getHeight()); + assertStyle(row.getCell(0), COLORS[group]); + assertStyle(row.getCell(1), COLORS[group]); + } + } + assertEquals("Footer: 12", sheet.getRow(18).getCell(0).getStringCellValue()); + assertEquals(18, sheet.getLastRowNum()); + } + } + + @ParameterizedTest + @MethodSource("workbooks") + void horizontalCursorSurvivesRowsInsertedAbove(ExcelFormat format, boolean inMemory) throws Exception { + File result = createTempFile("horizontal", format); + WriteSheet writeSheet = FesodSheet.writerSheet("Report").build(); + FillConfig vertical = FillConfig.builder().forceNewRow(true).build(); + FillConfig horizontal = + FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build(); + ByteArrayOutputStream template = new ByteArrayOutputStream(); + try (Workbook workbook = newWorkbook(format)) { + Sheet sheet = workbook.createSheet("Report"); + sheet.createRow(0).createCell(0).setCellValue("{upper.name}"); + Cell cell = sheet.createRow(3).createCell(0); + cell.setCellValue("{lower.name}"); + cell.setCellStyle(style(workbook, COLORS[2])); + sheet.createRow(6).createCell(0).setCellValue("Footer"); + workbook.write(template); + } + try (ExcelWriter writer = FesodSheet.write(result) + .inMemory(inMemory) + .withTemplate(new ByteArrayInputStream(template.toByteArray())) + .build()) { + writer.fill(new FillWrapper("lower", records("lower", 1, 2)), horizontal, writeSheet); + writer.fill(new FillWrapper("upper", records("upper", 1, 3)), vertical, writeSheet); + writer.fill(new FillWrapper("lower", records("lower", 3, 2)), horizontal, writeSheet); + writer.fill(new FillWrapper("upper", records("upper", 4, 2)), vertical, writeSheet); + writer.fill(new FillWrapper("lower", records("lower", 5, 1)), horizontal, writeSheet); + } + try (Workbook workbook = WorkbookFactory.create(result)) { + Sheet sheet = workbook.getSheet("Report"); + for (int record = 0; record < 5; record++) { + assertEquals( + "upper-" + (record + 1), sheet.getRow(record).getCell(0).getStringCellValue()); + Cell cell = sheet.getRow(7).getCell(record); + assertEquals("lower-" + (record + 1), cell.getStringCellValue()); + assertStyle(cell, COLORS[2]); + } + assertEquals("Footer", sheet.getRow(10).getCell(0).getStringCellValue()); + } + } + + @ParameterizedTest + @MethodSource("workbooks") + void rowsInsertedOnAnotherSheetDoNotMoveCachedPositions(ExcelFormat format, boolean inMemory) throws Exception { + File result = createTempFile("sheets", format); + FillConfig config = FillConfig.builder().forceNewRow(true).build(); + WriteSheet first = FesodSheet.writerSheet("Report").build(); + WriteSheet second = FesodSheet.writerSheet("Other").build(); + try (ExcelWriter writer = FesodSheet.write(result) + .inMemory(inMemory) + .withTemplate(new ByteArrayInputStream(template(format, true))) + .build()) { + writer.fill(new FillWrapper("lower", records("other", 1, 1)), config, second); + writer.fill(new FillWrapper("upper", records("upper", 1, 3)), config, first); + writer.fill(new FillWrapper("lower", records("other", 2, 1)), config, second); + } + try (Workbook workbook = WorkbookFactory.create(result)) { + Sheet other = workbook.getSheet("Other"); + assertEquals("other-1", other.getRow(6).getCell(0).getStringCellValue()); + assertEquals("other-2", other.getRow(7).getCell(0).getStringCellValue()); + assertEquals("Footer: ", other.getRow(10).getCell(0).getStringCellValue()); + assertNull(other.getRow(11)); + } + } + + private byte[] template(ExcelFormat format, boolean secondSheet) throws Exception { + ByteArrayOutputStream template = new ByteArrayOutputStream(); + try (Workbook workbook = newWorkbook(format)) { + createTemplateSheet(workbook, "Report"); + if (secondSheet) { + createTemplateSheet(workbook, "Other"); + } + workbook.write(template); + } + return template.toByteArray(); + } + + private void createTemplateSheet(Workbook workbook, String name) { + Sheet sheet = workbook.createSheet(name); + for (int group = 0; group < GROUPS.length; group++) { + Row row = sheet.createRow(group * 3); + row.setHeight((short) (400 + group * 100)); + CellStyle style = style(workbook, COLORS[group]); + row.createCell(0).setCellValue("{" + GROUPS[group] + ".name}"); + row.createCell(1).setCellValue("Value: {" + GROUPS[group] + ".name}"); + row.getCell(0).setCellStyle(style); + row.getCell(1).setCellStyle(style); + } + sheet.createRow(9).createCell(0).setCellValue("Footer: {total}"); + } + + private Workbook newWorkbook(ExcelFormat format) { + return format == ExcelFormat.XLS ? new HSSFWorkbook() : new XSSFWorkbook(); + } + + private CellStyle style(Workbook workbook, short color) { + CellStyle style = workbook.createCellStyle(); + style.setFillForegroundColor(color); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setBorderBottom(BorderStyle.THIN); + return style; + } + + private void assertStyle(Cell cell, short color) { + assertEquals(color, cell.getCellStyle().getFillForegroundColor()); + assertEquals(FillPatternType.SOLID_FOREGROUND, cell.getCellStyle().getFillPattern()); + assertEquals(BorderStyle.THIN, cell.getCellStyle().getBorderBottom()); + } + + private List> records(String group, int start, int count) { + List> records = new ArrayList<>(); + for (int record = start; record < start + count; record++) { + records.add(Collections.singletonMap("name", group + "-" + record)); + } + return records; + } +} diff --git a/website/docs/sheet/fill/fill.md b/website/docs/sheet/fill/fill.md index 24ea99aa22..49b7b1ee3f 100644 --- a/website/docs/sheet/fill/fill.md +++ b/website/docs/sheet/fill/fill.md @@ -489,3 +489,13 @@ public void compositeFill() { + +### Interleaved Pages + +For named lists in separate vertical sections, calls can alternate between lists, for example +`upper` page 1, `lower` page 1, `upper` page 2, then `lower` page 2. Pass +`FillConfig.builder().forceNewRow(true).build()` to each vertical list fill to insert rows for +additional records and move the lower sections down. Each list resumes after its own previous records. + +As with other fills using `forceNewRow(true)`, the workbook is retained in memory while rows are shifted; +choose page and workbook sizes accordingly. diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md index d6f8458d79..f858120a71 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md @@ -478,3 +478,9 @@ public void compositeFill() { + +### 交错分页填充 + +对于上下分区排列的命名列表,可以交替填充各列表的分页,例如依次填充 `upper` 第 1 页、`lower` 第 1 页、`upper` 第 2 页、`lower` 第 2 页。每次纵向列表填充均传入 `FillConfig.builder().forceNewRow(true).build()`,为追加的数据插入行并将下方分区下移。每个列表会从自己上次填充的数据之后继续追加。 + +与其他使用 `forceNewRow(true)` 的填充一样,移动行时工作簿会保留在内存中,请据此选择分页和工作簿大小。