diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e4d5625 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index dff5f3a..d845c6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,12 @@ language: java +script: ./mvnw install --fail-at-end +matrix: + include: + - jdk: openjdk8 + env: JACOCO=true COVERALLS=true + - jdk: oraclejdk8 + - jdk: oraclejdk9 + - jdk: openjdk8 + env: GDMSESSION=sonar + - jdk: openjdk8 + env: SONAR=publish diff --git a/README.md b/README.md index c847e82..5bb0520 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,165 @@ # JPA mapping utilities for MapStruct - -TODO: write readme file -## Dependencies - - * Java >= 7 +[![Build Status](https://travis-ci.org/wavesoftware/java-mapstruct-jpa.svg?branch=master)](https://travis-ci.org/wavesoftware/java-mapstruct-jpa) [![Quality Gate](https://sonar.wavesoftware.pl/api/badges/gate?key=pl.wavesoftware.utils:mapstruct-jpa)](https://sonar.wavesoftware.pl/dashboard/index/pl.wavesoftware.utils:mapstruct-jpa) [![Coverage Status](https://coveralls.io/repos/github/wavesoftware/java-mapstruct-jpa/badge.svg?branch=master)](https://coveralls.io/github/wavesoftware/java-mapstruct-jpa?branch=master) [![Maven Central](https://img.shields.io/maven-central/v/pl.wavesoftware.utils/mapstruct-jpa.svg)](https://mvnrepository.com/artifact/pl.wavesoftware.utils/mapstruct-jpa) + +A set of utilities focused on mapping JPA managed entities with MapStruct. There are different utilities for different purposes and also a all-in-one utility for maximizing ease of use. + +## Features + +* Domain model graph with cycles - via `CyclicGraphContext` +* JPA aware mapping with update capability - via `JpaMappingContext` factory +* [N+1 problem](https://stackoverflow.com/questions/97197/what-is-the-n1-select-query-issue) solution via special uninitialized collection classes, that throws exceptions if used + +### Domain model graph with cycles + +If you need to map a domain model with cycles in entity graph for ex.: (Pet.owner -> Person, Person.pets -> Pet) you can use a `CyclicGraphContext` as a MapStruct `@Context` + +```java +@Mapper +interface PetMapper { + Pet map(PetData data, @Context CyclicGraphContext context); + PetData map(Pet pet, @Context CyclicGraphContext context); +} +``` + +### JPA aware mapping with update capability + +If you also need support for mapping JPA managed entities and be able to update them (not create new records) there more to be done. There is provided `JpaMappingContext` with factory. It requires couple more configuration to instantiate this context. + +`JpaMappingContext` factory requires: +* Supplier of `StoringMappingContext` to handle cycles - `CyclicGraphContext` can be used here, +* `Mappings` object that will provides mapping for given source and target class - mapping is information how to update existing object (managed entity) with data from source object, +* `IdentifierCollector` should collect managed entity ID from source object + +The easiest way to setup all of this is to extend `AbstractJpaContextProvider`, implement `IdentifierCollector` and implement a set of `MappingProvider` for each type of entity. To provide implementations of `MappingProvider` you should create update methods in your MapStruct mappers. It utilize `CompositeContext` which can incorporate any number of contexts as a composite. + +All of this can be managed by some DI container like Spring or Guice. + +**Mapping facade as Spring service:** + +```java +@Service +@RequiredArgsConstructor +final class MapperFacadeImpl implements MapperFacade { + + private final PetMapper petMapper; + private final MapStructContextProvider contextProvider; + + @Override + public PetJPA map(Pet pet) { + return petMapper.map(pet, contextProvider.createNewContext()); + } + + @Override + public Pet map(PetJPA jpa) { + return petMapper.map(jpa, contextProvider.createNewContext()); + } +} +``` + +**Context provider as Spring service:** + +```java +@Service +@RequiredArgsConstructor +final class CompositeContextProvider extends AbstractCompositeContextProvider { + + @Getter + private final JpaMappingContextFactory jpaMappingContextFactory; + private final List> mappingProviders; + @Getter + private final IdentifierCollector identifierCollector; + + @Override + protected Iterable getMappingProviders() { + return Collections.unmodifiableSet(mappingProviders); + } + +} +``` + +**Example mapping provider for Pet as Spring service:** + +```java +@Service +@RequiredArgsConstructor +final class PetMappingProvider implements MappingProvider { + + private final PetMapper petMapper; + + @Override + public Mapping provide() { + return AbstractCompositeContextMapping.mapperFor( + Pet.class, PetJPA.class, + petMapper::updateFromPet + ); + } +} +``` + +**Identifier collector implementation as Spring service:** + +```java +@Service +final class IdentifierCollectorImpl implements IdentifierCollector { + @Override + public Optional getIdentifierFromSource(Object source) { + if (source instanceof AbstractEntity) { + AbstractEntity entity = AbstractEntity.class.cast(source); + return Optional.ofNullable( + entity.getReference() + ); + } + return Optional.empty(); + } +} +``` + +**HINT:** Complete working example in Spring can be seen in [coi-gov-pl/spring-clean-architecture hibernate module](https://github.com/coi-gov-pl/spring-clean-architecture/tree/develop/pets/persistence-hibernate/src/main/java/pl/gov/coi/cleanarchitecture/example/spring/pets/persistence/hibernate/mapper) + +**HINT:** An example for Guice can be seen in this repository in test packages. + +### N+1 problem solution via special uninitialized collection classes + +The N+1 problem is wide known and prominent problem when dealing with JPA witch utilizes lazy loading of data. Solution to this is that developers should fetch only data that they will need (for ex.: using `JOIN FETCH` in JPQL). In many cases that is not enough. It easy to slip some loop when dealing with couple of records. + +My solution is to detect that object is not loaded fully and provide a stub that will fail fast if data is not loaded and been tried to be used by other developer. To do that simple use `Uninitialized*` classes provided. There are `UninitializedList`, `UninitializedSet`, and `UninitializedMap`. + +```java +@Mapper +interface PetMapper { + // [..] + default List petJPASetToPetList(Set set, + @Context CompositeContext context) { + if (!Hibernate.isInitialized(set)) { + return new UninitializedList<>(PetJPA.class); + } + return set.stream() + .map(j -> map(j, context)) + .collect(Collectors.toList()); + } + // [..] +} +``` + +**Disclaimer:** In future we plan to provide an automatic solution using dynamic proxy objects. + +## Dependencies + + * Java >= 8 + * [MapStruct JDK8](https://github.com/mapstruct/mapstruct/tree/master/core-jdk8) >= 1.2.0 * [EID Exceptions](https://github.com/wavesoftware/java-eid-exceptions) library - -### Contributing - + +### Contributing + Contributions are welcome! - + To contribute, follow the standard [git flow](http://danielkummer.github.io/git-flow-cheatsheet/) of: - + 1. Fork it 1. Create your feature branch (`git checkout -b feature/my-new-feature`) 1. Commit your changes (`git commit -am 'Add some feature'`) 1. Push to the branch (`git push origin feature/my-new-feature`) 1. Create new Pull Request - + Even if you can't contribute code, if you have an idea for an improvement please open an [issue](https://github.com/wavesoftware/java-mapstruct-jpa/issues). diff --git a/pom.xml b/pom.xml index af8348e..7c2dff7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ pl.wavesoftware.utils mapstruct-jpa - 0.1.0-SNAPSHOT + 0.1.0 jar JPA mapping utilities for MapStruct @@ -67,7 +67,7 @@ ${project.build.directory}/sonar https://sonar.wavesoftware.pl jacoco - 7 + 8 ${java.source.version} 1.${java.source.version} ${maven.compiler.source} @@ -80,10 +80,12 @@ + - pl.wavesoftware - eid-exceptions - 1.2.0 + javax.persistence + javax.persistence-api + 2.2 + provided org.projectlombok @@ -92,7 +94,28 @@ provided true + + org.mapstruct + mapstruct-jdk8 + [1.2.0.Final,2.0.0) + provided + + + org.mapstruct + mapstruct-processor + [1.2.0.Final,2.0.0) + provided + true + + + + + pl.wavesoftware + eid-exceptions + 1.2.0 + + junit junit @@ -102,7 +125,25 @@ org.assertj assertj-core - 2.5.0 + 3.9.1 + test + + + org.mockito + mockito-core + 2.18.3 + test + + + com.google.inject + guice + 4.2.0 + test + + + org.hibernate + hibernate-core + 5.2.17.Final test diff --git a/src/main/java/pl/wavesoftware/lang/TriConsumer.java b/src/main/java/pl/wavesoftware/lang/TriConsumer.java new file mode 100644 index 0000000..618d41b --- /dev/null +++ b/src/main/java/pl/wavesoftware/lang/TriConsumer.java @@ -0,0 +1,54 @@ +package pl.wavesoftware.lang; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Represents an operation that accepts tree input arguments and returns no + * result. This is the tree-arity specialization of {@link Consumer}. + * Unlike most other functional interfaces, {@code TriConsumer} is expected + * to operate via side-effects. + * + *

This is a functional interface + * whose functional method is {@link #accept(Object, Object, Object)}. + * + * @param the type of the first argument to the operation + * @param the type of the second argument to the operation + * @param the type of the third argument to the operation + * + * @see Consumer + * @author Krzysztof Suszynski + * @since 25.04.18 + */ +@FunctionalInterface +public interface TriConsumer { + /** + * Performs this operation on the given arguments. + * + * @param t the first input argument + * @param u the second input argument + * @param v the third input argument + */ + void accept(T t, U u, V v); + + /** + * Returns a composed {@code TriConsumer} that performs, in sequence, this + * operation followed by the {@code after} operation. If performing either + * operation throws an exception, it is relayed to the caller of the + * composed operation. If performing this operation throws an exception, + * the {@code after} operation will not be performed. + * + * @param after the operation to perform after this operation + * @return a composed {@code TriConsumer} that performs in sequence this + * operation followed by the {@code after} operation + * @throws NullPointerException if {@code after} is null + */ + default TriConsumer andThen(TriConsumer after) { + Objects.requireNonNull(after); + + return (f, s, t) -> { + accept(f, s, t); + after.accept(f, s, t); + }; + } +} diff --git a/src/main/java/pl/wavesoftware/lang/package-info.java b/src/main/java/pl/wavesoftware/lang/package-info.java new file mode 100644 index 0000000..57dbc5d --- /dev/null +++ b/src/main/java/pl/wavesoftware/lang/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.lang; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractCompositeContextMapping.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractCompositeContextMapping.java new file mode 100644 index 0000000..f167c1f --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractCompositeContextMapping.java @@ -0,0 +1,82 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import pl.wavesoftware.lang.TriConsumer; + +/** + * An abstract class that can be extended to provide logic in how to apply data from + * {@link I} input object to an target {@link O} output object. It uses {@link CompositeContext} + * as a MapStruct context. + * + *

+ * It's designed to be used easily with + * + * MapStruct update methods. + *

+ * @Service
+ * @RequiredArgsConstructor
+ * final class PetCompositeContextMapping extends AbstractCompositeContextMapping<Pet,PetData> {
+ *   private final PetMapper petMapper;
+ *   @Override
+ *   public void accept(Pet pet, PetData data, CompositeContext context) {
+ *     petMapper.updateFromPet(pet, data, context);
+ *   }
+ * }
+ * 
+ * + * There is also a convenience method {@link #mappingFor(Class, Class, TriConsumer)} which can be + * used to create mapping easily with {@link AbstractJpaContextProvider}: + * + *
+ *
+ * @RequiredArgsConstructor
+ * final class OwnerMappingProvider implements MappingProvider<Owner, OwnerJPA, CompositeContext> {
+ *   private final OwnerMapper ownerMapper;
+ *
+ *   @Override
+ *   public Mapping<Owner, OwnerJPA, CompositeContext> provide() {
+ *     return AbstractCompositeContextMapping.mappingFor(
+ *       Owner.class, OwnerJPA.class,
+ *       ownerMapper::updateFromOwner
+ *     );
+ *   }
+ * }
+ * 
+ * + * @param a type of input object to map from + * @param a type of output object to map to + * @author Krzysztof Suszyński + * @since 2018-05-02 + */ +public abstract class AbstractCompositeContextMapping + extends AbstractMapping { + + protected AbstractCompositeContextMapping(Class sourceClass, + Class targetClass) { + super(sourceClass, targetClass, CompositeContext.class); + } + + /** + * A convenience method which can be used to create mapping easily with + * {@link AbstractJpaContextProvider}. + * + * @param inputClass a class of an input object + * @param outputClass a class of an output object + * @param consumer a consumer of 3 values: input object, output object + * and {@link CompositeContext} object + * @param a type of input object to map from + * @param a type of output object to map to + * @return a mapping for given values + */ + public static Mapping mappingFor( + Class inputClass, + Class outputClass, + TriConsumer consumer) { + + return new AbstractCompositeContextMapping(inputClass, outputClass) { + @Override + public void accept(I input, O output, CompositeContext context) { + consumer.accept(input, output, context); + } + }; + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractJpaContextProvider.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractJpaContextProvider.java new file mode 100644 index 0000000..2d656a4 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractJpaContextProvider.java @@ -0,0 +1,52 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import javax.persistence.EntityManager; +import java.util.function.Supplier; + +/** + * A base abstract class that can be used as a provider for configured and fully working + * JPA aware mapping context with update capability. It's designed to be implemented and + * configured in your DI container to used in your mapper facade to produce new context each + * time you are doing a mapping. + * + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +public abstract class AbstractJpaContextProvider + implements MapStructContextProvider { + + private final JpaMappingContextProviderImpl provider = + new JpaMappingContextProviderImpl(); + + /** + * A method to that returns a supplier of JPA's {@link EntityManager} bounded to + * current transaction context. + * + * @return a supplier of current EntityManager + */ + protected abstract Supplier getEntityManager(); + + /** + * Returns a collection of mapping providers to be used in mapping. Each mapping provider + * is for other mapping (for ex.: Pet to PetData etc.). + * + * @return a collection of mapping providers + */ + protected abstract Iterable> getMappingProviders(); + + /** + * Returns a identifier collector that can fetch an ID to be used to fetch a managed entity + * from {@link EntityManager}. It will try to fetch the ID from source mapping object, so + * it must contain some kind of way to provide it. + * + * @return a identifier collector + */ + protected abstract IdentifierCollector getIdentifierCollector(); + + @Override + public CompositeContext createNewContext() { + return provider.provide( + getEntityManager(), getMappingProviders(), getIdentifierCollector() + ); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractMapping.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractMapping.java new file mode 100644 index 0000000..6182034 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/AbstractMapping.java @@ -0,0 +1,22 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * An abstract mapping that holds all classes representations. + * + * @param a type of input object for map from + * @param a type of output (target) object for map to + * @param a type of context to be used in the mapping + * + * @author Krzysztof Suszynski + * @since 25.04.18 + */ +@Getter +@RequiredArgsConstructor +public abstract class AbstractMapping implements Mapping { + private final Class sourceClass; + private final Class targetClass; + private final Class contextClass; +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CompositeContext.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CompositeContext.java new file mode 100644 index 0000000..9f0c3c6 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CompositeContext.java @@ -0,0 +1,93 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetType; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * A composite mapping context that can utilize multiple mapping contexts to provide + * the joined processing. User can add any number of {@link MappingContext} to use in + * this composite class. + * + * @author Krzysztof Suszynski + * @since 02.05.18 + */ +public final class CompositeContext implements MapStructContext { + private final List mappingContexts = new ArrayList<>(); + + /** + * You can pass multiple mapping contexts to be used in this composite mapping context + * + * @param mappingContexts a array of mapping contexts + */ + public CompositeContext(MappingContext... mappingContexts) { + Collections.addAll(this.mappingContexts, mappingContexts); + } + + /** + * A builder interface for {@link CompositeContext}. + * + * @return a builder interface + */ + public static CompositeContextBuilder builder() { + return new CompositeContextBuilderImpl(); + } + + @Override + @BeforeMapping + public void storeMappedInstance(Object source, + @MappingTarget Object target) { + for (MappingContext mappingContext : mappingContexts) { + if (mappingContext instanceof StoringMappingContext) { + StoringMappingContext.class.cast(mappingContext) + .storeMappedInstance(source, target); + } + } + } + + @Override + @Nullable + @BeforeMapping + public T getMappedInstance(Object source, + @TargetType Class targetType) { + return getMappedInstanceOptional(source, targetType) + .orElse(null); + } + + @Override + public Optional getMappedInstanceOptional(Object source, + Class targetType) { + for (MappingContext mappingContext : mappingContexts) { + Optional instance = mappingContext.getMappedInstanceOptional(source, targetType); + if (instance.isPresent()) { + return instance; + } + } + return Optional.empty(); + } + + private static final class CompositeContextBuilderImpl implements CompositeContextBuilder { + private final List mappingContexts = new ArrayList<>(); + + @Override + public void addContext(MappingContext... mappingContexts) { + this.mappingContexts.addAll( + Arrays.asList(mappingContexts) + ); + } + + @Override + public CompositeContext build() { + return new CompositeContext( + mappingContexts.toArray(new MappingContext[0]) + ); + } + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CompositeContextBuilder.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CompositeContextBuilder.java new file mode 100644 index 0000000..f8503b6 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CompositeContextBuilder.java @@ -0,0 +1,25 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +/** + * This is a builder for composite context. + * + * @see CompositeContext + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +public interface CompositeContextBuilder { + /** + * Adds context to the builder. + * + * @param mappingContexts A mapping contexts to be added to builder and then created + * the composite context from it. + */ + void addContext(MappingContext... mappingContexts); + + /** + * Builds a composite context from provided set of other mapping contexts. + * + * @return a builded instance of composite context + */ + CompositeContext build(); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CyclicGraphContext.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CyclicGraphContext.java new file mode 100644 index 0000000..586d97f --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/CyclicGraphContext.java @@ -0,0 +1,50 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.Context; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetType; + +import javax.annotation.Nullable; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Optional; + +/** + * A type to be used as {@link Context} parameter to track cycles in graphs. + *

+ * Depending on the actual use case, the two methods below could also be changed to only accept certain argument types, + * e.g. base classes of graph nodes, avoiding the need to capture any other objects that wouldn't necessarily result in + * cycles. + * + * @author Andreas Gudian + * @author Krzysztof Suszyński + * @since 2018-04-12 + */ +public final class CyclicGraphContext implements MapStructContext { + + private Map knownInstances = new IdentityHashMap<>(); + + @Override + @Nullable + @BeforeMapping + public T getMappedInstance(Object source, + @TargetType Class targetType) { + return targetType.cast(knownInstances.get( source )); + } + + @Override + @BeforeMapping + public void storeMappedInstance(Object source, + @MappingTarget Object target) { + knownInstances.put(source, target); + } + + @Override + public Optional getMappedInstanceOptional(Object source, + @TargetType Class targetType) { + return Optional.ofNullable( + getMappedInstance(source, targetType) + ); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/IdentifierCollector.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/IdentifierCollector.java new file mode 100644 index 0000000..3b8764f --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/IdentifierCollector.java @@ -0,0 +1,36 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import java.util.Optional; + +/** + * Identifier collector is responsible for collecting identifier from object. If it can't + * fetch an identifier it should return {@link Optional#empty()} value. + *

+ * + * Example: + *

+ * final class LongIdentifierCollector implements IdentifierCollector {
+ *     public Optional<Object> getIdentifierFromSource(Object source) {
+ *         if (source instanceof AbstractEntity) {
+ *             return Optional.ofNullable(
+ *                 AbstractEntity.class.cast(source).getId()
+ *             );
+ *         }
+ *         return Optional.empty();
+ *     }
+ * }
+ * 
+ * + * @author Krzysztof Suszyński + * @since 2018-05-02 + */ +public interface IdentifierCollector { + /** + * Tries to collect an identifier of source object. An identifier can be any + * object, specific interface is not needed. + * + * @param source a source object to collect identifier from + * @return an optional identifier, if not found {@link Optional#empty()} is returned. + */ + Optional getIdentifierFromSource(Object source); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContext.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContext.java new file mode 100644 index 0000000..57b9256 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContext.java @@ -0,0 +1,11 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +/** + * This is provided {@link JpaMappingContext} and it have a factory + * {@link JpaMappingContextFactory}. This interface represents a JPA aware mapping context. + * + * @author Krzysztof Suszynski + * @since 02.05.18 + */ +public interface JpaMappingContext extends MappingContext { +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextFactory.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextFactory.java new file mode 100644 index 0000000..7710def --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextFactory.java @@ -0,0 +1,41 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import java.util.function.Supplier; + + +/** + * This factory produce a {@link JpaMappingContext} and to work it requires: + * + *
    + *
  • Supplier of {@link StoringMappingContext} to handle cycles - + * {@link CyclicGraphContext} can be used here
  • + *
  • {@link Mappings} object that will provides mapping for given source and target class + * - mapping is information how to update existing object (managed entity) with data + * from source object
  • + *
  • {@link IdentifierCollector} should collect managed entity ID from source object
  • + *
+ * + * To simplify configuration consider implementing {@link AbstractJpaContextProvider} and + * configure your DI container to provide {@link JpaMappingContextFactoryImpl} as a + * implementation of this factory. + * + * + * @author Krzysztof Suszynski + * @since 25.04.18 + */ +public interface JpaMappingContextFactory { + /** + * Produce a JPA aware mapping context. I will require couple of dependencies. + * + * @param storingMappingContext A context that can store bean instances to get over cycles + * in domain model graph. A good candidate can + * be {@link CyclicGraphContext}. + * @param mappings A mapping object that can provide a mapping for given input + * and output object for mapping. + * @param identifierCollector A collector of ID that can work on input (source) objects. + * @return a produces context + */ + JpaMappingContext produce(Supplier storingMappingContext, + Mappings mappings, + IdentifierCollector identifierCollector); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextFactoryImpl.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextFactoryImpl.java new file mode 100644 index 0000000..f03281a --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextFactoryImpl.java @@ -0,0 +1,28 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import lombok.RequiredArgsConstructor; + +import javax.persistence.EntityManager; +import java.util.function.Supplier; + +/** + * A default factory for JPA aware mapping context. It requires a supplier of + * {@link EntityManager} bound to transaction context. + * + * @see JpaMappingContextFactory + * @author Krzysztof Suszynski + * @since 25.04.18 + */ +@RequiredArgsConstructor +final class JpaMappingContextFactoryImpl implements JpaMappingContextFactory { + private final Supplier entityManager; + + @Override + public JpaMappingContext produce(Supplier storingMappingContext, + Mappings mappings, + IdentifierCollector identifierCollector) { + return new JpaMappingContextImpl( + entityManager, storingMappingContext, mappings, identifierCollector + ); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextImpl.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextImpl.java new file mode 100644 index 0000000..31d47f0 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextImpl.java @@ -0,0 +1,90 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import lombok.RequiredArgsConstructor; + +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.MappedSuperclass; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +/** + * @author Krzysztof Suszynski + * @since 24.04.18 + */ +@RequiredArgsConstructor +final class JpaMappingContextImpl implements JpaMappingContext { + private final Supplier entityManager; + private final Supplier storingMappingContext; + private final Mappings mappings; + private final IdentifierCollector identifierCollector; + private final Set mappedInstances = new HashSet<>(); + + @Override + public Optional getMappedInstanceOptional(Object source, + Class targetType) { + if (isJpaManagedEntity(targetType)) { + Optional reference = identifierCollector.getIdentifierFromSource(source); + if (reference.isPresent()) { + if (isBeingMapped(source, targetType)) { + return Optional.empty(); + } + markAsBeingMapped(source, targetType); + } + Optional managed = reference.map(ref -> load(ref, targetType)); + managed.ifPresent(m -> updateFromSource(m, source)); + return managed; + } + return Optional.empty(); + } + + private static boolean isJpaManagedEntity(Class targetType) { + return targetType.isAnnotationPresent(Entity.class) + || targetType.isAnnotationPresent(Embeddable.class) + || targetType.isAnnotationPresent(MappedSuperclass.class); + } + + private static Integer keyOf(Object source, Class targetType) { + return System.identityHashCode(source) + + System.identityHashCode(targetType); + } + + private void markAsBeingMapped(Object source, + Class targetType) { + Integer key = keyOf(source, targetType); + mappedInstances.add(key); + } + + private boolean isBeingMapped(Object source, + Class targetType) { + Integer key = keyOf(source, targetType); + return mappedInstances.contains(key); + } + + private T load(Object identifier, + Class targetType) { + return entityManager + .get() + .find(targetType, identifier); + } + + private void updateFromSource(T managed, + Object source) { + updateFromSourceTyped(source, managed); + } + + @SuppressWarnings("unchecked") + private void updateFromSourceTyped(I source, O managed) { + Class sourceClass = (Class) source.getClass(); + Class targetClass = (Class) managed.getClass(); + Mappings maps = (Mappings) mappings; + Mapping mapping = maps.getMapping(sourceClass, targetClass); + C context = mapping + .getContextClass() + .cast(storingMappingContext.get()); + mapping.accept(source, managed, context); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextProviderImpl.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextProviderImpl.java new file mode 100644 index 0000000..3b305de --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/JpaMappingContextProviderImpl.java @@ -0,0 +1,53 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.mapstruct.jpa.Mappings.MappingsBuilder; + +import javax.annotation.Nullable; +import javax.persistence.EntityManager; +import java.util.function.Supplier; + +/** + * @author Krzysztof Suszynski + * @since 07.05.18 + */ +final class JpaMappingContextProviderImpl { + CompositeContext provide(Supplier entityManager, + Iterable> mappingProviders, + IdentifierCollector identifierCollector) { + StoringMappingContext cyclicGraphCtx = new CyclicGraphContext(); + CompositeContextBuilder contextBuilder = CompositeContext.builder(); + Supplier contextSupplier = new StoringMappingContextSupplier(contextBuilder); + MappingsBuilder mappingsBuilder = Mappings.builder(CompositeContext.class); + for (MappingProvider mappingProvider : mappingProviders) { + mappingsBuilder.addMapping(mappingProvider.provide()); + } + JpaMappingContextFactory contextFactory = new JpaMappingContextFactoryImpl(entityManager); + JpaMappingContext jpaContext = contextFactory + .produce( + contextSupplier, + mappingsBuilder.build(), + identifierCollector + ); + + contextBuilder.addContext(cyclicGraphCtx); + contextBuilder.addContext(jpaContext); + + return contextSupplier.get(); + } + + @RequiredArgsConstructor + private static final class StoringMappingContextSupplier implements Supplier { + private final CompositeContextBuilder contextBuilder; + @Nullable + private CompositeContext context; + + @Override + public CompositeContext get() { + if (context == null) { + context = contextBuilder.build(); + } + return context; + } + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MapStructContext.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MapStructContext.java new file mode 100644 index 0000000..08c2fd2 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MapStructContext.java @@ -0,0 +1,29 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.TargetType; + +import javax.annotation.Nullable; + +/** + * A MapStruct compatible context to be used with {@link org.mapstruct.Context} on mapper method. + * + * @author Krzysztof Suszynski + * @since 02.05.18 + */ +public interface MapStructContext extends StoringMappingContext { + + /** + * Gets an already mapped instance of targetType for given source object. + * + * @param source an input source object + * @param targetType a target type class + * @param a type of target type + * @return an mapper instance if found, null otherwise + */ + @Nullable + @BeforeMapping + T getMappedInstance(Object source, + @TargetType Class targetType); + +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MapStructContextProvider.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MapStructContextProvider.java new file mode 100644 index 0000000..ce293c9 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MapStructContextProvider.java @@ -0,0 +1,16 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +/** + * Represents a provider for MapStruct compatible context. + * + * @param A type of context that must be compatible with MapStruct. + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +public interface MapStructContextProvider { + /** + * Creates a new context that is compatible with MapStruct. + * @return a created context + */ + C createNewContext(); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/Mapping.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/Mapping.java new file mode 100644 index 0000000..2b27da2 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/Mapping.java @@ -0,0 +1,34 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import pl.wavesoftware.lang.TriConsumer; + +/** + * Represents a mapping from input object (typed {@link I}) to target output object (typed {@link O}) + * within specified context (typed {@link C}). + * + * @param A type of input (source) object for map from. + * @param A type of output (target) object for map to. + * @param A type of context to be used in the mapping. + * + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +public interface Mapping extends TriConsumer { + /** + * Gets a source class + * @return a source class + */ + Class getSourceClass(); + + /** + * Gets a target class + * @return a target class + */ + Class getTargetClass(); + + /** + * Gets a context class + * @return a context class + */ + Class getContextClass(); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingContext.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingContext.java new file mode 100644 index 0000000..ad796f8 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingContext.java @@ -0,0 +1,23 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import java.util.Optional; + +/** + * A mapping context represents a basic context that can be used as a + * part of {@link MapStructContext}. + * + * @author Krzysztof Suszynski + * @since 02.05.18 + */ +public interface MappingContext { + + /** + * Gets an already mapped instance of targetType for given source object. + * + * @param source an input source object + * @param targetType a target type class + * @param a type of target type + * @return an optional mapper instance + */ + Optional getMappedInstanceOptional(Object source, Class targetType); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingProvider.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingProvider.java new file mode 100644 index 0000000..87b0c5d --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingProvider.java @@ -0,0 +1,21 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +/** + * Represents a mapping provider that can provide a mapping for given parameters. + * + * @param A type of input (source) object for map from. + * @param A type of output (target) object for map to. + * @param A type of context to be used in the mapping. + * + * @see Mapping + * @author Krzysztof Suszynski + * @since 26.04.18 + */ +public interface MappingProvider { + /** + * Provides a mapping + * + * @return a mapping + */ + Mapping provide(); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/Mappings.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/Mappings.java new file mode 100644 index 0000000..1687abe --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/Mappings.java @@ -0,0 +1,61 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +/** + * Mappings represents a set of {@link Mapping}'s with a method to search for specific one. + * + * @param a type of context that will be used for mapping + * + * @author Krzysztof Suszynski + * @since 25.04.18 + */ +public interface Mappings { + + /** + * Searches for a AbstractMapping for given source and target class. Source class is a class + * that we map from and target class is a class that we map to. + * + * @param sourceClass a source class to map from + * @param targetClass a target class to map to + * @param a type of source class + * @param a type of target class + * @return a AbstractMapping object for given configuration. If mapping is not found this + * method will fail with runtime exception as an indication of configuration error. + */ + Mapping getMapping(Class sourceClass, + Class targetClass); + + /** + * Returns a builder for Mappings. + * + * @param contextClass a class of a context + * @param a context type used for mappings + * @return a builder interface + */ + static MappingsBuilder builder(Class contextClass) { + return new MappingsBuilderImpl<>(contextClass); + } + + /** + * A builder interface for {@link Mappings} class. + * + * @param a type of context that will be used for mapping + */ + interface MappingsBuilder { + /** + * Adds a mapping to the builder. + *

+ * There is no type checking here, because it's done in runtime, while getting proper mapping. + * + * @param mapping a mapping to add + */ + void addMapping(Mapping mapping); + + /** + * Will build a Mappings class + * + * @return a built Mappings + */ + Mappings build(); + } + +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingsBuilderImpl.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingsBuilderImpl.java new file mode 100644 index 0000000..517d52f --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingsBuilderImpl.java @@ -0,0 +1,28 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.mapstruct.jpa.Mappings.MappingsBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +@RequiredArgsConstructor +final class MappingsBuilderImpl implements MappingsBuilder { + private final Class contextClass; + private final List> mappings = new ArrayList<>(); + + @Override + public void addMapping(Mapping mapping) { + mappings.add(mapping); + } + + @Override + public Mappings build() { + return new MappingsImpl<>(contextClass, mappings); + } + +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingsImpl.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingsImpl.java new file mode 100644 index 0000000..569df85 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/MappingsImpl.java @@ -0,0 +1,41 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.eid.exceptions.Eid; +import pl.wavesoftware.eid.exceptions.EidIllegalStateException; + +/** + * @author Krzysztof Suszynski + * @since 07.05.18 + */ +@RequiredArgsConstructor +final class MappingsImpl implements Mappings { + private final Class contextClass; + private final Iterable> mappings; + + @Override + @SuppressWarnings("unchecked") + public Mapping getMapping(Class sourceClass, + Class targetClass) { + for (Mapping mapping : mappings) { + if (isSuitedFor(sourceClass, targetClass, mapping)) { + return (Mapping) mapping; + } + } + throw new EidIllegalStateException( + new Eid("20180425:135245"), + "Mapping for source class %s and target class %s is not configured! You should probably " + + "implement and configure MappingProvider<%s,%s,%s>.", + sourceClass.getName(), targetClass.getName(), sourceClass.getName(), + targetClass.getName(), contextClass.getName() + ); + } + + private boolean isSuitedFor(Class sourceClass, + Class targetClass, + Mapping mapping) { + return contextClass.isAssignableFrom(mapping.getContextClass()) + && mapping.getSourceClass() == sourceClass + && mapping.getTargetClass() == targetClass; + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/StoringMappingContext.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/StoringMappingContext.java new file mode 100644 index 0000000..5a72140 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/StoringMappingContext.java @@ -0,0 +1,22 @@ +package pl.wavesoftware.utils.mapstruct.jpa; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.MappingTarget; + +/** + * Represents a context that can store created bean instances. + * + * @author Krzysztof Suszynski + * @since 02.05.18 + */ +public interface StoringMappingContext extends MappingContext { + /** + * Invoking this method stores a passed target object as a mapped + * instance for input source object. + * + * @param source a source object to set mapped instance to + * @param target a mapped instance object to set + */ + @BeforeMapping + void storeMappedInstance(Object source, @MappingTarget Object target); +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/LazyInitializationException.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/LazyInitializationException.java new file mode 100644 index 0000000..9318db9 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/LazyInitializationException.java @@ -0,0 +1,23 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import javax.persistence.PersistenceException; + +/** + * @author Krzysztof Suszyński + * @since 2018-05-02 + */ +public final class LazyInitializationException extends PersistenceException { + + private static final long serialVersionUID = 20180503145842L; + + /** + * Constructs a new LazyInitializationException exception with the + * specified detail message. + * + * @param message + * the detail message. + */ + LazyInitializationException(String message) { + super(message); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/Uninitialized.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/Uninitialized.java new file mode 100644 index 0000000..6c1f405 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/Uninitialized.java @@ -0,0 +1,9 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +/** + * @author Krzysztof Suszynski + * @since 07.05.18 + */ +public interface Uninitialized { + +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedList.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedList.java new file mode 100644 index 0000000..9772ad3 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedList.java @@ -0,0 +1,158 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * An uninitialized list that will throw an exception on every method called + * except {@link #toString()}. + * + * @param a type of collection + * @author Krzysztof Suszynski + * @since 17.04.18 + */ +@RequiredArgsConstructor +public final class UninitializedList implements List, Uninitialized { + + private final Class type; + + @Override + public int size() { + throw newLazyInitializationException(); + } + + @Override + public boolean isEmpty() { + throw newLazyInitializationException(); + } + + @Override + public boolean contains(Object o) { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Iterator iterator() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Object[] toArray() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public T1[] toArray(T1[] a) { + throw newLazyInitializationException(); + } + + @Override + public boolean add(T t) { + throw newLazyInitializationException(); + } + + @Override + public boolean remove(Object o) { + throw newLazyInitializationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean addAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public void clear() { + throw newLazyInitializationException(); + } + + @Override + public T get(int index) { + throw newLazyInitializationException(); + } + + @Override + public T set(int index, T element) { + throw newLazyInitializationException(); + } + + @Override + public void add(int index, T element) { + throw newLazyInitializationException(); + } + + @Override + public T remove(int index) { + throw newLazyInitializationException(); + } + + @Override + public int indexOf(Object o) { + throw newLazyInitializationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public ListIterator listIterator() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public ListIterator listIterator(int index) { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public List subList(int fromIndex, int toIndex) { + throw newLazyInitializationException(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "<" + type.getSimpleName() + ">"; + } + + private RuntimeException newLazyInitializationException() { + return new LazyInitializationException( + "Trying to use uninitialized collection for type: List<" + + type.getSimpleName() + + ">. You need to fetch this collection before using it, for ex. using " + + "JOIN FETCH in JPQL. This exception prevents lazy loading n+1 problem." + ); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedMap.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedMap.java new file mode 100644 index 0000000..40ee4ff --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedMap.java @@ -0,0 +1,104 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * An uninitialized map that will throw an exception on every method called + * except {@link #toString()}. + * + * @param a type of keys + * @param a type of value + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +@RequiredArgsConstructor +public final class UninitializedMap implements Map, Uninitialized { + + private final Class keyType; + private final Class valueType; + + @Override + public int size() { + throw newLazyInitializationException(); + } + + @Override + public boolean isEmpty() { + throw newLazyInitializationException(); + } + + @Override + public boolean containsKey(Object key) { + throw newLazyInitializationException(); + } + + @Override + public boolean containsValue(Object value) { + throw newLazyInitializationException(); + } + + @Override + public V get(Object key) { + throw newLazyInitializationException(); + } + + @Override + public V put(K key, V value) { + throw newLazyInitializationException(); + } + + @Override + public V remove(Object key) { + throw newLazyInitializationException(); + } + + @Override + public void putAll(Map m) { + throw newLazyInitializationException(); + } + + @Override + public void clear() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Set keySet() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Collection values() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Set> entrySet() { + throw newLazyInitializationException(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "<" + keyType.getSimpleName() + "," + + valueType.getSimpleName() + ">"; + } + + private RuntimeException newLazyInitializationException() { + return new LazyInitializationException( + "Trying to use uninitialized collection for type: Map<" + + keyType.getSimpleName() + "," + + valueType.getSimpleName() + + ">. You need to fetch this collection before using it, for ex. using " + + "JOIN FETCH in JPQL. This exception prevents lazy loading n+1 problem." + ); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedSet.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedSet.java new file mode 100644 index 0000000..63b5248 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedSet.java @@ -0,0 +1,104 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +/** + * An uninitialized set that will throw an exception on every method called + * except {@link #toString()}. + * + * @param a type of collection + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +@RequiredArgsConstructor +public final class UninitializedSet implements Set, Uninitialized { + + private final Class type; + + @Override + public boolean isEmpty() { + throw newLazyInitializationException(); + } + + @Override + public int size() { + throw newLazyInitializationException(); + } + + @Override + public boolean contains(Object o) { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Iterator iterator() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public Object[] toArray() { + throw newLazyInitializationException(); + } + + @Override + @Nonnull + public T[] toArray(T[] a) { + throw newLazyInitializationException(); + } + + @Override + public boolean add(E e) { + throw newLazyInitializationException(); + } + + @Override + public boolean remove(Object o) { + throw newLazyInitializationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean addAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw newLazyInitializationException(); + } + + @Override + public void clear() { + throw newLazyInitializationException(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "<" + type.getSimpleName() + ">"; + } + + private RuntimeException newLazyInitializationException() { + return new LazyInitializationException( + "Trying to use uninitialized collection for type: Set<" + + type.getSimpleName() + + ">. You need to fetch this collection before using it, for ex. using " + + "JOIN FETCH in JPQL. This exception prevents lazy loading n+1 problem." + ); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/package-info.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/package-info.java new file mode 100644 index 0000000..1a7666f --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/collection/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/package-info.java b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/package-info.java new file mode 100644 index 0000000..60f6cd0 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/mapstruct/jpa/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszyński + * @since 2018-05-03 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.utils.mapstruct.jpa; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/test/java/pl/wavesoftware/lang/TriConsumerTest.java b/src/test/java/pl/wavesoftware/lang/TriConsumerTest.java new file mode 100644 index 0000000..6f660b9 --- /dev/null +++ b/src/test/java/pl/wavesoftware/lang/TriConsumerTest.java @@ -0,0 +1,52 @@ +package pl.wavesoftware.lang; + +import org.junit.Test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Krzysztof Suszyński + * @since 2018-05-06 + */ +public class TriConsumerTest { + + private static final int HEX_RADIX = 16; + + @Test + public void testAndThen() throws NoSuchAlgorithmException { + // given + StringBuilder sb = new StringBuilder(); + TriConsumer consumer = (string, aBoolean, aLong) -> { + sb.append("string => ").append(string).append(", "); + sb.append("bool => ").append(aBoolean).append(", "); + sb.append("long => ").append(aLong); + }; + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + // when + consumer + .andThen((s, aBool, aLong) -> { + md.update(s.getBytes(StandardCharsets.UTF_8)); + md.update(aBool.toString().getBytes(StandardCharsets.UTF_8)); + md.update(aLong.toString().getBytes(StandardCharsets.UTF_8)); + sb.append(", digest => ") + .append( + new BigInteger(1, md.digest()).toString(HEX_RADIX) + ); + }) + .accept("Alice has a cat", true, 0L); + + // then + assertThat(sb) + .isEqualToIgnoringNewLines( + "string => Alice has a cat, bool => true, long => 0, digest => " + + "59283a245f0ef51936819eb4e80fb2f9528d4ddbd022badf45c56f1e8073e8aa" + ); + } +} diff --git a/src/test/java/pl/wavesoftware/test/MappingTest.java b/src/test/java/pl/wavesoftware/test/MappingTest.java new file mode 100644 index 0000000..2fbec54 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/MappingTest.java @@ -0,0 +1,184 @@ +package pl.wavesoftware.test; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import lombok.RequiredArgsConstructor; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import pl.wavesoftware.eid.exceptions.EidIllegalStateException; +import pl.wavesoftware.test.TestRepository.Database; +import pl.wavesoftware.test.TestRepository.Example; +import pl.wavesoftware.test.TestRepository.Execution; +import pl.wavesoftware.test.entity.Pet; +import pl.wavesoftware.test.jpa.PetJPA; +import pl.wavesoftware.test.mapper.MapperFacade; +import pl.wavesoftware.utils.mapstruct.jpa.collection.LazyInitializationException; + +import javax.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +public class MappingTest { + + private final TestRepository testRepository = new TestRepository(); + + @Mock + private EntityManager entityManager; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testMapFromPetToJPA() { + // given + Injector injector = createInjector((Module) binder -> + binder.bind(EntityManager.class).toInstance(entityManager) + ); + MapperFacade mapper = injector.getInstance(MapperFacade.class); + Execution execution = testRepository.forCase(Example.STANDARD); + Database entityDb = execution.createPetNamedAlice(); + Database database = execution.createJpaPetNamedAlice(); + bindEntityManager(database); + Pet alice = entityDb.getObject(); + Optional.ofNullable(alice.getOwner()) + .ifPresent(o -> o.setName("John Wick")); + + // when + PetJPA aliceJpa = mapper.map(alice); + + // then + assertThat(alice).isNotNull(); + assertThat(aliceJpa).isNotNull(); + assertThat(mapper).isNotNull(); + assertThat(aliceJpa.getId()).isEqualTo(TestRepository.ALICE_ID); + assertThat(aliceJpa.getOwner()).isNotNull(); + assertThat(aliceJpa.getOwner().getName()).isEqualTo(TestRepository.OWNER_NAME); + assertThat(aliceJpa.getOwner().getSurname()).isEqualTo("Wick"); + } + + @Test + public void testMapFromJPAToPet() { + // given + Injector injector = createInjector((Module) binder -> + binder.bind(EntityManager.class).toInstance(entityManager) + ); + MapperFacade mapper = injector.getInstance(MapperFacade.class); + Database database = testRepository + .forCase(Example.STANDARD) + .createJpaPetNamedAlice(); + PetJPA aliceJpa = database.getObject(); + + // when + Pet alice = mapper.map(aliceJpa); + + // then + assertThat(alice).isNotNull(); + assertThat(aliceJpa).isNotNull(); + assertThat(mapper).isNotNull(); + assertThat(alice.getReference()).isEqualTo(TestRepository.ALICE_ID); + assertThat(alice.getOwner()).isNotNull(); + assertThat(alice.getOwner().getReference()) + .isEqualTo(TestRepository.OWNER_ID); + assertThat(alice.getOwner().getName()) + .isEqualTo(TestRepository.OWNER_NAME + " " + TestRepository.OWNER_SURNAME); + } + + @Test + public void testMissingMappingForToy() { + // given + Injector injector = createInjector((Module) binder -> + binder.bind(EntityManager.class).toInstance(entityManager) + ); + MapperFacade mapper = injector.getInstance(MapperFacade.class); + Execution execution = testRepository.forCase(Example.WITH_TOY); + Database entityDb = execution.createPetNamedAlice(); + Database database = execution.createJpaPetNamedAlice(); + bindEntityManager(database); + Pet alice = entityDb.getObject(); + + // then + assertThat(alice).isNotNull(); + assertThat(mapper).isNotNull(); + thrown.expect(EidIllegalStateException.class); + thrown.expectMessage("20180425:135245"); + thrown.expectMessage("Mapping for source class pl.wavesoftware.test.entity.Toy " + + "and target class pl.wavesoftware.test.jpa.ToyJPA is not configured!"); + + // when + mapper.map(alice); + } + + @Test + public void testMappingOnLazy() { + // given + Injector injector = createInjector((Module) binder -> + binder.bind(EntityManager.class).toInstance(entityManager) + ); + MapperFacade mapper = injector.getInstance(MapperFacade.class); + Execution execution = testRepository.forCase(Example.LAZY); + Database database = execution.createJpaPetNamedAlice(); + PetJPA aliceJPA = database.getObject(); + + // when + Pet alice = mapper.map(aliceJPA); + + // then + assertThat(alice).isNotNull(); + assertThat(alice.getName()).isEqualTo("Alice"); + thrown.expect(LazyInitializationException.class); + thrown.expectMessage( + "Trying to use uninitialized collection for type: List. " + + "You need to fetch this collection before using it, for ex. using JOIN FETCH in JPQL. " + + "This exception prevents lazy loading n+1 problem." + ); + // mapping has worked okey, but pets list of owner should be lazy loaded and should + // cause error if used + Optional.ofNullable(alice.getOwner()) + .ifPresent(o -> o.getPets().size()); + } + + private void bindEntityManager(Database database) { + Class cls = any(Class.class); + when(entityManager.find(cls, anyLong())) + .thenAnswer(new EntityManagerAnswer(database)); + } + + private Injector createInjector(Module... modules) { + List allModules = new ArrayList<>(); + allModules.add(new TestModule()); + allModules.addAll(Arrays.asList(modules)); + return Guice.createInjector(allModules); + } + + @RequiredArgsConstructor + private static final class EntityManagerAnswer implements Answer { + private final Database database; + @Override + public Object answer(InvocationOnMock invocationOnMock) { + Class cls = invocationOnMock.getArgument(0); + Object id = invocationOnMock.getArgument(1); + return database.find(cls, id); + } + } +} diff --git a/src/test/java/pl/wavesoftware/test/TestModule.java b/src/test/java/pl/wavesoftware/test/TestModule.java new file mode 100644 index 0000000..da657d1 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/TestModule.java @@ -0,0 +1,16 @@ +package pl.wavesoftware.test; + +import com.google.inject.Binder; +import com.google.inject.Module; +import pl.wavesoftware.test.mapper.MapperModule; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +final class TestModule implements Module { + @Override + public void configure(Binder binder) { + binder.install(new MapperModule()); + } +} diff --git a/src/test/java/pl/wavesoftware/test/TestRepository.java b/src/test/java/pl/wavesoftware/test/TestRepository.java new file mode 100644 index 0000000..c6c7ac0 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/TestRepository.java @@ -0,0 +1,196 @@ +package pl.wavesoftware.test; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.hibernate.collection.internal.PersistentSet; +import pl.wavesoftware.test.entity.AbstractEntity; +import pl.wavesoftware.test.entity.Owner; +import pl.wavesoftware.test.entity.Pet; +import pl.wavesoftware.test.entity.Toy; +import pl.wavesoftware.test.jpa.AbstractRecord; +import pl.wavesoftware.test.jpa.OwnerJPA; +import pl.wavesoftware.test.jpa.PetJPA; +import pl.wavesoftware.test.jpa.ToyJPA; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import static pl.wavesoftware.eid.utils.EidPreconditions.checkNotNull; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +final class TestRepository { + + static final long ALICE_ID = 14L; + private static final String KITIE_NAME = "Kitie"; + + private static final long KITIE_ID = 15L; + private static final String ALICE_NAME = "Alice"; + + static final Long OWNER_ID = 16L; + static final String OWNER_NAME = "John"; + static final String OWNER_SURNAME = "Doe"; + + private static final Long TOY_ID = 17L; + + Execution forCase(Example example) { + return new ExampledExecution(example); + } + + enum Example { + STANDARD, + WITH_TOY, + LAZY + } + + interface Execution { + Database createPetNamedAlice(); + Database createJpaPetNamedAlice(); + } + + interface Database { + T getObject(); + @Nullable + E find(Class cls, Object id); + } + + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + private static final class ExampledExecution implements Execution { + private final Example example; + + @Override + public Database createPetNamedAlice() { + Pet alice = new Pet(); + alice.setReference(ALICE_ID); + alice.setName(ALICE_NAME); + Owner owner = new Owner(); + owner.setReference(OWNER_ID); + owner.setName(OWNER_NAME + " " + OWNER_SURNAME); + + Pet kitie = new Pet(); + kitie.setReference(KITIE_ID); + kitie.setName(KITIE_NAME); + + alice.setOwner(owner); + owner.getPets().addAll(Arrays.asList(alice, kitie)); + + DatabaseImpl db = new DatabaseImpl<>(alice, candidate -> { + if (candidate instanceof AbstractEntity) { + return Optional.ofNullable( + AbstractEntity.class.cast(candidate).getReference() + ); + } + return Optional.empty(); + }); + + if (example == Example.WITH_TOY) { + Toy toy = new Toy("ball"); + toy.setReference(TOY_ID); + kitie.setToy(toy); + db.add(toy); + } + + db.add(kitie); + db.add(owner); + return db; + } + + @Override + public Database createJpaPetNamedAlice() { + PetJPA alice = new PetJPA(); + alice.setId(ALICE_ID); + alice.setName(ALICE_NAME); + + PetJPA kitie = new PetJPA(); + kitie.setId(KITIE_ID); + kitie.setName(KITIE_NAME); + + OwnerJPA owner = new OwnerJPA(); + owner.setId(OWNER_ID); + owner.setName(OWNER_NAME); + owner.setSurname(OWNER_SURNAME); + + alice.setOwner(owner); + if (example == Example.LAZY) { + @SuppressWarnings({"rawtypes", "unchecked"}) + Set lazyPets = (Set) new PersistentSet(); + owner.setPets(lazyPets); + } else { + owner.getPets().addAll(Arrays.asList(alice, kitie)); + } + + DatabaseImpl db = new DatabaseImpl<>(alice, candidate -> { + if (candidate instanceof AbstractRecord) { + return Optional.ofNullable( + AbstractRecord.class.cast(candidate).getId() + ); + } + return Optional.empty(); + }); + + if (example == Example.WITH_TOY) { + ToyJPA toy = new ToyJPA("ball"); + toy.setId(TOY_ID); + kitie.setToy(toy); + db.add(toy); + } + + db.add(kitie); + db.add(owner); + return db; + } + } + + private static final class DatabaseImpl implements Database { + + private final Map, Set> objects = new HashMap<>(); + private final T entity; + private final Function> idCollector; + + private DatabaseImpl(T entity, + Function> idCollector) { + add(entity); + this.entity = entity; + this.idCollector = idCollector; + } + + @SuppressWarnings("unchecked") + private void add(E entity) { + Class cls = (Class) entity.getClass(); + if (!objects.containsKey(cls)) { + objects.put(cls, new LinkedHashSet<>()); + } + Set set = (Set) objects.get(cls); + set.add(entity); + } + + @Override + public T getObject() { + return checkNotNull(entity, "20180504:160340"); + } + + @Nullable + @Override + public E find(Class cls, Object id) { + if (objects.containsKey(cls)) { + @SuppressWarnings("unchecked") + Set set = (Set) objects.get(cls); + for (E e : set) { + Optional eId = idCollector.apply(e); + if (eId.isPresent() && eId.get().equals(id)) { + return e; + } + } + } + return null; + } + } +} diff --git a/src/test/java/pl/wavesoftware/test/entity/AbstractEntity.java b/src/test/java/pl/wavesoftware/test/entity/AbstractEntity.java new file mode 100644 index 0000000..ee60fc3 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/entity/AbstractEntity.java @@ -0,0 +1,16 @@ +package pl.wavesoftware.test.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Setter +@Getter +@NoArgsConstructor +public abstract class AbstractEntity { + private Object reference; +} diff --git a/src/test/java/pl/wavesoftware/test/entity/Owner.java b/src/test/java/pl/wavesoftware/test/entity/Owner.java new file mode 100644 index 0000000..2dcb951 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/entity/Owner.java @@ -0,0 +1,20 @@ +package pl.wavesoftware.test.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Getter +@Setter +@NoArgsConstructor +public class Owner extends AbstractEntity { + private String name; + private List pets = new ArrayList<>(); +} diff --git a/src/test/java/pl/wavesoftware/test/entity/Pet.java b/src/test/java/pl/wavesoftware/test/entity/Pet.java new file mode 100644 index 0000000..38c740d --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/entity/Pet.java @@ -0,0 +1,22 @@ +package pl.wavesoftware.test.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.annotation.Nullable; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Setter +@Getter +@NoArgsConstructor +public class Pet extends AbstractEntity { + private String name; + @Nullable + private Owner owner; + @Nullable + private Toy toy; +} diff --git a/src/test/java/pl/wavesoftware/test/entity/Toy.java b/src/test/java/pl/wavesoftware/test/entity/Toy.java new file mode 100644 index 0000000..85872a5 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/entity/Toy.java @@ -0,0 +1,18 @@ +package pl.wavesoftware.test.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Toy extends AbstractEntity { + private String name; +} diff --git a/src/test/java/pl/wavesoftware/test/entity/package-info.java b/src/test/java/pl/wavesoftware/test/entity/package-info.java new file mode 100644 index 0000000..1ae3061 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/entity/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.test.entity; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/test/java/pl/wavesoftware/test/jpa/AbstractRecord.java b/src/test/java/pl/wavesoftware/test/jpa/AbstractRecord.java new file mode 100644 index 0000000..0be9472 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/jpa/AbstractRecord.java @@ -0,0 +1,17 @@ +package pl.wavesoftware.test.jpa; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Entity; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Getter +@Setter +@Entity +public abstract class AbstractRecord { + private Long id; +} diff --git a/src/test/java/pl/wavesoftware/test/jpa/OwnerJPA.java b/src/test/java/pl/wavesoftware/test/jpa/OwnerJPA.java new file mode 100644 index 0000000..f71dcd3 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/jpa/OwnerJPA.java @@ -0,0 +1,23 @@ +package pl.wavesoftware.test.jpa; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Entity; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Setter +@Getter +@Entity +@NoArgsConstructor +public class OwnerJPA extends AbstractRecord { + private String name; + private String surname; + private Set pets = new HashSet<>(); +} diff --git a/src/test/java/pl/wavesoftware/test/jpa/PetJPA.java b/src/test/java/pl/wavesoftware/test/jpa/PetJPA.java new file mode 100644 index 0000000..55651a5 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/jpa/PetJPA.java @@ -0,0 +1,24 @@ +package pl.wavesoftware.test.jpa; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Getter +@Setter +@Entity +@NoArgsConstructor +public class PetJPA extends AbstractRecord { + private String name; + @Nullable + private OwnerJPA owner; + @Nullable + private ToyJPA toy; +} diff --git a/src/test/java/pl/wavesoftware/test/jpa/ToyJPA.java b/src/test/java/pl/wavesoftware/test/jpa/ToyJPA.java new file mode 100644 index 0000000..bc72cd0 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/jpa/ToyJPA.java @@ -0,0 +1,21 @@ +package pl.wavesoftware.test.jpa; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Entity; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Getter +@Setter +@Entity +@AllArgsConstructor +@NoArgsConstructor +public class ToyJPA extends AbstractRecord { + private String name; +} diff --git a/src/test/java/pl/wavesoftware/test/jpa/package-info.java b/src/test/java/pl/wavesoftware/test/jpa/package-info.java new file mode 100644 index 0000000..ae104fa --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/jpa/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.test.jpa; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/test/java/pl/wavesoftware/test/mapper/GeneratedMappersModule.java b/src/test/java/pl/wavesoftware/test/mapper/GeneratedMappersModule.java new file mode 100644 index 0000000..9cbad6c --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/GeneratedMappersModule.java @@ -0,0 +1,78 @@ +package pl.wavesoftware.test.mapper; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nullable; +import javax.inject.Provider; +import java.lang.reflect.Field; +import java.util.function.Supplier; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +final class GeneratedMappersModule implements Module { + + private final Provider petMapper = new HoldingProvider<>(PetMapperImpl::new); + private final Provider ownerMapper = new HoldingProvider<>(OwnerMapperImpl::new); + private final Provider toyMapper = new HoldingProvider<>(ToyMapperImpl::new); + + @Override + public void configure(Binder binder) { + /* + + CAUTION! + + There are some static object creation which is required to simulate jsr-330 + containers like Spring and JEE. If ever MapStruct provides support for Guice + and Dagger2 container model, those lines can be probably simplified + + */ + } + + @Provides + PetMapper providesPetMapper() throws NoSuchFieldException, IllegalAccessException { + PetMapper mapper = petMapper.get(); + setField(mapper, "ownerMapper", ownerMapper); + setField(mapper, "toyMapper", toyMapper); + return mapper; + } + + @Provides + OwnerMapper providesOwnerMapper() throws NoSuchFieldException, IllegalAccessException { + OwnerMapper mapper = ownerMapper.get(); + setField(mapper, "petMapper", petMapper); + return mapper; + } + + @Provides + ToyMapper providesToyMapper() { + return toyMapper.get(); + } + + private static void setField(Object mapper, String fieldName, Provider provider) + throws NoSuchFieldException, IllegalAccessException { + Field field = mapper.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(mapper, provider.get()); + } + + @RequiredArgsConstructor + private static final class HoldingProvider implements Provider { + + private final Supplier supplier; + @Nullable + private T instance; + + @Override + public T get() { + if (instance == null) { + instance = supplier.get(); + } + return instance; + } + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/IdentifierCollectorImpl.java b/src/test/java/pl/wavesoftware/test/mapper/IdentifierCollectorImpl.java new file mode 100644 index 0000000..66af9d7 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/IdentifierCollectorImpl.java @@ -0,0 +1,23 @@ +package pl.wavesoftware.test.mapper; + +import pl.wavesoftware.test.entity.AbstractEntity; +import pl.wavesoftware.utils.mapstruct.jpa.IdentifierCollector; + +import java.util.Optional; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +final class IdentifierCollectorImpl implements IdentifierCollector { + @Override + public Optional getIdentifierFromSource(Object source) { + if (source instanceof AbstractEntity) { + AbstractEntity entity = AbstractEntity.class.cast(source); + return Optional.ofNullable( + entity.getReference() + ); + } + return Optional.empty(); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/JpaContextProvider.java b/src/test/java/pl/wavesoftware/test/mapper/JpaContextProvider.java new file mode 100644 index 0000000..8bac7b9 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/JpaContextProvider.java @@ -0,0 +1,32 @@ +package pl.wavesoftware.test.mapper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.mapstruct.jpa.AbstractJpaContextProvider; +import pl.wavesoftware.utils.mapstruct.jpa.IdentifierCollector; +import pl.wavesoftware.utils.mapstruct.jpa.MappingProvider; + +import javax.persistence.EntityManager; +import java.util.Collections; +import java.util.Set; +import java.util.function.Supplier; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@RequiredArgsConstructor +final class JpaContextProvider extends AbstractJpaContextProvider { + + @Getter + private final Supplier entityManager; + private final Set> mappingProviders; + @Getter + private final IdentifierCollector identifierCollector; + + @Override + protected Iterable> getMappingProviders() { + return Collections.unmodifiableSet(mappingProviders); + } + +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/MapperFacade.java b/src/test/java/pl/wavesoftware/test/mapper/MapperFacade.java new file mode 100644 index 0000000..b68851a --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/MapperFacade.java @@ -0,0 +1,13 @@ +package pl.wavesoftware.test.mapper; + +import pl.wavesoftware.test.entity.Pet; +import pl.wavesoftware.test.jpa.PetJPA; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +public interface MapperFacade { + PetJPA map(Pet pet); + Pet map(PetJPA jpa); +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/MapperFacadeImpl.java b/src/test/java/pl/wavesoftware/test/mapper/MapperFacadeImpl.java new file mode 100644 index 0000000..1006b67 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/MapperFacadeImpl.java @@ -0,0 +1,28 @@ +package pl.wavesoftware.test.mapper; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.test.entity.Pet; +import pl.wavesoftware.test.jpa.PetJPA; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; +import pl.wavesoftware.utils.mapstruct.jpa.MapStructContextProvider; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@RequiredArgsConstructor +final class MapperFacadeImpl implements MapperFacade { + + private final PetMapper petMapper; + private final MapStructContextProvider contextProvider; + + @Override + public PetJPA map(Pet pet) { + return petMapper.map(pet, contextProvider.createNewContext()); + } + + @Override + public Pet map(PetJPA jpa) { + return petMapper.map(jpa, contextProvider.createNewContext()); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/MapperModule.java b/src/test/java/pl/wavesoftware/test/mapper/MapperModule.java new file mode 100644 index 0000000..4e05b96 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/MapperModule.java @@ -0,0 +1,70 @@ +package pl.wavesoftware.test.mapper; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import lombok.NoArgsConstructor; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; +import pl.wavesoftware.utils.mapstruct.jpa.IdentifierCollector; +import pl.wavesoftware.utils.mapstruct.jpa.MapStructContextProvider; +import pl.wavesoftware.utils.mapstruct.jpa.MappingProvider; + +import javax.inject.Provider; +import javax.persistence.EntityManager; +import java.util.Set; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@NoArgsConstructor +public class MapperModule implements Module { + + @Override + public void configure(Binder binder) { + try { + binder.install(new GeneratedMappersModule()); + bindMappingProviders(binder); + } catch (NoSuchMethodException e) { + binder.addError(e); + } + } + + @Provides + MapperFacade providesMapperFacade(PetMapper petMapper, + MapStructContextProvider contextProvider) { + return new MapperFacadeImpl( + petMapper, + contextProvider + ); + } + + @Provides + IdentifierCollector providesIdentifierCollector() { + return new IdentifierCollectorImpl(); + } + + @Provides + MapStructContextProvider providesContextProvider( + Provider entityManagerProvider, + Set> mappingProviders, + IdentifierCollector identifierCollector) { + + return new JpaContextProvider( + entityManagerProvider::get, mappingProviders, identifierCollector + ); + } + + private static void bindMappingProviders(Binder binder) throws NoSuchMethodException { + TypeLiteral> type + = new TypeLiteral>() {}; + Multibinder> multibinder = Multibinder + .newSetBinder(binder, type); + multibinder.addBinding() + .toConstructor(PetMappingProvider.class.getConstructor(PetMapper.class)); + multibinder.addBinding() + .toConstructor(OwnerMappingProvider.class.getConstructor(OwnerMapper.class)); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/OwnerMapper.java b/src/test/java/pl/wavesoftware/test/mapper/OwnerMapper.java new file mode 100644 index 0000000..252fbf8 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/OwnerMapper.java @@ -0,0 +1,46 @@ +package pl.wavesoftware.test.mapper; + +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.InheritConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Mappings; +import pl.wavesoftware.test.entity.Owner; +import pl.wavesoftware.test.jpa.OwnerJPA; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Mapper( + uses = PetMapper.class, + componentModel = "jsr330" +) +interface OwnerMapper { + @Mapping(target = "reference", ignore = true) + Owner map(OwnerJPA jpa, @Context CompositeContext context); + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(target = "surname", ignore = true) + }) + OwnerJPA map(Owner owner, @Context CompositeContext context); + @InheritConfiguration + void updateFromOwner(Owner owner, + @MappingTarget OwnerJPA jpa, + @Context CompositeContext context); + + @AfterMapping + default void after(OwnerJPA ownerJPA, @MappingTarget Owner owner) { + owner.setReference(ownerJPA.getId()); + owner.setName(ownerJPA.getName() + " " + ownerJPA.getSurname()); + } + + @AfterMapping + default void after(Owner owner, @MappingTarget OwnerJPA ownerJPA) { + ownerJPA.setName(owner.getName().split(" ")[0]); + ownerJPA.setSurname(owner.getName().split(" ")[1]); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/OwnerMappingProvider.java b/src/test/java/pl/wavesoftware/test/mapper/OwnerMappingProvider.java new file mode 100644 index 0000000..7131fcc --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/OwnerMappingProvider.java @@ -0,0 +1,26 @@ +package pl.wavesoftware.test.mapper; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.test.entity.Owner; +import pl.wavesoftware.test.jpa.OwnerJPA; +import pl.wavesoftware.utils.mapstruct.jpa.AbstractCompositeContextMapping; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; +import pl.wavesoftware.utils.mapstruct.jpa.Mapping; +import pl.wavesoftware.utils.mapstruct.jpa.MappingProvider; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@RequiredArgsConstructor +final class OwnerMappingProvider implements MappingProvider { + private final OwnerMapper ownerMapper; + + @Override + public Mapping provide() { + return AbstractCompositeContextMapping.mappingFor( + Owner.class, OwnerJPA.class, + ownerMapper::updateFromOwner + ); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/PetMapper.java b/src/test/java/pl/wavesoftware/test/mapper/PetMapper.java new file mode 100644 index 0000000..f2119bb --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/PetMapper.java @@ -0,0 +1,57 @@ +package pl.wavesoftware.test.mapper; + +import org.hibernate.Hibernate; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.InheritConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import pl.wavesoftware.test.entity.Pet; +import pl.wavesoftware.test.jpa.PetJPA; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; +import pl.wavesoftware.utils.mapstruct.jpa.collection.UninitializedList; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Mapper( + uses = { OwnerMapper.class, ToyMapper.class }, + componentModel = "jsr330" +) +interface PetMapper { + @Mapping(target = "reference", ignore = true) + Pet map(PetJPA jpa, @Context CompositeContext context); + @Mapping(target = "id", ignore = true) + PetJPA map(Pet pet, @Context CompositeContext context); + @InheritConfiguration + void updateFromPet(Pet pet, + @MappingTarget PetJPA jpa, + @Context CompositeContext context); + + default List petJPASetToPetList(Set set, @Context CompositeContext context) { + if (!Hibernate.isInitialized(set)) { + return new UninitializedList<>(PetJPA.class); + } + return set.stream() + .map(j -> map(j, context)) + .collect(Collectors.toList()); + } + + default Set petListToPetJPASet(List list, + @Context CompositeContext context) { + return list.stream() + .map(p -> map(p, context)) + .collect(Collectors.toSet()); + } + + @AfterMapping + default void after(PetJPA petData, @MappingTarget Pet pet) { + pet.setReference(petData.getId()); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/PetMappingProvider.java b/src/test/java/pl/wavesoftware/test/mapper/PetMappingProvider.java new file mode 100644 index 0000000..7ae7776 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/PetMappingProvider.java @@ -0,0 +1,27 @@ +package pl.wavesoftware.test.mapper; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.test.entity.Pet; +import pl.wavesoftware.test.jpa.PetJPA; +import pl.wavesoftware.utils.mapstruct.jpa.AbstractCompositeContextMapping; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; +import pl.wavesoftware.utils.mapstruct.jpa.Mapping; +import pl.wavesoftware.utils.mapstruct.jpa.MappingProvider; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@RequiredArgsConstructor +final class PetMappingProvider implements MappingProvider { + + private final PetMapper petMapper; + + @Override + public Mapping provide() { + return AbstractCompositeContextMapping.mappingFor( + Pet.class, PetJPA.class, + petMapper::updateFromPet + ); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/ToyMapper.java b/src/test/java/pl/wavesoftware/test/mapper/ToyMapper.java new file mode 100644 index 0000000..1dc0be6 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/ToyMapper.java @@ -0,0 +1,29 @@ +package pl.wavesoftware.test.mapper; + +import org.mapstruct.Context; +import org.mapstruct.InheritConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import pl.wavesoftware.test.entity.Toy; +import pl.wavesoftware.test.jpa.ToyJPA; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; + +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@Mapper( + uses = { OwnerMapper.class, PetMapper.class }, + componentModel = "jsr330" +) +public interface ToyMapper { + @Mapping(target = "reference", ignore = true) + Toy map(ToyJPA jpa, @Context CompositeContext context); + @Mapping(target = "id", ignore = true) + ToyJPA map(Toy toy, @Context CompositeContext context); + @InheritConfiguration + void updateFromToy(Toy toy, + @MappingTarget ToyJPA jpa, + @Context CompositeContext context); +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/ToyMapping.java b/src/test/java/pl/wavesoftware/test/mapper/ToyMapping.java new file mode 100644 index 0000000..aa7cc5e --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/ToyMapping.java @@ -0,0 +1,24 @@ +package pl.wavesoftware.test.mapper; + +import pl.wavesoftware.test.entity.Toy; +import pl.wavesoftware.test.jpa.ToyJPA; +import pl.wavesoftware.utils.mapstruct.jpa.AbstractMapping; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; + +/** + * @author Krzysztof Suszynski + * @since 07.05.18 + */ +final class ToyMapping extends AbstractMapping { + + ToyMapping(Class sourceClass, + Class targetClass, + Class contextClass) { + super(sourceClass, targetClass, contextClass); + } + + @Override + public void accept(Toy toy, ToyJPA jpa, CompositeContext context) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/ToyMappingTest.java b/src/test/java/pl/wavesoftware/test/mapper/ToyMappingTest.java new file mode 100644 index 0000000..622ee59 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/ToyMappingTest.java @@ -0,0 +1,33 @@ +package pl.wavesoftware.test.mapper; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import pl.wavesoftware.test.entity.Toy; +import pl.wavesoftware.test.jpa.ToyJPA; +import pl.wavesoftware.utils.mapstruct.jpa.CompositeContext; + +/** + * @author Krzysztof Suszynski + * @since 07.05.18 + */ +public class ToyMappingTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testAccept() { + // given + ToyMapping mapping = new ToyMapping(Toy.class, ToyJPA.class, CompositeContext.class); + Toy toy = new Toy(); + ToyJPA jpa = new ToyJPA(); + CompositeContext context = new CompositeContext(); + + // then + thrown.expect(UnsupportedOperationException.class); + + // when + mapping.accept(toy, jpa, context); + } +} diff --git a/src/test/java/pl/wavesoftware/test/mapper/package-info.java b/src/test/java/pl/wavesoftware/test/mapper/package-info.java new file mode 100644 index 0000000..0ee7925 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/mapper/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.test.mapper; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/test/java/pl/wavesoftware/test/package-info.java b/src/test/java/pl/wavesoftware/test/package-info.java new file mode 100644 index 0000000..e396066 --- /dev/null +++ b/src/test/java/pl/wavesoftware/test/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Krzysztof Suszynski + * @since 04.05.18 + */ +@ParametersAreNonnullByDefault +package pl.wavesoftware.test; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedListTest.java b/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedListTest.java new file mode 100644 index 0000000..9ad2dfe --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedListTest.java @@ -0,0 +1,101 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import lombok.RequiredArgsConstructor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +/** + * @author Krzysztof Suszyński + * @since 2018-05-06 + */ +@RequiredArgsConstructor +@RunWith(Parameterized.class) +public class UninitializedListTest { + + private final Method method; + private final List list = new UninitializedList<>(TestThing.class); + + @Parameters(name = "{0}") + public static Iterable methods() { + Method[] methods = UninitializedList.class.getDeclaredMethods(); + return Arrays.stream(methods) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .filter(m -> !"toString".equals(m.getName())) + .collect(Collectors.toList()); + } + + @Test + public void testToString() { + assertThat(list.toString()).isEqualTo("UninitializedList"); + } + + @Test + public void testMethodThrowViaReflection() throws + IllegalAccessException, InstantiationException { + // given + Object[] args = prepareArgs(); + + // when + try { + method.invoke(list, args); + failBecauseExceptionWasNotThrown(InvocationTargetException.class); + } catch (InvocationTargetException ex) { + // then + assertThat(ex).hasCauseInstanceOf(LazyInitializationException.class); + assertThat(ex.getCause()) + .hasMessage( + "Trying to use uninitialized collection for type: " + + "List. You need to fetch this collection before using it, for ex. using " + + "JOIN FETCH in JPQL. This exception prevents lazy loading n+1 problem." + ); + } + } + + private Object[] prepareArgs() throws IllegalAccessException, InstantiationException { + Object[] objects = new Object[method.getParameterCount()]; + for (int i = 0; i < method.getParameterCount(); i++) { + Parameter parameter = method.getParameters()[i]; + Object obj = newInstanceOfParameter(parameter); + objects[i] = obj; + } + return objects; + } + + private static Object newInstanceOfParameter(Parameter parameter) throws + InstantiationException, IllegalAccessException { + Class type = parameter.getType(); + if (type.isPrimitive()) { + if (type == int.class) { + return 1; + } else if (type == boolean.class) { + return true; + } + } + if (type.isArray()) { + return new Object[3]; + } + if (type == Collection.class) { + return new ArrayList<>(); + } + return parameter.getType().newInstance(); + } + + private interface TestThing { + + } +} diff --git a/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedMapTest.java b/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedMapTest.java new file mode 100644 index 0000000..4896e1e --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedMapTest.java @@ -0,0 +1,110 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + +import lombok.RequiredArgsConstructor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +/** + * @author Krzysztof Suszyński + * @since 2018-05-06 + */ +@RunWith(Parameterized.class) +@RequiredArgsConstructor +public class UninitializedMapTest { + private final Method method; + private final Map list = new UninitializedMap<>( + TestThing.class, OtherTestThing.class + ); + + @Parameters(name = "{0}") + public static Iterable methods() { + Method[] methods = UninitializedMap.class.getDeclaredMethods(); + return Arrays.stream(methods) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .filter(m -> !"toString".equals(m.getName())) + .collect(Collectors.toList()); + } + + @Test + public void testToString() { + assertThat(list.toString()).isEqualTo("UninitializedMap"); + } + + @Test + public void testMethodThrowViaReflection() throws + IllegalAccessException, InstantiationException { + // given + Object[] args = prepareArgs(); + + // when + try { + method.invoke(list, args); + failBecauseExceptionWasNotThrown(InvocationTargetException.class); + } catch (InvocationTargetException ex) { + // then + assertThat(ex).hasCauseInstanceOf(LazyInitializationException.class); + assertThat(ex.getCause()) + .hasMessage( + "Trying to use uninitialized collection for type: " + + "Map. You need to fetch this collection before using it, " + + "for ex. using JOIN FETCH in JPQL. This exception prevents lazy loading n+1 problem." + ); + } + } + + private Object[] prepareArgs() throws IllegalAccessException, InstantiationException { + Object[] objects = new Object[method.getParameterCount()]; + for (int i = 0; i < method.getParameterCount(); i++) { + Parameter parameter = method.getParameters()[i]; + Object obj = newInstanceOfParameter(parameter); + objects[i] = obj; + } + return objects; + } + + private static Object newInstanceOfParameter(Parameter parameter) throws + InstantiationException, IllegalAccessException { + Class type = parameter.getType(); + if (type.isPrimitive()) { + if (type == int.class) { + return 1; + } else if (type == boolean.class) { + return true; + } + } + if (type.isArray()) { + return new Object[1]; + } + if (type == Map.class) { + return new HashMap<>(); + } + if (type == Collection.class) { + return new ArrayList<>(); + } + return parameter.getType().newInstance(); + } + + private interface OtherTestThing { + + } + + private interface TestThing { + + } +} diff --git a/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedSetTest.java b/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedSetTest.java new file mode 100644 index 0000000..4263c02 --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/mapstruct/jpa/collection/UninitializedSetTest.java @@ -0,0 +1,101 @@ +package pl.wavesoftware.utils.mapstruct.jpa.collection; + + +import lombok.RequiredArgsConstructor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +/** + * @author Krzysztof Suszyński + * @since 2018-05-06 + */ +@RequiredArgsConstructor +@RunWith(Parameterized.class) +public class UninitializedSetTest { + private final Method method; + private final Set set = new UninitializedSet<>(TestThing.class); + + @Parameters(name = "{0}") + public static Iterable methods() { + Method[] methods = UninitializedSet.class.getDeclaredMethods(); + return Arrays.stream(methods) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .filter(m -> !"toString".equals(m.getName())) + .collect(Collectors.toList()); + } + + @Test + public void testToString() { + assertThat(set.toString()).isEqualTo("UninitializedSet"); + } + + @Test + public void testMethodThrowViaReflection() throws + IllegalAccessException, InstantiationException { + // given + Object[] args = prepareArgs(); + + // when + try { + method.invoke(set, args); + failBecauseExceptionWasNotThrown(InvocationTargetException.class); + } catch (InvocationTargetException ex) { + // then + assertThat(ex).hasCauseInstanceOf(LazyInitializationException.class); + assertThat(ex.getCause()) + .hasMessage( + "Trying to use uninitialized collection for type: " + + "Set. You need to fetch this collection before using it, for ex. using " + + "JOIN FETCH in JPQL. This exception prevents lazy loading n+1 problem." + ); + } + } + + private Object[] prepareArgs() throws IllegalAccessException, InstantiationException { + Object[] objects = new Object[method.getParameterCount()]; + for (int i = 0; i < method.getParameterCount(); i++) { + Parameter parameter = method.getParameters()[i]; + Object obj = newInstanceOfParameter(parameter); + objects[i] = obj; + } + return objects; + } + + private static Object newInstanceOfParameter(Parameter parameter) throws + InstantiationException, IllegalAccessException { + Class type = parameter.getType(); + if (type.isPrimitive()) { + if (type == int.class) { + return 1; + } else if (type == boolean.class) { + return true; + } + } + if (type.isArray()) { + return new Object[1]; + } + if (type == Collection.class) { + return new ArrayList<>(); + } + return parameter.getType().newInstance(); + } + + private interface TestThing { + + } +}