Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ public static PropertyAccessor findProperty(Class<?> beanClass, String propertyN
}

private static PropertyDescriptor findPropertyDescriptor(Class<?> beanClass, String propertyName) {
Map<String, PropertyDescriptor> descriptors = findPropertyDescriptors(beanClass);
Map<String, PropertyDescriptor> descriptors = beanClass.isRecord()
? findPropertyDescriptorsForRecord((Class<? extends Record>) beanClass)
: findPropertyDescriptors(beanClass);
return descriptors.get(propertyName);
}

Expand Down
64 changes: 29 additions & 35 deletions src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand All @@ -49,8 +50,9 @@ public <S, T> T map(final S source) {

Class<T> 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.
Expand All @@ -64,9 +66,12 @@ public <S, T> T map(final S source) {
}

Map<String, Field> sourceFields = getSourceFields(source);
Map<String, PropertyAccessor> 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<T> constructor = (Constructor<T>) getSuitableConstructor(sourceFields, targetClass);
String[] fieldNamesForConstructor = getNamesOfConstructorParameters(targetClass, constructor);
List<Object> values = getValuesOfFields(source, sourceFields, sourceFields, Arrays.stream(fieldNamesForConstructor));
List<Object> values = getValuesOfFields(source, sourcePropertyAccessors, Arrays.stream(fieldNamesForConstructor));

return targetClass.cast(constructTargetObject(constructor, values));
}
Expand All @@ -76,8 +81,8 @@ public <S, T> T map(final S source) {
* present.
*
* @param sourceClass The class of the source-object.
* @param <S> 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 <S> The type of the sourceClass.
*/
private <S> Map<String, Field> getFieldsOfClass(final Class<S> sourceClass) {
return Arrays.stream(sourceClass.getDeclaredFields())
Expand Down Expand Up @@ -156,15 +161,10 @@ private <T> String[] getNamesOfConstructorParameters(final Class<T> targetClass,
return getNamesOfRecordComponents(targetClass);
}

private <S> List<Object> getValuesOfFields(final S source, final Map<String, Field> sourceFields, final Map<String, Field> fieldMap,
final Stream<String> 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 <S> List<Object> getValuesOfFields(final S source, final Map<String, PropertyAccessor> accessors,
final Stream<String> fieldNamesForConstructor) {
return fieldNamesForConstructor.map(accessors::get)
.map(accessor -> getValueFromField(source, accessor))
.toList();
}

Expand Down Expand Up @@ -196,17 +196,11 @@ private <T> T constructTargetObject(final Constructor<T> targetConstructor, fina
}
}

private <S> Object getValueFromField(final S source, final Field field) {
if (field == null) {
private <S> 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 <T> Constructor<?> getSuitableConstructor(final Map<String, Field> sourceFields, final Class<T> targetClass) {
Expand Down Expand Up @@ -239,7 +233,7 @@ private <T> Constructor<?> getSuitableConstructor(final Map<String, Field> sourc
}

private <T> Optional<Constructor<T>> getConstructorWithMostMatchingParameters(final List<Constructor<T>> constructors,
final Map<String, Field> sourceFields) {
final Map<String, Field> sourceFields) {
for (var constructor : constructors) {
BeanRecordConstruct recordConstruct = constructor.getAnnotation(BeanRecordConstruct.class);
List<Field> relevantFields = Arrays.stream(recordConstruct.value())
Expand Down
45 changes: 31 additions & 14 deletions src/test/java/io/beanmapper/strategy/MapToRecordStrategyTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.beanmapper.testmodel.record;

import java.time.LocalDate;

public record ResultRecord(Integer i, String s, LocalDate d) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.beanmapper.testmodel.record;

import java.time.LocalDate;

public class SourceClassPrivate {
private Integer i;

Check warning on line 6 in src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java#L6

Avoid unused private fields such as 'i'.

Check warning on line 6 in src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java#L6

Perhaps 'i' could be replaced by a local variable.
private String s;

Check warning on line 7 in src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java#L7

Avoid unused private fields such as 's'.

Check warning on line 7 in src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java#L7

Perhaps 's' could be replaced by a local variable.
private LocalDate d;

Check warning on line 8 in src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java#L8

Avoid unused private fields such as 'd'.

Check warning on line 8 in src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/test/java/io/beanmapper/testmodel/record/SourceClassPrivate.java#L8

Perhaps 'd' could be replaced by a local variable.

public SourceClassPrivate(Integer i, String s, LocalDate d) {
this.i = i;
this.s = s;
this.d = d;
}
}