diff --git a/README.md b/README.md index 8a5c81e..e427993 100755 --- a/README.md +++ b/README.md @@ -36,6 +36,26 @@ Exporter.exportAsExcel(grid, gridHeaderMap) Exporter works as using reflection to read property from each item, any column without a valid key will be ignored. +When grid is created using bean type create map where Column is key and header text is value. Pass that map as argument to Exporter subclass constructor. +If map is null then default column key will be used for creating column header in excel. +``` +private Map, String> gridHeaderMap; +... +Exporter.exportAsExcel(grid, gridHeaderMap) +``` + +In case when grid is created without bean type (adding columns with value providers) then map with column headers is mandatory. + +### Data formats, excel built-in and custom data formats + +By default excel exporter will use default excel data formats for date, time and numbers. To set custom formats: +``` +DataFormats formats = new TypeDataFormats(); +formats.localDateFormat("DDD-MMM-YY"); +Exporter.exportAsExcel(grid, gridHeaderMap, formats); +``` + + ## Development instructions Starting the test/demo server: diff --git a/src/main/java/org/vaadin/haijian/CellValueTypeConverter.java b/src/main/java/org/vaadin/haijian/CellValueTypeConverter.java new file mode 100644 index 0000000..3fd0d35 --- /dev/null +++ b/src/main/java/org/vaadin/haijian/CellValueTypeConverter.java @@ -0,0 +1,38 @@ +package org.vaadin.haijian; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; + +import org.apache.poi.ss.usermodel.Cell; + +/** + * + * @author Krunoslav Magazin + * Oct 5, 2019 + */ +public class CellValueTypeConverter { + + public void setValue(Cell cell, LocalDateTime localDateTime) { + cell.setCellValue(convertLocalDateTimeToDate(localDateTime)); + } + + public void setValue(Cell cell, LocalDate localDateTime) { + cell.setCellValue(convertLocalDateToDate(localDateTime)); + } + + public void setValue(Cell cell, Calendar localDateTime) { + cell.setCellValue(localDateTime.getTime()); + } + + public Date convertLocalDateToDate(LocalDate dateToConvert) { + return Date.from(dateToConvert.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + } + + public Date convertLocalDateTimeToDate(LocalDateTime dateToConvert) { + return Date.from(dateToConvert.atZone(ZoneId.systemDefault()).toInstant()); + } + +} diff --git a/src/main/java/org/vaadin/haijian/DataFormats.java b/src/main/java/org/vaadin/haijian/DataFormats.java new file mode 100644 index 0000000..1373792 --- /dev/null +++ b/src/main/java/org/vaadin/haijian/DataFormats.java @@ -0,0 +1,24 @@ +package org.vaadin.haijian; + +import java.util.Map; + +/** + * + * @author Krunoslav Magazin + * Oct 5, 2019 + */ +public interface DataFormats { + + void numberFormat(Class clazz, String format); + + void localDateTimeFormat( String format); + + void localDateFormat( String format); + + void calendarFormat( String format); + + void dateFormat( String format); + + public Map, String> getTypeFormatsMap(); + +} diff --git a/src/main/java/org/vaadin/haijian/ExcelFileBuilder.java b/src/main/java/org/vaadin/haijian/ExcelFileBuilder.java index d3ab673..93cb61c 100644 --- a/src/main/java/org/vaadin/haijian/ExcelFileBuilder.java +++ b/src/main/java/org/vaadin/haijian/ExcelFileBuilder.java @@ -1,14 +1,30 @@ package org.vaadin.haijian; -import com.vaadin.flow.component.grid.Grid; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.ss.usermodel.*; -import org.slf4j.LoggerFactory; - import java.io.FileOutputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Calendar; +import java.util.Date; import java.util.Map; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.grid.Grid; + +/** + * + * Modified Oct 5, 2019 by Krunoslav Magazin - added data formats and type + * conversion + * + */ public class ExcelFileBuilder extends FileBuilder { private static final String EXCEL_FILE_EXTENSION = ".xls"; @@ -20,8 +36,30 @@ public class ExcelFileBuilder extends FileBuilder { private Cell cell; private CellStyle boldStyle; + private DataFormats dataFormats; + private CellValueTypeConverter converter; + private WorkbookStyles styles; + ExcelFileBuilder(Grid grid, Map, String> columnHeaders) { super(grid, columnHeaders); + this.dataFormats = new TypeDataFormats(); + this.converter = new CellValueTypeConverter(); + } + + ExcelFileBuilder(Grid grid, Map, String> columnHeaders, DataFormats dataFormats) { + this(grid, columnHeaders); + this.dataFormats = dataFormats; + } + + ExcelFileBuilder(Grid grid, Map, String> columnHeaders, CellValueTypeConverter converter) { + this(grid, columnHeaders); + this.converter = converter; + } + + ExcelFileBuilder(Grid grid, Map, String> columnHeaders, DataFormats dataFormats, + CellValueTypeConverter converter) { + this(grid, columnHeaders, dataFormats); + this.converter = converter; } @Override @@ -58,17 +96,37 @@ protected void buildCell(Object value) { } else if (value instanceof Boolean) { cell.setCellValue((Boolean) value); cell.setCellType(Cell.CELL_TYPE_BOOLEAN); + } else if (value instanceof Number) { + buildNumericCell((Number) value); } else if (value instanceof Calendar) { - Calendar calendar = (Calendar) value; - cell.setCellValue(calendar.getTime()); - cell.setCellType(Cell.CELL_TYPE_STRING); - } else if (value instanceof Double) { - cell.setCellValue((Double) value); - cell.setCellType(Cell.CELL_TYPE_NUMERIC); + converter.setValue(cell, (Calendar) value); + } else if (value instanceof Date) { + Date date = (Date) value; + cell.setCellValue(date); + } else if (value instanceof LocalDate) { + converter.setValue(cell, (LocalDate) value); + } else if (value instanceof LocalDateTime) { + converter.setValue(cell, (LocalDateTime) value); } else { cell.setCellValue(value.toString()); cell.setCellType(Cell.CELL_TYPE_STRING); } + + styles.setStyle(cell, value.getClass()); + + } + + protected void buildNumericCell(Number value) { + cell.setCellType(Cell.CELL_TYPE_NUMERIC); + + if (value instanceof BigDecimal) { + cell.setCellValue(((BigDecimal) value).doubleValue()); + } else { + cell.setCellValue(value.doubleValue()); + } + + styles.setStyle(cell, value.getClass()); + } @Override @@ -104,5 +162,11 @@ protected void resetContent() { row = null; cell = null; boldStyle = null; + + createCellStyleDataFormats(); + } + + private void createCellStyleDataFormats() { + styles = new WorkbookStyles(workbook, dataFormats); } } diff --git a/src/main/java/org/vaadin/haijian/Exporter.java b/src/main/java/org/vaadin/haijian/Exporter.java index 39626d4..c987130 100644 --- a/src/main/java/org/vaadin/haijian/Exporter.java +++ b/src/main/java/org/vaadin/haijian/Exporter.java @@ -6,13 +6,35 @@ public class Exporter { - private Exporter(){} + private Exporter() { + } + + public static InputStreamFactory exportAsExcel(Grid grid) { + return new ExcelFileBuilder<>(grid, null)::build; + } - public static InputStreamFactory exportAsExcel(Grid grid, Map, String> columnHeaders){ + public static InputStreamFactory exportAsExcel(Grid grid, Map, String> columnHeaders) { return new ExcelFileBuilder<>(grid, columnHeaders)::build; } - public static InputStreamFactory exportAsCSV(Grid grid, Map, String> columnHeaders){ + public static InputStreamFactory exportAsExcel(Grid grid, Map, String> columnHeaders, DataFormats dataFormats) { + return new ExcelFileBuilder<>(grid, columnHeaders, dataFormats)::build; + } + + public static InputStreamFactory exportAsExcel(Grid grid, Map, String> columnHeaders, CellValueTypeConverter converter) { + return new ExcelFileBuilder<>(grid, columnHeaders, converter)::build; + } + + public static InputStreamFactory exportAsExcel(Grid grid, Map, String> columnHeaders, DataFormats dataFormats, + CellValueTypeConverter converter) { + return new ExcelFileBuilder<>(grid, columnHeaders, dataFormats, converter)::build; + } + + public static InputStreamFactory exportAsCSV(Grid grid) { + return new CSVFileBuilder<>(grid, null)::build; + } + + public static InputStreamFactory exportAsCSV(Grid grid, Map, String> columnHeaders) { return new CSVFileBuilder<>(grid, columnHeaders)::build; } } diff --git a/src/main/java/org/vaadin/haijian/FileBuilder.java b/src/main/java/org/vaadin/haijian/FileBuilder.java index 51edf23..7397a7b 100644 --- a/src/main/java/org/vaadin/haijian/FileBuilder.java +++ b/src/main/java/org/vaadin/haijian/FileBuilder.java @@ -1,13 +1,5 @@ package org.vaadin.haijian; -import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.data.binder.BeanPropertySet; -import com.vaadin.flow.data.binder.PropertyDefinition; -import com.vaadin.flow.data.binder.PropertySet; -import com.vaadin.flow.data.provider.DataCommunicator; -import com.vaadin.flow.data.provider.Query; -import org.slf4j.LoggerFactory; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -20,19 +12,28 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.data.binder.BeanPropertySet; +import com.vaadin.flow.data.binder.PropertyDefinition; +import com.vaadin.flow.data.binder.PropertySet; +import com.vaadin.flow.data.provider.DataCommunicator; +import com.vaadin.flow.data.provider.Query; + public abstract class FileBuilder { private static final String TMP_FILE_NAME = "tmp"; File file; private Grid grid; - private Map, String> columnHeaders; + private Map, String> customColumnHeaders; private Collection columns; private PropertySet propertySet; @SuppressWarnings("unchecked") FileBuilder(Grid grid, Map, String> columnHeaders) { this.grid = grid; - this.columnHeaders = columnHeaders; + this.customColumnHeaders = columnHeaders; columns = grid.getColumns().stream().filter(this::isExportable).collect(Collectors.toList()); try { Field field = Grid.class.getDeclaredField("propertySet"); @@ -41,6 +42,9 @@ public abstract class FileBuilder { if (propertySetRaw != null) { propertySet = (PropertySet) propertySetRaw; } + if (propertySet == null && columnHeaders == null) { + throw new ExporterException("Create Grid with bean type or create Grid with adding columns and columns headers map."); + } } catch (Exception e) { throw new ExporterException("couldn't read propertyset information from grid", e); } @@ -83,29 +87,32 @@ protected void resetContent() { } private void buildHeaderRow() { - onNewRow(); - if (columnHeaders == null) { - columns.forEach(column -> { - Optional> propertyDefinition = propertySet.getProperty(column.getKey()); - if (propertyDefinition.isPresent()) { - onNewCell(); - buildColumnHeaderCell(propertyDefinition.get().getCaption()); - } else { - LoggerFactory.getLogger(this.getClass()).warn(String.format("Column key %s is a property which cannot be found", column.getKey())); - } - }); - } else { - columns.forEach(column -> { - String columnHeader = columnHeaders.get(column); - if (columnHeader != null) { - onNewCell(); - buildColumnHeaderCell(columnHeader); - } else { - LoggerFactory.getLogger(this.getClass()).warn(String.format("Column with key %s have not column header value defined in map", column.getKey())); - } - }); + onNewRow(); + if (customColumnHeaders == null) { + columns.forEach(column -> { + // when grid created with bean type + if (propertySet != null) { + Optional> propertyDefinition = propertySet.getProperty(column.getKey()); + if (propertyDefinition.isPresent()) { + onNewCell(); + buildColumnHeaderCell(propertyDefinition.get().getCaption()); + } else { + LoggerFactory.getLogger(this.getClass()).warn(String.format("Column key %s is a property which cannot be found", column.getKey())); + } + } + }); + } else { + columns.forEach(column -> { + String columnHeader = customColumnHeaders.get(column); + if (columnHeader != null) { + onNewCell(); + buildColumnHeaderCell(columnHeader); + } else { + LoggerFactory.getLogger(this.getClass()).warn(String.format("Column with key %s have not column header value defined in map", column.getKey())); + } + }); + } } - } void buildColumnHeaderCell(String header) { @@ -177,14 +184,12 @@ int getNumberOfColumns() { return columns.size(); } - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({ "unchecked", "rawtypes" }) private Stream getDataStream(Query newQuery) { Stream stream = grid.getDataProvider().fetch(newQuery); if (stream.isParallel()) { - LoggerFactory.getLogger(DataCommunicator.class) - .debug("Data provider {} has returned " - + "parallel stream on 'fetch' call", - grid.getDataProvider().getClass()); + LoggerFactory.getLogger(DataCommunicator.class).debug("Data provider {} has returned " + "parallel stream on 'fetch' call", + grid.getDataProvider().getClass()); stream = stream.collect(Collectors.toList()).stream(); assert !stream.isParallel(); } diff --git a/src/main/java/org/vaadin/haijian/TypeDataFormats.java b/src/main/java/org/vaadin/haijian/TypeDataFormats.java new file mode 100644 index 0000000..7460b77 --- /dev/null +++ b/src/main/java/org/vaadin/haijian/TypeDataFormats.java @@ -0,0 +1,71 @@ +package org.vaadin.haijian; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.poi.ss.usermodel.BuiltinFormats; + +/** + * + * @author Krunoslav Magazin + * Oct 5, 2019 + * + * Initialize default excel formats. Excel {@link BuiltinFormats} use locale to format value.

+ * + */ +public class TypeDataFormats implements DataFormats { + + private final Map, String> typeFormatsMap; + + public TypeDataFormats() { + this.typeFormatsMap = new HashMap, String>(); + typeFormatsMap.put(Integer.class, "0"); + typeFormatsMap.put(Long.class, "0"); + typeFormatsMap.put(Double.class, "0.00"); + typeFormatsMap.put(BigDecimal.class, "0.00"); + typeFormatsMap.put(LocalDateTime.class, "m/d/yy h:mm"); + typeFormatsMap.put(Date.class, "m/d/yy h:mm"); + typeFormatsMap.put(Calendar.class, "m/d/yy h:mm"); + typeFormatsMap.put(LocalDate.class, "m/d/yy"); + } + + @Override + public void numberFormat(Class clazz, String format) { + setTypeFormat(clazz, format); + } + + @Override + public void localDateTimeFormat( String format) { + setTypeFormat(LocalDateTime.class, format); + } + + @Override + public void localDateFormat( String format) { + setTypeFormat(LocalDate.class, format); + } + + @Override + public void calendarFormat( String format) { + setTypeFormat(Calendar.class, format); + } + + @Override + public void dateFormat( String format) { + setTypeFormat(Date.class, format); + } + + private void setTypeFormat(Class clazz, String format) { + typeFormatsMap.put(clazz, format); + } + + @Override + public Map, String> getTypeFormatsMap() { + return typeFormatsMap; + } + +} diff --git a/src/main/java/org/vaadin/haijian/WorkbookStyles.java b/src/main/java/org/vaadin/haijian/WorkbookStyles.java new file mode 100644 index 0000000..9b26394 --- /dev/null +++ b/src/main/java/org/vaadin/haijian/WorkbookStyles.java @@ -0,0 +1,42 @@ +package org.vaadin.haijian; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * + * @author Krunoslav Magazin + * Oct 5, 2019 + */ +public class WorkbookStyles { + + private final Map, CellStyle> stylesWithDataFormats; + private CreationHelper createHelper; + private Workbook workbook; + + public WorkbookStyles(Workbook workbook, DataFormats dataFormats) { + this.workbook = workbook; + this.stylesWithDataFormats = new HashMap, CellStyle>(); + + createHelper = workbook.getCreationHelper(); + + dataFormats.getTypeFormatsMap().forEach(this::createCellStyeWithDataFormat); + } + + private void createCellStyeWithDataFormat(Class clazz, String format) { + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setDataFormat(createHelper.createDataFormat().getFormat(format)); + stylesWithDataFormats.put(clazz, cellStyle); + } + + public void setStyle(Cell cell, Class clazz) { + if (stylesWithDataFormats.containsKey(clazz)) { + cell.setCellStyle(stylesWithDataFormats.get(clazz)); + } + } +} diff --git a/src/test/java/org/vaadin/haijian/DemoView.java b/src/test/java/org/vaadin/haijian/DemoView.java index 7bffc37..bea1070 100755 --- a/src/test/java/org/vaadin/haijian/DemoView.java +++ b/src/test/java/org/vaadin/haijian/DemoView.java @@ -12,6 +12,19 @@ public class DemoView extends HorizontalLayout { public DemoView() { setWidth("100%"); + + VerticalLayout withBeanTypeGrid = new VerticalLayout(); + expand(withBeanTypeGrid); + withBeanTypeGrid.add(new Span("Grid With Bean type default property names")); + Component beanTypeGrid = GridDemoViewCreator.createGridWithBeanTypeDemo(); + withBeanTypeGrid.add(beanTypeGrid); + + VerticalLayout withBeanTypeGridCustomHeaders = new VerticalLayout(); + expand(withBeanTypeGridCustomHeaders); + withBeanTypeGridCustomHeaders.add(new Span("Grid With Bean type custom property names")); + Component beanTypeGridCustomHeaders = GridDemoViewCreator.createGridWithBeanTypeCustomHeadersDemo(); + withBeanTypeGridCustomHeaders.add(beanTypeGridCustomHeaders); + VerticalLayout withNormalGrid = new VerticalLayout(); expand(withNormalGrid); withNormalGrid.add(new Span("Grid With List data provider")); @@ -23,6 +36,7 @@ public DemoView() { Component lazyGrid = GridDemoViewCreator.createGridWithLazyLoadingDemo(); withLazyLoadingGrid.add(new Span("Grid with Lazy loading data provider")); withLazyLoadingGrid.add(lazyGrid); - add(withNormalGrid, withLazyLoadingGrid); + + add(withBeanTypeGrid, withBeanTypeGridCustomHeaders, withNormalGrid, withLazyLoadingGrid); } } diff --git a/src/test/java/org/vaadin/haijian/helpers/GridDemoViewCreator.java b/src/test/java/org/vaadin/haijian/helpers/GridDemoViewCreator.java index 0f1ea11..a3b7091 100644 --- a/src/test/java/org/vaadin/haijian/helpers/GridDemoViewCreator.java +++ b/src/test/java/org/vaadin/haijian/helpers/GridDemoViewCreator.java @@ -3,6 +3,7 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.Grid.Column; import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; @@ -17,20 +18,68 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class GridDemoViewCreator { final static PersonService service = new PersonService(); + public static Component createGridWithBeanTypeDemo() { + return createGridBeanTypeDemo(false); + } + + public static Component createGridWithBeanTypeCustomHeadersDemo() { + return createGridBeanTypeDemo(true); + } - public static Component createGridWithListDataProviderDemo(){ + public static Component createGridWithListDataProviderDemo() { return createGridDemo(false); } - public static Component createGridWithLazyLoadingDemo(){ + public static Component createGridWithLazyLoadingDemo() { return createGridDemo(true); } + private static Component createGridBeanTypeDemo(boolean isCustomHeaders) { + VerticalLayout result = new VerticalLayout(); + final List groups = new ArrayList<>(); + groups.add(new AgeGroup(0, 18)); + groups.add(new AgeGroup(19, 26)); + groups.add(new AgeGroup(27, 40)); + groups.add(new AgeGroup(41, 100)); + + final ComboBox filter = new ComboBox<>("Filter", groups); + result.add(filter); + + final Grid grid = new Grid<>(Person.class); + grid.setPageSize(10); + result.setHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH, grid); + + result.add(grid); + + setupListDataProviderForGrid(grid, filter); + + // custom columns names + Map, String> customHeaderCaptions = new HashMap, String>(); + customHeaderCaptions.put(grid.getColumnByKey("name"), "Full name"); + customHeaderCaptions.put(grid.getColumnByKey("email"), "E-mail"); + customHeaderCaptions.put(grid.getColumnByKey("age"), "Old"); + customHeaderCaptions.put(grid.getColumnByKey("birthday"), "Day of birth"); + + Anchor downloadAsExcel = null; + + if (isCustomHeaders) { + downloadAsExcel = new Anchor(new StreamResource("my-excel.xls", Exporter.exportAsExcel(grid, customHeaderCaptions)), "Download As Excel"); + } else { + downloadAsExcel = new Anchor(new StreamResource("my-excel.xls", Exporter.exportAsExcel(grid)), "Download As Excel"); + } + Anchor downloadAsCSV = new Anchor(new StreamResource("my-csv.csv", Exporter.exportAsCSV(grid)), "Download As CSV"); + result.add(new HorizontalLayout(downloadAsExcel, downloadAsCSV)); + + return result; + } + private static Component createGridDemo(boolean lazyLoading) { VerticalLayout result = new VerticalLayout(); final List groups = new ArrayList<>(); @@ -52,14 +101,21 @@ private static Component createGridDemo(boolean lazyLoading) { result.add(grid); - if(lazyLoading){ + if (lazyLoading) { setupLazyLoadingDataProviderForGrid(grid, filter); - }else{ + } else { setupListDataProviderForGrid(grid, filter); } - Anchor downloadAsExcel = new Anchor(new StreamResource("my-excel.xls", Exporter.exportAsExcel(grid)), "Download As Excel"); - Anchor downloadAsCSV = new Anchor(new StreamResource("my-csv.csv", Exporter.exportAsCSV(grid)), "Download As CSV"); + // custom columns names + Map, String> customHeaderCaptions = new HashMap, String>(); + customHeaderCaptions.put(grid.getColumnByKey("name"), "Full name"); + customHeaderCaptions.put(grid.getColumnByKey("email"), "E-mail"); + customHeaderCaptions.put(grid.getColumnByKey("age"), "Old"); + customHeaderCaptions.put(grid.getColumnByKey("birthday"), "Day of birth"); + + Anchor downloadAsExcel = new Anchor(new StreamResource("my-excel.xls", Exporter.exportAsExcel(grid, customHeaderCaptions)), "Download As Excel"); + Anchor downloadAsCSV = new Anchor(new StreamResource("my-csv.csv", Exporter.exportAsCSV(grid, customHeaderCaptions)), "Download As CSV"); result.add(new HorizontalLayout(downloadAsExcel, downloadAsCSV)); return result;