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 0000000..1df8c76 Binary files /dev/null and b/src/test/resources/regex/album.xls differ diff --git a/src/test/resources/regex/album.xlsx b/src/test/resources/regex/album.xlsx new file mode 100644 index 0000000..d952d47 Binary files /dev/null and b/src/test/resources/regex/album.xlsx differ