Skip to content

Commit

Permalink
Make BeanProperty repeatable
Browse files Browse the repository at this point in the history
- Made BeanProperty repeatable.
- Added BeanProperty#targets, allowing users to specify which mappings
the specified BeanProperty should apply to.
- Compatibility with existing usages of the BeanProperty-annotation.

Closes: #188
  • Loading branch information
marcus-talbot42 committed Aug 6, 2024
1 parent e326fa8 commit 2b0e3c3
Show file tree
Hide file tree
Showing 24 changed files with 1,051 additions and 158 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Issue [#188](https://github.com/42BV/beanmapper/issues/188) Made BeanProperty-annotation repeatable. Added targets-property to BeanProperty-annotation, allowing the user to specify which mappings a BeanProperty should apply to.

## [4.1.6]

### Fixed

- Added diagnostics, allowing users to check what mappings and conversion are performed as part of a given mapping.

### NB
Expand Down
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<slf4j.version>2.0.13</slf4j.version>

<jackson-databind.version>2.17.0</jackson-databind.version>
<junit.version>5.10.2</junit.version>
<junit.version>5.10.3</junit.version>

<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
<maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
Expand Down Expand Up @@ -84,6 +84,12 @@
<version>${jackson-databind.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>

<!-- Used for dynamic creation of bean classes -->
<dependency>
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/io/beanmapper/annotations/BeanProperty.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.beanmapper.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand All @@ -9,12 +10,34 @@
* Setting a name allows you to map the property to a property on the other side with a different
* name. This annotation can be used on both sides.
*/
@Repeatable(BeanProperty.BeanProperties.class)
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanProperty {

Class<?> WILDCARD_TYPE = Void.class;

/**
* Making this property available makes application code require guard clauses. As such, best to remove this.
*/
@Deprecated(forRemoval = true)
String name() default "";

String value() default "";

/**
* Allows the user to specify which mappings this BeanProperty should apply to. When this property contains Void.class, the BeanProperty will apply to any mappings that do not have a BeanProperty specified.
*
* @return The mappings to which this BeanProperty should apply.
*/
Class<?>[] targets() default { Void.class };

/**
* Allows the BeanProperty-annotation to be repeated.
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@interface BeanProperties {
BeanProperty[] value();
}
}
3 changes: 2 additions & 1 deletion src/main/java/io/beanmapper/config/CoreConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.beanmapper.core.constructor.DefaultBeanInitializer;
import io.beanmapper.core.converter.BeanConverter;
import io.beanmapper.core.converter.BeanConverterStore;
import io.beanmapper.core.inspector.BeanPropertySelector;
import io.beanmapper.core.unproxy.BeanUnproxy;
import io.beanmapper.core.unproxy.DefaultBeanUnproxy;
import io.beanmapper.core.unproxy.SkippingBeanUnproxy;
Expand Down Expand Up @@ -44,7 +45,7 @@ public class CoreConfiguration implements Configuration {
* Contains a store of matches for source and target class pairs. A pair is created only
* once and reused every time thereafter.
*/
private final BeanMatchStore beanMatchStore = new BeanMatchStore(collectionHandlerStore, beanUnproxy);
private final BeanMatchStore beanMatchStore = new BeanMatchStore(collectionHandlerStore, beanUnproxy, new BeanPropertySelector());

private final BeanConverterStore beanConverterStore = new BeanConverterStore();

Expand Down
168 changes: 24 additions & 144 deletions src/main/java/io/beanmapper/core/BeanMatchStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
import static io.beanmapper.core.converter.collections.CollectionElementType.derived;
import static io.beanmapper.core.converter.collections.CollectionElementType.set;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -28,13 +23,12 @@
import io.beanmapper.core.converter.collections.AnnotationClass;
import io.beanmapper.core.converter.collections.BeanCollectionInstructions;
import io.beanmapper.core.converter.collections.CollectionElementType;
import io.beanmapper.core.inspector.BeanPropertySelector;
import io.beanmapper.core.inspector.PropertyAccessor;
import io.beanmapper.core.inspector.PropertyAccessors;
import io.beanmapper.core.unproxy.BeanUnproxy;
import io.beanmapper.exceptions.BeanMissingPathException;
import io.beanmapper.exceptions.BeanNoSuchPropertyException;
import io.beanmapper.exceptions.FieldShadowingException;
import io.beanmapper.utils.BeanMapperTraceLogger;
import io.beanmapper.utils.Trinary;

public class BeanMatchStore {
Expand All @@ -45,9 +39,12 @@ public class BeanMatchStore {

private final Map<Class<?>, Map<Class<?>, BeanMatch>> store = new HashMap<>();

public BeanMatchStore(CollectionHandlerStore collectionHandlerStore, BeanUnproxy beanUnproxy) {
private final BeanPropertySelector beanPropertySelector;

public BeanMatchStore(CollectionHandlerStore collectionHandlerStore, BeanUnproxy beanUnproxy, BeanPropertySelector beanPropertySelector) {
this.collectionHandlerStore = collectionHandlerStore;
this.beanUnproxy = beanUnproxy;
this.beanPropertySelector = beanPropertySelector;
}

public void validateStrictBeanPairs(List<BeanPair> beanPairs) {
Expand Down Expand Up @@ -82,19 +79,15 @@ public BeanMatch getBeanMatch(BeanPair beanPair) {
}

public BeanMatch addBeanMatch(BeanMatch beanMatch) {
Map<Class<?>, BeanMatch> targetsForSource = store.get(beanMatch.getSourceClass());
if (targetsForSource == null) {
targetsForSource = new HashMap<>();
store.put(beanMatch.getSourceClass(), targetsForSource);
}
Map<Class<?>, BeanMatch> targetsForSource = store.computeIfAbsent(beanMatch.getSourceClass(), k -> new HashMap<>());
targetsForSource.put(beanMatch.getTargetClass(), beanMatch);
return beanMatch;
}

private BeanMatch determineBeanMatch(BeanPair beanPair) {

Map<String, BeanProperty> sourceNode = new HashMap<>();
Map<String, BeanProperty>targetNode = new HashMap<>();
Map<String, BeanProperty> targetNode = new HashMap<>();
Map<String, BeanProperty> aliases = new HashMap<>();

return new BeanMatch(
Expand All @@ -106,16 +99,21 @@ private BeanMatch determineBeanMatch(BeanPair beanPair) {
beanPair.getSourceClass(),
beanPair.getTargetClass(),
null,
BeanPropertyMatchupDirection.SOURCE_TO_TARGET),

BeanPropertyMatchupDirection.SOURCE_TO_TARGET
),
getAllFields(
targetNode,
sourceNode,
aliases,
beanPair.getTargetClass(),
beanPair.getSourceClass(),
null,
BeanPropertyMatchupDirection.TARGET_TO_SOURCE),
aliases);

BeanPropertyMatchupDirection.TARGET_TO_SOURCE
),
aliases
);
}

private Map<String, BeanProperty> getAllFields(Map<String, BeanProperty> ourNodes, Map<String, BeanProperty> otherNodes, Map<String, BeanProperty> aliases,
Expand All @@ -126,46 +124,28 @@ private Map<String, BeanProperty> getAllFields(Map<String, BeanProperty> ourNode
for (PropertyAccessor accessor : accessors) {

BeanPropertyAccessType accessType = matchupDirection.accessType(accessor);
if (accessType == BeanPropertyAccessType.NO_ACCESS) {
continue;
}

// Ignore fields
if (accessor.isAnnotationPresent(BeanIgnore.class)) {
if (accessType == BeanPropertyAccessType.NO_ACCESS || accessor.isAnnotationPresent(BeanIgnore.class)) {
continue;
}

// BeanProperty allows the field to match with a field from the other side with a different name
// and even a different nesting level.
BeanPropertyWrapper beanPropertyWrapper;
try {
beanPropertyWrapper = dealWithBeanProperty(matchupDirection, otherNodes, otherType, accessor);
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
BeanPropertyWrapper beanPropertyWrapper = beanPropertySelector.dealWithBeanProperty(matchupDirection, otherNodes, otherType, accessor);

// Unwrap the fields which exist in the unwrap class
BeanProperty currentBeanProperty;
try {
currentBeanProperty = new BeanPropertyCreator(
matchupDirection, ourType, accessor.getName()).determineNodesForPath(precedingBeanProperty);
currentBeanProperty = new BeanPropertyCreator(matchupDirection, ourType, accessor.getName()).determineNodesForPath(precedingBeanProperty);
currentBeanProperty.setMustMatch(beanPropertyWrapper.isMustMatch());
} catch (BeanNoSuchPropertyException e) {
throw new BeanMissingPathException(ourType, accessor.getName(), e);
}

handleBeanCollectionAnnotation(
accessor.findAnnotation(BeanCollection.class),
currentBeanProperty,
matchupDirection);
handleBeanCollectionAnnotation(accessor.findAnnotation(BeanCollection.class), currentBeanProperty, matchupDirection);

handleBeanRoleSecuredAnnotation(
currentBeanProperty,
accessor.findAnnotation(BeanRoleSecured.class));
handleBeanRoleSecuredAnnotation(currentBeanProperty, accessor.findAnnotation(BeanRoleSecured.class));

handleBeanLogicSecuredAnnotation(
currentBeanProperty,
accessor.findAnnotation(BeanLogicSecured.class));
handleBeanLogicSecuredAnnotation(currentBeanProperty, accessor.findAnnotation(BeanLogicSecured.class));

if (accessor.isAnnotationPresent(BeanAlias.class)) {
BeanAlias beanAlias = accessor.findAnnotation(BeanAlias.class);
Expand All @@ -176,14 +156,7 @@ private Map<String, BeanProperty> getAllFields(Map<String, BeanProperty> ourNode
}

if (accessor.isAnnotationPresent(BeanUnwrap.class)) {
ourCurrentNodes = getAllFields(
ourCurrentNodes,
otherNodes,
aliases,
accessor.getType(),
otherType,
currentBeanProperty,
matchupDirection);
ourCurrentNodes = getAllFields(ourCurrentNodes, otherNodes, aliases, accessor.getType(), otherType, currentBeanProperty, matchupDirection);
} else {
ourCurrentNodes.put(beanPropertyWrapper.getName(), currentBeanProperty);
}
Expand All @@ -205,10 +178,7 @@ private void handleBeanRoleSecuredAnnotation(BeanProperty beanProperty, BeanRole
beanProperty.setRequiredRoles(beanRoleSecured.value());
}

private void handleBeanCollectionAnnotation(
BeanCollection beanCollection,
BeanProperty beanProperty,
BeanPropertyMatchupDirection matchupDirection) {
private void handleBeanCollectionAnnotation(BeanCollection beanCollection, BeanProperty beanProperty, BeanPropertyMatchupDirection matchupDirection) {

CollectionElementType elementType = EMPTY_COLLECTION_ELEMENT_TYPE;
BeanCollectionUsage beanCollectionUsage = null;
Expand Down Expand Up @@ -238,8 +208,7 @@ private void handleBeanCollectionAnnotation(
if (collectionHandler == null) {
collectionHandler = getCollectionHandlerFor(beanProperty);
}
Class<?> genericClassOfField =
beanProperty.getGenericClassOfField(collectionHandler.getGenericParameterIndex());
Class<?> genericClassOfField = beanProperty.getGenericClassOfField(collectionHandler.getGenericParameterIndex());
if (genericClassOfField != null) {
elementType = derived(genericClassOfField);
}
Expand All @@ -256,93 +225,4 @@ private void handleBeanCollectionAnnotation(
private CollectionHandler getCollectionHandlerFor(BeanProperty beanProperty) {
return collectionHandlerStore.getCollectionHandlerFor(beanProperty.getAccessor().getType(), beanUnproxy);
}

private BeanPropertyWrapper dealWithBeanProperty(BeanPropertyMatchupDirection matchupDirection, Map<String, BeanProperty> otherNodes, Class<?> otherType,
PropertyAccessor accessor) throws IntrospectionException {
BeanPropertyWrapper wrapper = new BeanPropertyWrapper(accessor.getName());
if (accessor.isAnnotationPresent(io.beanmapper.annotations.BeanProperty.class)) {
io.beanmapper.annotations.BeanProperty beanProperty = accessor.findAnnotation(io.beanmapper.annotations.BeanProperty.class);

detectBeanPropertyFieldShadowing(accessor, beanProperty);

wrapper.setMustMatch();
wrapper.setName(getBeanPropertyName(beanProperty));
// Get the other field from the location that is specified in the beanProperty annotation.
// If the field is referred to by a path, store the custom field in the other map
try {
otherNodes.put(
wrapper.getName(),
new BeanPropertyCreator(matchupDirection.getInverse(), otherType, wrapper.getName())
.determineNodesForPath());
} catch (BeanNoSuchPropertyException err) {
BeanMapperTraceLogger.log("""
BeanNoSuchPropertyException thrown by BeanMatchStore#dealWithBeanProperty(BeanPropertyMatchupDirection, Map<String, BeanProperty>, Class, PropertyAccessor), for {}.
{}""", wrapper.getName(), err.getMessage());
}
}
return wrapper;
}

private String getBeanPropertyName(io.beanmapper.annotations.BeanProperty annotation) {
return annotation.value().isBlank() ? annotation.name() : annotation.value();
}

/**
* Detects whether a field annotated with the BeanProperty-annotation shadows an existing, accessible field.
*
* In this context, an accessible field is any field which is either public, or exposes a public
* accessor-method.
*
* @param accessor The accessor that can be used to access to the value within the field.
* @param beanProperty The BeanProperty-annotation annotating the relevant field.
* @throws IntrospectionException May be thrown whenever an Exception occurs during the introspection of the
* relevant bean.
*/
private void detectBeanPropertyFieldShadowing(final PropertyAccessor accessor, final io.beanmapper.annotations.BeanProperty beanProperty)
throws IntrospectionException {
var beanPropertyName = getBeanPropertyName(beanProperty);

var fields = accessor.getDeclaringClass().getDeclaredFields();
for (var field : fields) {
var fieldName = field.getName();
if (!fieldName.equals(accessor.getName())
&& !field.isAnnotationPresent(BeanIgnore.class)
&& fieldName.equals(beanPropertyName)
&& (Modifier.isPublic(field.getModifiers())
|| hasAccessibleWriteMethod(field))) {
if (field.isAnnotationPresent(io.beanmapper.annotations.BeanProperty.class)) {
var fieldBeanProperty = field.getAnnotation(io.beanmapper.annotations.BeanProperty.class);
if (getBeanPropertyName(fieldBeanProperty).equals(beanPropertyName))
throw new FieldShadowingException(
String.format("%s %s.%s shadows %s.%s.", beanProperty, accessor.getDeclaringClass().getName(), accessor.getName(),
field.getDeclaringClass().getName(), fieldName));
} else {
throw new FieldShadowingException(
String.format("%s %s.%s shadows %s.%s.", beanProperty, accessor.getDeclaringClass().getName(), accessor.getName(),
field.getDeclaringClass().getName(), fieldName));
}
}
}
}

/**
* Determines whether a field exposes an accessible mutator-method.
*
* <p>This method first retrieves the array of PropertyDescriptors, and turns it into a Stream. If any of the
* PropertyDescriptor-objects have the same name as the field, this method returns true. If not, this method returns
* false.</p>
*
* In this context, an accessible mutator-method is any method which is public and adheres to the Java
* Beans definition of a mutator.
*
* @param field The Field of which the method attempts to find a mutator-method.
* @return True, if the field exposes an accessible mutator, false otherwise.
* @throws IntrospectionException May be thrown whenever an Exception occurs during the introspection of the
* relevant bean.
*/
private boolean hasAccessibleWriteMethod(final Field field) throws IntrospectionException {
return Arrays.stream(Introspector.getBeanInfo(field.getDeclaringClass()).getPropertyDescriptors())
.anyMatch(propertyDescriptor -> propertyDescriptor.getName().equals(field.getName()));
}

}
Loading

0 comments on commit 2b0e3c3

Please sign in to comment.