From cebe9a02f1b25db5215743a78d0bdce102810565 Mon Sep 17 00:00:00 2001 From: Nikita Marunko Date: Tue, 20 Feb 2024 22:10:28 +0900 Subject: [PATCH] feat(core): support MultiValueMap mapping for columns using Regex Feature request Refs: 304 --- .../annotation/ExcelCellsJoinedByName.java | 20 +++++ .../poiji/bind/mapping/HSSFUnmarshaller.java | 70 +++++++++++++++++- .../com/poiji/bind/mapping/PoijiHandler.java | 14 ++++ src/main/java/com/poiji/util/ReflectUtil.java | 12 +++ .../ReadExcelWithRegexAndJoinValuesTest.java | 55 ++++++++++++++ .../com/poiji/deserialize/model/Album.java | 47 ++++++++++++ src/test/java/com/poiji/util/Data.java | 21 ++++++ src/test/resources/regex/album.xls | Bin 0 -> 26624 bytes src/test/resources/regex/album.xlsx | Bin 0 -> 8898 bytes 9 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/poiji/annotation/ExcelCellsJoinedByName.java create mode 100644 src/test/java/com/poiji/deserialize/ReadExcelWithRegexAndJoinValuesTest.java create mode 100644 src/test/java/com/poiji/deserialize/model/Album.java create mode 100644 src/test/resources/regex/album.xls create mode 100644 src/test/resources/regex/album.xlsx diff --git a/src/main/java/com/poiji/annotation/ExcelCellsJoinedByName.java b/src/main/java/com/poiji/annotation/ExcelCellsJoinedByName.java new file mode 100644 index 0000000..7f5fedd --- /dev/null +++ b/src/main/java/com/poiji/annotation/ExcelCellsJoinedByName.java @@ -0,0 +1,20 @@ +package com.poiji.annotation; + +import java.lang.annotation.*; + +/** + * Created by aerfus on 18/02/2024 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +public @interface ExcelCellsJoinedByName { + + /** + * Specifies the column regular expression where the corresponding values are mapped from the + * Excel data + * + * @return column regular expression + */ + String expression(); +} diff --git a/src/main/java/com/poiji/bind/mapping/HSSFUnmarshaller.java b/src/main/java/com/poiji/bind/mapping/HSSFUnmarshaller.java index 3300e0d..2550c79 100644 --- a/src/main/java/com/poiji/bind/mapping/HSSFUnmarshaller.java +++ b/src/main/java/com/poiji/bind/mapping/HSSFUnmarshaller.java @@ -6,6 +6,7 @@ import com.poiji.annotation.ExcelCellRange; import com.poiji.annotation.ExcelRow; import com.poiji.annotation.ExcelUnknownCells; +import com.poiji.annotation.ExcelCellsJoinedByName; import com.poiji.bind.Unmarshaller; import com.poiji.config.Casting; import com.poiji.config.Formatting; @@ -15,6 +16,8 @@ import com.poiji.option.PoijiOptions; import com.poiji.util.AnnotationUtil; import com.poiji.util.ReflectUtil; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MultiValuedMap; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.formula.BaseFormulaEvaluator; @@ -247,6 +250,14 @@ private Integer tailSetFieldValue(Row currentRow, T instance, Field field) { if (annotationDetail.getColumn() != null) { constructTypeValue(currentRow, instance, field, annotationDetail); } + + if (CollectionUtils.isNotEmpty(annotationDetail.getColumns())) { + for (Integer column : annotationDetail.getColumns()) { + annotationDetail.setColumn(column); + constructTypeValue(currentRow, instance, field, annotationDetail); + } + } + return annotationDetail.getColumn(); } @@ -271,6 +282,23 @@ private FieldAnnotationDetail getFieldColumn(final Field field) { Integer column = findTitleColumn(excelCellName); annotationDetail.setColumn(column); } + + ExcelCellsJoinedByName excelCellsJoinedByName = field.getAnnotation(ExcelCellsJoinedByName.class); + if (excelCellsJoinedByName != null) { + String expression = excelCellsJoinedByName.expression(); + Pattern pattern = Pattern.compile(expression); + + List columns = indexToTitle.entrySet().stream() + .filter(entry -> pattern.matcher( + entry.getValue().replaceAll("@[0-9]+", "")) + .matches()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + annotationDetail.setColumns(columns); + annotationDetail.setMultiValueMap(CollectionUtils.isNotEmpty(columns)); + } + return annotationDetail; } @@ -294,7 +322,7 @@ public Integer findTitleColumn(ExcelCellName excelCellName) { } private void constructTypeValue(Row currentRow, T instance, Field field, - FieldAnnotationDetail annotationDetail) { + FieldAnnotationDetail annotationDetail) { Cell cell = currentRow.getCell(annotationDetail.getColumn()); if (cell != null) { @@ -309,7 +337,14 @@ private void constructTypeValue(Row currentRow, T instance, Field field, } Object data = casting.castValue(field, value, currentRow.getRowNum(), annotationDetail.getColumn(), options); - setFieldData(instance, field, data); + + if (!annotationDetail.isMultiValueMap()) { + setFieldData(instance, field, data); + } else { + String titleColumn = indexToTitle.get(annotationDetail.getColumn()); + titleColumn = titleColumn.replaceAll("@[0-9]+", ""); + putFieldMultiValueMapData(instance, field, titleColumn, data); + } } else if (annotationDetail.isMandatoryCell()) { throw new PoijiRowSpecificException(annotationDetail.getColumnName(), field.getName(), currentRow.getRowNum()); @@ -331,11 +366,21 @@ private void setFieldData(T instance, Field field, Object data) { } } + public void putFieldMultiValueMapData(Object instance, Field field, String columnName, Object o) { + try { + field.setAccessible(true); + MultiValuedMap multiValuedMap = (MultiValuedMap) field.get(instance); + multiValuedMap.put(columnName, o); + } catch (ClassCastException | IllegalAccessException e) { + throw new IllegalCastException("Unexpected cast type {" + o + "} of field" + field.getName()); + } + } + private T setFieldValuesFromRowIntoInstance(Row currentRow, Class subclass, T instance) { return subclass == null ? instance : tailSetFieldValue(currentRow, subclass, - setFieldValuesFromRowIntoInstance(currentRow, subclass.getSuperclass(), instance)); + setFieldValuesFromRowIntoInstance(currentRow, subclass.getSuperclass(), instance)); } boolean skip(final Row currentRow, int skip) { @@ -358,6 +403,10 @@ private static class FieldAnnotationDetail { private boolean disabledCellFormat; private boolean mandatoryCell; + private List columns; + + private boolean multiValueMap; + Integer getColumn() { return column; } @@ -390,6 +439,21 @@ public void setMandatoryCell(boolean mandatoryCell) { this.mandatoryCell = mandatoryCell; } + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + + public boolean isMultiValueMap() { + return multiValueMap; + } + + public void setMultiValueMap(boolean multiValueMap) { + this.multiValueMap = multiValueMap; + } } } diff --git a/src/main/java/com/poiji/bind/mapping/PoijiHandler.java b/src/main/java/com/poiji/bind/mapping/PoijiHandler.java index 3585254..065718f 100644 --- a/src/main/java/com/poiji/bind/mapping/PoijiHandler.java +++ b/src/main/java/com/poiji/bind/mapping/PoijiHandler.java @@ -5,6 +5,7 @@ import com.poiji.annotation.ExcelCellRange; import com.poiji.annotation.ExcelRow; import com.poiji.annotation.ExcelUnknownCells; +import com.poiji.annotation.ExcelCellsJoinedByName; import com.poiji.config.Casting; import com.poiji.config.Formatting; import com.poiji.exception.IllegalCastException; @@ -183,6 +184,19 @@ private boolean setValue(Field field, int column, String content, Object ins) { } } + ExcelCellsJoinedByName excelCellsJoinedByName = field.getAnnotation(ExcelCellsJoinedByName.class); + if (excelCellsJoinedByName != null) { + String titleColumn = indexToTitle.get(column).replaceAll("@[0-9]+", ""); + + String expression = excelCellsJoinedByName.expression(); + Pattern pattern = Pattern.compile(expression); + if (pattern.matcher(titleColumn).matches()) { + Object o = casting.castValue(field, content, internalRow, column, options); + ReflectUtil.putFieldMultiValueMapData(field, titleColumn, o, ins); + return true; + } + } + return false; } diff --git a/src/main/java/com/poiji/util/ReflectUtil.java b/src/main/java/com/poiji/util/ReflectUtil.java index 3f95382..aeb9a8d 100644 --- a/src/main/java/com/poiji/util/ReflectUtil.java +++ b/src/main/java/com/poiji/util/ReflectUtil.java @@ -3,6 +3,7 @@ import com.poiji.annotation.ExcelCellRange; import com.poiji.exception.IllegalCastException; import com.poiji.exception.PoijiInstantiationException; +import org.apache.commons.collections4.MultiValuedMap; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; @@ -64,4 +65,15 @@ public static void setFieldData(Field field, Object o, Object instance) { throw new IllegalCastException("Unexpected cast type {" + o + "} of field" + field.getName()); } } + + @SuppressWarnings("unchecked") + public static void putFieldMultiValueMapData(Field field, String columnName, Object o, Object instance) { + try { + field.setAccessible(true); + MultiValuedMap multiValuedMap = (MultiValuedMap) field.get(instance); + multiValuedMap.put(columnName, o); + } catch (ClassCastException | IllegalAccessException e) { + throw new IllegalCastException("Unexpected cast type {" + o + "} of field" + field.getName()); + } + } } diff --git a/src/test/java/com/poiji/deserialize/ReadExcelWithRegexAndJoinValuesTest.java b/src/test/java/com/poiji/deserialize/ReadExcelWithRegexAndJoinValuesTest.java new file mode 100644 index 0000000..8e6fc8c --- /dev/null +++ b/src/test/java/com/poiji/deserialize/ReadExcelWithRegexAndJoinValuesTest.java @@ -0,0 +1,55 @@ +package com.poiji.deserialize; + +import com.poiji.bind.Poiji; +import com.poiji.deserialize.model.Album; +import com.poiji.option.PoijiOptions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +import static com.poiji.option.PoijiOptions.PoijiOptionsBuilder.settings; +import static com.poiji.util.Data.unmarshallingAlbums; +import static org.junit.Assert.assertEquals; + +/** + * Test for Reading Excel columns with Regular expression (Regex) + */ +@RunWith(Parameterized.class) +public class ReadExcelWithRegexAndJoinValuesTest { + private final String path; + private final List expectedData; + private final PoijiOptions options; + + public ReadExcelWithRegexAndJoinValuesTest(String path, List expectedData, PoijiOptions options) { + this.path = path; + this.expectedData = expectedData; + this.options = options; + } + + @Parameterized.Parameters(name = "{index}: ({0})={1}") + public static Iterable queries() { + return List.of(new Object[][]{ + { + "src/test/resources/regex/album.xlsx", + unmarshallingAlbums(), + settings().sheetName("Sheet 1").build() + }, + { + "src/test/resources/regex/album.xls", + unmarshallingAlbums(), + settings().sheetName("Sheet 1").build() + }, + }); + } + + @Test + public void shouldReadAlbumData() { + List actualData = Poiji.fromExcel(new File(path), Album.class, options); + + assertEquals(expectedData, actualData); + } + +} diff --git a/src/test/java/com/poiji/deserialize/model/Album.java b/src/test/java/com/poiji/deserialize/model/Album.java new file mode 100644 index 0000000..0b8632c --- /dev/null +++ b/src/test/java/com/poiji/deserialize/model/Album.java @@ -0,0 +1,47 @@ +package com.poiji.deserialize.model; + +import com.poiji.annotation.ExcelCellsJoinedByName; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; + +import java.util.Objects; + +/** + * An Album POJO. + */ +public class Album { + @ExcelCellsJoinedByName(expression = "Artist") + private MultiValuedMap artists = new ArrayListValuedHashMap<>(); + + @ExcelCellsJoinedByName(expression = "Track[0-9]+") + private MultiValuedMap tracks = new ArrayListValuedHashMap<>(); + + public void setArtists(MultiValuedMap artists) { + this.artists = artists; + } + + public void setTracks(MultiValuedMap tracks) { + this.tracks = tracks; + } + + @Override + public String toString() { + return "Album{" + + "artists=" + artists + + ", tracks=" + tracks + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Album album = (Album) o; + return Objects.equals(artists, album.artists) && Objects.equals(tracks, album.tracks); + } + + @Override + public int hashCode() { + return Objects.hash(artists, tracks); + } +} diff --git a/src/test/java/com/poiji/util/Data.java b/src/test/java/com/poiji/util/Data.java index e2af6f0..87b88fe 100644 --- a/src/test/java/com/poiji/util/Data.java +++ b/src/test/java/com/poiji/util/Data.java @@ -1,10 +1,12 @@ package com.poiji.util; +import com.poiji.deserialize.model.Album; import com.poiji.deserialize.model.InventoryData; import com.poiji.deserialize.model.Student; import com.poiji.deserialize.model.byid.Employee; import com.poiji.deserialize.model.byid.Person; import com.poiji.deserialize.model.byid.Sample; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import java.util.ArrayList; import java.util.List; @@ -140,4 +142,23 @@ public static List unmarshallingInventoryData() { return List.of(record1, record2); } + + + public static List unmarshallingAlbums() { + ArrayListValuedHashMap artists = new ArrayListValuedHashMap<>(); + + artists.put("Artist", "Michael Jackson"); + artists.put("Artist", "Lionel Richie"); + artists.put("Artist", "Stevie Wonder"); + + ArrayListValuedHashMap tracks = new ArrayListValuedHashMap<>(); + tracks.put("Track1", "We are the World"); + tracks.put("Track2", "We are the World (instrumental)"); + + Album album = new Album(); + album.setArtists(artists); + album.setTracks(tracks); + + return List.of(album); + } } diff --git a/src/test/resources/regex/album.xls b/src/test/resources/regex/album.xls new file mode 100644 index 0000000000000000000000000000000000000000..1df8c766b61c1251f28911c1ef53d100ed797f4c GIT binary patch literal 26624 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 kz7-bgd6Gq4}2pczW@LL literal 0 HcmV?d00001 diff --git a/src/test/resources/regex/album.xlsx b/src/test/resources/regex/album.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d952d4712b63a5a66d894cbfa2e07a28af4bb72b GIT binary patch literal 8898 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