Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #188: Make BeanProperty repeatable #206

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
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
Loading