From e963f246b279464dbdda429539accafbf16177d7 Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Tue, 15 Oct 2024 16:21:48 -0300 Subject: [PATCH] feat: add support for multiple header rows Add support for multiple header rows only for excel files --- .../BaseStreamResourceWriter.java | 48 ++++++-- .../DocxStreamResourceWriter.java | 5 +- .../ExcelStreamResourceWriter.java | 80 +++++++------ .../gridexporter/GridExporterDemoView.java | 1 + .../GridExporterMultipleHeaderRowsDemo.java | 107 ++++++++++++++++++ 5 files changed, 192 insertions(+), 49 deletions(-) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java index ea8d033..4dd71a7 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java @@ -23,6 +23,7 @@ import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid.Column; +import com.vaadin.flow.component.grid.HeaderRow; import com.vaadin.flow.component.grid.dataview.GridLazyDataView; import com.vaadin.flow.data.provider.AbstractBackEndDataProvider; import com.vaadin.flow.data.provider.DataCommunicator; @@ -30,11 +31,11 @@ import com.vaadin.flow.data.provider.Query; import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider; import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery; +import com.vaadin.flow.function.SerializableFunction; import com.vaadin.flow.server.StreamResourceWriter; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -94,28 +95,46 @@ protected Stream getDataStream(Query newQuery) { return stream; } - protected List>> getGridHeaders(Grid grid) { + protected List, Column>> getGridHeaders(Grid grid) { return exporter.getColumnsOrdered().stream() - .map( - column -> - ImmutablePair.of( - renderCellTextContent(grid, column, GridExporter.COLUMN_HEADER), column)) + .map(column -> ImmutablePair.of(getHeaderTexts(grid, column), column)) .collect(Collectors.toList()); } + + private List getHeaderTexts(Grid grid, Column column) { + List headerTexts = new ArrayList<>(); + List headerRows = grid.getHeaderRows(); + for (HeaderRow headerRow : headerRows) { + String headerText = renderCellTextContent(grid, column, GridExporter.COLUMN_HEADER, (col) -> { + String value = headerRow.getCell(col).getText(); + if (Strings.isBlank(value)) { + Component component = headerRow.getCell(col).getComponent(); + if (component != null) { + value = component.getElement().getTextRecursively(); + } + } + return value; + }); + headerTexts.add(headerText); + } + return headerTexts; + } protected List>> getGridFooters(Grid grid) { return exporter.getColumnsOrdered().stream() .map( column -> ImmutablePair.of( - renderCellTextContent(grid, column, GridExporter.COLUMN_FOOTER), column)) + renderCellTextContent(grid, column, GridExporter.COLUMN_FOOTER, null),column + ) + ) .collect(Collectors.toList()); } - private String renderCellTextContent(Grid grid, Column column, String columnType) { + private String renderCellTextContent(Grid grid, Column column, String columnType, SerializableFunction,String> obtainCellFunction) { String headerOrFooter = (String) ComponentUtil.getData(column, columnType); if (Strings.isBlank(headerOrFooter)) { - Function, Component> getHeaderOrFooterComponent; + SerializableFunction, Component> getHeaderOrFooterComponent; if (GridExporter.COLUMN_HEADER.equals(columnType)) { getHeaderOrFooterComponent = Column::getHeaderComponent; headerOrFooter = column.getHeaderText(); @@ -127,9 +146,14 @@ private String renderCellTextContent(Grid grid, Column column, String colu } if (Strings.isBlank(headerOrFooter)) { try { - Component component = getHeaderOrFooterComponent.apply(column); - if (component != null) { - headerOrFooter = component.getElement().getTextRecursively(); + Component component; + if (obtainCellFunction!=null) { + headerOrFooter = obtainCellFunction.apply(column); + } else { + component = getHeaderOrFooterComponent.apply(column); + if (component != null) { + headerOrFooter = component.getElement().getTextRecursively(); + } } } catch (RuntimeException e) { throw new IllegalStateException( diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxStreamResourceWriter.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxStreamResourceWriter.java index b67fb33..0b018d3 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxStreamResourceWriter.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxStreamResourceWriter.java @@ -128,7 +128,10 @@ private XWPFDocument createDoc() throws IOException { cctblgridcol, "" + Math.round(9638 / exporter.getColumns().size())); }); - List>> headers = getGridHeaders(grid); + List>> headers = getGridHeaders(grid).stream() + .map(pair -> + Pair.of(pair.getLeft().get(0), pair.getRight()) + ).collect(Collectors.toList()); XWPFTableCell cell = findCellWithPlaceHolder(table, exporter.headersPlaceHolder); if (cell != null) { fillHeaderOrFooter(table, cell, headers, true, exporter.headersPlaceHolder); diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelStreamResourceWriter.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelStreamResourceWriter.java index fc70c6c..07962a1 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelStreamResourceWriter.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelStreamResourceWriter.java @@ -95,7 +95,7 @@ private Workbook createWorkbook(VaadinSession session) { } Cell cell = findCellWithPlaceHolder(sheet, exporter.headersPlaceHolder); - List>> headers = getGridHeaders(grid); + List, Column>> headers = getGridHeaders(grid); fillHeaderOrFooter(sheet, cell, headers, true); if (exporter.autoMergeTitle && titleCell != null && exporter.getColumns().size()>1) { @@ -125,7 +125,7 @@ private Workbook createWorkbook(VaadinSession session) { cell = findCellWithPlaceHolder(sheet, exporter.footersPlaceHolder); List>> footers = getGridFooters(grid); if (cell != null) { - fillHeaderOrFooter(sheet, cell, footers, false); + fillFooter(sheet, cell, footers, false); } if (exporter.isAutoSizeColumns()) { @@ -411,39 +411,47 @@ private Cell findCellWithPlaceHolder(Sheet sheet, String placeholder) { return null; } - private void fillHeaderOrFooter( - Sheet sheet, - Cell headersOrFootersCell, - List>> headersOrFooters, - boolean isHeader) { - CellStyle style = headersOrFootersCell.getCellStyle(); - sheet.setActiveCell(headersOrFootersCell.getAddress()); - headersOrFooters.forEach( - headerOrFooter -> { - if (!isHeader) { - // clear the styles before processing the column in the footer - ComponentUtil.setData(headerOrFooter.getRight(), COLUMN_CELLSTYLE_MAP, null); - } - Cell cell = - sheet - .getRow(sheet.getActiveCell().getRow()) - .getCell(sheet.getActiveCell().getColumn()); - if (cell == null) { - cell = - sheet - .getRow(sheet.getActiveCell().getRow()) - .createCell(sheet.getActiveCell().getColumn()); - } - cell.setCellStyle(style); - Object value = - (isHeader - ? headerOrFooter.getLeft() - : transformToType(headerOrFooter.getLeft(), headerOrFooter.getRight())); - buildCell(value, cell, headerOrFooter.getRight(), null); - configureAlignment(headerOrFooter.getRight(), cell, isHeader?ExcelCellType.HEADER:ExcelCellType.FOOTER); - sheet.setActiveCell( - new CellAddress( - sheet.getActiveCell().getRow(), sheet.getActiveCell().getColumn() + 1)); - }); + private void fillFooter(Sheet sheet, Cell headersOrFootersCell, + List>> headersOrFooters, boolean isHeader) { + List, Column>> headersOrFootersCellSingleRow = headersOrFooters.stream() + .map(pair -> Pair.of(List.of(pair.getLeft()), pair.getRight())).collect(Collectors.toList()); + fillHeaderOrFooter(sheet, headersOrFootersCell, headersOrFootersCellSingleRow, isHeader); } + private void fillHeaderOrFooter(Sheet sheet, Cell headersOrFootersCell, + List, Column>> headersOrFooters, boolean isHeader) { + CellStyle style = headersOrFootersCell.getCellStyle(); + + int startRow = headersOrFootersCell.getRowIndex(); + int currentColumn = headersOrFootersCell.getColumnIndex(); + boolean shiftFirstTime = true; + for (Pair, Column> headerOrFooter : headersOrFooters) { + List headerOrFooterTexts = headerOrFooter.getLeft(); + Column column = headerOrFooter.getRight(); + if (!isHeader) { + ComponentUtil.setData(column, COLUMN_CELLSTYLE_MAP, null); + } + if (shiftFirstTime) { + if (headerOrFooterTexts.size()>1) { + sheet.shiftRows(startRow, sheet.getLastRowNum(), headerOrFooterTexts.size()-1); + } + shiftFirstTime = false; + } + for (int i = 0; i < headerOrFooterTexts.size(); i++) { + Row row = sheet.getRow(startRow + i); + if (row == null) { + row = sheet.createRow(startRow + i); + } + Cell cell = row.getCell(currentColumn); + if (cell == null) { + cell = row.createCell(currentColumn); + } + cell.setCellStyle(style); + Object value = + (isHeader ? headerOrFooterTexts.get(i) : transformToType(headerOrFooterTexts.get(i), column)); + buildCell(value, cell, column, null); + configureAlignment(column, cell, isHeader ? ExcelCellType.HEADER : ExcelCellType.FOOTER); + } + currentColumn++; + } +} } diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java index 0cd4ed9..288aeb7 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java @@ -39,6 +39,7 @@ public GridExporterDemoView() { addDemo(GridExporterCustomColumnsDemo.class); addDemo(GridExporterHierarchicalDataDemo.class); addDemo(GridExporterBigDatasetDemo.class); + addDemo(GridExporterMultipleHeaderRowsDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java new file mode 100644 index 0000000..7c72a7c --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java @@ -0,0 +1,107 @@ +/*- + * #%L + * Grid Exporter Add-on + * %% + * Copyright (C) 2022 - 2023 Flowing Code + * %% + * Licensed 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. + * #L% + */ +package com.flowingcode.vaadin.addons.gridexporter; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.github.javafaker.Faker; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.Grid.Column; +import com.vaadin.flow.component.grid.HeaderRow; +import com.vaadin.flow.component.grid.HeaderRow.HeaderCell; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.data.renderer.LitRenderer; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.io.IOException; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.poi.EncryptedDocumentException; + +@DemoSource +@PageTitle("Multiple Header Rows") +@Route(value = "gridexporter/headers", layout = GridExporterDemoView.class) +@SuppressWarnings("serial") +public class GridExporterMultipleHeaderRowsDemo extends Div { + + private static final String NUMBER_FORMAT_PATTERN = "$#,###.##"; + private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd"; + private static final String EXCEL_TEMPLATE_PATH = "/custom-template.xlsx"; + private static final String WORD_TEMPLATE_PATH = "/custom-template.docx"; + + public GridExporterMultipleHeaderRowsDemo() throws EncryptedDocumentException, IOException { + Grid grid = new Grid<>(Person.class); + DecimalFormat decimalFormat = new DecimalFormat(NUMBER_FORMAT_PATTERN); + grid.removeAllColumns(); + grid.addColumn( + LitRenderer.of("${item.name}").withProperty("name", Person::getName)) + .setHeader("Name"); + grid.addColumn("lastName").setHeader("Last Name"); + grid.addColumn(item -> Faker.instance().lorem().characters(30, 50)).setHeader("Big column"); + Column budgetColumn = grid.addColumn(item -> decimalFormat.format(item.getBudget())) + .setHeader("Budget").setTextAlign(ColumnTextAlign.END); + List people = IntStream.range(0, 100).asLongStream().mapToObj(number -> { + Faker faker = new Faker(); + Double budget = faker.number().randomDouble(2, 10000, 100000); + return new Person(faker.name().firstName(), + (Math.random() > 0.3 ? faker.name().lastName() : null), + faker.number().numberBetween(15, 50), budget); + }).collect(Collectors.toList()); + @SuppressWarnings("null") + BigDecimal total = people.stream().map(Person::getBudget).map(BigDecimal::valueOf) + .reduce(BigDecimal.ZERO, BigDecimal::add); + budgetColumn.setFooter(new DecimalFormat(NUMBER_FORMAT_PATTERN).format(total)); + + grid.setItems(people); + grid.setWidthFull(); + this.setSizeFull(); + + HeaderRow firstExtraHeaderRow = grid.appendHeaderRow(); + HeaderRow secondExtraHeaderRow = grid.appendHeaderRow(); + for (Column column : grid.getColumns()) { + String columnHeader = grid.getHeaderRows().get(0).getCell(column).getText(); + + HeaderCell firstHeaderCell = firstExtraHeaderRow.getCell(column); + firstHeaderCell.setComponent(new Span(columnHeader + " 1")); + HeaderCell secondHeaderCell = secondExtraHeaderRow.getCell(column); + secondHeaderCell.setComponent(new Span(columnHeader + " 2")); + } + + GridExporter exporter = GridExporter.createFor(grid, EXCEL_TEMPLATE_PATH, WORD_TEMPLATE_PATH); + HashMap placeholders = new HashMap<>(); + placeholders.put("${date}", new SimpleDateFormat(DATE_FORMAT_PATTERN).format(Calendar.getInstance().getTime())); + exporter.setAdditionalPlaceHolders(placeholders); + exporter.setSheetNumber(1); + exporter.setCsvExportEnabled(false); + exporter.setNumberColumnFormat(budgetColumn, decimalFormat, NUMBER_FORMAT_PATTERN); + exporter.setTitle("People information"); + exporter.setNullValueHandler(() -> "(No lastname)"); + exporter.setFileName( + "GridExport" + new SimpleDateFormat(DATE_FORMAT_PATTERN).format(Calendar.getInstance().getTime())); + add(grid); + } +}