From e2ec2c48786c2e85d47a6b05cfea494ee40347f2 Mon Sep 17 00:00:00 2001 From: Jeroen van Schagen Date: Thu, 21 Apr 2016 16:02:57 +0200 Subject: [PATCH 1/3] can now map to interface --- pom.xml | 21 +++++ .../annotations/BeanExpression.java | 16 ++++ .../beanmapper/strategy/MapStrategyType.java | 12 ++- .../strategy/MapToInterfaceStrategy.java | 90 +++++++++++++++++++ .../java/io/beanmapper/BeanMapperTest.java | 52 +++++++++-- .../projection/PersonProjection.java | 18 ++++ 6 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 src/main/java/io/beanmapper/annotations/BeanExpression.java create mode 100644 src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java create mode 100644 src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java diff --git a/pom.xml b/pom.xml index 94498497..53d5c9c5 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ 4.10 1.7.2 + 4.1.6.RELEASE 2.4 2.10.3 @@ -72,6 +73,26 @@ 2.5.2 test + + + + org.springframework + spring-beans + ${spring.version} + provided + + + org.springframework + spring-expression + ${spring.version} + provided + + + org.springframework + spring-aop + ${spring.version} + provided + diff --git a/src/main/java/io/beanmapper/annotations/BeanExpression.java b/src/main/java/io/beanmapper/annotations/BeanExpression.java new file mode 100644 index 00000000..812f182e --- /dev/null +++ b/src/main/java/io/beanmapper/annotations/BeanExpression.java @@ -0,0 +1,16 @@ +package io.beanmapper.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares evaluation expressions. + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface BeanExpression { + + String value(); +} diff --git a/src/main/java/io/beanmapper/strategy/MapStrategyType.java b/src/main/java/io/beanmapper/strategy/MapStrategyType.java index d749b6c6..ea91617b 100644 --- a/src/main/java/io/beanmapper/strategy/MapStrategyType.java +++ b/src/main/java/io/beanmapper/strategy/MapStrategyType.java @@ -23,6 +23,12 @@ public MapStrategy generateMapStrategy(BeanMapper beanMapper, Configuration conf return new MapToClassStrategy(beanMapper, configuration); } }, + MAP_TO_INTERFACE() { + @Override + public MapStrategy generateMapStrategy(BeanMapper beanMapper, Configuration configuration) { + return new MapToInterfaceStrategy(beanMapper, configuration); + } + }, MAP_TO_INSTANCE() { @Override public MapStrategy generateMapStrategy(BeanMapper beanMapper, Configuration configuration) { @@ -40,7 +46,11 @@ public static MapStrategyType determineStrategy(Configuration configuration) { } else if (configuration.getCollectionClass() != null) { return MAP_COLLECTION; } else if (configuration.getTargetClass() != null) { - return MAP_TO_CLASS; + if (configuration.getTargetClass().isInterface()) { + return MAP_TO_INTERFACE; + } else { + return MAP_TO_CLASS; + } } else if (configuration.getTarget() != null) { return MAP_TO_INSTANCE; } else { diff --git a/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java new file mode 100644 index 00000000..f9d8a75c --- /dev/null +++ b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java @@ -0,0 +1,90 @@ +package io.beanmapper.strategy; + +import io.beanmapper.BeanMapper; +import io.beanmapper.annotations.BeanExpression; +import io.beanmapper.config.Configuration; + +import java.lang.reflect.Method; + +/** + * + * + * @author jeroen + * @since Apr 21, 2016 + */ +public class MapToInterfaceStrategy extends MapToInstanceStrategy { + + public MapToInterfaceStrategy(BeanMapper beanMapper, Configuration configuration) { + super(beanMapper, configuration); + } + + @Override + public Object map(Object source) { + Class targetClass = getConfiguration().getTargetClass(); + return buildNewProxy(source, targetClass); + } + + private T buildNewProxy(Object instance, Class interfaceClass) { + org.springframework.aop.framework.ProxyFactory proxyFactory = new org.springframework.aop.framework.ProxyFactory(); + proxyFactory.addInterface(interfaceClass); + proxyFactory.addAdvisor(new org.springframework.aop.support.DefaultPointcutAdvisor(new AlwaysPointcut(), new MappingInterceptor(instance))); + return (T) proxyFactory.getProxy(); + } + + public static class AlwaysPointcut extends org.springframework.aop.support.StaticMethodMatcherPointcut { + + @Override + public boolean matches(Method method, Class targetClass) { + return true; + } + + } + + public static class MappingInterceptor implements org.aopalliance.intercept.MethodInterceptor { + + private final org.springframework.expression.spel.standard.SpelExpressionParser parser; + + private final Object source; + + public MappingInterceptor(Object source) { + this.source = source; + + org.springframework.expression.spel.SpelParserConfiguration config = new org.springframework.expression.spel.SpelParserConfiguration(true, true); + parser = new org.springframework.expression.spel.standard.SpelExpressionParser(config); + } + + @Override + public Object invoke(org.aopalliance.intercept.MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + BeanExpression[] expressions = method.getDeclaredAnnotationsByType(BeanExpression.class); + if (expressions.length == 1) { + return evaluate(expressions[0].value()); + } else { + return evaluate("#{" + method.getName() + "()}"); + } + } + + private Object evaluate(String expressions) { + StringBuilder result = new StringBuilder(); + for (int index = 0; index < expressions.length(); index++) { + String remainder = expressions.substring(index); + if (remainder.startsWith("#{") && remainder.contains("}")) { + int endIndex = remainder.indexOf("}"); + String rawExpression = remainder.substring(2, endIndex); + result.append(evaluateSingle(rawExpression)); + index += endIndex; + } else { + result.append(expressions.charAt(index)); + } + } + return result.toString(); + } + + private Object evaluateSingle(String rawExpression) { + org.springframework.expression.Expression expression = parser.parseExpression(rawExpression); + return expression.getValue(source); + } + + } + +} diff --git a/src/test/java/io/beanmapper/BeanMapperTest.java b/src/test/java/io/beanmapper/BeanMapperTest.java index dc25c84c..0d2f5fd8 100644 --- a/src/test/java/io/beanmapper/BeanMapperTest.java +++ b/src/test/java/io/beanmapper/BeanMapperTest.java @@ -1,5 +1,8 @@ package io.beanmapper; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import io.beanmapper.config.BeanMapperBuilder; import io.beanmapper.core.converter.impl.LocalDateTimeToLocalDate; import io.beanmapper.core.converter.impl.LocalDateToLocalDateTime; @@ -11,7 +14,15 @@ import io.beanmapper.testmodel.beanAlias.NestedSourceWithAlias; import io.beanmapper.testmodel.beanAlias.SourceWithAlias; import io.beanmapper.testmodel.beanAlias.TargetWithAlias; -import io.beanmapper.testmodel.collections.*; +import io.beanmapper.testmodel.collections.CollectionListSource; +import io.beanmapper.testmodel.collections.CollectionListTarget; +import io.beanmapper.testmodel.collections.CollectionListTargetClear; +import io.beanmapper.testmodel.collections.CollectionMapSource; +import io.beanmapper.testmodel.collections.CollectionMapTarget; +import io.beanmapper.testmodel.collections.CollectionSetSource; +import io.beanmapper.testmodel.collections.CollectionSetTarget; +import io.beanmapper.testmodel.collections.SourceWithListGetter; +import io.beanmapper.testmodel.collections.TargetWithListPublicField; import io.beanmapper.testmodel.construct.NestedSourceWithoutConstruct; import io.beanmapper.testmodel.construct.SourceWithConstruct; import io.beanmapper.testmodel.construct.TargetWithoutConstruct; @@ -30,7 +41,13 @@ import io.beanmapper.testmodel.emptyobject.EmptySource; import io.beanmapper.testmodel.emptyobject.EmptyTarget; import io.beanmapper.testmodel.emptyobject.NestedEmptyTarget; -import io.beanmapper.testmodel.encapsulate.*; +import io.beanmapper.testmodel.encapsulate.Address; +import io.beanmapper.testmodel.encapsulate.Country; +import io.beanmapper.testmodel.encapsulate.House; +import io.beanmapper.testmodel.encapsulate.ResultAddress; +import io.beanmapper.testmodel.encapsulate.ResultManyToMany; +import io.beanmapper.testmodel.encapsulate.ResultManyToOne; +import io.beanmapper.testmodel.encapsulate.ResultOneToMany; import io.beanmapper.testmodel.encapsulate.sourceAnnotated.Car; import io.beanmapper.testmodel.encapsulate.sourceAnnotated.CarDriver; import io.beanmapper.testmodel.encapsulate.sourceAnnotated.Driver; @@ -62,6 +79,7 @@ import io.beanmapper.testmodel.person.PersonView; import io.beanmapper.testmodel.project.CodeProject; import io.beanmapper.testmodel.project.CodeProjectResult; +import io.beanmapper.testmodel.projection.PersonProjection; import io.beanmapper.testmodel.publicfields.SourceWithPublicFields; import io.beanmapper.testmodel.publicfields.TargetWithPublicFields; import io.beanmapper.testmodel.rule.NestedWithRule; @@ -75,20 +93,25 @@ import io.beanmapper.testmodel.similarsubclasses.SimilarSubclass; import io.beanmapper.testmodel.tostring.SourceWithNonString; import io.beanmapper.testmodel.tostring.TargetWithString; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.TreeSet; + import mockit.Expectations; import mockit.Mocked; import mockit.Verifications; + +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.*; - -import static org.junit.Assert.*; - public class BeanMapperTest { private BeanMapper beanMapper; @@ -850,6 +873,19 @@ public void overrideConverterTest() { beanMapper.map(source, TargetWithDateTime.class); } + @Test + public void sourceToInterfaceWithExpression() { + BeanMapper beanMapper = new BeanMapperBuilder().addPackagePrefix(BeanMapper.class).build(); + + Person person = new Person(); + person.setName("Jan"); + person.setPlace("Zoetermeer"); + + PersonProjection projection = beanMapper.map(person, PersonProjection.class); + Assert.assertEquals("Jan", projection.getName()); + Assert.assertEquals("Jan Zoetermeer", projection.getNameAndPlace()); + } + public Person createPerson(String name) { Person person = new Person(); person.setId(1984L); diff --git a/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java b/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java new file mode 100644 index 00000000..c09df3ee --- /dev/null +++ b/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java @@ -0,0 +1,18 @@ +package io.beanmapper.testmodel.projection; + +import io.beanmapper.annotations.BeanExpression; + +/** + * + * + * @author jeroen + * @since Apr 21, 2016 + */ +public interface PersonProjection { + + String getName(); + + @BeanExpression("#{name} #{place}") + String getNameAndPlace(); + +} From e25eedb16976c06a5960d84d60ea6203713f4236 Mon Sep 17 00:00:00 2001 From: Jeroen van Schagen Date: Thu, 21 Apr 2016 16:24:51 +0200 Subject: [PATCH 2/3] can now perform single evaluations too while preserving the original value type --- .../strategy/MapToInterfaceStrategy.java | 18 ++++++++++++++++-- .../java/io/beanmapper/BeanMapperTest.java | 2 ++ .../testmodel/projection/PersonProjection.java | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java index f9d8a75c..5ae6e609 100644 --- a/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java +++ b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java @@ -42,6 +42,9 @@ public boolean matches(Method method, Class targetClass) { public static class MappingInterceptor implements org.aopalliance.intercept.MethodInterceptor { + private static final String EXPRESSION_PREFFIX = "#{"; + private static final String EXPRESSION_SUFFIX = "}"; + private final org.springframework.expression.spel.standard.SpelExpressionParser parser; private final Object source; @@ -65,11 +68,22 @@ public Object invoke(org.aopalliance.intercept.MethodInvocation invocation) thro } private Object evaluate(String expressions) { + // Attempt to evaluate as a single value + if (expressions.startsWith(EXPRESSION_PREFFIX) && expressions.endsWith(EXPRESSION_SUFFIX)) { + String rawExpression = expressions.substring(2, expressions.length() - 1); + if (!rawExpression.contains(EXPRESSION_PREFFIX)) { + return evaluateSingle(rawExpression); + } + } + return evaluateConcatenated(expressions); + } + + private Object evaluateConcatenated(String expressions) { StringBuilder result = new StringBuilder(); for (int index = 0; index < expressions.length(); index++) { String remainder = expressions.substring(index); - if (remainder.startsWith("#{") && remainder.contains("}")) { - int endIndex = remainder.indexOf("}"); + if (remainder.startsWith(EXPRESSION_PREFFIX) && remainder.contains(EXPRESSION_SUFFIX)) { + int endIndex = remainder.indexOf(EXPRESSION_SUFFIX); String rawExpression = remainder.substring(2, endIndex); result.append(evaluateSingle(rawExpression)); index += endIndex; diff --git a/src/test/java/io/beanmapper/BeanMapperTest.java b/src/test/java/io/beanmapper/BeanMapperTest.java index 0d2f5fd8..44df9b0a 100644 --- a/src/test/java/io/beanmapper/BeanMapperTest.java +++ b/src/test/java/io/beanmapper/BeanMapperTest.java @@ -878,10 +878,12 @@ public void sourceToInterfaceWithExpression() { BeanMapper beanMapper = new BeanMapperBuilder().addPackagePrefix(BeanMapper.class).build(); Person person = new Person(); + person.setId(42L); person.setName("Jan"); person.setPlace("Zoetermeer"); PersonProjection projection = beanMapper.map(person, PersonProjection.class); + Assert.assertEquals(Long.valueOf(42), projection.getId()); Assert.assertEquals("Jan", projection.getName()); Assert.assertEquals("Jan Zoetermeer", projection.getNameAndPlace()); } diff --git a/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java b/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java index c09df3ee..c473bed2 100644 --- a/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java +++ b/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java @@ -10,6 +10,8 @@ */ public interface PersonProjection { + Long getId(); + String getName(); @BeanExpression("#{name} #{place}") From c5ab61cd46c26bda563a30373edb4689e2488f03 Mon Sep 17 00:00:00 2001 From: Jeroen van Schagen Date: Thu, 21 Apr 2016 16:26:19 +0200 Subject: [PATCH 3/3] updated comments --- .../java/io/beanmapper/strategy/MapToInterfaceStrategy.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java index 5ae6e609..669e058c 100644 --- a/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java +++ b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java @@ -75,6 +75,8 @@ private Object evaluate(String expressions) { return evaluateSingle(rawExpression); } } + + // Otherwise concatenate the evaluations in a string value return evaluateConcatenated(expressions); }