Skip to content

Commit 4886305

Browse files
authored
Merge pull request #217 from marcustalbot-ov/fix/#215-inaccessible-private-fields-are-mapped-to-record
#215 Prevent mapping of inaccessible fields to record
2 parents 7613a83 + c26bf49 commit 4886305

File tree

5 files changed

+84
-50
lines changed

5 files changed

+84
-50
lines changed

src/main/java/io/beanmapper/core/inspector/PropertyAccessors.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ public static PropertyAccessor findProperty(Class<?> beanClass, String propertyN
131131
}
132132

133133
private static PropertyDescriptor findPropertyDescriptor(Class<?> beanClass, String propertyName) {
134-
Map<String, PropertyDescriptor> descriptors = findPropertyDescriptors(beanClass);
134+
Map<String, PropertyDescriptor> descriptors = beanClass.isRecord()
135+
? findPropertyDescriptorsForRecord((Class<? extends Record>) beanClass)
136+
: findPropertyDescriptors(beanClass);
135137
return descriptors.get(propertyName);
136138
}
137139

src/main/java/io/beanmapper/strategy/MapToRecordStrategy.java

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
package io.beanmapper.strategy;
22

3+
import io.beanmapper.BeanMapper;
4+
import io.beanmapper.annotations.BeanAlias;
5+
import io.beanmapper.annotations.BeanRecordConstruct;
6+
import io.beanmapper.annotations.BeanRecordConstructMode;
7+
import io.beanmapper.config.Configuration;
8+
import io.beanmapper.core.converter.BeanConverter;
9+
import io.beanmapper.core.inspector.PropertyAccessor;
10+
import io.beanmapper.core.inspector.PropertyAccessors;
11+
import io.beanmapper.exceptions.BeanInstantiationException;
12+
import io.beanmapper.exceptions.RecordConstructorConflictException;
13+
import io.beanmapper.exceptions.RecordNoAvailableConstructorsExceptions;
14+
import io.beanmapper.utils.BeanMapperTraceLogger;
15+
import io.beanmapper.utils.Records;
16+
317
import java.lang.reflect.Constructor;
418
import java.lang.reflect.Field;
519
import java.lang.reflect.InvocationTargetException;
@@ -16,19 +30,6 @@
1630
import java.util.stream.Collectors;
1731
import java.util.stream.Stream;
1832

19-
import io.beanmapper.BeanMapper;
20-
import io.beanmapper.annotations.BeanAlias;
21-
import io.beanmapper.annotations.BeanRecordConstruct;
22-
import io.beanmapper.annotations.BeanRecordConstructMode;
23-
import io.beanmapper.config.Configuration;
24-
import io.beanmapper.core.converter.BeanConverter;
25-
import io.beanmapper.exceptions.BeanInstantiationException;
26-
import io.beanmapper.exceptions.RecordConstructorConflictException;
27-
import io.beanmapper.exceptions.RecordNoAvailableConstructorsExceptions;
28-
import io.beanmapper.exceptions.SourceFieldAccessException;
29-
import io.beanmapper.utils.BeanMapperTraceLogger;
30-
import io.beanmapper.utils.Records;
31-
3233
/**
3334
* MapToRecordStrategy offers a comprehensive implementation of the MapToClassStrategy, targeted towards mapping a class
3435
* to a record.
@@ -49,8 +50,9 @@ public <S, T> T map(final S source) {
4950

5051
Class<T> targetClass = this.getConfiguration().getTargetClass();
5152

52-
if (source.getClass().equals(targetClass))
53+
if (source.getClass() == targetClass) {
5354
return targetClass.cast(source);
55+
}
5456

5557
// We use the RecordToAny-converter in case the source is also a Record. Furthermore, allowing the use of custom
5658
// converters increases flexibility of the library.
@@ -64,9 +66,12 @@ public <S, T> T map(final S source) {
6466
}
6567

6668
Map<String, Field> sourceFields = getSourceFields(source);
69+
Map<String, PropertyAccessor> sourcePropertyAccessors = sourceFields.entrySet().stream()
70+
.map(entry -> Map.entry(entry.getKey(), PropertyAccessors.findProperty(source.getClass(), entry.getValue().getName())))
71+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
6772
Constructor<T> constructor = (Constructor<T>) getSuitableConstructor(sourceFields, targetClass);
6873
String[] fieldNamesForConstructor = getNamesOfConstructorParameters(targetClass, constructor);
69-
List<Object> values = getValuesOfFields(source, sourceFields, sourceFields, Arrays.stream(fieldNamesForConstructor));
74+
List<Object> values = getValuesOfFields(source, sourcePropertyAccessors, Arrays.stream(fieldNamesForConstructor));
7075

7176
return targetClass.cast(constructTargetObject(constructor, values));
7277
}
@@ -76,8 +81,8 @@ public <S, T> T map(final S source) {
7681
* present.
7782
*
7883
* @param sourceClass The class of the source-object.
84+
* @param <S> The type of the sourceClass.
7985
* @return A Map containing the fields of the source-class, mapped by the name of the field, or the value of an available BeanAlias.
80-
* @param <S> The type of the sourceClass.
8186
*/
8287
private <S> Map<String, Field> getFieldsOfClass(final Class<S> sourceClass) {
8388
return Arrays.stream(sourceClass.getDeclaredFields())
@@ -156,15 +161,10 @@ private <T> String[] getNamesOfConstructorParameters(final Class<T> targetClass,
156161
return getNamesOfRecordComponents(targetClass);
157162
}
158163

159-
private <S> List<Object> getValuesOfFields(final S source, final Map<String, Field> sourceFields, final Map<String, Field> fieldMap,
160-
final Stream<String> fieldNamesForConstructor) {
161-
return fieldNamesForConstructor.map(fieldName -> {
162-
Field field = fieldMap.get(fieldName);
163-
if (field != null) {
164-
return field;
165-
}
166-
return sourceFields.get(fieldName);
167-
}).map(field -> getValueFromField(source, field))
164+
private <S> List<Object> getValuesOfFields(final S source, final Map<String, PropertyAccessor> accessors,
165+
final Stream<String> fieldNamesForConstructor) {
166+
return fieldNamesForConstructor.map(accessors::get)
167+
.map(accessor -> getValueFromField(source, accessor))
168168
.toList();
169169
}
170170

@@ -196,17 +196,11 @@ private <T> T constructTargetObject(final Constructor<T> targetConstructor, fina
196196
}
197197
}
198198

199-
private <S> Object getValueFromField(final S source, final Field field) {
200-
if (field == null) {
199+
private <S> Object getValueFromField(final S source, PropertyAccessor accessor) {
200+
if (accessor == null || !accessor.isReadable()) {
201201
return null;
202202
}
203-
try {
204-
if (!field.canAccess(source))
205-
field.setAccessible(true);
206-
return field.get(source);
207-
} catch (IllegalAccessException ex) {
208-
throw new SourceFieldAccessException(this.getConfiguration().getTargetClass(), source.getClass(), "Could not access field " + field.getName(), ex);
209-
}
203+
return accessor.getValue(source);
210204
}
211205

212206
private <T> Constructor<?> getSuitableConstructor(final Map<String, Field> sourceFields, final Class<T> targetClass) {
@@ -239,7 +233,7 @@ private <T> Constructor<?> getSuitableConstructor(final Map<String, Field> sourc
239233
}
240234

241235
private <T> Optional<Constructor<T>> getConstructorWithMostMatchingParameters(final List<Constructor<T>> constructors,
242-
final Map<String, Field> sourceFields) {
236+
final Map<String, Field> sourceFields) {
243237
for (var constructor : constructors) {
244238
BeanRecordConstruct recordConstruct = constructor.getAnnotation(BeanRecordConstruct.class);
245239
List<Field> relevantFields = Arrays.stream(recordConstruct.value())

src/test/java/io/beanmapper/strategy/MapToRecordStrategyTest.java

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
package io.beanmapper.strategy;
22

3-
import static io.beanmapper.shared.AssertionUtils.assertFieldWithNameHasValue;
4-
import static org.junit.jupiter.api.Assertions.assertEquals;
5-
import static org.junit.jupiter.api.Assertions.assertNotNull;
6-
import static org.junit.jupiter.api.Assertions.assertNull;
7-
import static org.junit.jupiter.api.Assertions.assertThrows;
8-
import static org.junit.jupiter.api.Assertions.assertTrue;
9-
10-
import java.util.ArrayDeque;
11-
import java.util.ArrayList;
12-
import java.util.List;
13-
import java.util.Map;
14-
import java.util.Set;
15-
163
import io.beanmapper.BeanMapper;
174
import io.beanmapper.config.BeanMapperBuilder;
185
import io.beanmapper.exceptions.RecordConstructorConflictException;
@@ -48,11 +35,26 @@
4835
import io.beanmapper.strategy.record.model.collection.result.ResultRecordWithSet;
4936
import io.beanmapper.strategy.record.model.inheritance.Layer3;
5037
import io.beanmapper.testmodel.person.Person;
38+
import io.beanmapper.testmodel.record.ResultRecord;
39+
import io.beanmapper.testmodel.record.SourceClassPrivate;
5140
import io.beanmapper.utils.DefaultValues;
52-
5341
import org.junit.jupiter.api.BeforeEach;
5442
import org.junit.jupiter.api.Test;
5543

44+
import java.time.LocalDate;
45+
import java.util.ArrayDeque;
46+
import java.util.ArrayList;
47+
import java.util.List;
48+
import java.util.Map;
49+
import java.util.Set;
50+
51+
import static io.beanmapper.shared.AssertionUtils.assertFieldWithNameHasValue;
52+
import static org.junit.jupiter.api.Assertions.assertEquals;
53+
import static org.junit.jupiter.api.Assertions.assertNotNull;
54+
import static org.junit.jupiter.api.Assertions.assertNull;
55+
import static org.junit.jupiter.api.Assertions.assertThrows;
56+
import static org.junit.jupiter.api.Assertions.assertTrue;
57+
5658
class MapToRecordStrategyTest {
5759

5860
private BeanMapper beanMapper;
@@ -423,4 +425,19 @@ void testRecordWithNestedToTargetWithNested() {
423425
assertEquals(source.name(), result.name());
424426
assertEquals(source.nested().name(), result.nested().name());
425427
}
428+
429+
@Test
430+
void testInaccessibleFieldsShouldNotBeMappedToRecord() {
431+
Integer i = 123;
432+
String s = "123";
433+
LocalDate d = LocalDate.of(2001, 2, 23);
434+
435+
SourceClassPrivate sourceClassPrivate = new SourceClassPrivate(i, s, d);
436+
437+
// !!! private -> public | Maps values !!!
438+
ResultRecord resultRecord = beanMapper.map(sourceClassPrivate, ResultRecord.class);
439+
assertNull(resultRecord.i());
440+
assertNull(resultRecord.s());
441+
assertNull(resultRecord.d());
442+
}
426443
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.beanmapper.testmodel.record;
2+
3+
import java.time.LocalDate;
4+
5+
public record ResultRecord(Integer i, String s, LocalDate d) {
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.beanmapper.testmodel.record;
2+
3+
import java.time.LocalDate;
4+
5+
public class SourceClassPrivate {
6+
private Integer i;
7+
private String s;
8+
private LocalDate d;
9+
10+
public SourceClassPrivate(Integer i, String s, LocalDate d) {
11+
this.i = i;
12+
this.s = s;
13+
this.d = d;
14+
}
15+
}

0 commit comments

Comments
 (0)