Skip to content

Commit e963f24

Browse files
mlopezFCjavier-godoy
authored andcommitted
feat: add support for multiple header rows
Add support for multiple header rows only for excel files
1 parent 864a072 commit e963f24

File tree

5 files changed

+192
-49
lines changed

5 files changed

+192
-49
lines changed

src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseStreamResourceWriter.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,19 @@
2323
import com.vaadin.flow.component.ComponentUtil;
2424
import com.vaadin.flow.component.grid.Grid;
2525
import com.vaadin.flow.component.grid.Grid.Column;
26+
import com.vaadin.flow.component.grid.HeaderRow;
2627
import com.vaadin.flow.component.grid.dataview.GridLazyDataView;
2728
import com.vaadin.flow.data.provider.AbstractBackEndDataProvider;
2829
import com.vaadin.flow.data.provider.DataCommunicator;
2930
import com.vaadin.flow.data.provider.DataProvider;
3031
import com.vaadin.flow.data.provider.Query;
3132
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider;
3233
import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery;
34+
import com.vaadin.flow.function.SerializableFunction;
3335
import com.vaadin.flow.server.StreamResourceWriter;
3436
import java.lang.reflect.Method;
3537
import java.util.ArrayList;
3638
import java.util.List;
37-
import java.util.function.Function;
3839
import java.util.stream.Collectors;
3940
import java.util.stream.Stream;
4041
import org.apache.commons.lang3.tuple.ImmutablePair;
@@ -94,28 +95,46 @@ protected Stream<T> getDataStream(Query newQuery) {
9495
return stream;
9596
}
9697

97-
protected List<Pair<String, Column<T>>> getGridHeaders(Grid<T> grid) {
98+
protected List<Pair<List<String>, Column<T>>> getGridHeaders(Grid<T> grid) {
9899
return exporter.getColumnsOrdered().stream()
99-
.map(
100-
column ->
101-
ImmutablePair.of(
102-
renderCellTextContent(grid, column, GridExporter.COLUMN_HEADER), column))
100+
.map(column -> ImmutablePair.of(getHeaderTexts(grid, column), column))
103101
.collect(Collectors.toList());
104102
}
103+
104+
private List<String> getHeaderTexts(Grid<T> grid, Column<T> column) {
105+
List<String> headerTexts = new ArrayList<>();
106+
List<HeaderRow> headerRows = grid.getHeaderRows();
107+
for (HeaderRow headerRow : headerRows) {
108+
String headerText = renderCellTextContent(grid, column, GridExporter.COLUMN_HEADER, (col) -> {
109+
String value = headerRow.getCell(col).getText();
110+
if (Strings.isBlank(value)) {
111+
Component component = headerRow.getCell(col).getComponent();
112+
if (component != null) {
113+
value = component.getElement().getTextRecursively();
114+
}
115+
}
116+
return value;
117+
});
118+
headerTexts.add(headerText);
119+
}
120+
return headerTexts;
121+
}
105122

106123
protected List<Pair<String, Column<T>>> getGridFooters(Grid<T> grid) {
107124
return exporter.getColumnsOrdered().stream()
108125
.map(
109126
column ->
110127
ImmutablePair.of(
111-
renderCellTextContent(grid, column, GridExporter.COLUMN_FOOTER), column))
128+
renderCellTextContent(grid, column, GridExporter.COLUMN_FOOTER, null),column
129+
)
130+
)
112131
.collect(Collectors.toList());
113132
}
114133

115-
private String renderCellTextContent(Grid<T> grid, Column<T> column, String columnType) {
134+
private String renderCellTextContent(Grid<T> grid, Column<T> column, String columnType, SerializableFunction<Column<T>,String> obtainCellFunction) {
116135
String headerOrFooter = (String) ComponentUtil.getData(column, columnType);
117136
if (Strings.isBlank(headerOrFooter)) {
118-
Function<Column<?>, Component> getHeaderOrFooterComponent;
137+
SerializableFunction<Column<?>, Component> getHeaderOrFooterComponent;
119138
if (GridExporter.COLUMN_HEADER.equals(columnType)) {
120139
getHeaderOrFooterComponent = Column::getHeaderComponent;
121140
headerOrFooter = column.getHeaderText();
@@ -127,9 +146,14 @@ private String renderCellTextContent(Grid<T> grid, Column<T> column, String colu
127146
}
128147
if (Strings.isBlank(headerOrFooter)) {
129148
try {
130-
Component component = getHeaderOrFooterComponent.apply(column);
131-
if (component != null) {
132-
headerOrFooter = component.getElement().getTextRecursively();
149+
Component component;
150+
if (obtainCellFunction!=null) {
151+
headerOrFooter = obtainCellFunction.apply(column);
152+
} else {
153+
component = getHeaderOrFooterComponent.apply(column);
154+
if (component != null) {
155+
headerOrFooter = component.getElement().getTextRecursively();
156+
}
133157
}
134158
} catch (RuntimeException e) {
135159
throw new IllegalStateException(

src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxStreamResourceWriter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ private XWPFDocument createDoc() throws IOException {
128128
cctblgridcol, "" + Math.round(9638 / exporter.getColumns().size()));
129129
});
130130

131-
List<Pair<String, Column<T>>> headers = getGridHeaders(grid);
131+
List<Pair<String, Column<T>>> headers = getGridHeaders(grid).stream()
132+
.map(pair ->
133+
Pair.of(pair.getLeft().get(0), pair.getRight())
134+
).collect(Collectors.toList());
132135
XWPFTableCell cell = findCellWithPlaceHolder(table, exporter.headersPlaceHolder);
133136
if (cell != null) {
134137
fillHeaderOrFooter(table, cell, headers, true, exporter.headersPlaceHolder);

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelStreamResourceWriter.java

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private Workbook createWorkbook(VaadinSession session) {
9595
}
9696

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

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

131131
if (exporter.isAutoSizeColumns()) {
@@ -411,39 +411,47 @@ private Cell findCellWithPlaceHolder(Sheet sheet, String placeholder) {
411411
return null;
412412
}
413413

414-
private void fillHeaderOrFooter(
415-
Sheet sheet,
416-
Cell headersOrFootersCell,
417-
List<Pair<String, Column<T>>> headersOrFooters,
418-
boolean isHeader) {
419-
CellStyle style = headersOrFootersCell.getCellStyle();
420-
sheet.setActiveCell(headersOrFootersCell.getAddress());
421-
headersOrFooters.forEach(
422-
headerOrFooter -> {
423-
if (!isHeader) {
424-
// clear the styles before processing the column in the footer
425-
ComponentUtil.setData(headerOrFooter.getRight(), COLUMN_CELLSTYLE_MAP, null);
426-
}
427-
Cell cell =
428-
sheet
429-
.getRow(sheet.getActiveCell().getRow())
430-
.getCell(sheet.getActiveCell().getColumn());
431-
if (cell == null) {
432-
cell =
433-
sheet
434-
.getRow(sheet.getActiveCell().getRow())
435-
.createCell(sheet.getActiveCell().getColumn());
436-
}
437-
cell.setCellStyle(style);
438-
Object value =
439-
(isHeader
440-
? headerOrFooter.getLeft()
441-
: transformToType(headerOrFooter.getLeft(), headerOrFooter.getRight()));
442-
buildCell(value, cell, headerOrFooter.getRight(), null);
443-
configureAlignment(headerOrFooter.getRight(), cell, isHeader?ExcelCellType.HEADER:ExcelCellType.FOOTER);
444-
sheet.setActiveCell(
445-
new CellAddress(
446-
sheet.getActiveCell().getRow(), sheet.getActiveCell().getColumn() + 1));
447-
});
414+
private void fillFooter(Sheet sheet, Cell headersOrFootersCell,
415+
List<Pair<String, Column<T>>> headersOrFooters, boolean isHeader) {
416+
List<Pair<List<String>, Column<T>>> headersOrFootersCellSingleRow = headersOrFooters.stream()
417+
.map(pair -> Pair.of(List.of(pair.getLeft()), pair.getRight())).collect(Collectors.toList());
418+
fillHeaderOrFooter(sheet, headersOrFootersCell, headersOrFootersCellSingleRow, isHeader);
448419
}
420+
private void fillHeaderOrFooter(Sheet sheet, Cell headersOrFootersCell,
421+
List<Pair<List<String>, Column<T>>> headersOrFooters, boolean isHeader) {
422+
CellStyle style = headersOrFootersCell.getCellStyle();
423+
424+
int startRow = headersOrFootersCell.getRowIndex();
425+
int currentColumn = headersOrFootersCell.getColumnIndex();
426+
boolean shiftFirstTime = true;
427+
for (Pair<List<String>, Column<T>> headerOrFooter : headersOrFooters) {
428+
List<String> headerOrFooterTexts = headerOrFooter.getLeft();
429+
Column<T> column = headerOrFooter.getRight();
430+
if (!isHeader) {
431+
ComponentUtil.setData(column, COLUMN_CELLSTYLE_MAP, null);
432+
}
433+
if (shiftFirstTime) {
434+
if (headerOrFooterTexts.size()>1) {
435+
sheet.shiftRows(startRow, sheet.getLastRowNum(), headerOrFooterTexts.size()-1);
436+
}
437+
shiftFirstTime = false;
438+
}
439+
for (int i = 0; i < headerOrFooterTexts.size(); i++) {
440+
Row row = sheet.getRow(startRow + i);
441+
if (row == null) {
442+
row = sheet.createRow(startRow + i);
443+
}
444+
Cell cell = row.getCell(currentColumn);
445+
if (cell == null) {
446+
cell = row.createCell(currentColumn);
447+
}
448+
cell.setCellStyle(style);
449+
Object value =
450+
(isHeader ? headerOrFooterTexts.get(i) : transformToType(headerOrFooterTexts.get(i), column));
451+
buildCell(value, cell, column, null);
452+
configureAlignment(column, cell, isHeader ? ExcelCellType.HEADER : ExcelCellType.FOOTER);
453+
}
454+
currentColumn++;
455+
}
456+
}
449457
}

src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public GridExporterDemoView() {
3939
addDemo(GridExporterCustomColumnsDemo.class);
4040
addDemo(GridExporterHierarchicalDataDemo.class);
4141
addDemo(GridExporterBigDatasetDemo.class);
42+
addDemo(GridExporterMultipleHeaderRowsDemo.class);
4243
setSizeFull();
4344
}
4445
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*-
2+
* #%L
3+
* Grid Exporter Add-on
4+
* %%
5+
* Copyright (C) 2022 - 2023 Flowing Code
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package com.flowingcode.vaadin.addons.gridexporter;
21+
22+
import com.flowingcode.vaadin.addons.demo.DemoSource;
23+
import com.github.javafaker.Faker;
24+
import com.vaadin.flow.component.grid.ColumnTextAlign;
25+
import com.vaadin.flow.component.grid.Grid;
26+
import com.vaadin.flow.component.grid.Grid.Column;
27+
import com.vaadin.flow.component.grid.HeaderRow;
28+
import com.vaadin.flow.component.grid.HeaderRow.HeaderCell;
29+
import com.vaadin.flow.component.html.Div;
30+
import com.vaadin.flow.component.html.Span;
31+
import com.vaadin.flow.data.renderer.LitRenderer;
32+
import com.vaadin.flow.router.PageTitle;
33+
import com.vaadin.flow.router.Route;
34+
import java.io.IOException;
35+
import java.math.BigDecimal;
36+
import java.text.DecimalFormat;
37+
import java.text.SimpleDateFormat;
38+
import java.util.Calendar;
39+
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.stream.Collectors;
42+
import java.util.stream.IntStream;
43+
import org.apache.poi.EncryptedDocumentException;
44+
45+
@DemoSource
46+
@PageTitle("Multiple Header Rows")
47+
@Route(value = "gridexporter/headers", layout = GridExporterDemoView.class)
48+
@SuppressWarnings("serial")
49+
public class GridExporterMultipleHeaderRowsDemo extends Div {
50+
51+
private static final String NUMBER_FORMAT_PATTERN = "$#,###.##";
52+
private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd";
53+
private static final String EXCEL_TEMPLATE_PATH = "/custom-template.xlsx";
54+
private static final String WORD_TEMPLATE_PATH = "/custom-template.docx";
55+
56+
public GridExporterMultipleHeaderRowsDemo() throws EncryptedDocumentException, IOException {
57+
Grid<Person> grid = new Grid<>(Person.class);
58+
DecimalFormat decimalFormat = new DecimalFormat(NUMBER_FORMAT_PATTERN);
59+
grid.removeAllColumns();
60+
grid.addColumn(
61+
LitRenderer.<Person>of("<b>${item.name}</b>").withProperty("name", Person::getName))
62+
.setHeader("Name");
63+
grid.addColumn("lastName").setHeader("Last Name");
64+
grid.addColumn(item -> Faker.instance().lorem().characters(30, 50)).setHeader("Big column");
65+
Column<Person> budgetColumn = grid.addColumn(item -> decimalFormat.format(item.getBudget()))
66+
.setHeader("Budget").setTextAlign(ColumnTextAlign.END);
67+
List<Person> people = IntStream.range(0, 100).asLongStream().mapToObj(number -> {
68+
Faker faker = new Faker();
69+
Double budget = faker.number().randomDouble(2, 10000, 100000);
70+
return new Person(faker.name().firstName(),
71+
(Math.random() > 0.3 ? faker.name().lastName() : null),
72+
faker.number().numberBetween(15, 50), budget);
73+
}).collect(Collectors.toList());
74+
@SuppressWarnings("null")
75+
BigDecimal total = people.stream().map(Person::getBudget).map(BigDecimal::valueOf)
76+
.reduce(BigDecimal.ZERO, BigDecimal::add);
77+
budgetColumn.setFooter(new DecimalFormat(NUMBER_FORMAT_PATTERN).format(total));
78+
79+
grid.setItems(people);
80+
grid.setWidthFull();
81+
this.setSizeFull();
82+
83+
HeaderRow firstExtraHeaderRow = grid.appendHeaderRow();
84+
HeaderRow secondExtraHeaderRow = grid.appendHeaderRow();
85+
for (Column<Person> column : grid.getColumns()) {
86+
String columnHeader = grid.getHeaderRows().get(0).getCell(column).getText();
87+
88+
HeaderCell firstHeaderCell = firstExtraHeaderRow.getCell(column);
89+
firstHeaderCell.setComponent(new Span(columnHeader + " 1"));
90+
HeaderCell secondHeaderCell = secondExtraHeaderRow.getCell(column);
91+
secondHeaderCell.setComponent(new Span(columnHeader + " 2"));
92+
}
93+
94+
GridExporter<Person> exporter = GridExporter.createFor(grid, EXCEL_TEMPLATE_PATH, WORD_TEMPLATE_PATH);
95+
HashMap<String, String> placeholders = new HashMap<>();
96+
placeholders.put("${date}", new SimpleDateFormat(DATE_FORMAT_PATTERN).format(Calendar.getInstance().getTime()));
97+
exporter.setAdditionalPlaceHolders(placeholders);
98+
exporter.setSheetNumber(1);
99+
exporter.setCsvExportEnabled(false);
100+
exporter.setNumberColumnFormat(budgetColumn, decimalFormat, NUMBER_FORMAT_PATTERN);
101+
exporter.setTitle("People information");
102+
exporter.setNullValueHandler(() -> "(No lastname)");
103+
exporter.setFileName(
104+
"GridExport" + new SimpleDateFormat(DATE_FORMAT_PATTERN).format(Calendar.getInstance().getTime()));
105+
add(grid);
106+
}
107+
}

0 commit comments

Comments
 (0)