├── .gitignore ├── src ├── test │ ├── java │ │ └── io │ │ │ └── beanmapper │ │ │ └── spring │ │ │ ├── unproxy │ │ │ ├── Shop.java │ │ │ ├── HibernateProxyContainer.java │ │ │ └── HibernateAwareBeanUnproxyTest.java │ │ │ ├── model │ │ │ ├── Spoon.java │ │ │ ├── Tag.java │ │ │ ├── ArtistDto.java │ │ │ ├── PersonView.java │ │ │ ├── AssetDto.java │ │ │ ├── OrganizationDto.java │ │ │ ├── PersonDto.java │ │ │ ├── PersonRepository.java │ │ │ ├── PersonForm.java │ │ │ ├── SpoonDrawerForm.java │ │ │ ├── Artist.java │ │ │ ├── ProductDto.java │ │ │ ├── BaseEntity.java │ │ │ ├── Asset.java │ │ │ ├── Organization.java │ │ │ ├── Product.java │ │ │ └── Person.java │ │ │ ├── web │ │ │ ├── mockmvc │ │ │ │ ├── fakedomain │ │ │ │ │ ├── FakeForm.java │ │ │ │ │ ├── FakeResult.java │ │ │ │ │ ├── ContainingFakeService.java │ │ │ │ │ ├── ContainingFakeResult.java │ │ │ │ │ ├── FakeRepository.java │ │ │ │ │ ├── ContainingFakeForm.java │ │ │ │ │ ├── ContainingFake.java │ │ │ │ │ ├── FakeBuildCommand.java │ │ │ │ │ ├── FakeService.java │ │ │ │ │ ├── Populator.java │ │ │ │ │ ├── FakeBuilder.java │ │ │ │ │ ├── FakeApplicationConfig.java │ │ │ │ │ ├── Fake.java │ │ │ │ │ ├── ContainingFakeController.java │ │ │ │ │ ├── FakeWebMvcConfig.java │ │ │ │ │ └── FakeControllerTest.java │ │ │ │ ├── FakeController.java │ │ │ │ ├── AbstractControllerTest.java │ │ │ │ └── ContainingFakeControllerTest.java │ │ │ ├── EntityFinderTest.java │ │ │ ├── PersonController.java │ │ │ ├── SpringDataEntityFinderTest.java │ │ │ └── PersonControllerTest.java │ │ │ ├── flusher │ │ │ ├── CollTarget.java │ │ │ ├── CollSource.java │ │ │ └── JpaAfterClearFlusherTest.java │ │ │ ├── exceptions │ │ │ ├── ClassExpectationNotRegisteredExceptionTest.java │ │ │ └── PrincipalIsNoInstanceOfUserDetailsExceptionTest.java │ │ │ ├── converter │ │ │ ├── BeanMapperConverterAdapterTest.java │ │ │ ├── ConversionServiceBeanConverterTest.java │ │ │ └── IdToEntityBeanConverterTest.java │ │ │ ├── AbstractSpringTest.java │ │ │ ├── PageableMapperTest.java │ │ │ └── ApplicationConfig.java │ └── resources │ │ └── log4j.properties └── main │ └── java │ └── io │ └── beanmapper │ └── spring │ ├── security │ ├── SpringRoleSecuredCheck.java │ └── AbstractSpringSecuredCheck.java │ ├── exceptions │ ├── ClassExpectationNotRegisteredException.java │ └── PrincipalIsNoInstanceOfUserDetailsException.java │ ├── web │ ├── converter │ │ ├── StructuredBody.java │ │ └── StructuredJsonMessageConverter.java │ ├── mockmvc │ │ ├── MockEntityConverter.java │ │ ├── MockIdToEntityBeanConverter.java │ │ ├── MockEntityFinder.java │ │ └── MockMvcBeanMapper.java │ ├── EntityFinder.java │ ├── MergedForm.java │ ├── WebRequestParameters.java │ ├── MergePair.java │ ├── SpringDataEntityFinder.java │ └── MergedFormMethodArgumentResolver.java │ ├── Lazy.java │ ├── unproxy │ └── HibernateAwareBeanUnproxy.java │ ├── flusher │ └── JpaAfterClearFlusher.java │ ├── converter │ ├── BeanMapperConverterAdapter.java │ ├── ConversionServiceBeanConverter.java │ └── IdToEntityBeanConverter.java │ ├── util │ └── JsonUtil.java │ └── PageableMapper.java ├── owasp-suppressions.xml ├── .github └── workflows │ ├── maven.yml │ └── release.yml ├── README.md ├── CHANGELOG.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .settings 3 | .project 4 | /target/ 5 | *.iml 6 | .idea 7 | .java-version 8 | .claude 9 | pom.xml.versionsBackup -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/unproxy/Shop.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.unproxy; 2 | 3 | public class Shop { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Spoon.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public record Spoon(boolean polished) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Tag.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public enum Tag { 4 | CUSTOMER, 5 | DEBTOR, 6 | UPSELLING 7 | } 8 | -------------------------------------------------------------------------------- /owasp-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeForm.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | public class FakeForm { 4 | 5 | public String name; 6 | } 7 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/ArtistDto.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class ArtistDto { 4 | 5 | public Long id; 6 | 7 | public String name; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/PersonView.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class PersonView { 4 | 5 | public String name; 6 | 7 | public String place; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeResult.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | public class FakeResult { 4 | 5 | public Long id; 6 | public String name; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/AssetDto.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class AssetDto { 4 | 5 | public Long id; 6 | 7 | public String name; 8 | 9 | public String isrc; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/OrganizationDto.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class OrganizationDto { 4 | 5 | public Long id; 6 | 7 | public String name; 8 | 9 | public String contact; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/flusher/CollTarget.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.flusher; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class CollTarget { 7 | 8 | public List items = new ArrayList<>(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/security/SpringRoleSecuredCheck.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.security; 2 | 3 | import io.beanmapper.config.RoleSecuredCheck; 4 | 5 | public class SpringRoleSecuredCheck extends AbstractSpringSecuredCheck implements RoleSecuredCheck { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/PersonDto.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class PersonDto { 4 | 5 | public Long id; 6 | 7 | public String name; 8 | 9 | public String street; 10 | 11 | public String houseNumber; 12 | 13 | public String city; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/ContainingFakeService.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | public class ContainingFakeService { 4 | 5 | public ContainingFake create(ContainingFake containingFake) { 6 | return containingFake; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 2 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 3 | log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n 4 | 5 | log4j.rootCategory=INFO, stdout 6 | log4j.logger.com.mangofactory.swagger=WARN, stdout 7 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/ContainingFakeResult.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import io.beanmapper.annotations.BeanProperty; 4 | 5 | public class ContainingFakeResult { 6 | 7 | @BeanProperty("fake.name") 8 | public String fakeName; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/exceptions/ClassExpectationNotRegisteredException.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.exceptions; 2 | 3 | public class ClassExpectationNotRegisteredException extends RuntimeException { 4 | 5 | public ClassExpectationNotRegisteredException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeRepository.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface FakeRepository extends JpaRepository { 8 | 9 | Optional findByName(String name); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/PersonRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.model; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | public interface PersonRepository extends JpaRepository { 9 | 10 | Person findByName(String string); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/PersonForm.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | import java.util.List; 4 | 5 | import io.beanmapper.annotations.BeanCollection; 6 | 7 | public class PersonForm { 8 | 9 | public String name; 10 | 11 | public String city; 12 | 13 | @BeanCollection(elementType = Tag.class) 14 | public List tags; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/ContainingFakeForm.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import io.beanmapper.annotations.BeanIgnore; 4 | import io.beanmapper.annotations.BeanProperty; 5 | 6 | public class ContainingFakeForm { 7 | 8 | @BeanProperty("fake") 9 | public Long fakeId; 10 | 11 | @BeanIgnore 12 | public String passMe; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/converter/StructuredBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.web.converter; 5 | 6 | import java.util.Set; 7 | 8 | /** 9 | * Wrapper for a read request body. 10 | * 11 | * @author Jeroen van Schagen 12 | * @since Nov 25, 2015 13 | */ 14 | public record StructuredBody(Object body, Set propertyNames) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/exceptions/PrincipalIsNoInstanceOfUserDetailsException.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.exceptions; 2 | 3 | public class PrincipalIsNoInstanceOfUserDetailsException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "The Security Principal is not an instance of UserDetails"; 6 | 7 | public PrincipalIsNoInstanceOfUserDetailsException() { 8 | super(MESSAGE); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/SpoonDrawerForm.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | import java.util.List; 4 | 5 | import io.beanmapper.annotations.BeanCollection; 6 | 7 | public class SpoonDrawerForm { 8 | 9 | @BeanCollection(elementType = Spoon.class) 10 | public List spoons; 11 | 12 | public static class SpoonForm { 13 | public Long id; 14 | public Boolean polished; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/flusher/CollSource.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.flusher; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import io.beanmapper.annotations.BeanCollection; 7 | import io.beanmapper.utils.Trinary; 8 | 9 | public class CollSource { 10 | 11 | @BeanCollection(elementType = CollTarget.class, flushAfterClear = Trinary.ENABLED) 12 | public List items = new ArrayList<>(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/Lazy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring; 5 | 6 | /** 7 | * Wrapper on object to retrieve only when desired. 8 | * 9 | * @author Jeroen van Schagen 10 | * @since Nov 13, 2015 11 | */ 12 | public interface Lazy { 13 | 14 | /** 15 | * Retrieve the entity instance. 16 | * @return the entity instance 17 | */ 18 | T get() throws Exception; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/ContainingFake.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | 5 | public class ContainingFake { 6 | 7 | private Fake fake; 8 | 9 | @NotNull 10 | private String mustNotBeNull; 11 | 12 | public Fake getFake() { 13 | return fake; 14 | } 15 | 16 | public void setFake(Fake fake) { 17 | this.fake = fake; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Artist.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class Artist { 4 | 5 | private Long id; 6 | 7 | private String name; 8 | 9 | public Long getId() { 10 | return id; 11 | } 12 | 13 | public void setId(Long id) { 14 | this.id = id; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | call-workflow: 14 | uses: 42BV/42-github-workflows/.github/workflows/maven-test.yml@main 15 | with: 16 | java-version: 21 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/exceptions/ClassExpectationNotRegisteredExceptionTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.exceptions; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertThrows; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class ClassExpectationNotRegisteredExceptionTest { 8 | 9 | @Test 10 | void classExpectationNotRegisteredExceptionTest() { 11 | assertThrows(ClassExpectationNotRegisteredException.class, () -> {throw new ClassExpectationNotRegisteredException("Test");}); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeBuildCommand.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import nl._42.heph.AbstractBuildCommand; 4 | 5 | public interface FakeBuildCommand extends AbstractBuildCommand { 6 | 7 | @Override 8 | default Fake findEntity(Fake entity) { 9 | return getRepository().findByName(entity.getName()).orElse(null); 10 | } 11 | 12 | FakeBuildCommand withName(String name); 13 | 14 | FakeBuildCommand withId(Long id); 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeService.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | @Service 6 | public class FakeService { 7 | 8 | public Fake read(Fake fake) { 9 | return fake; 10 | } 11 | 12 | public Fake create(Fake fake) { 13 | return fake; 14 | } 15 | 16 | public Fake update(Fake fake) { 17 | return fake; 18 | } 19 | 20 | public Fake delete(Fake fake) { 21 | return fake; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/exceptions/PrincipalIsNoInstanceOfUserDetailsExceptionTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.exceptions; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertThrows; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class PrincipalIsNoInstanceOfUserDetailsExceptionTest { 8 | 9 | @Test 10 | void testPrincipalIsNoInstanceOfUserDetailsExceptionTest() { 11 | assertThrows(PrincipalIsNoInstanceOfUserDetailsException.class, () -> { throw new PrincipalIsNoInstanceOfUserDetailsException(); }); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/ProductDto.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | import java.util.List; 4 | 5 | import io.beanmapper.annotations.BeanCollection; 6 | 7 | public class ProductDto { 8 | 9 | public Long id; 10 | 11 | public String name; 12 | 13 | public String upc; 14 | 15 | @BeanCollection(elementType = AssetDto.class) 16 | public List assets; 17 | 18 | @BeanCollection(elementType = ArtistDto.class) 19 | public List artists; 20 | 21 | public OrganizationDto organization; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/Populator.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class Populator { 9 | 10 | private final FakeBuilder fakeBuilder; 11 | 12 | public Populator(FakeBuilder fakeBuilder) { 13 | this.fakeBuilder = fakeBuilder; 14 | } 15 | 16 | @PostConstruct 17 | public void initData() { 18 | fakeBuilder.henk(); 19 | fakeBuilder.piet(); 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/unproxy/HibernateProxyContainer.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.unproxy; 2 | 3 | import org.hibernate.proxy.HibernateProxy; 4 | import org.hibernate.proxy.LazyInitializer; 5 | 6 | public class HibernateProxyContainer { 7 | 8 | public class GeneratedProxy extends Shop implements HibernateProxy { 9 | 10 | @Override 11 | public Object writeReplace() { 12 | return null; 13 | } 14 | 15 | @Override 16 | public LazyInitializer getHibernateLazyInitializer() { 17 | return null; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/BaseEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.model; 5 | 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.MappedSuperclass; 10 | 11 | @MappedSuperclass 12 | public class BaseEntity { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | private Long id; 17 | 18 | public Long getId() { 19 | return id; 20 | } 21 | 22 | public void setId(Long id) { 23 | this.id = id; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/unproxy/HibernateAwareBeanUnproxy.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.unproxy; 2 | 3 | import java.util.Arrays; 4 | 5 | import io.beanmapper.core.unproxy.BeanUnproxy; 6 | 7 | import org.hibernate.proxy.HibernateProxy; 8 | 9 | public class HibernateAwareBeanUnproxy implements BeanUnproxy { 10 | 11 | /** 12 | * {@inheritDoc} 13 | */ 14 | public Class unproxy(Class beanClass) { 15 | if (beanClass.getName().contains("$") 16 | && Arrays.asList(beanClass.getInterfaces()).contains(HibernateProxy.class)) { 17 | return beanClass.getSuperclass(); 18 | } 19 | return beanClass; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeBuilder.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import nl._42.heph.AbstractBuilder; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class FakeBuilder extends AbstractBuilder { 9 | 10 | @Override 11 | public FakeBuildCommand base() { 12 | return blank(); 13 | } 14 | 15 | public Fake henk() { 16 | return base() 17 | .withName("Henk") 18 | .create(); 19 | } 20 | 21 | public Fake piet() { 22 | return base() 23 | .withName("Piet") 24 | .create(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/flusher/JpaAfterClearFlusher.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.flusher; 2 | 3 | import jakarta.persistence.EntityManager; 4 | 5 | import io.beanmapper.config.AfterClearFlusher; 6 | 7 | /** 8 | * Specific AfterClearFlusher for flushing JPA's EntityManager. This is called by 9 | * BeanMapper after calling clear on a collection. 10 | */ 11 | public class JpaAfterClearFlusher implements AfterClearFlusher { 12 | 13 | private final EntityManager entityManager; 14 | 15 | public JpaAfterClearFlusher(EntityManager entityManager) { 16 | this.entityManager = entityManager; 17 | } 18 | 19 | @Override 20 | public void flush() { 21 | entityManager.flush(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/mockmvc/MockEntityConverter.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.data.domain.Persistable; 5 | import org.springframework.data.repository.CrudRepository; 6 | 7 | public class MockEntityConverter> implements Converter { 8 | 9 | private final CrudRepository repository; 10 | 11 | public MockEntityConverter(CrudRepository respository) { 12 | this.repository = respository; 13 | } 14 | 15 | @Override 16 | public T convert(String id) { 17 | return repository.findById(Long.valueOf(id)).orElse(null); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release-version: 7 | required: false 8 | description: Release-version (not required) 9 | next-version: 10 | required: false 11 | description: Next development-version. (not required) 12 | java-version: 13 | required: true 14 | default: '21' 15 | description: Java-version to use for the deployment. 16 | 17 | jobs: 18 | call-workflow: 19 | uses: 42BV/42-github-workflows/.github/workflows/maven-release.yml@main 20 | secrets: inherit 21 | with: 22 | release-version: ${{ github.event.inputs.release-version }} 23 | next-version: ${{ github.event.inputs.next-version }} 24 | java-version: ${{ github.event.inputs.java-version }} 25 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Asset.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class Asset { 4 | 5 | private Long id; 6 | 7 | private String name; 8 | 9 | private String isrc; 10 | 11 | private String internalMemo; 12 | 13 | public Long getId() { 14 | return id; 15 | } 16 | 17 | public void setId(Long id) { 18 | this.id = id; 19 | } 20 | 21 | public String getInternalMemo() { 22 | return internalMemo; 23 | } 24 | 25 | public void setInternalMemo(String internalMemo) { 26 | this.internalMemo = internalMemo; 27 | } 28 | 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | public void setName(String name) { 34 | this.name = name; 35 | } 36 | 37 | public String getIsrc() { 38 | return isrc; 39 | } 40 | 41 | public void setIsrc(String isrc) { 42 | this.isrc = isrc; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Organization.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | public class Organization { 4 | 5 | private Long id; 6 | 7 | private String name; 8 | 9 | private String contact; 10 | 11 | private String ourSecret; 12 | 13 | public Long getId() { 14 | return id; 15 | } 16 | 17 | public void setId(Long id) { 18 | this.id = id; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | 29 | public String getContact() { 30 | return contact; 31 | } 32 | 33 | public void setContact(String contact) { 34 | this.contact = contact; 35 | } 36 | 37 | public String getOurSecret() { 38 | return ourSecret; 39 | } 40 | 41 | public void setOurSecret(String ourSecret) { 42 | this.ourSecret = ourSecret; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import io.beanmapper.BeanMapper; 4 | import io.beanmapper.config.BeanMapperBuilder; 5 | import io.beanmapper.spring.converter.IdToEntityBeanConverter; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.annotation.Bean; 10 | 11 | public class FakeApplicationConfig { 12 | 13 | @Autowired 14 | private ApplicationContext applicationContext; 15 | 16 | @Bean 17 | public BeanMapper beanMapper() { 18 | BeanMapperBuilder bm = new BeanMapperBuilder() 19 | .addPackagePrefix(FakeApplicationConfig.class); 20 | if (applicationContext != null) { 21 | bm.addConverter(new IdToEntityBeanConverter(applicationContext)); 22 | } 23 | return bm.build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/converter/BeanMapperConverterAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.converter; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | import io.beanmapper.BeanMapper; 9 | import io.beanmapper.config.BeanMapperBuilder; 10 | import io.beanmapper.spring.model.Person; 11 | import io.beanmapper.spring.model.PersonView; 12 | 13 | import org.junit.jupiter.api.Test; 14 | import org.springframework.core.convert.support.DefaultConversionService; 15 | 16 | public class BeanMapperConverterAdapterTest { 17 | 18 | @Test 19 | public void testConvert() { 20 | BeanMapper beanMapper = new BeanMapperBuilder().build(); 21 | DefaultConversionService conversionService = new DefaultConversionService(); 22 | conversionService.addConverter(new BeanMapperConverterAdapter(beanMapper)); 23 | Person person = new Person(); 24 | person.setName("Jan"); 25 | PersonView personView = conversionService.convert(person, PersonView.class); 26 | assertEquals("Jan", personView.name); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/Fake.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import static jakarta.persistence.GenerationType.IDENTITY; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | 10 | import org.springframework.data.domain.Persistable; 11 | 12 | import com.fasterxml.jackson.annotation.JsonIgnore; 13 | 14 | @Entity 15 | public class Fake implements Persistable { 16 | 17 | @Id 18 | @GeneratedValue(strategy = IDENTITY) 19 | private Long id; 20 | 21 | @Column(name = "name") 22 | private String name; 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | @Override 28 | public Long getId() { 29 | return id; 30 | } 31 | 32 | public void setId(Long id) { 33 | this.id = id; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | @Override 40 | @JsonIgnore 41 | public boolean isNew() { 42 | return id == null; 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public void setName(String name) { 50 | this.name = name; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/mockmvc/MockIdToEntityBeanConverter.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import io.beanmapper.BeanMapper; 4 | import io.beanmapper.core.BeanPropertyMatch; 5 | import io.beanmapper.core.converter.BeanConverter; 6 | 7 | import org.springframework.data.domain.Persistable; 8 | import org.springframework.data.repository.CrudRepository; 9 | 10 | public class MockIdToEntityBeanConverter implements BeanConverter { 11 | 12 | private final Class targetClass; 13 | 14 | private final CrudRepository, Long> repository; 15 | 16 | public MockIdToEntityBeanConverter(CrudRepository, Long> repository, Class targetClass) { 17 | this.repository = repository; 18 | this.targetClass = targetClass; 19 | } 20 | 21 | @Override 22 | public T convert(BeanMapper beanMapper, S source, Class targetClass, BeanPropertyMatch beanFieldMatch) { 23 | return targetClass.cast(repository.findById((Long)source).orElse(null)); 24 | } 25 | 26 | @Override 27 | public boolean match(Class sourceClass, Class targetClass) { 28 | return sourceClass.equals(Long.class) && targetClass.equals(this.targetClass); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/converter/ConversionServiceBeanConverterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.converter; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.core.convert.ConversionService; 12 | import org.springframework.core.convert.support.DefaultConversionService; 13 | 14 | /** 15 | * 16 | * 17 | * @author Jeroen van Schagen 18 | * @since Jun 18, 2015 19 | */ 20 | class ConversionServiceBeanConverterTest { 21 | 22 | private ConversionService conversionService; 23 | 24 | private ConversionServiceBeanConverter beanConverter; 25 | 26 | @BeforeEach 27 | void setUp() { 28 | conversionService = new DefaultConversionService(); 29 | beanConverter = new ConversionServiceBeanConverter(conversionService); 30 | } 31 | 32 | @Test 33 | void testCanConvert() { 34 | assertTrue(beanConverter.match(String.class, Long.class)); 35 | } 36 | 37 | @Test 38 | void testConvert() { 39 | assertEquals(Long.valueOf(1), beanConverter.convert(null, "1", Long.class, null)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/EntityFinder.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import jakarta.persistence.EntityNotFoundException; 4 | 5 | /** 6 | * Provides a generic interface for custom entity finders. The entity finder is 7 | * used within MergedFormMethodArgumentResolver to fetch the entity. 8 | * @author Robert Bor 9 | */ 10 | public interface EntityFinder { 11 | 12 | /** 13 | * Returns the entity on the basis of the entity class and its ID. 14 | * @param entityClass the class of the entity 15 | * @param id the ID of the entity 16 | * @return the entity if found 17 | * @throws jakarta.persistence.EntityNotFoundException if the repository or the entity 18 | * could not be found 19 | */ 20 | T find(Long id, Class entityClass) throws EntityNotFoundException; 21 | 22 | /** 23 | * Returns the entity on the basis of the entity class and its ID bypassing the 24 | * Hibernate cache 25 | * @param entityClass the class of the entity 26 | * @param id the ID of the entity 27 | * @return the entity if found 28 | * @throws jakarta.persistence.EntityNotFoundException if the repository or the entity 29 | * could not be found 30 | */ 31 | T findAndDetach(Long id, Class entityClass) throws EntityNotFoundException; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/flusher/JpaAfterClearFlusherTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.flusher; 2 | 3 | import static org.mockito.Mockito.doNothing; 4 | import static org.mockito.Mockito.verify; 5 | 6 | import jakarta.persistence.EntityManager; 7 | 8 | import io.beanmapper.BeanMapper; 9 | import io.beanmapper.config.BeanMapperBuilder; 10 | 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | public class JpaAfterClearFlusherTest { 18 | 19 | @Mock 20 | private EntityManager entityManager; 21 | 22 | @Test 23 | public void persistenceManagerFlushCalled() { 24 | doNothing().when(entityManager).flush(); 25 | 26 | JpaAfterClearFlusher flusher = new JpaAfterClearFlusher(entityManager); 27 | CollSource source = new CollSource() {{ 28 | items.add("A"); 29 | }}; 30 | CollTarget target = new CollTarget() {{ 31 | items.add("B"); 32 | }}; 33 | BeanMapper beanMapper = new BeanMapperBuilder() 34 | .setFlushEnabled(true) 35 | .addAfterClearFlusher(flusher) 36 | .build(); 37 | beanMapper.map(source, target); 38 | 39 | verify(entityManager).flush(); 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/ContainingFakeController.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import jakarta.validation.Valid; 4 | 5 | import io.beanmapper.BeanMapper; 6 | import io.beanmapper.spring.web.MergedForm; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RestController 14 | @RequestMapping("/containing-fake") 15 | public class ContainingFakeController { 16 | 17 | @Autowired 18 | private ContainingFakeService containingFakeService; 19 | 20 | public BeanMapper beanMapper; 21 | 22 | @RequestMapping(method = RequestMethod.POST) 23 | public ContainingFakeResult create(@MergedForm(value = ContainingFakeForm.class) ContainingFake containingFake) { 24 | return beanMapper.map(containingFakeService.create(containingFake), ContainingFakeResult.class); 25 | } 26 | 27 | @RequestMapping(value = "validate", method = RequestMethod.POST) 28 | public ContainingFakeResult createValid(@Valid @MergedForm(value = ContainingFakeForm.class) ContainingFake containingFake) { 29 | return beanMapper.map(containingFakeService.create(containingFake), ContainingFakeResult.class); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/AbstractSpringTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.jdbc.core.JdbcTemplate; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit.jupiter.SpringExtension; 14 | 15 | /** 16 | * Base for Spring dependent tests. 17 | * 18 | * @author Jeroen van Schagen 19 | * @since Aug 24, 2015 20 | */ 21 | @ExtendWith(MockitoExtension.class) 22 | @ExtendWith(SpringExtension.class) 23 | @ContextConfiguration(classes = { ApplicationConfig.class }) 24 | @SpringBootTest 25 | public abstract class AbstractSpringTest { 26 | 27 | @Mock 28 | private JdbcTemplate jdbcTemplate; 29 | 30 | @AfterEach 31 | public void cleanUp() { 32 | jdbcTemplate.execute("TRUNCATE SCHEMA public AND COMMIT"); 33 | } 34 | 35 | @Autowired 36 | public void setDataSource(DataSource dataSource) { 37 | jdbcTemplate = new JdbcTemplate(dataSource); 38 | } 39 | 40 | public JdbcTemplate getJdbcTemplate() { 41 | return jdbcTemplate; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/MergedForm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.web; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * Capable of reading an input form and mapping this 13 | * into an existing/new entity. The entity is declared 14 | * as parameter in our handler mapping method. 15 | * 16 | * @since Nov 15, 2015 17 | */ 18 | @Target({ ElementType.PARAMETER }) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | public @interface MergedForm { 21 | 22 | /** 23 | * Type of input form. 24 | * @return the input form class 25 | */ 26 | Class value(); 27 | 28 | /** 29 | * When patch, we only map the provided properties 30 | * from our input form to the entity. 31 | * @return the patch 32 | */ 33 | boolean patch() default false; 34 | 35 | /** 36 | * Entity identifier variable in our path mapping. 37 | * @return the identifier variable 38 | */ 39 | String mergeId() default ""; 40 | 41 | /** 42 | * Class types of the before-/afterMerge instances maintained in MergePair 43 | */ 44 | Class mergePairClass() default Object.class; 45 | 46 | /** 47 | * The name of the request part in the multipart form 48 | * @return the name of the request part in the multipart form 49 | */ 50 | String multiPart() default ""; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/unproxy/HibernateAwareBeanUnproxyTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.unproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.beanmapper.spring.model.Spoon; 6 | import io.beanmapper.spring.model.SpoonDrawerForm; 7 | 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class HibernateAwareBeanUnproxyTest { 12 | 13 | private HibernateAwareBeanUnproxy beanUnproxy; 14 | 15 | @BeforeEach 16 | void setUp() { 17 | this.beanUnproxy = new HibernateAwareBeanUnproxy(); 18 | } 19 | 20 | @Test 21 | void unproxyHibernateProxy() { 22 | HibernateProxyContainer shopHibernateProxy = new HibernateProxyContainer(); 23 | Object object = shopHibernateProxy.new GeneratedProxy(); 24 | Class unproxyClass = beanUnproxy.unproxy(object.getClass()); 25 | assertEquals(Shop.class, unproxyClass); 26 | } 27 | 28 | @Test 29 | void testUnproxy_ShouldNotConsiderStaticNestedClassAHibernateProxy() { 30 | Object object = new SpoonDrawerForm.SpoonForm(); 31 | Class unproxyClass = beanUnproxy.unproxy(object.getClass()); 32 | assertEquals(SpoonDrawerForm.SpoonForm.class, unproxyClass); 33 | } 34 | 35 | @Test 36 | void testUnproxy_ShouldReturnObjectUnchangedWhenNotAHibernateProxy() { 37 | Object object = new Spoon(true); 38 | Class unproxyClass = beanUnproxy.unproxy(object.getClass()); 39 | assertEquals(Spoon.class, unproxyClass); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/converter/BeanMapperConverterAdapter.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.converter; 2 | 3 | import java.util.Collections; 4 | import java.util.Set; 5 | 6 | import io.beanmapper.BeanMapper; 7 | 8 | import org.springframework.core.convert.TypeDescriptor; 9 | import org.springframework.core.convert.converter.GenericConverter; 10 | 11 | /** 12 | * Adapter that allows the bean mapper to be used in 13 | * the spring conversion service. 14 | * 15 | * @author Jeroen van Schagen 16 | * @since Aug 21, 2015 17 | */ 18 | public class BeanMapperConverterAdapter implements GenericConverter { 19 | 20 | /** 21 | * Delegate bean mapper. 22 | */ 23 | private final BeanMapper beanMapper; 24 | 25 | /** 26 | * Construct a new instance. 27 | * @param beanMapper the bean mapper 28 | */ 29 | public BeanMapperConverterAdapter(BeanMapper beanMapper) { 30 | this.beanMapper = beanMapper; 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | @Override 37 | public Set getConvertibleTypes() { 38 | return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | @Override 45 | public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 46 | if (sourceType.getType().equals(targetType.getType())) { 47 | return source; 48 | } 49 | return beanMapper.map(source, targetType.getType()); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/converter/ConversionServiceBeanConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.converter; 5 | 6 | import io.beanmapper.BeanMapper; 7 | import io.beanmapper.core.BeanPropertyMatch; 8 | import io.beanmapper.core.converter.BeanConverter; 9 | 10 | import org.springframework.core.convert.ConversionService; 11 | 12 | /** 13 | * Spring based bean converter adapter. Makes use of Spring's 14 | * build-in conversion service. 15 | * 16 | * @author Jeroen van Schagen 17 | * @since Jun 18, 2015 18 | */ 19 | public class ConversionServiceBeanConverter implements BeanConverter { 20 | 21 | /** 22 | * Performs type conversions in Spring. 23 | */ 24 | private final ConversionService conversionService; 25 | 26 | /** 27 | * Construct a new {@link ConversionServiceBeanConverter}. 28 | * @param conversionService the converter delegate 29 | */ 30 | public ConversionServiceBeanConverter(ConversionService conversionService) { 31 | this.conversionService = conversionService; 32 | } 33 | 34 | @Override 35 | public T convert(BeanMapper beanMapper, S source, Class targetClass, BeanPropertyMatch beanFieldMatch) { 36 | return conversionService.convert(source, targetClass); 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | @Override 43 | public boolean match(Class sourceClass, Class targetClass) { 44 | return conversionService.canConvert(sourceClass, targetClass); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.util; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import tools.jackson.core.JacksonException; 7 | import tools.jackson.databind.JsonNode; 8 | import tools.jackson.databind.ObjectMapper; 9 | 10 | /** 11 | * Utilities for working with JSON. 12 | * 13 | * @author Jeroen van Schagen 14 | * @since Nov 13, 2015 15 | */ 16 | public class JsonUtil { 17 | 18 | /** 19 | * Retrieve the property names mentioned in a JSON content. 20 | * 21 | * @param json the JSON content 22 | * @param objectMapper the object mapper 23 | * @return set of the property names 24 | */ 25 | public static Set getPropertyNamesFromJson(String json, ObjectMapper objectMapper) { 26 | try { 27 | JsonNode tree = objectMapper.readTree(json); 28 | return getPropertyNames(tree, ""); 29 | } catch (JacksonException e) { 30 | throw new IllegalStateException("Could not retrieve property names from JSON.", e); 31 | } 32 | } 33 | 34 | private static Set getPropertyNames(JsonNode node, String base) { 35 | Set propertyNames = new HashSet<>(); 36 | node.propertyNames().forEach(fieldName -> { 37 | String propertyName = isEmpty(base) ? fieldName : base + "." + fieldName; 38 | propertyNames.add(propertyName); 39 | propertyNames.addAll(getPropertyNames(node.get(fieldName), propertyName)); 40 | }); 41 | return propertyNames; 42 | } 43 | 44 | private static boolean isEmpty(String str) { 45 | return str == null || str.isEmpty(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Product.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | import java.util.List; 4 | 5 | public class Product { 6 | 7 | private Long id; 8 | 9 | private String name; 10 | 11 | private String upc; 12 | 13 | private String internalMemo; 14 | 15 | private List assets; 16 | 17 | private List artists; 18 | 19 | private Organization organization; 20 | 21 | public Long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(Long id) { 26 | this.id = id; 27 | } 28 | 29 | public String getInternalMemo() { 30 | return internalMemo; 31 | } 32 | 33 | public void setInternalMemo(String internalMemo) { 34 | this.internalMemo = internalMemo; 35 | } 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public void setName(String name) { 42 | this.name = name; 43 | } 44 | 45 | public String getUpc() { 46 | return upc; 47 | } 48 | 49 | public void setUpc(String upc) { 50 | this.upc = upc; 51 | } 52 | 53 | public List getAssets() { 54 | return assets; 55 | } 56 | 57 | public void setAssets(List assets) { 58 | this.assets = assets; 59 | } 60 | 61 | public List getArtists() { 62 | return artists; 63 | } 64 | 65 | public void setArtists(List artists) { 66 | this.artists = artists; 67 | } 68 | 69 | public Organization getOrganization() { 70 | return organization; 71 | } 72 | 73 | public void setOrganization(Organization organization) { 74 | this.organization = organization; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/PageableMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import io.beanmapper.BeanMapper; 11 | 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.data.domain.PageImpl; 14 | import org.springframework.data.domain.PageRequest; 15 | import org.springframework.data.domain.Pageable; 16 | 17 | /** 18 | * Mapping utilities for pages. 19 | * 20 | * @author Jeroen van Schagen 21 | * @since Nov 13, 2015 22 | */ 23 | public class PageableMapper { 24 | 25 | /** 26 | * Private constructor to hide the implicit public constructor of 27 | * utility-class. 28 | */ 29 | private PageableMapper() {} 30 | 31 | /** 32 | * Converts a page into the desired target type. 33 | * 34 | * @param source the source page 35 | * @param targetClass the target type 36 | * @param beanMapper the bean mapper used to perform mappings 37 | * @return the same page, but with result type 38 | */ 39 | public static Page map(Page source, Class targetClass, BeanMapper beanMapper) { 40 | List transformed; 41 | if (source.hasContent()) { 42 | List content = new ArrayList<>(source.getContent()); 43 | transformed = beanMapper.map(content, targetClass); 44 | } else { 45 | transformed = Collections.emptyList(); 46 | } 47 | Pageable pageable = PageRequest.of(source.getNumber(), source.getSize(), source.getSort()); 48 | return new PageImpl<>(transformed, pageable, source.getTotalElements()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/WebRequestParameters.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import org.springframework.core.Conventions; 4 | import org.springframework.core.MethodParameter; 5 | import org.springframework.validation.BindingResult; 6 | import org.springframework.web.bind.WebDataBinder; 7 | import org.springframework.web.bind.support.WebDataBinderFactory; 8 | import org.springframework.web.context.request.NativeWebRequest; 9 | import org.springframework.web.method.support.ModelAndViewContainer; 10 | 11 | public class WebRequestParameters { 12 | 13 | private final MethodParameter parameter; 14 | private final ModelAndViewContainer mavContainer; 15 | private final NativeWebRequest webRequest; 16 | private final WebDataBinderFactory binderFactory; 17 | 18 | public WebRequestParameters(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, 19 | WebDataBinderFactory binderFactory) { 20 | this.parameter = parameter; 21 | this.mavContainer = mavContainer; 22 | this.webRequest = webRequest; 23 | this.binderFactory = binderFactory; 24 | } 25 | 26 | public WebDataBinder createBinder(Object objectToValidate) throws Exception { 27 | String name = Conventions.getVariableNameForParameter(parameter); 28 | return binderFactory.createBinder(webRequest, objectToValidate, name); 29 | } 30 | 31 | public void setBindingResult(BindingResult bindingResult) { 32 | String name = Conventions.getVariableNameForParameter(parameter); 33 | mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, bindingResult); 34 | } 35 | 36 | public MethodParameter getParameter() { 37 | return parameter; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/mockmvc/MockEntityFinder.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import jakarta.persistence.EntityNotFoundException; 7 | 8 | import io.beanmapper.exceptions.BeanConstructException; 9 | import io.beanmapper.spring.exceptions.ClassExpectationNotRegisteredException; 10 | import io.beanmapper.spring.web.EntityFinder; 11 | 12 | import org.springframework.data.domain.Persistable; 13 | import org.springframework.data.repository.CrudRepository; 14 | 15 | public class MockEntityFinder implements EntityFinder { 16 | 17 | private final Map, CrudRepository, Long>> repositories = new HashMap<>(); 18 | 19 | @Override 20 | public T find(Long id, Class entityClass) throws EntityNotFoundException, BeanConstructException { 21 | CrudRepository repository = (CrudRepository)repositories.get(entityClass); 22 | if (repository == null) { 23 | throw new BeanConstructException(entityClass, new ClassExpectationNotRegisteredException("No constructor found for " + entityClass.getSimpleName() + 24 | ". Make sure to register the class in addConverters.registerExpectation.")); 25 | } 26 | return repository.findById(id).orElse(null); 27 | } 28 | 29 | @Override 30 | public T findAndDetach(Long id, Class entityClass) throws EntityNotFoundException, BeanConstructException { 31 | // Not supported, same as find 32 | return find(id, entityClass); 33 | } 34 | 35 | public void addRepository(CrudRepository, Long> repository, Class entityClass) { 36 | repositories.put(entityClass, repository); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/security/AbstractSpringSecuredCheck.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.security; 2 | 3 | import io.beanmapper.spring.exceptions.PrincipalIsNoInstanceOfUserDetailsException; 4 | 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | 10 | public abstract class AbstractSpringSecuredCheck { 11 | 12 | protected Object getPrincipal() { 13 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 14 | return authentication.getPrincipal(); 15 | } 16 | 17 | protected UserDetails getUserDetails() { 18 | Object principal = getPrincipal(); 19 | if (!(getPrincipal() instanceof UserDetails)) { 20 | throw new PrincipalIsNoInstanceOfUserDetailsException(); 21 | } 22 | return (UserDetails)principal; 23 | } 24 | 25 | public boolean hasRole(String... roles) { 26 | if (hasNoRequiredRoles(roles)) { 27 | return true; 28 | } 29 | UserDetails userDetails = getUserDetails(); 30 | for (String role : roles) { 31 | for (GrantedAuthority authority : userDetails.getAuthorities()) { 32 | if (hasRole(authority, role)) { 33 | return true; 34 | } 35 | } 36 | } 37 | return false; 38 | } 39 | 40 | private boolean hasRole(GrantedAuthority authority, String role) { 41 | String prefixedRole = "ROLE_" + role; 42 | return prefixedRole.equals(authority.getAuthority()) || 43 | role.equals(authority.getAuthority()); 44 | } 45 | 46 | private boolean hasNoRequiredRoles(String[] roles) { 47 | return roles.length == 0; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/MergePair.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import io.beanmapper.BeanMapper; 4 | 5 | public class MergePair { 6 | 7 | private T beforeMerge; 8 | private T afterMerge; 9 | private final BeanMapper beanMapper; 10 | private final EntityFinder entityFinder; 11 | private final Class entityClass; 12 | private final MergedForm annotation; 13 | 14 | public MergePair(BeanMapper beanMapper, EntityFinder entityFinder, Class entityClass, MergedForm annotation) { 15 | this.beanMapper = beanMapper; 16 | this.entityFinder = entityFinder; 17 | this.entityClass = entityClass; 18 | this.annotation = annotation; 19 | } 20 | 21 | public void initNew(Object source) { 22 | setAfterMerge(beanMapper.map(source, targetEntityClass())); 23 | } 24 | 25 | public void merge(Object source, Long id) { 26 | if (isMergePair()) { 27 | setBeforeMerge(entityFinder.findAndDetach(id, targetEntityClass())); 28 | } 29 | T target = entityFinder.find(id, targetEntityClass()); 30 | setAfterMerge(beanMapper.map(source, target)); 31 | } 32 | 33 | public T getBeforeMerge() { 34 | return beforeMerge; 35 | } 36 | 37 | public T getAfterMerge() { 38 | return afterMerge; 39 | } 40 | 41 | public Object result() { 42 | return isMergePair() ? this : getAfterMerge(); 43 | } 44 | 45 | private boolean isMergePair() { 46 | return entityClass.equals(MergePair.class); 47 | } 48 | 49 | private Class targetEntityClass() { 50 | return isMergePair() ? (Class)annotation.mergePairClass() : (Class)entityClass; 51 | } 52 | 53 | private void setBeforeMerge(T beforeMerge) { 54 | this.beforeMerge = beforeMerge; 55 | } 56 | 57 | private void setAfterMerge(T afterMerge) { 58 | this.afterMerge = afterMerge; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/converter/IdToEntityBeanConverter.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.converter; 2 | 3 | import java.io.Serializable; 4 | 5 | import jakarta.persistence.EntityNotFoundException; 6 | 7 | import io.beanmapper.BeanMapper; 8 | import io.beanmapper.core.BeanPropertyMatch; 9 | import io.beanmapper.core.converter.BeanConverter; 10 | 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.data.repository.CrudRepository; 13 | import org.springframework.data.repository.core.EntityInformation; 14 | import org.springframework.data.repository.support.Repositories; 15 | 16 | public class IdToEntityBeanConverter implements BeanConverter { 17 | 18 | private final Repositories repositories; 19 | 20 | public IdToEntityBeanConverter(ApplicationContext applicationContext) { 21 | this.repositories = new Repositories(applicationContext); 22 | } 23 | 24 | @Override 25 | public T convert(BeanMapper beanMapper, S source, Class targetClass, BeanPropertyMatch beanFieldMatch) { 26 | if (source == null) { 27 | return null; 28 | } 29 | 30 | CrudRepository repository = (CrudRepository) repositories.getRepositoryFor(targetClass).orElseThrow(() -> new EntityNotFoundException( 31 | "No repository found for " + targetClass.getName() 32 | )); 33 | 34 | return targetClass.cast(repository.findById(source).orElse(null)); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | @Override 41 | public boolean match(Class sourceClass, Class targetClass) { 42 | if (targetClass.isAssignableFrom(sourceClass)) { 43 | return false; 44 | } 45 | if (!repositories.hasRepositoryFor(targetClass)) { 46 | return false; 47 | } 48 | // No need for a null check. Repository#getEntityInformation fails if null. 49 | return sourceClass.equals(repositories.getEntityInformationFor(targetClass).getIdType()); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/SpringDataEntityFinder.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.EntityNotFoundException; 5 | 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.data.repository.CrudRepository; 8 | import org.springframework.data.repository.support.Repositories; 9 | 10 | /** 11 | * The default implementation is based on the Repositories class of Spring Data. It 12 | * first looks up the repository. If found, it then calls findOne on the repository. 13 | * @author Robert Bor 14 | */ 15 | public class SpringDataEntityFinder implements EntityFinder { 16 | 17 | private final Repositories repositories; 18 | 19 | private final EntityManager entityManager; 20 | 21 | public SpringDataEntityFinder(ApplicationContext applicationContext, EntityManager entityManager) { 22 | this.repositories = new Repositories(applicationContext); 23 | this.entityManager = entityManager; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | @Override 30 | public T findAndDetach(Long id, Class entityClass) throws EntityNotFoundException { 31 | T entity = find(id, entityClass); 32 | entityManager.detach(entity); 33 | return entity; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | @Override 40 | @SuppressWarnings("unchecked") 41 | public T find(Long id, Class entityClass) throws EntityNotFoundException { 42 | CrudRepository repository = 43 | (CrudRepository) repositories.getRepositoryFor(entityClass) 44 | .orElseThrow(() -> new EntityNotFoundException("No repository found for " + entityClass.getName()) 45 | ); 46 | 47 | return repository.findById(id).orElseThrow(() -> new EntityNotFoundException( 48 | "Entity with ID " + id + "was not found in repository " + repository.getClass().getName()) 49 | ); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/FakeController.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import io.beanmapper.BeanMapper; 4 | import io.beanmapper.spring.web.MergedForm; 5 | import io.beanmapper.spring.web.mockmvc.fakedomain.Fake; 6 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeForm; 7 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeResult; 8 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeService; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.DeleteMapping; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.PutMapping; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | @RestController 20 | @RequestMapping("/fake") 21 | public class FakeController { 22 | 23 | @Autowired 24 | private FakeService fakeService; 25 | 26 | // Made public due to issues 27 | public BeanMapper beanMapper; 28 | 29 | @GetMapping(value = "/{fake}") 30 | public FakeResult read(@PathVariable("fake") Fake fake) { 31 | return beanMapper.map(fakeService.read(fake), FakeResult.class); 32 | } 33 | 34 | @PostMapping 35 | public FakeResult create(@MergedForm(value = FakeForm.class) Fake fake) { 36 | return beanMapper.map(fakeService.create(fake), FakeResult.class); 37 | } 38 | 39 | @PutMapping(value = "/{id}") 40 | public FakeResult update(@MergedForm(mergeId = "id", value = FakeForm.class) Fake fake) { 41 | return beanMapper.map(fakeService.update(fake), FakeResult.class); 42 | } 43 | 44 | @DeleteMapping(value = "/{fake}") 45 | public FakeResult delete(@PathVariable("fake") Fake fake) { 46 | return beanMapper.map(fakeService.delete(fake), FakeResult.class); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/EntityFinderTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.mockito.Mockito.when; 6 | 7 | import java.util.Optional; 8 | 9 | import jakarta.persistence.EntityNotFoundException; 10 | 11 | import io.beanmapper.spring.AbstractSpringTest; 12 | import io.beanmapper.spring.model.Person; 13 | import io.beanmapper.spring.model.PersonRepository; 14 | 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | public class EntityFinderTest extends AbstractSpringTest { 24 | 25 | @Mock 26 | private PersonRepository personRepository; 27 | 28 | @Test 29 | public void find() { 30 | Person person = new Person(); 31 | person.setName("Henk"); 32 | person.setCity("Lisse"); 33 | person.setId(42L); 34 | 35 | when(this.personRepository.findById(person.getId())).thenReturn(Optional.of(person)); 36 | 37 | EntityFinder entityFinder = new EntityFinder() { 38 | 39 | @Override 40 | public T find(Long id, Class entityClass) throws EntityNotFoundException { 41 | return (T)personRepository.findById(id).orElse(null); 42 | } 43 | 44 | @Override 45 | public T findAndDetach(Long id, Class entityClass) throws EntityNotFoundException { 46 | return find(id, entityClass); 47 | } 48 | 49 | }; 50 | 51 | Person foundPerson = entityFinder.find(person.getId(), Person.class); 52 | assertNotNull(foundPerson); 53 | assertEquals("Henk", foundPerson.getName()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/model/Person.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import jakarta.persistence.CollectionTable; 7 | import jakarta.persistence.Column; 8 | import jakarta.persistence.ElementCollection; 9 | import jakarta.persistence.Entity; 10 | import jakarta.persistence.EnumType; 11 | import jakarta.persistence.Enumerated; 12 | import jakarta.persistence.FetchType; 13 | import jakarta.validation.constraints.NotNull; 14 | 15 | @Entity 16 | public class Person extends BaseEntity { 17 | 18 | @NotNull 19 | private String name; 20 | 21 | private String street; 22 | 23 | private String houseNumber; 24 | 25 | private String city; 26 | 27 | private String bankAccount; 28 | 29 | @ElementCollection(fetch = FetchType.EAGER) 30 | @CollectionTable(name = "owner_tag") 31 | @Enumerated(EnumType.STRING) 32 | @Column(name = "tag") 33 | private List tags = new ArrayList<>(); 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | public String getStreet() { 44 | return street; 45 | } 46 | 47 | public void setStreet(String street) { 48 | this.street = street; 49 | } 50 | 51 | public String getHouseNumber() { 52 | return houseNumber; 53 | } 54 | 55 | public void setHouseNumber(String houseNumber) { 56 | this.houseNumber = houseNumber; 57 | } 58 | 59 | public String getCity() { 60 | return city; 61 | } 62 | 63 | public void setCity(String city) { 64 | this.city = city; 65 | } 66 | 67 | public String getBankAccount() { 68 | return bankAccount; 69 | } 70 | 71 | public void setBankAccount(String bankAccount) { 72 | this.bankAccount = bankAccount; 73 | } 74 | 75 | public List getTags() { 76 | return tags; 77 | } 78 | 79 | public void setTags(List tags) { 80 | this.tags = tags; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/AbstractControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import java.util.Collections; 4 | 5 | import io.beanmapper.BeanMapper; 6 | import io.beanmapper.spring.AbstractSpringTest; 7 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeApplicationConfig; 8 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeWebMvcConfig; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 12 | import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; 13 | import org.springframework.data.domain.Persistable; 14 | import org.springframework.data.repository.CrudRepository; 15 | import org.springframework.format.support.FormattingConversionService; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 18 | 19 | @AutoConfigureMockMvc 20 | @EnableAutoConfiguration 21 | public class AbstractControllerTest extends AbstractSpringTest { 22 | 23 | private FakeWebMvcConfig config = new FakeWebMvcConfig(); 24 | 25 | @Autowired 26 | protected MockMvc webClient; 27 | 28 | protected MockMvcBeanMapper mockMvcBeanMapper; 29 | 30 | protected void initWebClient(Object controller) { 31 | 32 | this.mockMvcBeanMapper = new MockMvcBeanMapper( 33 | new FormattingConversionService(), 34 | Collections.singletonList(config.structuredJsonMessageConverter()), 35 | new FakeApplicationConfig().beanMapper() 36 | ); 37 | } 38 | 39 | public void createWebClient(Object controller) { 40 | this.webClient = MockMvcBuilders.standaloneSetup(controller) 41 | .setMessageConverters(config.structuredJsonMessageConverter()) 42 | .setCustomArgumentResolvers(mockMvcBeanMapper.createHandlerMethodArgumentResolvers()) 43 | .setConversionService(mockMvcBeanMapper.getConversionService()) 44 | .build(); 45 | } 46 | 47 | public BeanMapper beanMapper() { 48 | return mockMvcBeanMapper.getBeanMapper(); 49 | } 50 | 51 | public void registerRepository(CrudRepository repository, Class entityClass) { 52 | mockMvcBeanMapper.registerRepository(repository, entityClass); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/mockmvc/MockMvcBeanMapper.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import java.util.List; 4 | 5 | import io.beanmapper.BeanMapper; 6 | import io.beanmapper.spring.web.MergedFormMethodArgumentResolver; 7 | 8 | import org.springframework.data.domain.Persistable; 9 | import org.springframework.data.repository.CrudRepository; 10 | import org.springframework.format.support.FormattingConversionService; 11 | import org.springframework.http.converter.HttpMessageConverter; 12 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 13 | 14 | public class MockMvcBeanMapper { 15 | 16 | private final FormattingConversionService conversionService; 17 | 18 | private final List> messageConverters; 19 | 20 | private BeanMapper beanMapper; 21 | 22 | private MockEntityFinder mockEntityFinder = new MockEntityFinder(); 23 | 24 | public MockMvcBeanMapper(FormattingConversionService conversionService, 25 | List> messageConverters, 26 | BeanMapper beanMapper) { 27 | this.conversionService = conversionService; 28 | this.messageConverters = messageConverters; 29 | this.beanMapper = beanMapper.wrap().build(); 30 | } 31 | 32 | public > void registerRepository(CrudRepository repository, Class entityClass) { 33 | 34 | // Add a converter for the target class to the generic conversion service 35 | conversionService.addConverter(String.class, entityClass, new MockEntityConverter<>(repository)); 36 | 37 | // Add a BeanConverter for the target class to the BeanMapper 38 | beanMapper = beanMapper.wrap().addConverter(new MockIdToEntityBeanConverter(repository, entityClass)).build(); 39 | 40 | // Add the repository to the MockEntityFinder 41 | mockEntityFinder.addRepository(repository, entityClass); 42 | } 43 | 44 | public HandlerMethodArgumentResolver[] createHandlerMethodArgumentResolvers() { 45 | return new HandlerMethodArgumentResolver[] { 46 | new MergedFormMethodArgumentResolver(messageConverters, beanMapper, mockEntityFinder) 47 | }; 48 | } 49 | 50 | public FormattingConversionService getConversionService() { 51 | return this.conversionService; 52 | } 53 | 54 | public BeanMapper getBeanMapper() { 55 | return beanMapper; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/PageableMapperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertNotNull; 8 | 9 | import java.util.Arrays; 10 | 11 | import io.beanmapper.BeanMapper; 12 | import io.beanmapper.config.BeanMapperBuilder; 13 | import io.beanmapper.spring.model.Person; 14 | import io.beanmapper.spring.model.PersonForm; 15 | 16 | import org.junit.jupiter.api.Test; 17 | import org.springframework.data.domain.Page; 18 | import org.springframework.data.domain.PageImpl; 19 | import org.springframework.data.domain.PageRequest; 20 | import org.springframework.data.domain.Pageable; 21 | import org.springframework.data.domain.Sort; 22 | 23 | public class PageableMapperTest { 24 | 25 | private final Pageable pageable = PageRequest.of(0, 10, Sort.by("id")); 26 | 27 | private BeanMapper beanMapper; 28 | 29 | { 30 | beanMapper = new BeanMapperBuilder() 31 | .addPackagePrefix(ApplicationConfig.class) 32 | .build(); 33 | } 34 | 35 | @Test 36 | public void testMap() { 37 | PersonForm form = new PersonForm(); 38 | form.name = "Henk"; 39 | 40 | Page source = new PageImpl(Arrays.asList(form), pageable, 1); 41 | Page target = PageableMapper.map(source, Person.class, beanMapper); 42 | 43 | assertNotNull(target); 44 | assertEquals(pageable.getPageNumber(), target.getNumber()); 45 | assertEquals(pageable.getPageSize(), target.getSize()); 46 | assertEquals(pageable.getSort(), target.getSort()); 47 | assertEquals(1, target.getTotalElements()); 48 | 49 | Person person = target.getContent().get(0); 50 | assertEquals(Person.class, person.getClass()); 51 | assertEquals("Henk", person.getName()); 52 | } 53 | 54 | @Test 55 | public void testMapEmpty() { 56 | Page source = new PageImpl(Arrays. asList(), pageable, 0); 57 | Page target = PageableMapper.map(source, Person.class, beanMapper); 58 | 59 | assertNotNull(target); 60 | assertEquals(pageable.getPageNumber(), target.getNumber()); 61 | assertEquals(pageable.getPageSize(), target.getSize()); 62 | assertEquals(pageable.getSort(), target.getSort()); 63 | assertEquals(0, target.getTotalElements()); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/converter/IdToEntityBeanConverterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.converter; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertFalse; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import io.beanmapper.spring.AbstractSpringTest; 12 | import io.beanmapper.spring.model.Artist; 13 | import io.beanmapper.spring.model.Asset; 14 | import io.beanmapper.spring.model.Person; 15 | import io.beanmapper.spring.model.PersonRepository; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.context.ApplicationContext; 21 | 22 | public class IdToEntityBeanConverterTest extends AbstractSpringTest { 23 | 24 | @Autowired 25 | private ApplicationContext applicationContext; 26 | 27 | @Autowired 28 | private PersonRepository personRepository; 29 | 30 | private IdToEntityBeanConverter beanConverter; 31 | 32 | @BeforeEach 33 | public void setUp() { 34 | beanConverter = new IdToEntityBeanConverter(applicationContext); 35 | } 36 | 37 | @Test 38 | public void testCanConvert() { 39 | assertTrue(beanConverter.match(Long.class, Person.class)); 40 | } 41 | 42 | @Test 43 | public void testCannotConvertInvalidSource() {assertFalse(beanConverter.match(String.class, Person.class)); 44 | } 45 | 46 | @Test 47 | public void testCannotConvertInvalidTarget() { 48 | assertFalse(beanConverter.match(Long.class, Asset.class)); 49 | } 50 | 51 | @Test 52 | public void testConvert() { 53 | Person person = new Person(); 54 | person.setName("Henk"); 55 | personRepository.save(person); 56 | 57 | assertEquals(person.getId(), ((Person) beanConverter.convert(null, person.getId(), Person.class, null)).getId()); 58 | } 59 | 60 | @Test 61 | public void testSameClassNoMatch() { 62 | Person person = new Person(); 63 | person.setName("Henk"); 64 | assertFalse(beanConverter.match(person.getClass(), Person.class)); 65 | } 66 | 67 | @Test 68 | public void noRepoForArtist() { 69 | assertFalse(beanConverter.match(Long.class, Artist.class)); 70 | } 71 | 72 | @Test 73 | public void testConvertNull() { 74 | assertNull(beanConverter.convert(null, null, Person.class, null)); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/PersonController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.web; 5 | 6 | import jakarta.validation.Valid; 7 | 8 | import io.beanmapper.spring.Lazy; 9 | import io.beanmapper.spring.model.Person; 10 | import io.beanmapper.spring.model.PersonForm; 11 | 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestMethod; 16 | import org.springframework.web.bind.annotation.RequestPart; 17 | import org.springframework.web.bind.annotation.ResponseBody; 18 | import org.springframework.web.multipart.MultipartFile; 19 | 20 | @Controller 21 | @RequestMapping("/person") 22 | public class PersonController { 23 | 24 | @RequestMapping(method = RequestMethod.POST) 25 | @ResponseBody 26 | public Person create(@MergedForm(PersonForm.class) Person person) { 27 | return person; 28 | } 29 | 30 | @RequestMapping(value = "/query-param", method = RequestMethod.PUT) 31 | @ResponseBody 32 | public Person updateMergeIdInQueryParam(@MergedForm(value = PersonForm.class, mergeId = "id") Person person) { 33 | return person; 34 | } 35 | 36 | @RequestMapping(value = "/{id}/no-patch", method = RequestMethod.PUT) 37 | @ResponseBody 38 | public Person updateNoPatch(@MergedForm(value = PersonForm.class, mergeId = "id") Person person) { 39 | return person; 40 | } 41 | 42 | @RequestMapping(value = "/{id}/patch", method = RequestMethod.PUT) 43 | @ResponseBody 44 | public Person updatePatch(@MergedForm(value = PersonForm.class, patch = true, mergeId = "id") Person person) { 45 | return person; 46 | } 47 | 48 | @PutMapping("/{id}/lazy") 49 | @ResponseBody 50 | public Person updateLazy(@Valid @MergedForm(value = PersonForm.class, mergeId = "id") Lazy person) throws Exception { 51 | return person.get(); 52 | } 53 | 54 | @RequestMapping(value = "/{id}/multipart", method = RequestMethod.POST) 55 | @ResponseBody 56 | public Person updateForMultipart( 57 | @Valid @MergedForm(value = PersonForm.class, mergeId = "id", multiPart = "personForm") Person person, 58 | @RequestPart(value = "photo", required = false) MultipartFile photo) { 59 | return person; 60 | } 61 | 62 | @RequestMapping(value = "/{id}/pair", method = RequestMethod.PUT) 63 | @ResponseBody 64 | public MergePair updateReturnPair(@MergedForm(value = PersonForm.class, mergeId = "id", mergePairClass = Person.class) MergePair personPair) { 65 | return personPair; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/SpringDataEntityFinderTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import jakarta.persistence.EntityManager; 9 | import jakarta.persistence.EntityNotFoundException; 10 | import jakarta.persistence.PersistenceContext; 11 | 12 | import io.beanmapper.BeanMapper; 13 | import io.beanmapper.spring.AbstractSpringTest; 14 | import io.beanmapper.spring.model.Person; 15 | import io.beanmapper.spring.model.PersonRepository; 16 | 17 | import org.junit.jupiter.api.Test; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.boot.test.context.SpringBootTest; 20 | import org.springframework.context.ApplicationContext; 21 | import org.springframework.transaction.annotation.Transactional; 22 | 23 | @SpringBootTest 24 | public class SpringDataEntityFinderTest extends AbstractSpringTest { 25 | 26 | @Autowired 27 | private ApplicationContext applicationContext; 28 | 29 | @PersistenceContext 30 | private EntityManager entityManager; 31 | 32 | @Autowired 33 | private PersonRepository personRepository; 34 | 35 | @Test 36 | public void find() { 37 | Person person = new Person(); 38 | person.setName("Henk"); 39 | person.setCity("Lisse"); 40 | person = personRepository.save(person); 41 | 42 | EntityFinder entityFinder = new SpringDataEntityFinder(applicationContext, entityManager); 43 | Person foundPerson = (Person) entityFinder.find(person.getId(), Person.class); 44 | assertNotNull(foundPerson); 45 | assertEquals("Henk", foundPerson.getName()); 46 | } 47 | 48 | @Test 49 | @Transactional 50 | public void findAndDetach() { 51 | Person person = new Person(); 52 | person.setName("Henk"); 53 | person.setCity("Lisse"); 54 | person = personRepository.save(person); 55 | 56 | EntityFinder entityFinder = new SpringDataEntityFinder(applicationContext, entityManager); 57 | Person person1 = entityFinder.find(person.getId(), Person.class); 58 | Person person2 = entityFinder.find(person.getId(), Person.class); 59 | assertEquals(person1, person2); 60 | 61 | person1 = entityFinder.findAndDetach(person.getId(), Person.class); 62 | person2 = entityFinder.find(person.getId(), Person.class); 63 | 64 | assertNotEquals(person1, person2); 65 | } 66 | 67 | @Test 68 | public void noRepository() { 69 | EntityFinder entityFinder = new SpringDataEntityFinder(applicationContext, entityManager); 70 | assertThrows(EntityNotFoundException.class, () -> entityFinder.find(42L, BeanMapper.class)); 71 | } 72 | 73 | @Test 74 | public void entityNotFound() { 75 | EntityFinder entityFinder = new SpringDataEntityFinder(applicationContext, entityManager); 76 | assertThrows(EntityNotFoundException.class, () -> entityFinder.find(42L, Person.class)); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/converter/StructuredJsonMessageConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.web.converter; 5 | 6 | import static java.nio.charset.StandardCharsets.UTF_8; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | import io.beanmapper.spring.util.JsonUtil; 15 | 16 | import org.springframework.http.HttpHeaders; 17 | import org.springframework.http.HttpInputMessage; 18 | import org.springframework.http.HttpOutputMessage; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.http.converter.HttpMessageConverter; 21 | import org.springframework.http.converter.HttpMessageNotReadableException; 22 | import org.springframework.http.converter.HttpMessageNotWritableException; 23 | import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; 24 | 25 | /** 26 | * Wraps the default Jackson message converter with read 27 | * functionality that returns both the message and property 28 | * names. 29 | * 30 | * @author Jeroen van Schagen 31 | * @since Nov 25, 2015 32 | */ 33 | public class StructuredJsonMessageConverter implements HttpMessageConverter { 34 | 35 | private final JacksonJsonHttpMessageConverter delegate; 36 | 37 | public StructuredJsonMessageConverter(JacksonJsonHttpMessageConverter delegate) { 38 | this.delegate = delegate; 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | @Override 45 | public boolean canRead(Class clazz, MediaType mediaType) { 46 | return delegate.canRead(clazz, mediaType); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | @Override 53 | public boolean canWrite(Class clazz, MediaType mediaType) { 54 | return delegate.canWrite(clazz, mediaType); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | @Override 61 | public List getSupportedMediaTypes() { 62 | return delegate.getSupportedMediaTypes(); 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | @Override 69 | public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { 70 | String json = new String(inputMessage.getBody().readAllBytes(), UTF_8); 71 | Object body = delegate.read(clazz, new StringHttpInputMessage(inputMessage.getHeaders(), json)); 72 | Set propertyNames = JsonUtil.getPropertyNamesFromJson(json, delegate.getMapper()); 73 | return new StructuredBody(body, propertyNames); 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | @Override 80 | public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { 81 | delegate.write(t, contentType, outputMessage); 82 | } 83 | 84 | private static class StringHttpInputMessage implements HttpInputMessage { 85 | 86 | private final HttpHeaders headers; 87 | 88 | private final String content; 89 | 90 | public StringHttpInputMessage(HttpHeaders headers, String content) { 91 | this.headers = headers; 92 | this.content = content; 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | @Override 99 | public HttpHeaders getHeaders() { 100 | return headers; 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | @Override 107 | public InputStream getBody() throws IOException { 108 | return new ByteArrayInputStream(content.getBytes()); 109 | } 110 | 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeWebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | import io.beanmapper.BeanMapper; 7 | import io.beanmapper.spring.web.MergedFormMethodArgumentResolver; 8 | import io.beanmapper.spring.web.converter.StructuredJsonMessageConverter; 9 | import jakarta.persistence.EntityManager; 10 | import jakarta.persistence.PersistenceContext; 11 | import tools.jackson.core.Version; 12 | import tools.jackson.databind.DeserializationFeature; 13 | import tools.jackson.databind.json.JsonMapper; 14 | import tools.jackson.databind.module.SimpleModule; 15 | 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.context.ApplicationContext; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.ComponentScan; 20 | import org.springframework.context.annotation.Configuration; 21 | import org.springframework.data.repository.support.DomainClassConverter; 22 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 23 | import org.springframework.format.support.FormattingConversionService; 24 | import org.springframework.http.converter.HttpMessageConverters.ServerBuilder; 25 | import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; 26 | import org.springframework.stereotype.Controller; 27 | import org.springframework.stereotype.Repository; 28 | import org.springframework.stereotype.Service; 29 | import org.springframework.web.bind.annotation.ControllerAdvice; 30 | import org.springframework.web.bind.annotation.RestController; 31 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 32 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 33 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 34 | 35 | import com.fasterxml.jackson.annotation.JsonInclude; 36 | 37 | @EnableWebMvc 38 | @EnableSpringDataWebSupport 39 | @ComponentScan(basePackageClasses = FakeApplicationConfig.class, 40 | includeFilters = @ComponentScan.Filter({ ControllerAdvice.class, Controller.class, RestController.class }), 41 | excludeFilters = @ComponentScan.Filter({ Configuration.class, Service.class, Repository.class })) 42 | public class FakeWebMvcConfig implements WebMvcConfigurer { 43 | 44 | @Autowired 45 | private FormattingConversionService mvcConversionService; 46 | 47 | @Autowired 48 | private BeanMapper beanMapper; 49 | 50 | @Autowired 51 | private ApplicationContext applicationContext; 52 | 53 | @PersistenceContext 54 | private EntityManager entityManager; 55 | 56 | @Override 57 | public void configureMessageConverters(ServerBuilder builder) { 58 | builder.addCustomConverter(structuredJsonMessageConverter()); 59 | } 60 | 61 | public StructuredJsonMessageConverter structuredJsonMessageConverter() { 62 | return new StructuredJsonMessageConverter(new JacksonJsonHttpMessageConverter(objectMapper())); 63 | } 64 | 65 | @Override 66 | public void addArgumentResolvers(List argumentResolvers) { 67 | argumentResolvers.add(new MergedFormMethodArgumentResolver( 68 | Collections.singletonList(structuredJsonMessageConverter()), 69 | beanMapper, 70 | applicationContext, 71 | entityManager 72 | )); 73 | } 74 | 75 | @Bean 76 | public JsonMapper objectMapper() { 77 | return JsonMapper.builder() 78 | .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) 79 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 80 | .addModule(new SimpleModule("BeanMapperSpring", new Version(1, 0, 0, null, "org.beanmapper", "spring"))) 81 | .build(); 82 | } 83 | 84 | @Bean 85 | public DomainClassConverter domainClassConverter() { 86 | return new DomainClassConverter<>(mvcConversionService); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/ContainingFakeControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.util.Optional; 7 | 8 | import io.beanmapper.config.BeanMapperBuilder; 9 | import io.beanmapper.spring.ApplicationConfig; 10 | import io.beanmapper.spring.web.mockmvc.fakedomain.ContainingFake; 11 | import io.beanmapper.spring.web.mockmvc.fakedomain.ContainingFakeController; 12 | import io.beanmapper.spring.web.mockmvc.fakedomain.ContainingFakeForm; 13 | import io.beanmapper.spring.web.mockmvc.fakedomain.ContainingFakeService; 14 | import io.beanmapper.spring.web.mockmvc.fakedomain.Fake; 15 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeRepository; 16 | import io.beanmapper.spring.web.mockmvc.fakedomain.FakeWebMvcConfig; 17 | 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.mockito.InjectMocks; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 23 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 24 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 25 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 26 | 27 | class ContainingFakeControllerTest extends AbstractControllerTest { 28 | 29 | @InjectMocks 30 | private ContainingFakeController controller; 31 | 32 | @MockitoBean 33 | private FakeRepository fakeRepository; 34 | 35 | @MockitoBean 36 | private ContainingFakeService containingFakeService; 37 | 38 | private Fake fake; 39 | 40 | private ContainingFake containingFake; 41 | 42 | @BeforeEach 43 | void setup() { 44 | controller.beanMapper = new BeanMapperBuilder() 45 | .addPackagePrefix(ApplicationConfig.class) 46 | .build(); 47 | initWebClient(controller); 48 | registerRepository(fakeRepository, Fake.class); 49 | createWebClient(controller); 50 | 51 | fake = new Fake(); 52 | fake.setName("Henk"); 53 | 54 | containingFake = new ContainingFake(); 55 | containingFake.setFake(fake); 56 | } 57 | 58 | @Test 59 | void create() throws Exception { 60 | when(this.fakeRepository.findById(42L)).thenReturn(Optional.ofNullable(this.fake)); 61 | when(containingFakeService.create(any())).thenAnswer(i -> { 62 | ContainingFake fake = (ContainingFake) i.getArguments()[0]; 63 | fake.setFake(this.fake); 64 | return fake; 65 | }); 66 | 67 | ContainingFakeForm containingFakeForm = new ContainingFakeForm(); 68 | containingFakeForm.fakeId = 42L; 69 | 70 | this.webClient.perform(MockMvcRequestBuilders.post("/containing-fake") 71 | .contentType(MediaType.APPLICATION_JSON) 72 | .content(new FakeWebMvcConfig().objectMapper().writeValueAsString(containingFakeForm))) 73 | .andDo(MockMvcResultHandlers.print()) 74 | .andExpect(MockMvcResultMatchers.status().is(200)) 75 | .andExpect(MockMvcResultMatchers.jsonPath("$.fakeName").value("Henk")); 76 | } 77 | 78 | @Test 79 | void createThrowsValidationExceptionForForm() throws Exception { 80 | ContainingFakeForm containingFakeForm = new ContainingFakeForm(); 81 | containingFakeForm.fakeId = 42L; 82 | 83 | this.webClient.perform(MockMvcRequestBuilders.post("/containing-fake/validate") 84 | .contentType(MediaType.APPLICATION_JSON) 85 | .content(new FakeWebMvcConfig().objectMapper().writeValueAsString(containingFakeForm))) 86 | .andDo(MockMvcResultHandlers.print()) 87 | .andExpect(MockMvcResultMatchers.status().is(400)); 88 | } 89 | 90 | @Test 91 | void createThrowsValidationExceptionForMappedTarget() throws Exception { 92 | ContainingFakeForm containingFakeForm = new ContainingFakeForm(); 93 | containingFakeForm.fakeId = 42L; 94 | containingFakeForm.passMe = "somevalue"; 95 | 96 | this.webClient.perform(MockMvcRequestBuilders.post("/containing-fake/validate") 97 | .contentType(MediaType.APPLICATION_JSON) 98 | .content(new FakeWebMvcConfig().objectMapper().writeValueAsString(containingFakeForm))) 99 | .andDo(MockMvcResultHandlers.print()) 100 | .andExpect(MockMvcResultMatchers.status().is(400)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring; 5 | 6 | import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.HSQL; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import javax.sql.DataSource; 12 | 13 | import jakarta.persistence.EntityManagerFactory; 14 | import tools.jackson.databind.json.JsonMapper; 15 | 16 | import org.hibernate.dialect.HSQLDialect; 17 | import org.hibernate.jpa.HibernatePersistenceProvider; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.beans.factory.annotation.Qualifier; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.ComponentScan; 22 | import org.springframework.context.annotation.Configuration; 23 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 24 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 25 | import org.springframework.orm.jpa.JpaTransactionManager; 26 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 27 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 28 | import org.springframework.stereotype.Controller; 29 | import org.springframework.transaction.annotation.EnableTransactionManagement; 30 | import org.springframework.web.bind.annotation.ControllerAdvice; 31 | import org.springframework.web.bind.annotation.RestController; 32 | 33 | @ComponentScan( 34 | basePackageClasses = ApplicationConfig.class, 35 | excludeFilters = { 36 | @ComponentScan.Filter({ ControllerAdvice.class, Controller.class, RestController.class }) 37 | } 38 | ) 39 | @EnableTransactionManagement 40 | @EnableJpaRepositories(basePackageClasses = ApplicationConfig.class) 41 | @Configuration 42 | public class ApplicationConfig { 43 | 44 | @Autowired 45 | private DataSource dataSource; 46 | 47 | @Autowired 48 | @Qualifier("hibernateDialect") 49 | private String hibernateDialect; 50 | 51 | @Bean 52 | public LocalContainerEntityManagerFactoryBean entityManagerFactory() { 53 | LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); 54 | entityManagerFactoryBean.setDataSource(dataSource); 55 | entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); 56 | entityManagerFactoryBean.setPackagesToScan(ApplicationConfig.class.getPackage().getName()); 57 | HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); 58 | jpaVendorAdapter.setGenerateDdl(false); 59 | jpaVendorAdapter.setDatabasePlatform(hibernateDialect); 60 | entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter); 61 | 62 | Map jpaProperties = getHibernatePropertyMap(); 63 | entityManagerFactoryBean.setJpaPropertyMap(jpaProperties); 64 | entityManagerFactoryBean.setEntityManagerFactoryInterface(EntityManagerFactory.class); 65 | 66 | return entityManagerFactoryBean; 67 | } 68 | 69 | private Map getHibernatePropertyMap() { 70 | Map jpaProperties = new HashMap<>(); 71 | jpaProperties.put("hibernate.dialect", hibernateDialect); 72 | jpaProperties.put("hibernate.hbm2ddl.auto", "create-drop"); 73 | jpaProperties.put("hibernate.jdbc.use_get_generated_keys", true); 74 | jpaProperties.put("hibernate.id.new_generator_mappings", true); 75 | jpaProperties.put("hibernate.generate_statistics", false); 76 | return jpaProperties; 77 | } 78 | 79 | @Bean 80 | public JpaTransactionManager transactionManager(@Autowired LocalContainerEntityManagerFactoryBean entityManagerFactory) { 81 | JpaTransactionManager transactionManager = new JpaTransactionManager(); 82 | transactionManager.setEntityManagerFactory(entityManagerFactory.getObject()); 83 | return transactionManager; 84 | } 85 | 86 | @Bean 87 | public JsonMapper objectMapper() { 88 | return JsonMapper.builder() 89 | .build(); 90 | } 91 | 92 | @Configuration 93 | public static class HsqlConfig { 94 | 95 | @Bean 96 | public DataSource dataSource() { 97 | return new EmbeddedDatabaseBuilder().setName("dev").setType(HSQL).build(); 98 | } 99 | 100 | @Bean 101 | public String hibernateDialect() { 102 | return HSQLDialect.class.getName(); 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/42BV/beanmapper-spring/workflows/Java%20CI%20with%20Maven/badge.svg)](https://github.com/42BV/beanmapper-spring/actions?query=workflow%3A%22Java+CI+with+Maven%22) 2 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e2784c9a7592423390df7888a9dc30e0)](https://app.codacy.com/gh/42BV/beanmapper-spring/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 3 | [![codecov](https://codecov.io/gh/42BV/beanmapper-spring/branch/master/graph/badge.svg)](https://codecov.io/gh/42BV/beanmapper-spring) 4 | [![Maven Central](https://img.shields.io/maven-central/v/io.beanmapper/beanmapper-spring.svg?color=green)](https://central.sonatype.com/artifact/io.beanmapper/beanmapper-spring) 5 | [![Javadocs](https://www.javadoc.io/badge2/io.beanmapper/beanmapper-spring/javadoc.svg)](https://www.javadoc.io/doc/io.beanmapper/beanmapper-spring) 6 | [![Apache 2](http://img.shields.io/badge/license-Apache%202-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 7 | 8 | # BeanMapper Spring 9 | 10 | ## Input merging argument resolver 11 | 12 | ### Configuration 13 | 14 | ```java 15 | @Override 16 | public void addArgumentResolvers(List argumentResolvers) { 17 | argumentResolvers.add(new MergeFormMethodArgumentResolver( 18 | Collections.singletonList(mappingJacksonHttpMessageConverter()), 19 | beanMapper, 20 | applicationContext, 21 | entityManager 22 | )); 23 | } 24 | ``` 25 | 26 | ### Usage 27 | 28 | ```java 29 | @RequestMapping(value = "/bla", method = RequestMethod.POST) 30 | @ResponseBody 31 | public Temp create(@MergeForm(TempForm.class) Temp tempEntity) { 32 | return tempEntity; 33 | } 34 | 35 | @RequestMapping(value = "/bla/{id}", method = RequestMethod.PUT) 36 | @ResponseBody 37 | public Temp update(@MergeForm(value = TempForm.class, id = "id") Temp tempEntity) { 38 | return tempEntity; 39 | } 40 | ``` 41 | 42 | ### MockMvcBeanMapper 43 | 44 | Since mapping to an Entity is done at an early stage, your Spring MVC controller level tests must be configured to deal with the usage of repositories. The MockMvcBeanMapper is configured at the level of your abstract controller test, ie the class your controller tests all extend from. 45 | 46 | The reason why you need to do this is because: 47 | * Spring's DomainClassConverter (working on @RequestParam and @PathVariable) makes use of your repositories 48 | * BeanMapper @MergedForm makes use of your repositories 49 | * BeanMapper IdToEntityBeanConverter makes use of your repositories 50 | 51 | Each of these vectors need to be addressed to set up controller tests that can deal with repositories. 52 | 53 | Assuming you use Spring's MockMvcBuilders and assuming you have a web configuration class called WebMvcConfig, this is what you could do: 54 | 55 | ```java 56 | public abstract class AbstractControllerTest { 57 | 58 | private WebMvcConfig config = new WebMvcConfig(); 59 | 60 | protected MockMvc webClient; 61 | 62 | protected MockMvcBeanMapper mockMvcBeanMapper; 63 | 64 | protected void initWebClient(Object controller) { 65 | 66 | this.mockMvcBeanMapper = new MockMvcBeanMapper( 67 | new FormattingConversionService(), 68 | Collections.singletonList(config.mappingJacksonHttpMessageConverter()), 69 | new ApplicationConfig().beanMapper() 70 | ); 71 | 72 | this.webClient = MockMvcBuilders.standaloneSetup(controller) 73 | .setMessageConverters(config.mappingJacksonHttpMessageConverter()) 74 | .setCustomArgumentResolvers(mockMvcBeanMapper.createHandlerMethodArgumentResolvers()) 75 | .setConversionService(mockMvcBeanMapper.getConversionService()) 76 | .build(); 77 | } 78 | 79 | public BeanMapper beanMapper() { 80 | return mockMvcBeanMapper.getBeanMapper(); 81 | } 82 | 83 | public void registerRepository(CrudRepository repository, Class entityClass) { 84 | mockMvcBeanMapper.registerRepository(repository, entityClass); 85 | } 86 | 87 | } 88 | ``` 89 | 90 | In your controller test, you will have to register all the repositories (presumably mock classes) that need to be added, ostensibly in a @Before method.. 91 | 92 | ```java 93 | registerRepository(ownerRepository, Owner.class); 94 | ``` 95 | 96 | You can take program your mock repositories as you normally would, for example in JMockit: 97 | 98 | ```java 99 | new NonStrictExpectations() {{ 100 | ownerRepository.findOne(1138L); 101 | result = new Owner(); 102 | }}; 103 | ``` 104 | 105 | ## License 106 | 107 | Licensed under the Apache License, Version 2.0 (the "License"); 108 | you may not use this file except in compliance with the License. 109 | You may obtain a copy of the License at 110 | 111 | http://www.apache.org/licenses/LICENSE-2.0 112 | 113 | Unless required by applicable law or agreed to in writing, software 114 | distributed under the License is distributed on an "AS IS" BASIS, 115 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 116 | See the License for the specific language governing permissions and 117 | limitations under the License. 118 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/mockmvc/fakedomain/FakeControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web.mockmvc.fakedomain; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Optional; 8 | 9 | import io.beanmapper.BeanMapper; 10 | import io.beanmapper.config.BeanMapperBuilder; 11 | import io.beanmapper.spring.ApplicationConfig; 12 | import io.beanmapper.spring.web.mockmvc.AbstractControllerTest; 13 | import io.beanmapper.spring.web.mockmvc.FakeController; 14 | import tools.jackson.databind.json.JsonMapper; 15 | 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.mockito.InjectMocks; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; 22 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 23 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 24 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 25 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 26 | 27 | class FakeControllerTest extends AbstractControllerTest { 28 | 29 | @InjectMocks 30 | private FakeController controller; 31 | 32 | @MockitoBean 33 | private FakeService service; 34 | 35 | @MockitoBean 36 | private FakeRepository repository; 37 | 38 | @Autowired 39 | private FakeBuilder fakeBuilder; 40 | 41 | @MockitoBean 42 | private JsonMapper objectMapper; 43 | 44 | private BeanMapper beanMapper; 45 | 46 | private JacksonJsonHttpMessageConverter converter; 47 | 48 | @BeforeEach 49 | void setup() { 50 | converter = new JacksonJsonHttpMessageConverter(objectMapper); 51 | 52 | beanMapper = new BeanMapperBuilder() 53 | .addPackagePrefix(ApplicationConfig.class) 54 | .build(); 55 | 56 | // Made it due to problems with AutoWire. 57 | controller.beanMapper = beanMapper; 58 | 59 | initWebClient(controller); 60 | registerRepository(repository, Fake.class); 61 | createWebClient(controller); 62 | 63 | when(this.repository.findById(42L)).thenAnswer(i -> Optional.ofNullable(this.fakeBuilder.base().withId(42L).withName("Henk").construct())); 64 | } 65 | 66 | @Test 67 | void find() throws Exception { 68 | when(this.service.read(any())).thenReturn(this.fakeBuilder.base().withId(42L).withName("Henk").construct()); 69 | 70 | this.webClient.perform(MockMvcRequestBuilders.get("/fake/42") 71 | .characterEncoding(StandardCharsets.UTF_8) 72 | .contentType(MediaType.APPLICATION_JSON)) 73 | .andDo(MockMvcResultHandlers.print()) 74 | .andExpect(MockMvcResultMatchers.status().is(200)) 75 | .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(42)) 76 | .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Henk")); 77 | } 78 | 79 | @Test 80 | void create() throws Exception { 81 | when(service.create(any())).thenAnswer(i -> { 82 | Fake fake = (Fake) i.getArguments()[0]; 83 | fake.setId(42L); 84 | return fake; 85 | }); 86 | 87 | FakeForm fakeForm = new FakeForm(); 88 | fakeForm.name = "Henk"; 89 | 90 | this.webClient.perform(MockMvcRequestBuilders.post("/fake") 91 | .contentType(MediaType.APPLICATION_JSON) 92 | .characterEncoding(StandardCharsets.UTF_8) 93 | .content(new FakeWebMvcConfig().objectMapper().writeValueAsString(fakeForm))) 94 | .andDo(MockMvcResultHandlers.print()) 95 | .andExpect(MockMvcResultMatchers.status().is(200)) 96 | .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(42)) 97 | .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Henk")); 98 | } 99 | 100 | @Test 101 | void update() throws Exception { 102 | when(this.service.update(any())).thenAnswer(i -> i.getArguments()[0]); 103 | 104 | FakeForm fakeForm = new FakeForm(); 105 | fakeForm.name = "Henk"; 106 | 107 | super.webClient.perform(MockMvcRequestBuilders.put("/fake/42") 108 | .contentType(MediaType.APPLICATION_JSON) 109 | .content(new FakeWebMvcConfig().objectMapper().writeValueAsString(fakeForm))) 110 | .andDo(MockMvcResultHandlers.print()) 111 | .andExpect(MockMvcResultMatchers.status().is(200)) 112 | .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(42)) 113 | .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Henk")); 114 | } 115 | 116 | @Test 117 | void delete() throws Exception { 118 | when(this.service.delete(any())).thenAnswer(i -> i.getArguments()[0]); 119 | 120 | this.webClient.perform(MockMvcRequestBuilders.delete("/fake/42") 121 | .contentType(MediaType.APPLICATION_JSON)) 122 | .andDo(MockMvcResultHandlers.print()) 123 | .andExpect(MockMvcResultMatchers.status().is(200)) 124 | .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(42)) 125 | .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Henk")); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## [7.0.0] - 2025-12-01 10 | 11 | - Upgraded to Spring 7 and Jackson 3 12 | 13 | ## [5.0.4] - 2024-04-11 14 | 15 | ### Updated 16 | 17 | - Updated BeanMapper (4.1.5) 18 | 19 | 20 | ## [5.0.3] - 2024-04-04 21 | 22 | ### Updated 23 | 24 | - Updated BeanMapper (4.1.4) 25 | 26 | ## [5.0.2] - 2024-03-27 27 | 28 | ### Updated 29 | 30 | - Updated BeanMapper (4.1.3) 31 | - Updated jackson-core (2.17.0) 32 | - Updated jackson-databind (2.17.0) 33 | 34 | ## [5.0.0] - 2023-02-22 35 | 36 | ### Updated 37 | 38 | - Updated BeanMapper (4.1.2) 39 | 40 | ## [4.1.0] - 2022-11-10 41 | ### Updated 42 | - Updated BeanMapper (4.1.0) 43 | 44 | ## [4.0.1] - 2022-09-22 45 | ### Updated 46 | - Updated beanmapper(4.0.1) 47 | ### Fixed 48 | - Issue [138](https://github.com/42BV/beanmapper/issues/138), **Beanmapper wrongfully assumes a HibernateProxy is there when you use an inner class**; Properly checking whether nested/inner class implements HibernateProxy fixes the problem where the HibernateAwareBeanUnproxy would attempt to return a non-existent interface. 49 | 50 | ## [4.0.0] - 2022-09-15 51 | ### Changed 52 | - Updated to Java 17 53 | - Updated dependencies to their most recent, stable versions 54 | - Rewrote tests to use JUnit 5, and Mockito, rather than JUnit 4 and JMockit 55 | 56 | ## [3.1.0] - 2020-12-16 57 | ### Owasp vulnerability dependency upgrade 58 | - Added owasp dependency check maven plugin to be able to check dependencies 59 | - Upgraded guava- and jackson deps to resolve owasp findings 60 | - Fixed maven surefire plugin setting to be able to run JMockit tests with Junit4 61 | - Removed travis build config; replaced with github actions CI 62 | 63 | ## [3.0.0] - 2018-07-17 64 | ### Version upgrade 65 | - Full upgrade to **Spring 5 / Spring Boot 2**. This version of BeanMapper Spring is no longer usable with lower versions. 66 | - Issue [#32](https://github.com/42BV/beanmapper-spring/issues/32); **MergePair.beforeMerge must be untouched, not BeanMapped** when a MergePair is used, the ```beforeMerge``` is now retrieved and immediately detached before the target is fetched. Take note that this means that the original's proxies can no longer be resolved. This restriction does not apply to the target. 67 | 68 | ## [2.4.1] - 2018-06-20 69 | ### Version upgrade 70 | - Adoption of underlying BeanMapper library v2.4.1 71 | 72 | ## [2.4.0] - 2018-03-28 73 | ### Added 74 | - Added an AbstractSpringSecuredCheck, which contains the logic for fetching the security Principal and checking against the roles. This class can be used to implement LogicSecuredCheck classes. See also [#107](https://github.com/42BV/beanmapper/issues/107) 75 | - Issue [#29](https://github.com/42BV/beanmapper-spring/issues/29); **Spring Security based implementation for @RoleSecuredCheck** Added a Spring Security implementation for the @BeanRoleSecured handler. It will compare the Principal's authorities against the required authorities. At least one match will suffice to grant access. 76 | 77 | ## [2.2.0] - 2017-11-01 78 | ### Added 79 | - **BREAKING CHANGE** Issue [#26](https://github.com/42BV/beanmapper-spring/issues/26), **Validations for the entity are not run for Lazy targets**; the @MergedForm maps to a Lazy object, it delays the process of mapping until the time that get() is called on the Lazy object. At that time, it should work exactly the same way as a regular validation run. Forms are always validated right away. However, the final objects are only validated when direct mapping takes place. The process has been refactored so that validation on the final target is included in the delayed mapping process. Note that Lazy.get() must now deal with Exception. The pro of this approach is that it hooks onto the regular binding result handler. 80 | - Issue [#28](https://github.com/42BV/beanmapper-spring/issues/28), **when Lazy is used, set flushEnabled=true**; make sure to enable the global flushing after calling clear() when Lazy is used. In this case, the EntityManager is the most likely to be in a transaction context. This change works with [#90](https://github.com/42BV/beanmapper/issues/90), where the default for the BeanCollection is set to true. This means flushing will be called by default for BeanCollections operating in a Lazy "context". 81 | 82 | ## [2.1.0] - 2017-10-25 83 | ### Added 84 | - Issue [#24](https://github.com/42BV/beanmapper-spring/issues/24), **JpaAfterClearFlusher**; a class is offered than can be used to register an EntityManager so that it can be called after BeanMapper has called clear() on a collection. This might come in handy to force the ORM to flush. One use case is to force the inversion of executed SQL statements from insert/delete to delete/insert. 85 | 86 | ## [2.0.2] - 2017-10-18 87 | ### Fixed 88 | - Issue [#21](https://github.com/42BV/beanmapper-spring/issues/21), **Multipart part name not used correctly**; the multipart part name was not handled correctly. Spring's multipart handler is now passed a MethodParameter which has the correct part name and also disable the parameter name explorer, so it is forced to used the overwritten part name. 89 | 90 | ## [2.0.1] - 2017-10-18 91 | ### Fixed 92 | - Issue [#19](https://github.com/42BV/beanmapper-spring/issues/19), **Spring handles multipart forms differently**; v4.1.6 deal with the multipart form by getting the parameterType as the target class. Later Spring versions (at least from 4.3.10.RELEASE onwards), do this by checking the genericParameterType. The solution is to check for the genericParameterType. If it exists, it is overwritten for the multipart form resolution attempt. 93 | 94 | ## [2.0.0] - 2017-10-12 95 | ### Fixed 96 | - Issue [#68](https://github.com/42BV/beanmapper/issues/68), **Change to PageableMapper**; the BeanMapper interface has changed, resulting in an internal change to PageableMapper. Check [BeanMapper changelog](https://github.com/42BV/beanmapper/blob/master/CHANGELOG.md) for more information on the changes. 97 | 98 | ## [1.0.0] - 2017-10-04 99 | ### Added 100 | - Issue [#15](https://github.com/42BV/beanmapper-spring/issues/15), **Retain both pre-merged and merged entities**; on using the MergedForm annotation, when the class MergePair is set as a result and when the annotation field mergePairClass is set, both the original and the target class will be preserved. This allows the developer to compare the before and after situation and react accordingly. One note that must be understood; the original is not the real original (as in; the exact instance found in the database), but is mapped by BeanMapper from the fetched entity to a new, similar entity. The reason for this is that the original instance is cached by Hibernate and will be reused by the target. It cannot be preserved. 101 | - Issue [#16](https://github.com/42BV/beanmapper-spring/issues/16), **@MergedForm must be able to read from RequestPart**; MergedForm can now read from multipart request bodies as well. When the annotation field multipart is set, the value is used to determine which part the content must be read from. Spring's RequestPartMethodArgumentResolver is reused for the process of actually reading the multipart form. 102 | -------------------------------------------------------------------------------- /src/main/java/io/beanmapper/spring/web/MergedFormMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package io.beanmapper.spring.web; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.ParameterizedType; 5 | import java.lang.reflect.Type; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | import jakarta.persistence.EntityManager; 12 | 13 | import io.beanmapper.BeanMapper; 14 | import io.beanmapper.config.BeanMapperBuilder; 15 | import io.beanmapper.spring.Lazy; 16 | import io.beanmapper.spring.web.converter.StructuredBody; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.context.ApplicationContext; 21 | import org.springframework.core.MethodParameter; 22 | import org.springframework.http.converter.HttpMessageConverter; 23 | import org.springframework.web.bind.MethodArgumentNotValidException; 24 | import org.springframework.web.bind.WebDataBinder; 25 | import org.springframework.web.bind.support.WebDataBinderFactory; 26 | import org.springframework.web.context.request.NativeWebRequest; 27 | import org.springframework.web.context.request.RequestAttributes; 28 | import org.springframework.web.method.support.ModelAndViewContainer; 29 | import org.springframework.web.servlet.HandlerMapping; 30 | import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver; 31 | import org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver; 32 | 33 | public class MergedFormMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { 34 | 35 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 36 | 37 | private final BeanMapper beanMapper; 38 | 39 | private final EntityFinder entityFinder; 40 | 41 | private final RequestPartMethodArgumentResolver multiPartResolver; 42 | 43 | private static final boolean FLUSH = true; 44 | private static final boolean NO_FLUSH = false; 45 | 46 | public MergedFormMethodArgumentResolver(List> messageConverters, 47 | BeanMapper beanMapper, 48 | ApplicationContext applicationContext, 49 | EntityManager entityManager) { 50 | this(messageConverters, beanMapper, new SpringDataEntityFinder(applicationContext, entityManager)); 51 | } 52 | 53 | public MergedFormMethodArgumentResolver(List> messageConverters, 54 | BeanMapper beanMapper, 55 | EntityFinder entityFinder) { 56 | super(messageConverters); 57 | this.beanMapper = beanMapper; 58 | this.entityFinder = entityFinder; 59 | this.multiPartResolver = new RequestPartMethodArgumentResolver(messageConverters); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | @Override 66 | public boolean supportsParameter(MethodParameter parameter) { 67 | return parameter.hasParameterAnnotation(MergedForm.class); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | @Override 74 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 75 | NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 76 | 77 | MergedForm annotation = parameter.getParameterAnnotation(MergedForm.class); 78 | Class parameterType = parameter.getParameterType(); 79 | Long id = resolveId(webRequest, annotation.mergeId()); 80 | 81 | final Object form; 82 | if (annotation.multiPart().length() > 0) { 83 | form = readFromMultiPartForm(parameter, mavContainer, webRequest, binderFactory, annotation); 84 | } else { 85 | form = readWithMessageConverters(webRequest, parameter, annotation.value()); 86 | } 87 | 88 | WebRequestParameters webRequestParameters = new WebRequestParameters(parameter, mavContainer, webRequest, binderFactory); 89 | 90 | // Check for @Valid on the mapped target and apply the validation rules to form 91 | validateObject(webRequestParameters, getBody(form)); 92 | 93 | if (Lazy.class.isAssignableFrom(parameterType)) { 94 | ParameterizedType genericType = (ParameterizedType) parameter.getGenericParameterType(); 95 | Type entityType = genericType.getActualTypeArguments()[0]; 96 | return new LazyResolveEntity(form, id, (Class) entityType, annotation, webRequestParameters); 97 | } else { 98 | return resolveEntity(form, id, parameterType, annotation, webRequestParameters, NO_FLUSH); 99 | } 100 | } 101 | 102 | private Object readFromMultiPartForm( 103 | MethodParameter parameter, ModelAndViewContainer mavContainer, 104 | NativeWebRequest webRequest, WebDataBinderFactory binderFactory, 105 | MergedForm annotation) throws Exception { 106 | MethodParameter formParameter = new MethodParameter(parameter); 107 | setMethodParameterField(formParameter, "parameterType", annotation.value()); 108 | setMethodParameterField(formParameter, "genericParameterType", annotation.value()); 109 | setMethodParameterField(formParameter, "parameterName", annotation.multiPart()); 110 | setMethodParameterField(formParameter, "parameterNameDiscoverer", null); 111 | return multiPartResolver.resolveArgument(formParameter, mavContainer, webRequest, binderFactory); 112 | } 113 | 114 | private void setMethodParameterField(MethodParameter formParameter, String fieldName, Object value) throws IllegalAccessException { 115 | try { 116 | Field f = formParameter.getClass().getDeclaredField(fieldName); 117 | f.setAccessible(true); 118 | f.set(formParameter, value); 119 | } catch (NoSuchFieldException err) { 120 | logger.warn("Older Spring version? Update to at least 4.3.10.RELEASE, see https://github.com/42BV/beanmapper-spring/issues/19"); 121 | } 122 | } 123 | 124 | private void validateObject(WebRequestParameters webRequestParameters, Object objectToValidate) throws Exception { 125 | WebDataBinder binder = webRequestParameters.createBinder(objectToValidate); 126 | if (objectToValidate != null) { 127 | validateIfApplicable(binder, webRequestParameters.getParameter()); 128 | if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, webRequestParameters.getParameter())) { 129 | throw new MethodArgumentNotValidException(webRequestParameters.getParameter(), binder.getBindingResult()); 130 | } 131 | } 132 | webRequestParameters.setBindingResult(binder.getBindingResult()); 133 | } 134 | 135 | private Long resolveId(NativeWebRequest webRequest, String mergeId) { 136 | if(mergeId == null || mergeId.isEmpty()) { 137 | return null; 138 | } 139 | 140 | // First check the URI variables (ie, comparable to @PathVariable) 141 | Map uriTemplateVars = getUriTemplateVars(webRequest); 142 | String mergeIdValue = uriTemplateVars != null ? uriTemplateVars.get(mergeId) : null; 143 | 144 | // If the mergeIdValue was not found, check the query parameters 145 | if (mergeIdValue == null) { 146 | mergeIdValue = webRequest.getParameter(mergeId); 147 | } 148 | 149 | return mergeIdValue != null ? Long.valueOf(mergeIdValue) : null; 150 | } 151 | 152 | @SuppressWarnings("unchecked") 153 | private Map getUriTemplateVars(NativeWebRequest webRequest) { 154 | return (Map) webRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); 155 | } 156 | 157 | private Object getBody(Object form) { 158 | if (form == null) return null; 159 | return form instanceof StructuredBody structuredBody ? structuredBody.body() : form; 160 | } 161 | 162 | private Set getPropertyNames(Object form) { 163 | if (form == null) return null; 164 | return form instanceof StructuredBody structuredBody ? structuredBody.propertyNames() : null; 165 | } 166 | 167 | private Object resolveEntity( 168 | Object form, Long id, Class entityClass, MergedForm annotation, 169 | WebRequestParameters webRequestParameters, boolean mustFlush) throws Exception { 170 | Object data = getBody(form); 171 | Set propertyNames = getPropertyNames(form); 172 | 173 | // Modify the BeanMapper to deal with special situations 174 | BeanMapperBuilder customBeanMapperBuilder = beanMapper.wrap(); 175 | if (annotation.patch() && propertyNames != null) { 176 | customBeanMapperBuilder.downsizeSource(new ArrayList<>(propertyNames)); 177 | } 178 | if (mustFlush) { 179 | customBeanMapperBuilder.setFlushEnabled(true); 180 | } 181 | BeanMapper customBeanMapper = customBeanMapperBuilder.build(); 182 | 183 | final MergePair mergePair = new MergePair<>(customBeanMapper, entityFinder, entityClass, annotation); 184 | 185 | if (id == null) { 186 | // Create a new entity using our form data 187 | mergePair.initNew(data); 188 | } else { 189 | // Map our input form on the already persisted entity 190 | mergePair.merge(data, id); 191 | } 192 | Object mappedTarget = mergePair.result(); 193 | // Check for @Valid on the mapped target and apply the validation rules to the mapped target 194 | validateObject(webRequestParameters, mappedTarget); 195 | return mappedTarget; 196 | } 197 | 198 | private class LazyResolveEntity implements Lazy { 199 | 200 | private final Object form; 201 | private final Long id; 202 | private final Class entityClass; 203 | private final MergedForm annotation; 204 | private final WebRequestParameters webRequestParameters; 205 | 206 | public LazyResolveEntity( 207 | Object form, Long id, Class entityClass, 208 | MergedForm annotation, WebRequestParameters webRequestParameters) { 209 | this.form = form; 210 | this.id = id; 211 | this.entityClass = entityClass; 212 | this.annotation = annotation; 213 | this.webRequestParameters = webRequestParameters; 214 | } 215 | 216 | /** 217 | * {@inheritDoc} 218 | */ 219 | @Override 220 | public Object get() throws Exception { 221 | return resolveEntity(form, id, entityClass, annotation, webRequestParameters, FLUSH); 222 | } 223 | 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | io.beanmapper 5 | beanmapper-spring 6 | 7.0.1-SNAPSHOT 7 | jar 8 | 9 | 42 Bean Mapper Spring Support 10 | Spring support for the Bean Mapper 11 | 2015 12 | https://www.42.nl 13 | 14 | 15 | 42 BV 16 | https://www.42.nl 17 | 18 | 19 | 20 | 21 | The Apache Software License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | repo 24 | 25 | 26 | 27 | 28 | scm:git:git@github.com:42BV/beanmapper-spring.git 29 | scm:git:git@github.com:42BV/beanmapper-spring.git 30 | https://github.com/42BV/beanmapper-spring 31 | HEAD 32 | 33 | 34 | 35 | 36 | Robert Bor 37 | 42 B.V. 38 | https://www.42.nl 39 | 40 | 41 | Jeroen van Schagen 42 | 42 B.V. 43 | https://www.42.nl 44 | 45 | 46 | Marcus Talbot 47 | 42 B.V. 48 | https://www.42.nl 49 | 50 | 51 | 52 | 53 | 21 54 | UTF-8 55 | 56 | 4.0.0 57 | 6.0.0 58 | 6.0.1 59 | 4.0.2 60 | 2.7.4 61 | 6.1.0 62 | 2.10.0 63 | 2.0.1 64 | 65 | 3.5.4 66 | 3.13.0 67 | 0.8.14 68 | 3.2.8 69 | 3.11.1 70 | 3.3.1 71 | 0.8.0 72 | 12.1.9 73 | 74 | 75 | 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-dependencies 80 | ${spring.boot.version} 81 | pom 82 | import 83 | 84 | 85 | 86 | 87 | 88 | io.beanmapper 89 | beanmapper 90 | ${beanmapper.version} 91 | 92 | 93 | org.springframework 94 | spring-web 95 | provided 96 | 97 | 98 | org.springframework 99 | spring-webmvc 100 | provided 101 | 102 | 103 | org.springframework.security 104 | spring-security-config 105 | provided 106 | 107 | 108 | org.springframework.data 109 | spring-data-jpa 110 | provided 111 | 112 | 113 | org.hibernate.orm 114 | hibernate-core 115 | provided 116 | 117 | 118 | jakarta.servlet 119 | jakarta.servlet-api 120 | ${jakarta.servlet.version} 121 | provided 122 | 123 | 124 | tools.jackson.core 125 | jackson-core 126 | provided 127 | 128 | 129 | tools.jackson.core 130 | jackson-databind 131 | provided 132 | 133 | 134 | 135 | org.mockito 136 | mockito-junit-jupiter 137 | test 138 | 139 | 140 | org.springframework.boot 141 | spring-boot-webmvc-test 142 | 143 | 144 | com.jayway.jsonpath 145 | json-path 146 | ${jsonpath.version} 147 | test 148 | 149 | 150 | org.hsqldb 151 | hsqldb 152 | ${hsqldb.version} 153 | test 154 | 155 | 156 | nl.42 157 | heph 158 | ${heph.version} 159 | test 160 | 161 | 162 | org.springframework.boot 163 | spring-boot-starter-test 164 | test 165 | 166 | 167 | org.hibernate.validator 168 | hibernate-validator 169 | test 170 | 171 | 172 | jakarta.el 173 | jakarta.el-api 174 | ${el.api.version} 175 | test 176 | 177 | 178 | org.glassfish 179 | jakarta.el 180 | ${el.impl.version} 181 | test 182 | 183 | 184 | 185 | 186 | 187 | 188 | maven-surefire-plugin 189 | ${maven-surefire-plugin.version} 190 | 191 | 192 | org.apache.maven.plugins 193 | maven-compiler-plugin 194 | ${maven-compiler-plugin.version} 195 | 196 | ${java.version} 197 | ${java.version} 198 | ${project.build.sourceEncoding} 199 | 200 | 201 | 202 | 203 | org.jacoco 204 | jacoco-maven-plugin 205 | ${jacoco-maven-plugin.version} 206 | 207 | 208 | 209 | prepare-agent 210 | 211 | 212 | 213 | report 214 | test 215 | 216 | report 217 | 218 | 219 | 220 | 221 | 222 | 223 | org.owasp 224 | dependency-check-maven 225 | ${dependency-check-maven.version} 226 | 227 | true 228 | true 229 | true 230 | true 231 | false 232 | owasp-suppressions.xml 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | release 241 | 242 | 243 | 244 | org.apache.maven.plugins 245 | maven-javadoc-plugin 246 | ${maven-javadoc-plugin.version} 247 | 248 | 249 | attach-javadocs 250 | 251 | jar 252 | 253 | 254 | 255 | 256 | 257 | org.apache.maven.plugins 258 | maven-source-plugin 259 | ${maven-source-plugin.version} 260 | 261 | 262 | attach-sources 263 | 264 | jar-no-fork 265 | 266 | 267 | 268 | 269 | 270 | org.apache.maven.plugins 271 | maven-gpg-plugin 272 | ${maven-gpg-plugin.version} 273 | 274 | 275 | sign-artifacts 276 | verify 277 | 278 | sign 279 | 280 | 281 | 282 | 283 | 284 | org.sonatype.central 285 | central-publishing-maven-plugin 286 | ${central-publishing-maven-plugin.version} 287 | true 288 | 289 | central 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /src/test/java/io/beanmapper/spring/web/PersonControllerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) 2014 42 bv (www.42.nl). All rights reserved. 3 | */ 4 | package io.beanmapper.spring.web; 5 | 6 | import static org.mockito.Mockito.doNothing; 7 | import static org.mockito.Mockito.times; 8 | import static org.mockito.Mockito.verify; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | import io.beanmapper.BeanMapper; 17 | import io.beanmapper.config.BeanMapperBuilder; 18 | import io.beanmapper.spring.ApplicationConfig; 19 | import io.beanmapper.spring.flusher.JpaAfterClearFlusher; 20 | import io.beanmapper.spring.model.Person; 21 | import io.beanmapper.spring.model.PersonRepository; 22 | import io.beanmapper.spring.model.Tag; 23 | import io.beanmapper.spring.web.converter.StructuredJsonMessageConverter; 24 | import io.beanmapper.spring.web.mockmvc.AbstractControllerTest; 25 | import jakarta.persistence.EntityManager; 26 | 27 | import org.junit.jupiter.api.BeforeEach; 28 | import org.junit.jupiter.api.Test; 29 | import org.junit.jupiter.api.extension.ExtendWith; 30 | import org.mockito.Mock; 31 | import org.mockito.junit.jupiter.MockitoExtension; 32 | import org.springframework.beans.factory.annotation.Autowired; 33 | import org.springframework.context.ApplicationContext; 34 | import org.springframework.format.support.FormattingConversionService; 35 | import org.springframework.http.MediaType; 36 | import org.springframework.http.converter.HttpMessageConverter; 37 | import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; 38 | import org.springframework.mock.web.MockMultipartFile; 39 | import org.springframework.test.web.servlet.MockMvc; 40 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 41 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 42 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 43 | 44 | @ExtendWith(MockitoExtension.class) 45 | public class PersonControllerTest extends AbstractControllerTest { 46 | 47 | private HttpMessageConverter converter = new StructuredJsonMessageConverter(new JacksonJsonHttpMessageConverter()); 48 | private BeanMapper beanMapper; 49 | 50 | @Autowired 51 | private ApplicationContext applicationContext; 52 | 53 | @Autowired 54 | private PersonRepository personRepository; 55 | 56 | @Mock 57 | private EntityManager entityManager; 58 | 59 | @BeforeEach 60 | public void setUp() { 61 | beanMapper = new BeanMapperBuilder() 62 | .addPackagePrefix(ApplicationConfig.class) 63 | .build(); 64 | 65 | this.webClient = createWebClient(beanMapper); 66 | } 67 | 68 | private MockMvc createWebClient(BeanMapper beanMapper) { 69 | return MockMvcBuilders.standaloneSetup(new PersonController()) 70 | .setCustomArgumentResolvers(new MergedFormMethodArgumentResolver( 71 | List.of(converter), 72 | beanMapper, 73 | applicationContext, 74 | entityManager)) 75 | .setMessageConverters(converter) 76 | .setConversionService(new FormattingConversionService()) 77 | .build(); 78 | } 79 | 80 | @Test 81 | public void testCreate() throws Exception { 82 | this.webClient.perform(MockMvcRequestBuilders.post("/person") 83 | .content("{\"name\":\"Henk\"}") 84 | .contentType(MediaType.APPLICATION_JSON)) 85 | .andDo(MockMvcResultHandlers.print()) 86 | .andExpect(status().isOk()) 87 | .andExpect(jsonPath("$.name").value("Henk")); 88 | } 89 | 90 | @Test 91 | public void testUpdatePatch() throws Exception { 92 | Person person = new Person(); 93 | person.setName("Henk"); 94 | person.setCity("Lisse"); 95 | personRepository.save(person); 96 | 97 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/patch") 98 | .content("{\"name\":\"Jan\"}") 99 | .contentType(MediaType.APPLICATION_JSON)) 100 | .andDo(MockMvcResultHandlers.print()) 101 | .andExpect(status().isOk()) 102 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 103 | .andExpect(jsonPath("$.name").value("Jan")) 104 | .andExpect(jsonPath("$.city").value("Lisse")); 105 | } 106 | 107 | @Test 108 | public void testUpdateNoPatch() throws Exception { 109 | Person person = new Person(); 110 | person.setName("Henk"); 111 | person.setCity("Lisse"); 112 | personRepository.save(person); 113 | 114 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/no-patch") 115 | .content("{\"name\":\"Jan\"}") 116 | .contentType(MediaType.APPLICATION_JSON)) 117 | .andDo(MockMvcResultHandlers.print()) 118 | .andExpect(status().isOk()) 119 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 120 | .andExpect(jsonPath("$.name").value("Jan")) 121 | .andExpect(jsonPath("$.city").doesNotExist()); 122 | } 123 | 124 | @Test 125 | public void testUpdateNoPatchMergeIdAsRequestParam() throws Exception { 126 | Person person = new Person(); 127 | person.setName("Henk"); 128 | person.setCity("Lisse"); 129 | personRepository.save(person); 130 | 131 | this.webClient.perform(MockMvcRequestBuilders.put("/person/query-param") 132 | .param("id", person.getId().toString()) 133 | .content("{\"name\":\"Jan\"}") 134 | .contentType(MediaType.APPLICATION_JSON)) 135 | .andDo(MockMvcResultHandlers.print()) 136 | .andExpect(status().isOk()) 137 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 138 | .andExpect(jsonPath("$.name").value("Jan")) 139 | .andExpect(jsonPath("$.city").doesNotExist()); 140 | } 141 | 142 | @Test 143 | public void testLazy() throws Exception { 144 | Person person = new Person(); 145 | person.setName("Henk"); 146 | personRepository.save(person); 147 | 148 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/lazy") 149 | .content("{\"name\":\"Jan\"}") 150 | .contentType(MediaType.APPLICATION_JSON)) 151 | .andDo(MockMvcResultHandlers.print()) 152 | .andExpect(status().isOk()) 153 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 154 | .andExpect(jsonPath("$.name").value("Jan")); 155 | } 156 | 157 | @Test 158 | public void testLazyTriggersFlush() throws Exception { 159 | 160 | doNothing().when(entityManager).flush(); 161 | 162 | JpaAfterClearFlusher flusher = new JpaAfterClearFlusher(entityManager); 163 | 164 | this.webClient = createWebClient(new BeanMapperBuilder() 165 | .addPackagePrefix(ApplicationConfig.class) 166 | .addAfterClearFlusher(flusher) 167 | .build()); 168 | 169 | Person person = new Person(); 170 | person.setName("Henk"); 171 | person.setTags(Arrays.asList(Tag.CUSTOMER, Tag.UPSELLING)); 172 | personRepository.save(person); 173 | 174 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/lazy") 175 | .content("{\"name\":\"Jan\",\"tags\":[\"DEBTOR\",\"CUSTOMER\"]}") 176 | .contentType(MediaType.APPLICATION_JSON)) 177 | .andDo(MockMvcResultHandlers.print()) 178 | .andExpect(status().isOk()) 179 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 180 | .andExpect(jsonPath("$.name").value("Jan")); 181 | 182 | verify(entityManager, times(1)).flush(); 183 | } 184 | 185 | @Test 186 | public void testLazyFailFinalValidation() throws Exception { 187 | Person person = new Person(); 188 | person.setName("Henk"); 189 | personRepository.save(person); 190 | 191 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/lazy") 192 | .content("{}") 193 | .contentType(MediaType.APPLICATION_JSON)) 194 | .andDo(MockMvcResultHandlers.print()) 195 | .andExpect(status().is4xxClientError()); 196 | } 197 | 198 | @Test 199 | public void multipartForm() throws Exception { 200 | Person person = new Person(); 201 | person.setName("Henk"); 202 | person.setStreet("Stationsplein"); 203 | personRepository.save(person); 204 | 205 | byte[] bytes = "CAFEBABE".getBytes(); 206 | MockMultipartFile photoPart = new MockMultipartFile("photo", "photo.jpeg", "image/jpeg", bytes); 207 | MockMultipartFile personPart = new MockMultipartFile("personForm", "", "application/json", "{\"name\":\"Jan\"}".getBytes()); 208 | 209 | webClient.perform( 210 | MockMvcRequestBuilders 211 | .multipart("/person/" + person.getId() + "/multipart") 212 | .file(personPart) 213 | .file(photoPart) 214 | ) 215 | .andExpect(status().isOk()) 216 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 217 | .andExpect(jsonPath("$.name").value("Jan")) 218 | .andExpect(jsonPath("$.street").value("Stationsplein")); 219 | } 220 | 221 | @Test 222 | public void testUpdateTags() throws Exception { 223 | Person person = new Person(); 224 | person.setName("Henk"); 225 | person.setCity("Leiden"); 226 | person.setTags(new ArrayList() {{ 227 | add(Tag.DEBTOR); 228 | add(Tag.UPSELLING); 229 | }}); 230 | personRepository.save(person); 231 | 232 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/no-patch") 233 | .content("{\"name\":\"Jan\",\"city\":\"Lisse\",\"tags\":[\"DEBTOR\",\"CUSTOMER\"]}") 234 | .contentType(MediaType.APPLICATION_JSON)) 235 | .andDo(MockMvcResultHandlers.print()) 236 | .andExpect(status().isOk()) 237 | .andExpect(jsonPath("$.id").value(person.getId().intValue())) 238 | .andExpect(jsonPath("$.name").value("Jan")) 239 | .andExpect(jsonPath("$.city").value("Lisse")) 240 | .andExpect(jsonPath("$.tags[0]").value("DEBTOR")) 241 | .andExpect(jsonPath("$.tags[1]").value("CUSTOMER")); 242 | } 243 | 244 | @Test 245 | public void testPair() throws Exception { 246 | Person person = new Person(); 247 | person.setName("Henk"); 248 | person.setCity("Leiden"); 249 | person.setStreet("Stationsplein"); 250 | person.setHouseNumber("42"); 251 | person.setBankAccount("1234567890"); 252 | personRepository.save(person); 253 | 254 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/pair") 255 | .content("{\"name\":\"Jan\",\"city\":\"Delft\"}") 256 | .contentType(MediaType.APPLICATION_JSON)) 257 | .andDo(MockMvcResultHandlers.print()) 258 | .andExpect(status().isOk()) 259 | .andExpect(jsonPath("$.beforeMerge.id").value(person.getId().intValue())) 260 | .andExpect(jsonPath("$.beforeMerge.name").value("Henk")) 261 | .andExpect(jsonPath("$.beforeMerge.street").value("Stationsplein")) 262 | .andExpect(jsonPath("$.beforeMerge.city").value("Leiden")) 263 | .andExpect(jsonPath("$.beforeMerge.houseNumber").value("42")) 264 | .andExpect(jsonPath("$.beforeMerge.bankAccount").value("1234567890")) 265 | .andExpect(jsonPath("$.afterMerge.id").value(person.getId().intValue())) 266 | .andExpect(jsonPath("$.afterMerge.name").value("Jan")) 267 | .andExpect(jsonPath("$.afterMerge.street").value("Stationsplein")) 268 | .andExpect(jsonPath("$.afterMerge.city").value("Delft")) 269 | .andExpect(jsonPath("$.afterMerge.houseNumber").value("42")) 270 | .andExpect(jsonPath("$.afterMerge.bankAccount").value("1234567890")); 271 | } 272 | 273 | @Test 274 | public void testPairWithCollection() throws Exception { 275 | Person person = new Person(); 276 | person.setName("Henk"); 277 | person.setCity("Leiden"); 278 | person.setStreet("Stationsplein"); 279 | person.setHouseNumber("42"); 280 | person.setBankAccount("1234567890"); 281 | person.setTags(new ArrayList() {{ 282 | add(Tag.UPSELLING); 283 | }}); 284 | personRepository.save(person); 285 | 286 | this.webClient.perform(MockMvcRequestBuilders.put("/person/" + person.getId() + "/pair") 287 | .content("{\"name\":\"Jan\",\"city\":\"Lisse\",\"tags\":[\"CUSTOMER\",\"DEBTOR\"]}") 288 | .contentType(MediaType.APPLICATION_JSON)) 289 | .andDo(MockMvcResultHandlers.print()) 290 | .andExpect(status().isOk()) 291 | .andExpect(jsonPath("$.beforeMerge.id").value(person.getId().intValue())) 292 | .andExpect(jsonPath("$.beforeMerge.tags[0]").value("UPSELLING")) 293 | .andExpect(jsonPath("$.afterMerge.id").value(person.getId().intValue())) 294 | .andExpect(jsonPath("$.afterMerge.tags[0]").value("CUSTOMER")) 295 | .andExpect(jsonPath("$.afterMerge.tags[1]").value("DEBTOR")); 296 | } 297 | 298 | } 299 | --------------------------------------------------------------------------------