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..669e058c --- /dev/null +++ b/src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java @@ -0,0 +1,106 @@ +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 static final String EXPRESSION_PREFFIX = "#{"; + private static final String EXPRESSION_SUFFIX = "}"; + + 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) { + // 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); + } + } + + // Otherwise concatenate the evaluations in a string value + 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(EXPRESSION_PREFFIX) && remainder.contains(EXPRESSION_SUFFIX)) { + int endIndex = remainder.indexOf(EXPRESSION_SUFFIX); + 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..44df9b0a 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,21 @@ 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.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()); + } + 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..c473bed2 --- /dev/null +++ b/src/test/java/io/beanmapper/testmodel/projection/PersonProjection.java @@ -0,0 +1,20 @@ +package io.beanmapper.testmodel.projection; + +import io.beanmapper.annotations.BeanExpression; + +/** + * + * + * @author jeroen + * @since Apr 21, 2016 + */ +public interface PersonProjection { + + Long getId(); + + String getName(); + + @BeanExpression("#{name} #{place}") + String getNameAndPlace(); + +}