diff --git a/pom.xml b/pom.xml index ca22c25..4473cf0 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,12 @@ 4.12 test + + org.projectlombok + lombok + 1.18.10 + test + diff --git a/src/main/java/com/poiji/annotation/ExcelCellName.java b/src/main/java/com/poiji/annotation/ExcelCellName.java index 9f94e46..3d98424 100644 --- a/src/main/java/com/poiji/annotation/ExcelCellName.java +++ b/src/main/java/com/poiji/annotation/ExcelCellName.java @@ -14,10 +14,19 @@ @Documented public @interface ExcelCellName { + int ABSENT_ORDER = -1; + /** * Specifies the column name where the corresponding value is mapped from the excel data * * @return column name */ String value(); + + /** + * Specifies the column order in saved file + * + * @return column order + */ + int order() default ABSENT_ORDER; } diff --git a/src/main/java/com/poiji/bind/Poiji.java b/src/main/java/com/poiji/bind/Poiji.java index a614d57..098b9ed 100644 --- a/src/main/java/com/poiji/bind/Poiji.java +++ b/src/main/java/com/poiji/bind/Poiji.java @@ -7,8 +7,8 @@ import com.poiji.exception.PoijiException; import com.poiji.option.PoijiOptions; import com.poiji.option.PoijiOptions.PoijiOptionsBuilder; +import com.poiji.save.FileSaverFactory; import com.poiji.util.Files; - import java.io.File; import java.io.InputStream; import java.util.ArrayList; @@ -207,17 +207,25 @@ public static synchronized List fromExcel(final InputStream inputStream, * language access control and the underlying field is either inaccessible or final. * @see Poiji#fromExcel(File, Class) */ - public static synchronized void fromExcel(final InputStream inputStream, - final PoijiExcelType excelType, - final Class type, - final PoijiOptions options, - final Consumer consumer) { + public static synchronized void fromExcel(final InputStream inputStream, final PoijiExcelType excelType, + final Class type, final PoijiOptions options, final Consumer consumer + ) { Objects.requireNonNull(excelType); final Unmarshaller unmarshaller = deserializer(inputStream, excelType, options); unmarshaller.unmarshal(type, consumer); } + public static void toExcel(final File file, final Class clazz, final List data) { + toExcel(file, clazz, data, PoijiOptionsBuilder.settings().build()); + } + + public static void toExcel( + final File file, final Class clazz, final List data, final PoijiOptions options + ) { + new FileSaverFactory<>(clazz, options).toFile(file).save(data); + } + private static Unmarshaller deserializer(final File file, final PoijiOptions options) { final PoijiFile poijiFile = new PoijiFile<>(file); diff --git a/src/main/java/com/poiji/bind/mapping/SheetNameExtractor.java b/src/main/java/com/poiji/bind/mapping/SheetNameExtractor.java index b63741c..996d950 100644 --- a/src/main/java/com/poiji/bind/mapping/SheetNameExtractor.java +++ b/src/main/java/com/poiji/bind/mapping/SheetNameExtractor.java @@ -2,19 +2,19 @@ import com.poiji.annotation.ExcelSheet; import com.poiji.option.PoijiOptions; - import java.util.Optional; /** * Utility class to extract the sheet name. */ -class SheetNameExtractor { +public class SheetNameExtractor { /** * Extracts the sheet name from either the annotated value {@link ExcelSheet} from the model class or from the sheet name set * in the Poiji Options. Poiji first looks at {@link ExcelSheet} then {@link PoijiOptions}. - * @param type The class instance of the object model. + * + * @param type The class instance of the object model. * @param options The Poiji options. - * @param The type of the object model. + * @param The type of the object model. * @return an Optional sheet name */ public static Optional getSheetName(Class type, PoijiOptions options) { diff --git a/src/main/java/com/poiji/config/DefaultCasting.java b/src/main/java/com/poiji/config/DefaultCasting.java index 3a7a618..1d3bf35 100644 --- a/src/main/java/com/poiji/config/DefaultCasting.java +++ b/src/main/java/com/poiji/config/DefaultCasting.java @@ -1,5 +1,7 @@ package com.poiji.config; +import com.poiji.option.PoijiOptions; +import com.poiji.parser.Parsers; import java.math.BigDecimal; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -13,9 +15,6 @@ import java.util.Date; import java.util.List; -import com.poiji.option.PoijiOptions; -import com.poiji.parser.Parsers; - /** * Created by hakan on 22/01/2017. */ @@ -222,9 +221,24 @@ public Object castValue(Class fieldType, String rawValue, int row, int col, P } else if (fieldType == Float.class) { o = floatValue(value, sheetName, row, col, options); - } else if (fieldType == boolean.class || fieldType == Boolean.class) { + } else if (fieldType == boolean.class) { o = Boolean.valueOf(value); + } else if (fieldType == Boolean.class) { + o = value.isEmpty() ? options.preferNullOverDefault() ? null : false : Boolean.valueOf(value); + + } else if (fieldType == byte.class) { + o = Byte.valueOf(value); + + } else if (fieldType == Byte.class) { + o = value.isEmpty() ? options.preferNullOverDefault() ? null : (byte) 0 : Byte.valueOf(value); + + } else if (fieldType == short.class) { + o = Short.valueOf(value); + + } else if (fieldType == Short.class) { + o = value.isEmpty() ? options.preferNullOverDefault() ? null : (short) 0 : Short.valueOf(value); + } else if (fieldType == Date.class) { o = dateValue(value, sheetName, row, col, options); diff --git a/src/main/java/com/poiji/option/PoijiOptions.java b/src/main/java/com/poiji/option/PoijiOptions.java index 6734f10..cc3f3b6 100644 --- a/src/main/java/com/poiji/option/PoijiOptions.java +++ b/src/main/java/com/poiji/option/PoijiOptions.java @@ -4,13 +4,14 @@ import com.poiji.config.Casting; import com.poiji.config.DefaultCasting; import com.poiji.exception.PoijiException; - +import com.poiji.save.CellCasting; import java.time.format.DateTimeFormatter; import java.util.Objects; import static com.poiji.util.PoijiConstants.DEFAULT_DATE_FORMATTER; import static com.poiji.util.PoijiConstants.DEFAULT_DATE_PATTERN; import static com.poiji.util.PoijiConstants.DEFAULT_DATE_TIME_FORMATTER; +import static com.poiji.util.PoijiConstants.DEFAULT_DATE_TIME_PATTERN; /** * Created by hakan on 17/01/2017. @@ -24,6 +25,8 @@ public final class PoijiOptions { private String dateRegex; private String dateTimeRegex; private String datePattern; + private String localDatePattern; + private String localDateTimePattern; private boolean dateLenient; private boolean trimCellValue; private boolean ignoreHiddenSheets; @@ -31,6 +34,7 @@ public final class PoijiOptions { private DateTimeFormatter dateFormatter; private DateTimeFormatter dateTimeFormatter; private Casting casting; + private CellCasting cellCasting; private int headerStart; private String sheetName; private boolean caseInsensitive; @@ -58,6 +62,15 @@ private PoijiOptions setDatePattern(String datePattern) { return this; } + public String getLocalDatePattern() { + return localDatePattern; + } + + private PoijiOptions setLocalDatePattern(final String localDatePattern) { + this.localDatePattern = localDatePattern; + return this; + } + private PoijiOptions setDateFormatter(DateTimeFormatter dateFormatter) { this.dateFormatter = dateFormatter; return this; @@ -86,6 +99,15 @@ public String datePattern() { return datePattern; } + public String getLocalDateTimePattern() { + return localDateTimePattern; + } + + private PoijiOptions setLocalDateTimePattern(final String localDateTimePattern) { + this.localDateTimePattern = localDateTimePattern; + return this; + } + public DateTimeFormatter dateFormatter() { return dateFormatter; } @@ -134,6 +156,15 @@ public PoijiOptions setCasting(Casting casting) { return this; } + public CellCasting getCellCasting() { + return cellCasting; + } + + private PoijiOptions setCellCasting(final CellCasting cellCasting) { + this.cellCasting = cellCasting; + return this; + } + private PoijiOptions setSheetIndex(int sheetIndex) { this.sheetIndex = sheetIndex; return this; @@ -192,7 +223,7 @@ public boolean getCaseInsensitive() { return caseInsensitive; } - public PoijiOptions setCaseInsensitive(final boolean caseInsensitive) { + private PoijiOptions setCaseInsensitive(final boolean caseInsensitive) { this.caseInsensitive = caseInsensitive; return this; } @@ -209,8 +240,11 @@ public static class PoijiOptionsBuilder { private boolean preferNullOverDefault; private String datePattern = DEFAULT_DATE_PATTERN; private DateTimeFormatter dateFormatter = DEFAULT_DATE_FORMATTER; + private String localDatePattern = DEFAULT_DATE_PATTERN; + private String localDateTimePattern = DEFAULT_DATE_TIME_PATTERN; private DateTimeFormatter dateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER; private Casting casting = new DefaultCasting(); + private CellCasting cellCasting = new CellCasting(); private int headerStart = 0; private int skip = 0; private int limit = 0; @@ -269,7 +303,7 @@ public PoijiOptionsBuilder dateTimeFormatter(DateTimeFormatter dateTimeFormatter * set date pattern, default date format is "dd/M/yyyy" for * java.util.Date * - * @param datePattern date time formatter + * @param datePattern date pattern * @return this */ public PoijiOptionsBuilder datePattern(String datePattern) { @@ -277,6 +311,30 @@ public PoijiOptionsBuilder datePattern(String datePattern) { return this; } + /** + * set date time pattern, default date time format is "dd/M/yyyy HH:mm:ss" for + * writing java.time.LocalDate into excel + * + * @param localDatePattern date time pattern + * @return this + */ + public PoijiOptionsBuilder localDatePattern(String localDatePattern) { + this.localDatePattern = localDatePattern; + return this; + } + + /** + * set date time pattern, default date time format is "dd/M/yyyy HH:mm:ss" for + * writing java.time.LocalDateTime into excel + * + * @param localDateTimePattern date time pattern + * @return this + */ + public PoijiOptionsBuilder localDateTimePattern(String localDateTimePattern) { + this.localDateTimePattern = localDateTimePattern; + return this; + } + /** * set whether or not to use null instead of default values for Integer, * Double, Float, Long, String and java.util.Date types. @@ -291,23 +349,26 @@ public PoijiOptionsBuilder preferNullOverDefault(boolean preferNullOverDefault) public PoijiOptions build() { return new PoijiOptions() - .setSkip(skip + headerStart + 1) - .setPassword(password) - .setPreferNullOverDefault(preferNullOverDefault) - .setDatePattern(datePattern) - .setDateFormatter(dateFormatter) - .setDateTimeFormatter(dateTimeFormatter) - .setSheetIndex(sheetIndex) - .setSheetName(sheetName) - .setIgnoreHiddenSheets(ignoreHiddenSheets) - .setTrimCellValue(trimCellValue) - .setDateRegex(dateRegex) - .setDateTimeRegex(dateTimeRegex) - .setDateLenient(dateLenient) - .setHeaderStart(headerStart) - .setCasting(casting) - .setLimit(limit) - .setCaseInsensitive(caseInsensitive); + .setSkip(skip + headerStart + 1) + .setPassword(password) + .setPreferNullOverDefault(preferNullOverDefault) + .setDatePattern(datePattern) + .setLocalDatePattern(localDatePattern) + .setLocalDateTimePattern(localDateTimePattern) + .setDateFormatter(dateFormatter) + .setDateTimeFormatter(dateTimeFormatter) + .setSheetIndex(sheetIndex) + .setSheetName(sheetName) + .setIgnoreHiddenSheets(ignoreHiddenSheets) + .setTrimCellValue(trimCellValue) + .setDateRegex(dateRegex) + .setDateTimeRegex(dateTimeRegex) + .setDateLenient(dateLenient) + .setHeaderStart(headerStart) + .setCasting(casting) + .setCellCasting(cellCasting) + .setLimit(limit) + .setCaseInsensitive(caseInsensitive); } /** @@ -448,6 +509,19 @@ public PoijiOptionsBuilder withCasting(Casting casting) { return this; } + /** + * Use a modified cell casting implementation + * + * @param cellCasting modified cell casting implementation + * @return this + */ + public PoijiOptionsBuilder withCellCasting(CellCasting cellCasting) { + Objects.requireNonNull(cellCasting); + + this.cellCasting = cellCasting; + return this; + } + /** * This is to set the row which the unmarshall will * use to start reading header titles, incase the diff --git a/src/main/java/com/poiji/save/CellCasting.java b/src/main/java/com/poiji/save/CellCasting.java new file mode 100644 index 0000000..5465f40 --- /dev/null +++ b/src/main/java/com/poiji/save/CellCasting.java @@ -0,0 +1,134 @@ +package com.poiji.save; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import org.apache.poi.hpsf.CustomProperties; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLProperties; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; + +import static com.poiji.util.PoijiConstants.DATE_CELL_STYLE_INDEX_PROPERTY_NAME; +import static com.poiji.util.PoijiConstants.LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME; +import static com.poiji.util.PoijiConstants.LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME; + +public final class CellCasting { + + private final Map, BiConsumer> consumers; + + public CellCasting() { + consumers = new ConcurrentHashMap<>(); + consumers.put(Boolean.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((boolean) fieldValue); + } + }); + consumers.put(boolean.class, (Cell cell, Object fieldValue) -> cell.setCellValue((boolean) fieldValue)); + consumers.put(Double.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((double) fieldValue); + } + }); + consumers.put(double.class, (Cell cell, Object fieldValue) -> cell.setCellValue((double) fieldValue)); + consumers.put(Long.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((long) fieldValue); + } + }); + consumers.put(long.class, (Cell cell, Object fieldValue) -> cell.setCellValue((long) fieldValue)); + consumers.put(Integer.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((int) fieldValue); + } + }); + consumers.put(int.class, (Cell cell, Object fieldValue) -> cell.setCellValue((int) fieldValue)); + consumers.put(Byte.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((byte) fieldValue); + } + }); + consumers.put(byte.class, (Cell cell, Object fieldValue) -> cell.setCellValue((byte) fieldValue)); + consumers.put(Short.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((short) fieldValue); + } + }); + consumers.put(short.class, (Cell cell, Object fieldValue) -> cell.setCellValue((short) fieldValue)); + consumers.put(Float.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((float) fieldValue); + } + }); + consumers.put(float.class, (Cell cell, Object fieldValue) -> cell.setCellValue((float) fieldValue)); + consumers.put(java.util.Date.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((java.util.Date) fieldValue); + setStyleInCell(cell, DATE_CELL_STYLE_INDEX_PROPERTY_NAME); + } + }); + consumers.put(java.sql.Date.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((java.util.Date) fieldValue); + setStyleInCell(cell, DATE_CELL_STYLE_INDEX_PROPERTY_NAME); + } + }); + consumers.put(Calendar.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue((Calendar) fieldValue); + setStyleInCell(cell, LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME); + } + }); + consumers.put(LocalDateTime.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue(java.sql.Timestamp.valueOf((LocalDateTime) fieldValue)); + setStyleInCell(cell, LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME); + } + }); + consumers.put(LocalDate.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue(java.sql.Date.valueOf((LocalDate) fieldValue)); + setStyleInCell(cell, LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME); + } + }); + consumers.put(Object.class, (Cell cell, Object fieldValue) -> { + if (fieldValue != null) { + cell.setCellValue(fieldValue.toString()); + } + }); + } + + private void setStyleInCell(final Cell cell, final String propertyName) { + final Workbook workbook = cell.getSheet().getWorkbook(); + if (workbook instanceof SXSSFWorkbook) { + final POIXMLProperties.CustomProperties properties = ((SXSSFWorkbook) workbook) + .getXSSFWorkbook() + .getProperties() + .getCustomProperties(); + final int dateTimeCellStyleIndex = properties.getProperty(propertyName).getI4(); + cell.setCellStyle(workbook.getCellStyleAt(dateTimeCellStyleIndex)); + } else if (workbook instanceof HSSFWorkbook) { + final CustomProperties properties = ((HSSFWorkbook) workbook) + .getDocumentSummaryInformation() + .getCustomProperties(); + final Short dateTimeCellStyleIndex = (Short) properties.get(propertyName); + cell.setCellStyle(workbook.getCellStyleAt(dateTimeCellStyleIndex)); + } else { + throw new UnsupportedOperationException(workbook.getClass() + " is not supported"); + } + } + + public BiConsumer forType(final Class type) { + return consumers.getOrDefault(type, consumers.get(Object.class)); + } + + public CellCasting addCellCasting(final Class type, final BiConsumer castingRule) { + consumers.put(type, castingRule); + return this; + } + +} diff --git a/src/main/java/com/poiji/save/FileSaver.java b/src/main/java/com/poiji/save/FileSaver.java new file mode 100644 index 0000000..1aa6b95 --- /dev/null +++ b/src/main/java/com/poiji/save/FileSaver.java @@ -0,0 +1,8 @@ +package com.poiji.save; + +import java.util.List; + +public interface FileSaver { + + void save(List data); +} diff --git a/src/main/java/com/poiji/save/FileSaverFactory.java b/src/main/java/com/poiji/save/FileSaverFactory.java new file mode 100644 index 0000000..02da79e --- /dev/null +++ b/src/main/java/com/poiji/save/FileSaverFactory.java @@ -0,0 +1,31 @@ +package com.poiji.save; + +import com.poiji.exception.InvalidExcelFileExtension; +import com.poiji.option.PoijiOptions; +import java.io.File; + +import static com.poiji.util.PoijiConstants.XLSX_EXTENSION; +import static com.poiji.util.PoijiConstants.XLS_EXTENSION; + +public final class FileSaverFactory { + + private final Class entity; + private final PoijiOptions options; + + public FileSaverFactory(final Class entity, final PoijiOptions options) { + this.entity = entity; + this.options = options; + } + + public FileSaver toFile(final File file) { + final MappedFields mappedFields = new MappedFields(entity, options).parseEntity(); + if (file.toString().endsWith(XLSX_EXTENSION)) { + return new XlsxFileSaver(file, mappedFields, options); + } else if (file.toString().endsWith(XLS_EXTENSION)) { + return new XlsFileSaver(file, mappedFields, options); + } else { + throw new InvalidExcelFileExtension(file.getName() + " has unsupported extension. 'xlsx' and 'xls' are supported only."); + } + } + +} diff --git a/src/main/java/com/poiji/save/FileWorkbookSaver.java b/src/main/java/com/poiji/save/FileWorkbookSaver.java new file mode 100644 index 0000000..766a5e6 --- /dev/null +++ b/src/main/java/com/poiji/save/FileWorkbookSaver.java @@ -0,0 +1,48 @@ +package com.poiji.save; + +import com.poiji.exception.PoijiException; +import com.poiji.option.PoijiOptions; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.poi.ss.usermodel.Workbook; + +public class FileWorkbookSaver extends WorkbookSaver { + + private final File file; + + public FileWorkbookSaver( + final File file, final MappedFields mappedFields, final PoijiOptions options + ) { + super(mappedFields, options); + this.file = file; + } + + protected void save(final List data, final Workbook workbook) { + createFile(); + writeInFile(data, workbook); + } + + private void writeInFile(final List data, final Workbook workbook) { + try (final FileOutputStream outputStream = new FileOutputStream(file)) { + save(data, workbook, outputStream); + } catch (IOException e) { + throw new PoijiException(e.getMessage(), e); + } + } + + private void createFile() { + final Path absolutePath = file.toPath().toAbsolutePath(); + try { + Files.createDirectories(absolutePath.getParent()); + Files.deleteIfExists(absolutePath); + Files.createFile(absolutePath); + } catch (IOException e) { + throw new PoijiException(e.getMessage(), e); + } + } + +} diff --git a/src/main/java/com/poiji/save/MappedFields.java b/src/main/java/com/poiji/save/MappedFields.java new file mode 100644 index 0000000..c5ccea7 --- /dev/null +++ b/src/main/java/com/poiji/save/MappedFields.java @@ -0,0 +1,113 @@ +package com.poiji.save; + +import com.poiji.annotation.ExcelCell; +import com.poiji.annotation.ExcelCellName; +import com.poiji.annotation.ExcelUnknownCells; +import com.poiji.bind.mapping.SheetNameExtractor; +import com.poiji.exception.PoijiException; +import com.poiji.option.PoijiOptions; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static com.poiji.annotation.ExcelCellName.ABSENT_ORDER; + +public final class MappedFields { + + private final Class entity; + private String sheetName; + private final Map orders; + private final Map names; + private final List unknownCells; + private final Map unknownOrders; + private final PoijiOptions options; + + public MappedFields(final Class entity, final PoijiOptions options) { + this.entity = entity; + orders = new HashMap<>(); + names = new HashMap<>(); + unknownCells = new ArrayList<>(); + this.unknownOrders = new LinkedHashMap<>(); + this.options = options; + } + + public MappedFields parseEntity() { + SheetNameExtractor.getSheetName(entity, options).ifPresent(sheetName -> this.sheetName = sheetName); + final Field[] declaredFields = entity.getDeclaredFields(); + final List unordered = new ArrayList<>(); + for (Field field : declaredFields) { + if (field.getAnnotation(ExcelCell.class) != null) { + final Integer excelOrder = field.getAnnotation(ExcelCell.class).value(); + final String name = field.getName(); + orders.put(field, excelOrder); + names.put(field, name); + field.setAccessible(true); + } else if (field.getAnnotation(ExcelUnknownCells.class) != null) { + unknownCells.add(field); + field.setAccessible(true); + } else { + final ExcelCellName annotation = field.getAnnotation(ExcelCellName.class); + if (annotation != null) { + final String excelName = annotation.value(); + final int order = annotation.order(); + if (order == ABSENT_ORDER) { + unordered.add(field); + } else { + orders.put(field, order); + } + names.put(field, excelName); + field.setAccessible(true); + } + } + } + orders.putAll(new OrderedValues(orders.values()).toOrder(unordered)); + return this; + } + + public void addUnknownColumnNamesFromData(final List data) { + unknownOrders.putAll(extractUnknownColumnNamesFromData(data)); + } + + private Map extractUnknownColumnNamesFromData(final List data) { + final Collection unknownNames = new HashSet<>(); + for (final Field unknownCell : unknownCells) { + for (T instance : data) { + try { + final Map unknownCells = (Map) unknownCell.get(instance); + if (unknownCells != null) { + unknownNames.addAll(unknownCells.keySet()); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new PoijiException(e.getMessage(), e); + } + + } + } + return new OrderedValues(orders.values()).toOrder(unknownNames); + } + + public String getSheetName() { + return sheetName; + } + + public Map getOrders() { + return orders; + } + + public Map getNames() { + return names; + } + + public List getUnknownCells() { + return unknownCells; + } + + public Map getUnknownOrders() { + return unknownOrders; + } +} diff --git a/src/main/java/com/poiji/save/OrderedValues.java b/src/main/java/com/poiji/save/OrderedValues.java new file mode 100644 index 0000000..c2ab7a0 --- /dev/null +++ b/src/main/java/com/poiji/save/OrderedValues.java @@ -0,0 +1,26 @@ +package com.poiji.save; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public final class OrderedValues { + + private final Collection busyOrders; + + public OrderedValues(final Collection busyOrders) { + this.busyOrders = busyOrders; + } + + public Map toOrder(final Collection unorderedValues) { + final Map ordered = new HashMap<>(); + int order = 0; + for (final T name : unorderedValues) { + while (busyOrders.contains(order)) { + order++; + } + ordered.put(name, order++); + } + return ordered; + } +} diff --git a/src/main/java/com/poiji/save/WorkbookSaver.java b/src/main/java/com/poiji/save/WorkbookSaver.java new file mode 100644 index 0000000..f511134 --- /dev/null +++ b/src/main/java/com/poiji/save/WorkbookSaver.java @@ -0,0 +1,83 @@ +package com.poiji.save; + +import com.poiji.exception.PoijiException; +import com.poiji.option.PoijiOptions; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; + +public class WorkbookSaver { + + protected final PoijiOptions options; + private final MappedFields mappedFields; + + public WorkbookSaver( + final MappedFields mappedFields, final PoijiOptions options + ) { + this.mappedFields = mappedFields; + this.options = options; + } + + protected void save(final List data, final Workbook workbook, final OutputStream outputStream) { + try { + mappedFields.addUnknownColumnNamesFromData(data); + final Sheet sheet = mappedFields.getSheetName() == null + ? workbook.createSheet() + : workbook.createSheet(mappedFields.getSheetName()); + createColumnNames(sheet); + + int rowIndex = 1; + for (final T instance : data) { + final Row row = sheet.createRow(rowIndex++); + setValuesFromKnownFields(row, instance); + setValuesFromUnknownCellsMap(row, instance); + } + workbook.write(outputStream); + outputStream.flush(); + outputStream.close(); + workbook.close(); + } catch (IllegalAccessException | IOException e) { + throw new PoijiException(e.getMessage(), e); + } + } + + private void setValuesFromKnownFields(final Row row, final T instance) throws IllegalAccessException { + final CellCasting cellCasting = options.getCellCasting(); + for (Map.Entry orders : mappedFields.getOrders().entrySet()) { + final Cell cell = row.createCell(orders.getValue()); + final Field field = orders.getKey(); + cellCasting.forType(field.getType()).accept(cell, field.get(instance)); + } + } + + private void setValuesFromUnknownCellsMap(final Row row, final T instance) throws IllegalAccessException { + final Map unknownOrders = mappedFields.getUnknownOrders(); + for (final Field unknownCell : mappedFields.getUnknownCells()) { + final Map unknownValues = (Map) unknownCell.get(instance); + if (unknownValues != null) { + unknownValues.forEach((name, value) -> { + final Cell cell = row.createCell(unknownOrders.get(name)); + cell.setCellValue(value); + }); + } + } + } + + private void createColumnNames(final Sheet sheet) { + final Row row = sheet.createRow(0); + for (final Map.Entry entry : mappedFields.getOrders().entrySet()) { + final Cell cell = row.createCell(entry.getValue()); + cell.setCellValue(mappedFields.getNames().get(entry.getKey())); + } + for (final Map.Entry entry : mappedFields.getUnknownOrders().entrySet()) { + final Cell cell = row.createCell(entry.getValue()); + cell.setCellValue(entry.getKey()); + } + } +} diff --git a/src/main/java/com/poiji/save/XlsFileSaver.java b/src/main/java/com/poiji/save/XlsFileSaver.java new file mode 100644 index 0000000..3c57c5b --- /dev/null +++ b/src/main/java/com/poiji/save/XlsFileSaver.java @@ -0,0 +1,52 @@ +package com.poiji.save; + +import com.poiji.exception.PoijiException; +import com.poiji.option.PoijiOptions; +import java.io.File; +import java.io.IOException; +import java.util.List; +import org.apache.poi.hpsf.CustomProperties; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataFormat; + +import static com.poiji.util.PoijiConstants.DATE_CELL_STYLE_INDEX_PROPERTY_NAME; +import static com.poiji.util.PoijiConstants.LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME; +import static com.poiji.util.PoijiConstants.LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME; + +public final class XlsFileSaver extends FileWorkbookSaver implements FileSaver { + + public XlsFileSaver( + final File file, final MappedFields mappedFields, final PoijiOptions options + ) { + super(file, mappedFields, options); + } + + @Override + public void save(final List data) { + try (final HSSFWorkbook workbook = new HSSFWorkbook()) { + addStyles(workbook); + save(data, workbook); + } catch (IOException e) { + throw new PoijiException(e.getMessage(), e); + } + + } + + private void addStyles(final HSSFWorkbook workbook) { + final CellStyle dateCellStyle = workbook.createCellStyle(); + final DataFormat dataFormat = workbook.createDataFormat(); + dateCellStyle.setDataFormat(dataFormat.getFormat(options.datePattern())); + final CellStyle localDateCellStyle = workbook.createCellStyle(); + localDateCellStyle.setDataFormat(dataFormat.getFormat(options.getLocalDatePattern())); + final CellStyle localDateTimeCellStyle = workbook.createCellStyle(); + localDateTimeCellStyle.setDataFormat(dataFormat.getFormat(options.getLocalDateTimePattern())); + workbook.createInformationProperties(); + final CustomProperties customProperties = new CustomProperties(); + customProperties.put(DATE_CELL_STYLE_INDEX_PROPERTY_NAME, dateCellStyle.getIndex()); + customProperties.put(LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME, localDateCellStyle.getIndex()); + customProperties.put(LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME, localDateTimeCellStyle.getIndex()); + workbook.getDocumentSummaryInformation().setCustomProperties(customProperties); + } + +} diff --git a/src/main/java/com/poiji/save/XlsxFileSaver.java b/src/main/java/com/poiji/save/XlsxFileSaver.java new file mode 100644 index 0000000..743f358 --- /dev/null +++ b/src/main/java/com/poiji/save/XlsxFileSaver.java @@ -0,0 +1,61 @@ +package com.poiji.save; + +import com.poiji.exception.PoijiException; +import com.poiji.option.PoijiOptions; +import java.io.File; +import java.io.IOException; +import java.util.List; +import org.apache.poi.ooxml.POIXMLProperties; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; + +import static com.poiji.util.PoijiConstants.DATE_CELL_STYLE_INDEX_PROPERTY_NAME; +import static com.poiji.util.PoijiConstants.LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME; +import static com.poiji.util.PoijiConstants.LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME; + +public final class XlsxFileSaver extends FileWorkbookSaver implements FileSaver { + + public XlsxFileSaver( + final File file, final MappedFields mappedFields, final PoijiOptions options + ) { + super(file, mappedFields, options); + } + + @Override + public void save(final List data) { + try (final SXSSFWorkbook workbook = new SXSSFWorkbook()) { + workbook.setCompressTempFiles(true); + addStyles(workbook); + try { + save(data, workbook); + } finally { + if (!workbook.dispose()) { + System.out.println("Warning! SXSSFWorkbook wasn't disposed correctly. See " + System.getProperty( + "java.io.tmpdir") + File.separator + "poifiles"); + } + } + } catch (IOException e) { + throw new PoijiException(e.getMessage(), e); + } + + } + + private void addStyles(final SXSSFWorkbook workbook) { + final CellStyle dateCellStyle = workbook.createCellStyle(); + final DataFormat dataFormat = workbook.createDataFormat(); + dateCellStyle.setDataFormat(dataFormat.getFormat(options.datePattern())); + final CellStyle localDateCellStyle = workbook.createCellStyle(); + localDateCellStyle.setDataFormat(dataFormat.getFormat(options.getLocalDatePattern())); + final CellStyle localDateTimeCellStyle = workbook.createCellStyle(); + localDateTimeCellStyle.setDataFormat(dataFormat.getFormat(options.getLocalDateTimePattern())); + final POIXMLProperties.CustomProperties customProperties = workbook + .getXSSFWorkbook() + .getProperties() + .getCustomProperties(); + customProperties.addProperty(DATE_CELL_STYLE_INDEX_PROPERTY_NAME, dateCellStyle.getIndex()); + customProperties.addProperty(LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME, localDateCellStyle.getIndex()); + customProperties.addProperty(LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME, localDateTimeCellStyle.getIndex()); + } + +} diff --git a/src/main/java/com/poiji/util/PoijiConstants.java b/src/main/java/com/poiji/util/PoijiConstants.java index f147c47..2db5c37 100644 --- a/src/main/java/com/poiji/util/PoijiConstants.java +++ b/src/main/java/com/poiji/util/PoijiConstants.java @@ -8,10 +8,15 @@ public final class PoijiConstants { public static final String DEFAULT_DATE_PATTERN = "dd/M/yyyy"; - public static final DateTimeFormatter DEFAULT_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/M/yyyy"); - public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/M/yyyy HH:mm:ss"); + public static final String DEFAULT_DATE_TIME_PATTERN = "dd/M/yyyy HH:mm:ss"; + public static final DateTimeFormatter DEFAULT_DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN); + public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + DEFAULT_DATE_TIME_PATTERN); public static final String XLS_EXTENSION = ".xls"; public static final String XLSX_EXTENSION = ".xlsx"; + public static final String LOCAL_DATE_CELL_STYLE_INDEX_PROPERTY_NAME = "LocalDateCellStyleIndex"; + public static final String LOCAL_DATE_TIME_CELL_STYLE_INDEX_PROPERTY_NAME = "LocalDateTimeCellStyleIndex"; + public static final String DATE_CELL_STYLE_INDEX_PROPERTY_NAME = "DateCellStyleIndex"; private PoijiConstants() { } diff --git a/src/test/java/com/poiji/deserialize/ConcurrentTest.java b/src/test/java/com/poiji/deserialize/ConcurrentTest.java new file mode 100644 index 0000000..ed66091 --- /dev/null +++ b/src/test/java/com/poiji/deserialize/ConcurrentTest.java @@ -0,0 +1,101 @@ +package com.poiji.deserialize; + +import com.poiji.bind.Poiji; +import com.poiji.deserialize.model.ConcurrentEntity; +import com.poiji.option.PoijiOptions; +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.Ignore; +import org.junit.Test; + +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * For manual testing only. + */ +public class ConcurrentTest { + + /** + * 1000000 records, Intel® Core™ i3-4160, 24GB, SSD + *

+ * Synchronized read: + * Generated in 370 ms + * Written in 8761 ms + * Read in 39167 ms + *

+ * Synchronized write: + * Generated in 309 ms + * Written in 16225 ms + * Read in 24233 ms + */ + @Test + @Ignore("Test disabled to prevent huge xlsx files writing in CI") + public void writeThenRead() { + final long start = System.nanoTime(); + final int size = 1000000; + final List entities1 = generateEntities(size, "1"); + final List entities2 = generateEntities(size, "2"); + final List entities3 = generateEntities(size, "3"); + final List entities4 = generateEntities(size, "4"); + final Set> expected = new HashSet<>(asList(entities1, entities2, entities3, entities4)); + final String name1 = "src/test/resources/concurrent1.xlsx"; + final String name2 = "src/test/resources/concurrent2.xlsx"; + final String name3 = "src/test/resources/concurrent3.xlsx"; + final String name4 = "src/test/resources/concurrent4.xlsx"; + final List writeData = asList( + new WriteData(name1, entities1), + new WriteData(name2, entities2), + new WriteData(name3, entities3), + new WriteData(name4, entities4) + ); + + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().preferNullOverDefault(true).build(); + + final long generated = System.nanoTime(); + System.out.println("Generated in " + (generated - start) / 1000000 + " ms"); + + writeData + .parallelStream() + .forEach(data -> Poiji.toExcel(new File(data.path), ConcurrentEntity.class, data.entities, options)); + + final long written = System.nanoTime(); + System.out.println("Written in " + (written - generated) / 1000000 + " ms"); + + final Set> actual = asList(name1, name2, name3, name4) + .parallelStream() + .map(s -> Poiji.fromExcel(new File(s), ConcurrentEntity.class, options)) + .collect(Collectors.toSet()); + + final long read = System.nanoTime(); + System.out.println("Read in " + (read - written) / 1000000 + " ms"); + + assertThat(actual, equalTo(expected)); + + + } + + private List generateEntities(final int size, final String marker) { + final List result = new ArrayList<>(); + for (int i = 0; i < size; i++) { + result.add(new ConcurrentEntity().setPrimitiveLong(i).setText(marker)); + } + return result; + } + + public static class WriteData { + private final String path; + private final List entities; + + public WriteData(final String path, final List entities) { + this.path = path; + this.entities = entities; + } + } + +} diff --git a/src/test/java/com/poiji/deserialize/WriteTest.java b/src/test/java/com/poiji/deserialize/WriteTest.java new file mode 100644 index 0000000..1bcd103 --- /dev/null +++ b/src/test/java/com/poiji/deserialize/WriteTest.java @@ -0,0 +1,76 @@ +package com.poiji.deserialize; + +import com.poiji.bind.Poiji; +import com.poiji.deserialize.model.WriteEntity; +import com.poiji.option.PoijiOptions; +import java.io.File; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +@RunWith(Parameterized.class) +public class WriteTest { + + private final String path; + + public WriteTest(String path) { + this.path = path; + } + + @Parameterized.Parameters + public static List excel() { + return Arrays.asList("src/test/resources/write.xlsx", "src/test/resources/write.xls"); + } + + @Test + public void write() { + final Map unknown = new HashMap<>(); + unknown.put("unKnown1", "unknown value 1"); + unknown.put("unKnown2", "unknown value 2"); + final List expected = new ArrayList<>(); + final WriteEntity entity = new WriteEntity() + .setPrimitiveDouble(10.0) + .setWrappedDouble(11.0) + .setPrimitiveFloat(20.0f) + .setWrappedFloat(21.0f) + .setPrimitiveLong(1) + .setText("test") + .setPrimitiveBoolean(true) + .setWrappedBoolean(true) + .setDate(new Date(1234567890L)) + .setLocalDate(LocalDate.of(2020, 1, 2)) + .setLocalDateTime(LocalDateTime.of(2020, 1, 2, 12, 0)) + .setBigDecimal(new BigDecimal("123.3456")) + .setPrimitiveByte((byte) -1) + .setWrappedByte((byte) -2) + .setPrimitiveShort((short) -3) + .setWrappedShort((short) -4) + .setAnotherUnknown(unknown); + expected.add(entity); + expected.add(new WriteEntity()); + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder + .settings() + .datePattern("dd-MM-yyyy HH:mm:ss") + .preferNullOverDefault(true) + .build(); + Poiji.toExcel(new File(path), WriteEntity.class, expected, options); + + final List read = Poiji.fromExcel(new File(path), WriteEntity.class, options); + read.forEach(writeEntity -> writeEntity.setUnknown(new HashMap<>())); + assertThat(read.toString(), equalTo(expected.toString())); + + } + +} diff --git a/src/test/java/com/poiji/deserialize/model/ConcurrentEntity.java b/src/test/java/com/poiji/deserialize/model/ConcurrentEntity.java new file mode 100644 index 0000000..cb389d0 --- /dev/null +++ b/src/test/java/com/poiji/deserialize/model/ConcurrentEntity.java @@ -0,0 +1,19 @@ +package com.poiji.deserialize.model; + +import com.poiji.annotation.ExcelCell; +import com.poiji.annotation.ExcelCellName; +import com.poiji.annotation.ExcelSheet; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@ExcelSheet("test") +public final class ConcurrentEntity { + + @ExcelCell(0) + private long primitiveLong; + @ExcelCellName(value = "TexT") + private String text; + +} diff --git a/src/test/java/com/poiji/deserialize/model/WriteEntity.java b/src/test/java/com/poiji/deserialize/model/WriteEntity.java new file mode 100644 index 0000000..3271f54 --- /dev/null +++ b/src/test/java/com/poiji/deserialize/model/WriteEntity.java @@ -0,0 +1,58 @@ +package com.poiji.deserialize.model; + +import com.poiji.annotation.ExcelCell; +import com.poiji.annotation.ExcelCellName; +import com.poiji.annotation.ExcelSheet; +import com.poiji.annotation.ExcelUnknownCells; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@ExcelSheet("test") +public final class WriteEntity { + + @ExcelCell(0) + private long primitiveLong; + @ExcelCellName(value = "TexT", order = 5) + private String text; + @ExcelCell(4) + private Float wrappedFloat; + @ExcelCellName("float") + private float primitiveFloat; + @ExcelUnknownCells + private Map unknown = new ConcurrentHashMap<>(); + @ExcelUnknownCells + private Map anotherUnknown = new ConcurrentHashMap<>(); + @ExcelCellName("double") + private double primitiveDouble; + @ExcelCellName(value = "Double", order = 10) + private Double wrappedDouble; + @ExcelCellName("boolean") + private boolean primitiveBoolean; + @ExcelCellName("Boolean") + private Boolean wrappedBoolean; + @ExcelCellName("Date") + private Date date; + @ExcelCellName("LocalDate") + private LocalDate localDate; + @ExcelCellName("LocalDateTime") + private LocalDateTime localDateTime; + @ExcelCellName("BigDecimal") + private BigDecimal bigDecimal; + @ExcelCellName("byte") + private byte primitiveByte; + @ExcelCellName("Byte") + private Byte wrappedByte; + @ExcelCellName("short") + private short primitiveShort; + @ExcelCellName("Short") + private Short wrappedShort; + +} diff --git a/src/test/java/com/poiji/option/PoijiOptionsTest.java b/src/test/java/com/poiji/option/PoijiOptionsTest.java new file mode 100644 index 0000000..cf51e92 --- /dev/null +++ b/src/test/java/com/poiji/option/PoijiOptionsTest.java @@ -0,0 +1,46 @@ +package com.poiji.option; + +import com.poiji.save.CellCasting; +import java.time.format.DateTimeFormatter; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test for {@link PoijiOptions}. + */ +public final class PoijiOptionsTest { + + @Test + public void getLocalDatePattern() { + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().localDatePattern("custom").build(); + assertThat("custom", equalTo(options.getLocalDatePattern())); + } + + @Test + public void getLocalDateTimePattern() { + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().localDateTimePattern("custom").build(); + assertThat("custom", equalTo(options.getLocalDateTimePattern())); + } + + @Test + public void dateTimeFormatter() { + final DateTimeFormatter expected = DateTimeFormatter.BASIC_ISO_DATE; + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().dateTimeFormatter(expected).build(); + assertThat(expected, equalTo(options.dateTimeFormatter())); + } + + @Test + public void getCellCasting() { + final CellCasting expected = new CellCasting(); + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().withCellCasting(expected).build(); + assertThat(expected, equalTo(options.getCellCasting())); + } + + @Test + public void getCaseInsensitive() { + final PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().caseInsensitive(true).build(); + assertThat(true, equalTo(options.getCaseInsensitive())); + } +} diff --git a/src/test/java/com/poiji/save/CellCastingTest.java b/src/test/java/com/poiji/save/CellCastingTest.java new file mode 100644 index 0000000..c5596c7 --- /dev/null +++ b/src/test/java/com/poiji/save/CellCastingTest.java @@ -0,0 +1,43 @@ +package com.poiji.save; + +import java.util.function.BiConsumer; +import org.apache.poi.ss.usermodel.Cell; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test for {@link CellCasting}. + */ +public final class CellCastingTest { + + @Test + public void getObjectCellCastingInsteadAbsent() { + final CellCasting cellCasting = new CellCasting(); + + assertThat(cellCasting.forType(CellCasting.class), is(cellCasting.forType(Object.class))); + } + + @Test + public void addCellCasting() { + final CellCasting cellCasting = new CellCasting(); + final BiConsumer rule = (cell, o) -> {}; + cellCasting.addCellCasting(CellCasting.class, rule); + + assertThat(cellCasting.forType(CellCasting.class), is(rule)); + } + + @Test + public void rewriteCellCasting() { + final CellCasting cellCasting = new CellCasting(); + final BiConsumer rule = (cell, o) -> {}; + + assertThat(cellCasting.forType(Boolean.class), not(is(rule))); + + cellCasting.addCellCasting(Boolean.class, rule); + + assertThat(cellCasting.forType(Boolean.class), is(rule)); + } +}