Skip to content

Commit

Permalink
Merge pull request #4 from vaa25/1.4.1
Browse files Browse the repository at this point in the history
1.4.1 java.beans.ConstructorProperties and @ExcelConstructor
  • Loading branch information
vaa25 authored Mar 16, 2024
2 parents 4b0d2e9 + 3f577fa commit 165d0c9
Show file tree
Hide file tree
Showing 21 changed files with 608 additions and 68 deletions.
7 changes: 4 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This is an advanced fork of https://github.com/ozlerhakan/poiji. +
Many thanks to ozlerhakan for inspiration.

Apache POI library required as dependency to work with xlsx and xls. Tested with dependency 'org.apache.poi:poi-ooxml:4.1.2'.
Apache POI library required as dependency to work with xlsx and xls. Tested with dependency 'org.apache.poi:poi-ooxml:5.2.5'.

In your Maven/Gradle project, first add the corresponding dependency:

Expand All @@ -14,7 +14,7 @@ In your Maven/Gradle project, first add the corresponding dependency:
<dependency>
<groupId>io.github.vaa25</groupId>
<artifactId>poiji2</artifactId>
<version>1.4.0</version>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
Expand All @@ -29,7 +29,7 @@ In your Maven/Gradle project, first add the corresponding dependency:
[source,groovy]
----
dependencies {
implementation 'io.github.vaa25:poiji2:1.4.0'
implementation 'io.github.vaa25:poiji2:1.4.1'
implementation 'org.apache.poi:poi-ooxml:5.2.5'
}
----
Expand All @@ -51,3 +51,4 @@ Also:
- Poiji2 can read lists in row (use `@ExcelList` on `List`)
- Poiji2 can read and write huge xlsx files (see HugeTest.java)
- Poiji2 (since v1.4.0) can work with immutable java classes (see IgnoreTest.java). lombok @Value and java records applicable also.
- Poiji2 (since v1.4.1) can work with immutable java classes with many constructors (see ExcelConstructorTest.java). Apply @ExcelConstructor to choose one of.
15 changes: 15 additions & 0 deletions src/main/java/com/poiji/annotation/ExcelConstructor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.poiji.annotation;

import java.lang.annotation.*;

/**
* Marks constructor what should be used by Poiji.
*
* Created by vaa25 on 16.03.24.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
@Documented
public @interface ExcelConstructor {

}
28 changes: 7 additions & 21 deletions src/main/java/com/poiji/bind/mapping/ReadMappedFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,7 @@ private List<Field> parseExcelRow(final List<Field> fields) {
final ExcelRow annotation = field.getAnnotation(ExcelRow.class);
if (annotation != null) {
this.excelRow.add(field);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand All @@ -199,9 +197,7 @@ private List<Field> parseExcelError(final List<Field> fields) {
final ExcelParseExceptions annotation = field.getAnnotation(ExcelParseExceptions.class);
if (annotation != null) {
this.excelParseException.add(field);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand All @@ -223,9 +219,7 @@ private List<Field> parseExcelCellName(final List<Field> fields) {
final String name = options.getFormatting().transform(options, possibleFieldName);
namedFields.put(name, field);
}
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand All @@ -243,9 +237,7 @@ private List<Field> parseUnknownCells(final List<Field> fields) {
for (final Field field : fields) {
if (field.getAnnotation(ExcelUnknownCells.class) != null && field.getType().isAssignableFrom(Map.class)) {
unknownFields.add(field);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand All @@ -258,9 +250,7 @@ private List<Field> parseExcelCellRange(final List<Field> fields) {
for (final Field field : fields) {
if (field.getAnnotation(ExcelCellRange.class) != null) {
rangeFields.put(field, new ReadMappedFields(field.getType(), options).parseEntity());
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand All @@ -275,9 +265,7 @@ private List<Field> parseExcelList(final List<Field> fields) {
if (field.getType().isAssignableFrom(List.class) && annotation != null) {
final Class entity = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
listFields.put(field, new ReadMappedList(annotation, entity, options));
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand All @@ -291,9 +279,7 @@ private List<Field> parseExcelCell(final List<Field> fields) {
if (field.getAnnotation(ExcelCell.class) != null) {
final Integer excelOrder = field.getAnnotation(ExcelCell.class).value();
orderedFields.put(excelOrder, field);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
rest.add(field);
}
Expand Down
14 changes: 5 additions & 9 deletions src/main/java/com/poiji/save/MappedFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.poiji.bind.mapping.SheetNameExtractor;
import com.poiji.exception.PoijiException;
import com.poiji.option.PoijiOptions;
import com.poiji.util.ReflectUtil;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -50,14 +52,10 @@ public MappedFields parseEntity() {
final String name = field.getName();
orders.put(field, excelOrder);
names.put(field, name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else if (field.getAnnotation(ExcelUnknownCells.class) != null) {
unknownCells.add(field);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
} else {
final ExcelCellName annotation = field.getAnnotation(ExcelCellName.class);
if (annotation != null) {
Expand All @@ -72,9 +70,7 @@ public MappedFields parseEntity() {
orders.put(field, order);
}
names.put(field, excelName);
if (!field.isAccessible()) {
field.setAccessible(true);
}
ReflectUtil.setAccessible(field);
}
}
}
Expand Down
92 changes: 69 additions & 23 deletions src/main/java/com/poiji/util/ConstructorFieldMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.poiji.exception.PoijiInstantiationException;

import java.beans.ConstructorProperties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -18,40 +19,85 @@ static Object[] getConstructorFields(Constructor<?> constructor) {
}

private static Object[] getConstructorMapping(Constructor<?> constructor) {
final Object[] constructorMapping = new Object[constructor.getParameterCount()];
mapFieldsWithConstructorProperties(constructor, constructorMapping);
findNotMappedFieldsWithExamining(constructor, constructorMapping);
fillNotMappedParametersWithDefaults(constructor, constructorMapping);
return constructorMapping;
}

/**
* ConstructorProperties is used by lombok. It allows to map any custom fields easy without examining.
*/
private static Object[] mapFieldsWithConstructorProperties(Constructor<?> constructor, Object[] constructorMapping) {
final ConstructorProperties constructorProperties = constructor.getAnnotation(ConstructorProperties.class);
if (constructorProperties != null) {
final Field[] fields = constructor.getDeclaringClass().getDeclaredFields();
final String[] parameterNames = constructorProperties.value();
final Class<?>[] parameterTypes = constructor.getParameterTypes();
for (int i = 0; i < parameterNames.length; i++) {
final String parameterName = parameterNames[i];
for (Field field : fields) {
if (field.getName().equals(parameterName) && field.getType() == parameterTypes[i]) {
ReflectUtil.setAccessible(field);
constructorMapping[i] = field;
break;
}
}
}
}
return constructorMapping;
}

/**
* The only way to find mapping between constructor parameters and instance fields is to pass special value
* into constructor and look it up in every field in instance.
* Knowing what parameter was passed and what field was found in we can define one mapping.
*/
private static Object[] findNotMappedFieldsWithExamining(Constructor<?> constructor, Object[] constructorMapping) {
final Class<?> entity = constructor.getDeclaringClass();
final Class<?>[] parameterTypes = constructor.getParameterTypes();
final Object[] defaults = fieldDefaultsMapping.computeIfAbsent(constructor, ignored -> getDefaultValues(parameterTypes));
final Object[] constructorMapping = new Object[parameterTypes.length];
final Object[] defaultConstructorParameters = fieldDefaultsMapping.computeIfAbsent(constructor, ignored -> getDefaultValues(parameterTypes));
final Field[] fields = entity.getDeclaredFields();
for (int i = 0; i < parameterTypes.length; i++) {
final Object[] clone = defaults.clone();
final Class<?> parameterType = parameterTypes[i];
final Object example = ImmutableInstanceRegistrar.getEmptyInstance(parameterType);
clone[i] = example;
try {
final Object instance = constructor.newInstance(clone);
final Field[] fields = entity.getDeclaredFields();
for (Field field : fields) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
try {
if (isFieldHasExampleValue(field, instance, example)) {
constructorMapping[i] = field;
break;
if (constructorMapping[i] == null) {
final Object[] instanceConstructorParameters = defaultConstructorParameters.clone();
final Class<?> parameterType = parameterTypes[i];
final Object parameterToExamine = ImmutableInstanceRegistrar.getEmptyInstance(parameterType);
instanceConstructorParameters[i] = parameterToExamine;
try {
final Object instance = constructor.newInstance(instanceConstructorParameters);
for (Field field : fields) {
ReflectUtil.setAccessible(field);
try {
if (isFieldHasExampleValue(field, instance, parameterToExamine)) {
constructorMapping[i] = field;
break;
}
} catch (IllegalAccessException e) {
throw new PoijiInstantiationException("Can't get field " + field, e);
}
} catch (IllegalAccessException e) {
throw new PoijiInstantiationException("Can't get field " + field, e);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new PoijiInstantiationException("Can't create instance " + entity, e);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new PoijiInstantiationException("Can't create instance " + entity, e);
}
}
return constructorMapping;
}

/**
* Sometimes constructor can have parameter that not corresponds to any field.
* We have to fill it anyway to construct instance successfully.
*/
private static Object[] fillNotMappedParametersWithDefaults(Constructor<?> constructor, Object[] constructorMapping) {
final Object[] defaultConstructorParameters = fieldDefaultsMapping.computeIfAbsent(constructor, ignored -> getDefaultValues(constructor.getParameterTypes()));
for (int i = 0; i < constructorMapping.length; i++) {
if (constructorMapping[i] == null) {
constructorMapping[i] = defaults[i];
constructorMapping[i] = defaultConstructorParameters[i];
}
}
return constructorMapping;

}

private static boolean isFieldHasExampleValue(Field field, Object instance, Object example) throws IllegalAccessException {
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/poiji/util/ConstructorSelector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.poiji.util;

import com.poiji.annotation.ExcelConstructor;
import com.poiji.exception.PoijiInstantiationException;

import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConstructorSelector {

private static final Map<Class<?>, Constructor<?>> fieldDefaultsMapping = new ConcurrentHashMap<>();

public static Constructor<?> selectConstructor(Class<?> type) {
return fieldDefaultsMapping.computeIfAbsent(type, ignored -> setAccessible(defineConstructor(type)));
}

private static Constructor<?> setAccessible(Constructor<?> constructor) {
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return constructor;
}

private static Constructor<?> defineConstructor(Class<?> type) {
final Constructor<?>[] constructors = type.getDeclaredConstructors();
if (constructors.length > 1) {
final Constructor<?> constructor = getMarkedConstructor(type, constructors);
if (constructor == null) {
final String annotation = ExcelConstructor.class.getSimpleName();
final String message = String.format("Several constructors were found in %s. Mark one of it with @%s please.", type.getName(), annotation);
throw new PoijiInstantiationException(message, null);
}
return constructor;
}
return constructors[0];
}

private static Constructor<?> getMarkedConstructor(Class<?> type, Constructor<?>[] constructors) {
Constructor<?> result = null;
for (Constructor<?> constructor : constructors) {
if (constructor.isAnnotationPresent(ExcelConstructor.class)) {
if (result != null) {
final String annotation = ExcelConstructor.class.getSimpleName();
final String message = String.format("Several constructors are marked with @%s in %s. Mark only one of it please.", annotation, type.getName());
throw new PoijiInstantiationException(message, null);
}
result = constructor;
}
}
return result;
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/poiji/util/ImmutableInstanceRegistrar.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.poiji.util;

import com.poiji.exception.PoijiException;
import com.poiji.exception.PoijiInstantiationException;

import java.math.BigDecimal;
import java.time.LocalDate;
Expand Down Expand Up @@ -51,6 +52,6 @@ static <T> T getEmptyInstance(Class<T> type) {
}
final String message = String.format("Use %s.register() to register empty instance for '%s' first",
ImmutableInstanceRegistrar.class.getName(), type.getName());
throw new PoijiException(message);
throw new PoijiInstantiationException(message, null);
}
}
18 changes: 11 additions & 7 deletions src/main/java/com/poiji/util/ReflectUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.poiji.annotation.ExcelCellRange;
import com.poiji.bind.mapping.Data;
import com.poiji.exception.IllegalCastException;
import com.poiji.exception.PoijiException;
import com.poiji.exception.PoijiInstantiationException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
Expand Down Expand Up @@ -39,15 +40,14 @@ public static <T> T newInstanceOf(Data data) {

public static <T> T newInstanceOf(Class<?> type, Data data) {
try {
final Constructor<?> constructor = type.getDeclaredConstructors()[0];
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Constructor<?> constructor = ConstructorSelector.selectConstructor(type);
if (constructor.getParameterCount() == 0) {
return createInstanceUsingDefaultConstructor(data, constructor);
} else {
return createInstanceUsingParameterizedConstructor(data, constructor);
}
} catch (PoijiInstantiationException exception) {
throw exception;
} catch (Exception ex) {
throw new PoijiInstantiationException("Cannot create a new instance of " + type.getName(), ex);
}
Expand Down Expand Up @@ -158,12 +158,16 @@ public static <T, A extends Annotation> Collection<A> findRecursivePoijiAnnotati

public static void setFieldData(Field field, Object o, Object instance) {
try {
if (!field.isAccessible()) {
field.setAccessible(true);
}
setAccessible(field);
field.set(instance, o);
} catch (IllegalAccessException e) {
throw new IllegalCastException("Unexpected cast type {" + o + "} of field" + field.getName());
}
}

public static void setAccessible(Field field) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
}
}
Loading

0 comments on commit 165d0c9

Please sign in to comment.