diff --git a/src/main/java/io/beanmapper/core/inspector/PropertyAccessors.java b/src/main/java/io/beanmapper/core/inspector/PropertyAccessors.java index b94430a1..4e5f83e8 100644 --- a/src/main/java/io/beanmapper/core/inspector/PropertyAccessors.java +++ b/src/main/java/io/beanmapper/core/inspector/PropertyAccessors.java @@ -131,7 +131,9 @@ public static PropertyAccessor findProperty(Class beanClass, String propertyN } private static PropertyDescriptor findPropertyDescriptor(Class beanClass, String propertyName) { - Map descriptors = findPropertyDescriptors(beanClass); + Map descriptors = beanClass.isRecord() + ? findPropertyDescriptorsForRecord((Class) beanClass) + : findPropertyDescriptors(beanClass); return descriptors.get(propertyName); } diff --git a/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java b/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java index efe5736a..cbec4ef8 100644 --- a/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java +++ b/src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java @@ -1,5 +1,19 @@ package io.beanmapper.strategy; +import io.beanmapper.BeanMapper; +import io.beanmapper.annotations.BeanAlias; +import io.beanmapper.annotations.BeanRecordConstruct; +import io.beanmapper.annotations.BeanRecordConstructMode; +import io.beanmapper.config.Configuration; +import io.beanmapper.core.converter.BeanConverter; +import io.beanmapper.core.inspector.PropertyAccessor; +import io.beanmapper.core.inspector.PropertyAccessors; +import io.beanmapper.exceptions.BeanInstantiationException; +import io.beanmapper.exceptions.RecordConstructorConflictException; +import io.beanmapper.exceptions.RecordNoAvailableConstructorsExceptions; +import io.beanmapper.utils.BeanMapperTraceLogger; +import io.beanmapper.utils.Records; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -16,19 +30,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import io.beanmapper.BeanMapper; -import io.beanmapper.annotations.BeanAlias; -import io.beanmapper.annotations.BeanRecordConstruct; -import io.beanmapper.annotations.BeanRecordConstructMode; -import io.beanmapper.config.Configuration; -import io.beanmapper.core.converter.BeanConverter; -import io.beanmapper.exceptions.BeanInstantiationException; -import io.beanmapper.exceptions.RecordConstructorConflictException; -import io.beanmapper.exceptions.RecordNoAvailableConstructorsExceptions; -import io.beanmapper.exceptions.SourceFieldAccessException; -import io.beanmapper.utils.BeanMapperTraceLogger; -import io.beanmapper.utils.Records; - /** * MapToRecordStrategy offers a comprehensive implementation of the MapToClassStrategy, targeted towards mapping a class * to a record. @@ -49,8 +50,9 @@ public T map(final S source) { Class targetClass = this.getConfiguration().getTargetClass(); - if (source.getClass().equals(targetClass)) + if (source.getClass() == targetClass) { return targetClass.cast(source); + } // We use the RecordToAny-converter in case the source is also a Record. Furthermore, allowing the use of custom // converters increases flexibility of the library. @@ -64,9 +66,12 @@ public T map(final S source) { } Map sourceFields = getSourceFields(source); + Map sourcePropertyAccessors = sourceFields.entrySet().stream() + .map(entry -> Map.entry(entry.getKey(), PropertyAccessors.findProperty(source.getClass(), entry.getValue().getName()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Constructor constructor = (Constructor) getSuitableConstructor(sourceFields, targetClass); String[] fieldNamesForConstructor = getNamesOfConstructorParameters(targetClass, constructor); - List values = getValuesOfFields(source, sourceFields, sourceFields, Arrays.stream(fieldNamesForConstructor)); + List values = getValuesOfFields(source, sourcePropertyAccessors, Arrays.stream(fieldNamesForConstructor)); return targetClass.cast(constructTargetObject(constructor, values)); } @@ -76,8 +81,8 @@ public T map(final S source) { * present. * * @param sourceClass The class of the source-object. + * @param The type of the sourceClass. * @return A Map containing the fields of the source-class, mapped by the name of the field, or the value of an available BeanAlias. - * @param The type of the sourceClass. */ private Map getFieldsOfClass(final Class sourceClass) { return Arrays.stream(sourceClass.getDeclaredFields()) @@ -156,15 +161,10 @@ private String[] getNamesOfConstructorParameters(final Class targetClass, return getNamesOfRecordComponents(targetClass); } - private List getValuesOfFields(final S source, final Map sourceFields, final Map fieldMap, - final Stream fieldNamesForConstructor) { - return fieldNamesForConstructor.map(fieldName -> { - Field field = fieldMap.get(fieldName); - if (field != null) { - return field; - } - return sourceFields.get(fieldName); - }).map(field -> getValueFromField(source, field)) + private List getValuesOfFields(final S source, final Map accessors, + final Stream fieldNamesForConstructor) { + return fieldNamesForConstructor.map(accessors::get) + .map(accessor -> getValueFromField(source, accessor)) .toList(); } @@ -196,17 +196,11 @@ private T constructTargetObject(final Constructor targetConstructor, fina } } - private Object getValueFromField(final S source, final Field field) { - if (field == null) { + private Object getValueFromField(final S source, PropertyAccessor accessor) { + if (accessor == null || !accessor.isReadable()) { return null; } - try { - if (!field.canAccess(source)) - field.setAccessible(true); - return field.get(source); - } catch (IllegalAccessException ex) { - throw new SourceFieldAccessException(this.getConfiguration().getTargetClass(), source.getClass(), "Could not access field " + field.getName(), ex); - } + return accessor.getValue(source); } private Constructor getSuitableConstructor(final Map sourceFields, final Class targetClass) { @@ -239,7 +233,7 @@ private Constructor getSuitableConstructor(final Map sourc } private Optional> getConstructorWithMostMatchingParameters(final List> constructors, - final Map sourceFields) { + final Map sourceFields) { for (var constructor : constructors) { BeanRecordConstruct recordConstruct = constructor.getAnnotation(BeanRecordConstruct.class); List relevantFields = Arrays.stream(recordConstruct.value()) diff --git a/src/test/java/io/beanmapper/strategy/MapToRecordStrategyTest.java b/src/test/java/io/beanmapper/strategy/MapToRecordStrategyTest.java index e3c97378..1f1a0757 100644 --- a/src/test/java/io/beanmapper/strategy/MapToRecordStrategyTest.java +++ b/src/test/java/io/beanmapper/strategy/MapToRecordStrategyTest.java @@ -1,18 +1,5 @@ package io.beanmapper.strategy; -import static io.beanmapper.shared.AssertionUtils.assertFieldWithNameHasValue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - import io.beanmapper.BeanMapper; import io.beanmapper.config.BeanMapperBuilder; import io.beanmapper.exceptions.RecordConstructorConflictException; @@ -48,11 +35,26 @@ import io.beanmapper.strategy.record.model.collection.result.ResultRecordWithSet; import io.beanmapper.strategy.record.model.inheritance.Layer3; import io.beanmapper.testmodel.person.Person; +import io.beanmapper.testmodel.record.ResultRecord; +import io.beanmapper.testmodel.record.SourceClassPrivate; import io.beanmapper.utils.DefaultValues; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.LocalDate; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static io.beanmapper.shared.AssertionUtils.assertFieldWithNameHasValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + class MapToRecordStrategyTest { private BeanMapper beanMapper; @@ -423,4 +425,19 @@ void testRecordWithNestedToTargetWithNested() { assertEquals(source.name(), result.name()); assertEquals(source.nested().name(), result.nested().name()); } + + @Test + void testInaccessibleFieldsShouldNotBeMappedToRecord() { + Integer i = 123; + String s = "123"; + LocalDate d = LocalDate.of(2001, 2, 23); + + SourceClassPrivate sourceClassPrivate = new SourceClassPrivate(i, s, d); + + // !!! private -> public | Maps values !!! + ResultRecord resultRecord = beanMapper.map(sourceClassPrivate, ResultRecord.class); + assertNull(resultRecord.i()); + assertNull(resultRecord.s()); + assertNull(resultRecord.d()); + } } \ No newline at end of file diff --git a/src/test/java/io/beanmapper/testmodel/record/ResultRecord.java b/src/test/java/io/beanmapper/testmodel/record/ResultRecord.java new file mode 100644 index 00000000..a3e67a35 --- /dev/null +++ b/src/test/java/io/beanmapper/testmodel/record/ResultRecord.java @@ -0,0 +1,6 @@ +package io.beanmapper.testmodel.record; + +import java.time.LocalDate; + +public record ResultRecord(Integer i, String s, LocalDate d) { +} diff --git a/src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java b/src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java new file mode 100644 index 00000000..dab2c8ac --- /dev/null +++ b/src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java @@ -0,0 +1,15 @@ +package io.beanmapper.testmodel.record; + +import java.time.LocalDate; + +public class SourceClassPrivate { + private Integer i; + private String s; + private LocalDate d; + + public SourceClassPrivate(Integer i, String s, LocalDate d) { + this.i = i; + this.s = s; + this.d = d; + } +}