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

can now map to interface #57

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
21 changes: 21 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<properties>
<junit.version>4.10</junit.version>
<slf4j.version>1.7.2</slf4j.version>
<spring.version>4.1.6.RELEASE</spring.version>
<!-- Reporting -->
<maven.cobertura.version>2.4</maven.cobertura.version>
<maven.javadoc.version>2.10.3</maven.javadoc.version>
Expand Down Expand Up @@ -72,6 +73,26 @@
<version>2.5.2</version>
<scope>test</scope>
</dependency>

<!-- Expressions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>

<!-- Used for dynamic creation of bean classes -->
<dependency>
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/io/beanmapper/annotations/BeanExpression.java
Original file line number Diff line number Diff line change
@@ -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();
}
12 changes: 11 additions & 1 deletion src/main/java/io/beanmapper/strategy/MapStrategyType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
106 changes: 106 additions & 0 deletions src/main/java/io/beanmapper/strategy/MapToInterfaceStrategy.java
Original file line number Diff line number Diff line change
@@ -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> T buildNewProxy(Object instance, Class<T> 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);
}

}

}
54 changes: 46 additions & 8 deletions src/test/java/io/beanmapper/BeanMapperTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

}