Skip to content

Commit

Permalink
feat: add support for multiple header rows
Browse files Browse the repository at this point in the history
Add support for multiple header rows only for excel files
  • Loading branch information
mlopezFC authored and javier-godoy committed Oct 30, 2024
1 parent 864a072 commit e963f24
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@
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;
import com.vaadin.flow.data.provider.DataProvider;
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;
Expand Down Expand Up @@ -94,28 +95,46 @@ protected Stream<T> getDataStream(Query newQuery) {
return stream;
}

protected List<Pair<String, Column<T>>> getGridHeaders(Grid<T> grid) {
protected List<Pair<List<String>, Column<T>>> getGridHeaders(Grid<T> 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<String> getHeaderTexts(Grid<T> grid, Column<T> column) {
List<String> headerTexts = new ArrayList<>();
List<HeaderRow> 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<Pair<String, Column<T>>> getGridFooters(Grid<T> 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<T> grid, Column<T> column, String columnType) {
private String renderCellTextContent(Grid<T> grid, Column<T> column, String columnType, SerializableFunction<Column<T>,String> obtainCellFunction) {
String headerOrFooter = (String) ComponentUtil.getData(column, columnType);
if (Strings.isBlank(headerOrFooter)) {
Function<Column<?>, Component> getHeaderOrFooterComponent;
SerializableFunction<Column<?>, Component> getHeaderOrFooterComponent;
if (GridExporter.COLUMN_HEADER.equals(columnType)) {
getHeaderOrFooterComponent = Column::getHeaderComponent;
headerOrFooter = column.getHeaderText();
Expand All @@ -127,9 +146,14 @@ private String renderCellTextContent(Grid<T> grid, Column<T> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ private XWPFDocument createDoc() throws IOException {
cctblgridcol, "" + Math.round(9638 / exporter.getColumns().size()));
});

List<Pair<String, Column<T>>> headers = getGridHeaders(grid);
List<Pair<String, Column<T>>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private Workbook createWorkbook(VaadinSession session) {
}

Cell cell = findCellWithPlaceHolder(sheet, exporter.headersPlaceHolder);
List<Pair<String, Column<T>>> headers = getGridHeaders(grid);
List<Pair<List<String>, Column<T>>> headers = getGridHeaders(grid);

fillHeaderOrFooter(sheet, cell, headers, true);
if (exporter.autoMergeTitle && titleCell != null && exporter.getColumns().size()>1) {
Expand Down Expand Up @@ -125,7 +125,7 @@ private Workbook createWorkbook(VaadinSession session) {
cell = findCellWithPlaceHolder(sheet, exporter.footersPlaceHolder);
List<Pair<String, Column<T>>> footers = getGridFooters(grid);
if (cell != null) {
fillHeaderOrFooter(sheet, cell, footers, false);
fillFooter(sheet, cell, footers, false);
}

if (exporter.isAutoSizeColumns()) {
Expand Down Expand Up @@ -411,39 +411,47 @@ private Cell findCellWithPlaceHolder(Sheet sheet, String placeholder) {
return null;
}

private void fillHeaderOrFooter(
Sheet sheet,
Cell headersOrFootersCell,
List<Pair<String, Column<T>>> 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<Pair<String, Column<T>>> headersOrFooters, boolean isHeader) {
List<Pair<List<String>, Column<T>>> 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<Pair<List<String>, Column<T>>> headersOrFooters, boolean isHeader) {
CellStyle style = headersOrFootersCell.getCellStyle();

int startRow = headersOrFootersCell.getRowIndex();
int currentColumn = headersOrFootersCell.getColumnIndex();
boolean shiftFirstTime = true;
for (Pair<List<String>, Column<T>> headerOrFooter : headersOrFooters) {
List<String> headerOrFooterTexts = headerOrFooter.getLeft();
Column<T> 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++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public GridExporterDemoView() {
addDemo(GridExporterCustomColumnsDemo.class);
addDemo(GridExporterHierarchicalDataDemo.class);
addDemo(GridExporterBigDatasetDemo.class);
addDemo(GridExporterMultipleHeaderRowsDemo.class);
setSizeFull();
}
}
Original file line number Diff line number Diff line change
@@ -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<Person> grid = new Grid<>(Person.class);
DecimalFormat decimalFormat = new DecimalFormat(NUMBER_FORMAT_PATTERN);
grid.removeAllColumns();
grid.addColumn(
LitRenderer.<Person>of("<b>${item.name}</b>").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<Person> budgetColumn = grid.addColumn(item -> decimalFormat.format(item.getBudget()))
.setHeader("Budget").setTextAlign(ColumnTextAlign.END);
List<Person> 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<Person> 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<Person> exporter = GridExporter.createFor(grid, EXCEL_TEMPLATE_PATH, WORD_TEMPLATE_PATH);
HashMap<String, String> 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);
}
}

0 comments on commit e963f24

Please sign in to comment.