diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 37b9c09835..fd1e272769 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,10 +70,15 @@ jobs: jdk: 8 spring-data: spring-data-2.4.x deltaspike: deltaspike-1.9 + - rdbms: h2 + provider: hibernate-5.2 + jdk: 8 + spring-data: spring-data-2.5.x + deltaspike: deltaspike-1.9 - rdbms: postgresql provider: hibernate-apt jdk: 8 - spring-data: spring-data-2.5.x + spring-data: spring-data-2.6.x deltaspike: deltaspike-1.9 ################################################ @@ -479,37 +484,37 @@ jobs: provider: hibernate-5.6 jdk: 17 deltaspike: deltaspike-1.9 - spring-data: spring-data-2.6.x + spring-data: spring-data-2.7.x - rdbms: h2 provider: hibernate-5.6 jdk: 17 build-jdk: 17 deltaspike: deltaspike-1.9 - spring-data: spring-data-2.6.x + spring-data: spring-data-2.7.x # Latest GA JDK - rdbms: h2 provider: hibernate-5.6 jdk: 18 deltaspike: deltaspike-1.9 - spring-data: spring-data-2.6.x + spring-data: spring-data-2.7.x - rdbms: h2 provider: hibernate-5.6 jdk: 18 build-jdk: 18 deltaspike: deltaspike-1.9 - spring-data: spring-data-2.6.x + spring-data: spring-data-2.7.x # Early access JDKs - rdbms: h2 provider: hibernate-5.6 jdk: 19-ea deltaspike: deltaspike-1.9 - spring-data: spring-data-2.6.x + spring-data: spring-data-2.7.x - rdbms: h2 provider: hibernate-5.6 jdk: 19-ea build-jdk: 19-ea deltaspike: deltaspike-1.9 - spring-data: spring-data-2.6.x + spring-data: spring-data-2.7.x steps: - uses: actions/checkout@v2 - name: Update /etc/hosts file diff --git a/CHANGELOG.md b/CHANGELOG.md index b621a67222..2dfec6f5ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Not yet released * Add integration for Hibernate 6 supporting versions starting with 6.1.1.Final * Support for Java 16, 17, 18 and 19 EA * Support CTE's for left-nested set queries +* Add separate integration modules for Spring Data 2.5, 2.6 and 2.7 ### Bug fixes diff --git a/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-suppressions.xml b/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-suppressions.xml index e89a420375..467370b7d0 100644 --- a/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-suppressions.xml +++ b/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-suppressions.xml @@ -31,6 +31,8 @@ + + diff --git a/dist/bom/pom.xml b/dist/bom/pom.xml index 389d3968c6..00b2d30fdf 100644 --- a/dist/bom/pom.xml +++ b/dist/bom/pom.xml @@ -330,6 +330,24 @@ ${project.version} compile + + com.blazebit + blaze-persistence-integration-spring-data-2.5 + ${project.version} + compile + + + com.blazebit + blaze-persistence-integration-spring-data-2.6 + ${project.version} + compile + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 + ${project.version} + compile + com.blazebit blaze-persistence-integration-spring-data-webmvc diff --git a/dist/full/pom.xml b/dist/full/pom.xml index 27a1bf7bc1..d15821ff68 100644 --- a/dist/full/pom.xml +++ b/dist/full/pom.xml @@ -183,6 +183,18 @@ com.blazebit blaze-persistence-integration-spring-data-2.4 + + com.blazebit + blaze-persistence-integration-spring-data-2.5 + + + com.blazebit + blaze-persistence-integration-spring-data-2.6 + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 + com.blazebit blaze-persistence-integration-spring-data-webmvc diff --git a/documentation/src/main/asciidoc/entity-view/manual/en_US/getting_started.adoc b/documentation/src/main/asciidoc/entity-view/manual/en_US/getting_started.adoc index 8dabb648db..7a004a68d7 100644 --- a/documentation/src/main/asciidoc/entity-view/manual/en_US/getting_started.adoc +++ b/documentation/src/main/asciidoc/entity-view/manual/en_US/getting_started.adoc @@ -142,13 +142,13 @@ When you work with Spring Data you can additionally have first class integration ---- com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.7 ${blaze-persistence.version} compile ---- -For Spring-Data version 2.3, 2.2, 2.1, 2.0 or 1.x use the artifact with the respective suffix `2.3`, `2.2`, `2.1`, `2.0`, `1.x`. +For Spring-Data version 2.6, 2.5, 2.4, 2.3, 2.2, 2.1, 2.0 or 1.x use the artifact with the respective suffix `2.6`, `2.5`, `2.4`, `2.3`, `2.2`, `2.1`, `2.0`, `1.x`. NOTE: The Spring Data integration depends on the _jpa-criteria_ module diff --git a/documentation/src/main/asciidoc/entity-view/manual/en_US/spring_data.adoc b/documentation/src/main/asciidoc/entity-view/manual/en_US/spring_data.adoc index 96faed2f2c..3b12f33480 100644 --- a/documentation/src/main/asciidoc/entity-view/manual/en_US/spring_data.adoc +++ b/documentation/src/main/asciidoc/entity-view/manual/en_US/spring_data.adoc @@ -16,7 +16,7 @@ In short, the following Maven dependencies are required ---- com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.7 ${blaze-persistence.version} compile @@ -28,7 +28,7 @@ In short, the following Maven dependencies are required ---- -For Spring-Data version 2.4, 2.3, 2.2, 2.1, 2.0 or 1.x use the `blaze-persistence-integration-spring-data` artifact with the respective suffix `2.4`, `2.3`, `2.2`, `2.1`, `2.0`, `1.x`. +For Spring-Data version 2.6, 2.5, 2.4, 2.3, 2.2, 2.1, 2.0 or 1.x use the `blaze-persistence-integration-spring-data` artifact with the respective suffix `2.6`, `2.5` `2.4`, `2.3`, `2.2`, `2.1`, `2.0`, `1.x`. The dependencies for other JPA providers or other versions can be found in the link:{core_doc}#maven-setup[core module setup section]. diff --git a/examples/showcase/base/pom.xml b/examples/showcase/base/pom.xml index 578b8d885c..edea9989f4 100644 --- a/examples/showcase/base/pom.xml +++ b/examples/showcase/base/pom.xml @@ -32,9 +32,8 @@ - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api diff --git a/examples/showcase/runner/spring/pom.xml b/examples/showcase/runner/spring/pom.xml index c80e5e4bc8..56f963bb66 100644 --- a/examples/showcase/runner/spring/pom.xml +++ b/examples/showcase/runner/spring/pom.xml @@ -404,7 +404,7 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 @@ -422,7 +422,25 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + provided + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 diff --git a/examples/spring-data-dgs/pom.xml b/examples/spring-data-dgs/pom.xml index d27aded659..12f9a868ce 100644 --- a/examples/spring-data-dgs/pom.xml +++ b/examples/spring-data-dgs/pom.xml @@ -459,7 +459,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 ${project.groupId} @@ -497,7 +497,45 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.4 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.hibernate-5.4} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + + + org.aspectj + aspectjweaver + 1.9.4 + + + com.fasterxml.jackson.core + jackson-core + 2.10.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 ${project.groupId} diff --git a/examples/spring-data-graphql/pom.xml b/examples/spring-data-graphql/pom.xml index f31990d77c..5e716ca21b 100644 --- a/examples/spring-data-graphql/pom.xml +++ b/examples/spring-data-graphql/pom.xml @@ -426,7 +426,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 ${project.groupId} @@ -459,7 +459,40 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.6 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.hibernate-5.6} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + + + org.aspectj + aspectjweaver + 1.9.4 + + + com.fasterxml.jackson.core + jackson-core + 2.12.1 + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 ${project.groupId} diff --git a/examples/spring-data-spqr/pom.xml b/examples/spring-data-spqr/pom.xml index 1c8f0bf5be..f8465d82c9 100644 --- a/examples/spring-data-spqr/pom.xml +++ b/examples/spring-data-spqr/pom.xml @@ -409,7 +409,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 ${project.groupId} @@ -442,7 +442,40 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.6 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.hibernate-5.6} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + + + org.aspectj + aspectjweaver + 1.9.4 + + + com.fasterxml.jackson.core + jackson-core + 2.12.1 + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 ${project.groupId} diff --git a/examples/spring-data-webflux/pom.xml b/examples/spring-data-webflux/pom.xml index a1bc119e35..813e3331de 100644 --- a/examples/spring-data-webflux/pom.xml +++ b/examples/spring-data-webflux/pom.xml @@ -324,7 +324,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 ${project.groupId} @@ -352,7 +352,35 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.6 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.hibernate-5.6} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + + + org.aspectj + aspectjweaver + 1.9.4 + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 ${project.groupId} diff --git a/examples/spring-data-webmvc/pom.xml b/examples/spring-data-webmvc/pom.xml index 12250cf920..d3ecd2303a 100644 --- a/examples/spring-data-webmvc/pom.xml +++ b/examples/spring-data-webmvc/pom.xml @@ -362,7 +362,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 ${project.groupId} @@ -390,7 +390,35 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.6 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.hibernate-5.6} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + + + org.aspectj + aspectjweaver + 1.9.4 + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 ${project.groupId} diff --git a/examples/spring-hateoas/pom.xml b/examples/spring-hateoas/pom.xml index 3bb390d82d..6ee3298f4d 100644 --- a/examples/spring-hateoas/pom.xml +++ b/examples/spring-hateoas/pom.xml @@ -362,7 +362,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 ${project.groupId} @@ -390,7 +390,35 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.6 + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.hibernate-5.6} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + + + org.aspectj + aspectjweaver + 1.9.4 + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 ${project.groupId} diff --git a/integration/entity-view-cdi/pom.xml b/integration/entity-view-cdi/pom.xml index a6ed8cad7e..512865e3a7 100644 --- a/integration/entity-view-cdi/pom.xml +++ b/integration/entity-view-cdi/pom.xml @@ -45,9 +45,8 @@ - org.hibernate.javax.persistence - hibernate-jpa-2.0-api - 1.0.1.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/entity-view-spring/pom.xml b/integration/entity-view-spring/pom.xml index ee614c9c3f..ae12c9a57d 100644 --- a/integration/entity-view-spring/pom.xml +++ b/integration/entity-view-spring/pom.xml @@ -57,9 +57,8 @@ provided - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided @@ -260,5 +259,21 @@ + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + 1.8 + 1.8 + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + provided + + + \ No newline at end of file diff --git a/integration/jaxrs-jackson/pom.xml b/integration/jaxrs-jackson/pom.xml index cb94bc6452..13fa19ca2d 100644 --- a/integration/jaxrs-jackson/pom.xml +++ b/integration/jaxrs-jackson/pom.xml @@ -77,9 +77,8 @@ - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/jaxrs-jsonb/pom.xml b/integration/jaxrs-jsonb/pom.xml index 21c12809d8..e0ffa61566 100644 --- a/integration/jaxrs-jsonb/pom.xml +++ b/integration/jaxrs-jsonb/pom.xml @@ -82,9 +82,8 @@ - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/1.x/pom.xml b/integration/spring-data/1.x/pom.xml index 8841b6a043..5ff2e577a0 100644 --- a/integration/spring-data/1.x/pom.xml +++ b/integration/spring-data/1.x/pom.xml @@ -51,9 +51,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java b/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java index b74bb301a1..2faa12d302 100644 --- a/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java +++ b/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java @@ -118,4 +118,11 @@ public void afterPropertiesSet() { super.afterPropertiesSet(); } + public void setEscapeCharacter(char escapeCharacter) { + } + + public char getEscapeCharacter() { + return '\\'; + } + } diff --git a/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java index df7d6b617a..337b904a3c 100644 --- a/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java +++ b/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -54,7 +54,7 @@ protected Map tryGetFetchGraphHints(EntityManager entityManager, public S findOne(Example example) { try { - return getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getSingleResult(); + return getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult(); } catch (NoResultException e) { return null; } diff --git a/integration/spring-data/2.0/pom.xml b/integration/spring-data/2.0/pom.xml index 092d2827bc..a16f60533f 100644 --- a/integration/spring-data/2.0/pom.xml +++ b/integration/spring-data/2.0/pom.xml @@ -85,9 +85,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java b/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java index b74bb301a1..2faa12d302 100644 --- a/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java +++ b/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java @@ -118,4 +118,11 @@ public void afterPropertiesSet() { super.afterPropertiesSet(); } + public void setEscapeCharacter(char escapeCharacter) { + } + + public char getEscapeCharacter() { + return '\\'; + } + } diff --git a/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java index b6abd547eb..3b3da740ee 100644 --- a/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java +++ b/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -56,7 +56,7 @@ protected Map tryGetFetchGraphHints(EntityManager entityManager, @WithBridgeMethods(value = Object.class, adapterMethod = "convert") public Optional findOne(Example example) { try { - return Optional.of(getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getSingleResult()); + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); } catch (NoResultException e) { return Optional.empty(); } diff --git a/integration/spring-data/2.1/pom.xml b/integration/spring-data/2.1/pom.xml index 750fa9386e..0ce68ebc06 100644 --- a/integration/spring-data/2.1/pom.xml +++ b/integration/spring-data/2.1/pom.xml @@ -86,9 +86,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java index 2718e86781..9804bbd86f 100644 --- a/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java +++ b/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -54,7 +54,7 @@ public EntityViewAwareRepositoryImpl(JpaEntityInformation entityInformatio @WithBridgeMethods(value = Object.class, adapterMethod = "convert") public Optional findOne(Example example) { try { - return Optional.of(getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getSingleResult()); + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); } catch (NoResultException e) { return Optional.empty(); } diff --git a/integration/spring-data/2.2/pom.xml b/integration/spring-data/2.2/pom.xml index 00ca857f73..2d6cb0a6cc 100644 --- a/integration/spring-data/2.2/pom.xml +++ b/integration/spring-data/2.2/pom.xml @@ -86,9 +86,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/2.2/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.2/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java index c37f19de13..1c5ea835b7 100644 --- a/integration/spring-data/2.2/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java +++ b/integration/spring-data/2.2/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -59,7 +59,7 @@ protected Map tryGetFetchGraphHints(EntityManager entityManager, @WithBridgeMethods(value = Object.class, adapterMethod = "convert") public Optional findOne(Example example) { try { - return Optional.of(getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getSingleResult()); + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); } catch (NoResultException e) { return Optional.empty(); } diff --git a/integration/spring-data/2.3/pom.xml b/integration/spring-data/2.3/pom.xml index 684f0393aa..1731ff41c5 100644 --- a/integration/spring-data/2.3/pom.xml +++ b/integration/spring-data/2.3/pom.xml @@ -107,9 +107,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/2.3/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.3/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java index c37f19de13..1c5ea835b7 100644 --- a/integration/spring-data/2.3/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java +++ b/integration/spring-data/2.3/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -59,7 +59,7 @@ protected Map tryGetFetchGraphHints(EntityManager entityManager, @WithBridgeMethods(value = Object.class, adapterMethod = "convert") public Optional findOne(Example example) { try { - return Optional.of(getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getSingleResult()); + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); } catch (NoResultException e) { return Optional.empty(); } diff --git a/integration/spring-data/2.4/pom.xml b/integration/spring-data/2.4/pom.xml index 2763ba7f7f..7bc8dfb374 100644 --- a/integration/spring-data/2.4/pom.xml +++ b/integration/spring-data/2.4/pom.xml @@ -107,9 +107,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided diff --git a/integration/spring-data/2.4/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.4/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java index da3b83c327..e8e0a98efe 100644 --- a/integration/spring-data/2.4/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java +++ b/integration/spring-data/2.4/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -62,7 +62,7 @@ protected Map tryGetFetchGraphHints(EntityManager entityManager, @WithBridgeMethods(value = Object.class, adapterMethod = "convert") public Optional findOne(Example example) { try { - return Optional.of(getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getSingleResult()); + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); } catch (NoResultException e) { return Optional.empty(); } diff --git a/integration/spring-data/2.5/pom.xml b/integration/spring-data/2.5/pom.xml new file mode 100644 index 0000000000..1c90f0159a --- /dev/null +++ b/integration/spring-data/2.5/pom.xml @@ -0,0 +1,263 @@ + + + + blaze-persistence-integration-spring-data-parent + com.blazebit + 1.6.7-SNAPSHOT + ../pom.xml + + 4.0.0 + + Blazebit Persistence Integration Spring-Data 2.5 + blaze-persistence-integration-spring-data-2.5 + + + com.blazebit.persistence.integration.spring.data.impl + + + ${version.spring-data-2.5-spring} + 1.2.1 + 1.3.6 + 1.8 + 1.8 + + + + + + org.springframework + spring-framework-bom + ${version.spring} + import + pom + + + org.jetbrains.kotlinx + kotlinx-coroutines-bom + ${kotlin-coroutines} + pom + import + + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.5} + provided + + + + io.reactivex + rxjava-reactive-streams + ${rxjava-reactive-streams} + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + true + + + + com.infradna.tool + bridge-method-annotation + ${version.bridge-injector} + provided + + + org.jenkins-ci + annotation-indexer + + + + + + org.jenkins-ci + annotation-indexer + ${version.bridge-injector-indexer} + provided + + + + com.blazebit + blaze-persistence-entity-view-api + + + com.blazebit + blaze-persistence-jpa-criteria-api + + + com.blazebit + blaze-persistence-jpa-criteria-impl + + + jakarta.persistence + jakarta.persistence-api + provided + + + + + com.blazebit + blaze-persistence-integration-spring-data-base + compile + + + com.blazebit + blaze-persistence-integration-entity-view-spring + compile + + + com.blazebit + blaze-persistence-core-impl + runtime + + + com.blazebit + blaze-persistence-entity-view-impl + runtime + + + + + + org.springframework + spring-test + test + + + junit + junit + test + + + + + + + org.moditect + moditect-maven-plugin + + + add-module-infos + package + + add-module-info + + + + + module ${module.name} { + requires transitive com.blazebit.persistence.integration.spring.data; + exports com.blazebit.persistence.spring.data.impl.query; + exports com.blazebit.persistence.spring.data.impl.repository; + } + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-resource + generate-resources + + add-resource + + + + + target/generated/resources + + + + + + + + org.bsc.maven + maven-processor-plugin + + + process + + process + + generate-sources + + + target/generated/resources + + org.jvnet.hudson.annotation_indexer.AnnotationProcessorImpl + + + + + + + com.infradna.tool + bridge-method-annotation + ${version.bridge-injector} + + + + org.jenkins-ci + annotation-indexer + ${version.bridge-injector-indexer} + + + + + com.infradna.tool + bridge-method-injector + ${version.bridge-injector} + + + + process + + + + + + + org.ow2.asm + asm-debug-all + CUSTOM + + + org.ow2.asm + asm + ${version.bridge-injector-asm} + + + org.ow2.asm + asm-commons + ${version.bridge-injector-asm} + + + + + + + \ No newline at end of file diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java new file mode 100644 index 0000000000..356792688f --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.AbstractCriteriaQueryParameterBinder; +import com.blazebit.persistence.spring.data.base.query.JpaParameters; +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; + +/** + * Concrete version for Spring Data 2.x. + * + * @author Christian Beikov + * @since 1.3.0 + */ +public class CriteriaQueryParameterBinder extends AbstractCriteriaQueryParameterBinder { + + public CriteriaQueryParameterBinder(EntityManager em, EntityViewManager evm, JpaParameters parameters, Object[] values, Iterable> expressions) { + super(em, evm, parameters, values, expressions); + } + + @Override + protected int getOffset() { + Pageable pageable = getPageable(); + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java new file mode 100644 index 0000000000..295033fc58 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java @@ -0,0 +1,137 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityViewAwareRepositoryInformation implements RepositoryInformation, EntityViewAwareRepositoryMetadata { + + private final EntityViewAwareRepositoryMetadata metadata; + private final RepositoryInformation repositoryInformation; + + public EntityViewAwareRepositoryInformation(EntityViewAwareRepositoryMetadata metadata, RepositoryInformation repositoryInformation) { + this.metadata = metadata; + this.repositoryInformation = repositoryInformation; + } + + @Override + public EntityViewManager getEntityViewManager() { + return metadata.getEntityViewManager(); + } + + @Override + public Class getRepositoryBaseClass() { + return repositoryInformation.getRepositoryBaseClass(); + } + + @Override + public boolean hasCustomMethod() { + return repositoryInformation.hasCustomMethod(); + } + + @Override + public boolean isCustomMethod(Method method) { + return repositoryInformation.isCustomMethod(method); + } + + @Override + public boolean isQueryMethod(Method method) { + return repositoryInformation.isQueryMethod(method); + } + + @Override + public boolean isBaseClassMethod(Method method) { + return repositoryInformation.isBaseClassMethod(method); + } + + @Override + public Streamable getQueryMethods() { + return repositoryInformation.getQueryMethods(); + } + + @Override + public Method getTargetClassMethod(Method method) { + return repositoryInformation.getTargetClassMethod(method); + } + + @Override + public Class getIdType() { + return (Class) repositoryInformation.getIdType(); + } + + @Override + public Class getDomainType() { + return repositoryInformation.getDomainType(); + } + + @Override + public Class getEntityViewType() { + return metadata.getEntityViewType(); + } + + @Override + public Class getRepositoryInterface() { + return repositoryInformation.getRepositoryInterface(); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return repositoryInformation.getReturnedDomainClass(method); + } + + @Override + public Class getReturnedEntityViewClass(Method method) { + return metadata.getReturnedEntityViewClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return repositoryInformation.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return repositoryInformation.isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return repositoryInformation.getAlternativeDomainTypes(); + } + + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return repositoryInformation.getReturnType(method); + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java new file mode 100644 index 0000000000..33fd066780 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.metamodel.ManagedViewType; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.util.TypeInformation; + +import java.lang.reflect.Method; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityViewAwareRepositoryMetadataImpl implements EntityViewAwareRepositoryMetadata { + + private final RepositoryMetadata metadata; + private final EntityViewManager evm; + private final Class domainType; + private final Class entityViewType; + + public EntityViewAwareRepositoryMetadataImpl(RepositoryMetadata metadata, EntityViewManager evm) { + this.metadata = metadata; + this.evm = evm; + Class domainType = metadata.getDomainType(); + ManagedViewType managedViewType = evm.getMetamodel().managedView(domainType); + if (managedViewType == null) { + this.domainType = domainType; + this.entityViewType = null; + } else { + this.domainType = managedViewType.getEntityClass(); + this.entityViewType = managedViewType.getJavaType(); + } + } + + @Override + public EntityViewManager getEntityViewManager() { + return evm; + } + + @Override + public Class getIdType() { + return metadata.getIdType(); + } + + @Override + public Class getDomainType() { + return domainType; + } + + @Override + public Class getEntityViewType() { + return entityViewType; + } + + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public Class getReturnedDomainClass(Method method) { + Class returnedDomainClass = metadata.getReturnedDomainClass(method); + ManagedViewType managedViewType = evm.getMetamodel().managedView(returnedDomainClass); + if (managedViewType == null) { + return returnedDomainClass; + } else { + return managedViewType.getEntityClass(); + } + } + + @Override + public TypeInformation getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class getReturnedEntityViewClass(Method method) { + Class returnedDomainClass = metadata.getReturnedDomainClass(method); + ManagedViewType managedViewType = evm.getMetamodel().managedView(returnedDomainClass); + if (managedViewType == null) { + return null; + } else { + return managedViewType.getJavaType(); + } + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return metadata.isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return metadata.getAlternativeDomainTypes(); + } + + @Override + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java new file mode 100644 index 0000000000..c6f59f065d --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java @@ -0,0 +1,309 @@ +/* + * Copyright 2011-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.expression.Expression; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.ParameterExpression; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Helper class to allow easy creation of {@link ParameterMetadata}s. + * + * Christian Beikov: Copied while implementing the shared interface to be able to share code between Spring Data integrations for 1.x and 2.x. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Mark Paluch + * @author Christoph Strobl + * @author Jens Schauder + * @author Andrey Kovalev + */ +class ParameterMetadataProviderImpl implements ParameterMetadataProvider { + + private final CriteriaBuilder builder; + private final Iterator parameters; + private final List> expressions; + private final Iterator bindableParameterValues; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} and + * {@link ParametersParameterAccessor} with support for parameter value customizations via {@link PersistenceProvider} + * . + * + * @param builder must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + public ParameterMetadataProviderImpl(CriteriaBuilder builder, ParametersParameterAccessor accessor, + PersistenceProvider provider, EscapeCharacter escape) { + this(builder, accessor.iterator(), accessor.getParameters(), provider, escape); + } + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} and {@link Parameters} with + * support for parameter value customizations via {@link PersistenceProvider}. + * + * @param builder must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + public ParameterMetadataProviderImpl(CriteriaBuilder builder, Parameters parameters, PersistenceProvider provider, + EscapeCharacter escape) { + this(builder, null, parameters, provider, escape); + } + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} an {@link Iterable} of all + * bindable parameter values, and {@link Parameters} with support for parameter value customizations via + * {@link PersistenceProvider}. + * + * @param builder must not be {@literal null}. + * @param bindableParameterValues may be {@literal null}. + * @param parameters must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + private ParameterMetadataProviderImpl(CriteriaBuilder builder, Iterator bindableParameterValues, + Parameters parameters, PersistenceProvider provider, EscapeCharacter escape) { + + Assert.notNull(builder, "CriteriaBuilder must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + Assert.notNull(provider, "PesistenceProvider must not be null!"); + + this.builder = builder; + this.parameters = parameters.getBindableParameters().iterator(); + this.expressions = new ArrayList<>(); + this.bindableParameterValues = bindableParameterValues; + this.persistenceProvider = provider; + this.escape = escape; + } + + /** + * Returns all {@link ParameterMetadata}s built. + * + * @return the expressions + */ + public List> getExpressions() { + return expressions; + } + + /** + * Builds a new {@link ParameterMetadata} for given {@link Part} and the next {@link Parameter}. + */ + @SuppressWarnings("unchecked") + public ParameterMetadata next(Part part) { + if (!parameters.hasNext()) { + throw new IllegalArgumentException(String.format("No parameter available for part %s.", part)); + } + + Parameter parameter = parameters.next(); + return (ParameterMetadata) next(part, parameter.getType(), parameter); + } + + /** + * Builds a new {@link ParameterMetadata} of the given {@link Part} and type. Forwards the underlying + * {@link Parameters} as well. + * + * @param is the type parameter of the returend {@link ParameterMetadata}. + * @param type must not be {@literal null}. + * @return ParameterMetadata for the next parameter. + */ + @SuppressWarnings("unchecked") + public ParameterMetadata next(Part part, Class type) { + + Parameter parameter = parameters.next(); + Class typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type; + return (ParameterMetadata) next(part, typeToUse, parameter); + } + + /** + * Builds a new {@link ParameterMetadata} for the given type and name. + * + * @param type parameter for the returned {@link ParameterMetadata}. + * @param part must not be {@literal null}. + * @param type must not be {@literal null}. + * @param parameter providing the name for the returned {@link ParameterMetadata}. + * @return a new {@link ParameterMetadata} for the given type and name. + */ + private ParameterMetadata next(Part part, Class type, Parameter parameter) { + + Assert.notNull(type, "Type must not be null!"); + + /* + * We treat Expression types as Object vales since the real value to be bound as a parameter is determined at query time. + */ + @SuppressWarnings("unchecked") + Class reifiedType = Expression.class.equals(type) ? (Class) Object.class : type; + + Supplier name = () -> parameter.getName() + .orElseThrow(() -> new IllegalArgumentException("o_O Parameter needs to be named")); + + ParameterExpression expression = parameter.isExplicitlyNamed() // + ? builder.parameter(reifiedType, name.get()) // + : builder.parameter(reifiedType); + + Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next(); + + ParameterMetadata metadata = new ParameterMetadataImpl<>(expression, part, value, persistenceProvider, escape); + expressions.add(metadata); + + return metadata; + } + + EscapeCharacter getEscape() { + return escape; + } + + /** + * @author Oliver Gierke + * @author Thomas Darimont + * @author Andrey Kovalev + * @param + */ + static class ParameterMetadataImpl implements ParameterMetadata { + + private final Type type; + private final ParameterExpression expression; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; + private final boolean ignoreCase; + + /** + * Creates a new {@link ParameterMetadata}. + */ + public ParameterMetadataImpl(ParameterExpression expression, Part part, Object value, + PersistenceProvider provider, EscapeCharacter escape) { + + this.expression = expression; + this.persistenceProvider = provider; + this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType(); + this.ignoreCase = IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); + this.escape = escape; + } + + /** + * Returns the {@link ParameterExpression}. + * + * @return the expression + */ + public ParameterExpression getExpression() { + return expression; + } + + /** + * Returns whether the parameter shall be considered an {@literal IS NULL} parameter. + */ + public boolean isIsNullParameter() { + return Type.IS_NULL.equals(type); + } + + /** + * Prepares the object before it's actually bound to the {@link javax.persistence.Query;}. + * + * @param value must not be {@literal null}. + */ + public Object prepare(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + Class expressionType = expression.getJavaType(); + + if (String.class.equals(expressionType)) { + + switch (type) { + case STARTING_WITH: + return String.format("%s%%", escape.escape(value.toString())); + case ENDING_WITH: + return String.format("%%%s", escape.escape(value.toString())); + case CONTAINING: + case NOT_CONTAINING: + return String.format("%%%s%%", escape.escape(value.toString())); + default: + return value; + } + } + + return Collection.class.isAssignableFrom(expressionType) // + ? upperIfIgnoreCase(ignoreCase, toCollection(value)) // + : value; + } + + /** + * Returns the given argument as {@link Collection} which means it will return it as is if it's a + * {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element + * {@link Collections}. + * + * @param value the value to be converted to a {@link Collection}. + * @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value. + */ + private static Collection toCollection(Object value) { + + if (value == null) { + return null; + } + + if (value instanceof Collection) { + return (Collection) value; + } + + if (ObjectUtils.isArray(value)) { + return Arrays.asList(ObjectUtils.toObjectArray(value)); + } + + return Collections.singleton(value); + } + + @SuppressWarnings("unchecked") + private static Collection upperIfIgnoreCase(boolean ignoreCase, Collection collection) { + + if (!ignoreCase || CollectionUtils.isEmpty(collection)) { + return collection; + } + + return ((Collection) collection).stream() // + .map(it -> it == null // + ? null // + : it.toUpperCase()) // + .collect(Collectors.toList()); + } + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java new file mode 100644 index 0000000000..eb31c4cda6 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java @@ -0,0 +1,255 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.PagedList; +import com.blazebit.persistence.spring.data.base.query.AbstractPartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareJpaQueryMethod; +import com.blazebit.persistence.spring.data.base.query.JpaParameters; +import com.blazebit.persistence.spring.data.base.query.KeysetAwarePageImpl; +import com.blazebit.persistence.spring.data.base.query.KeysetAwareSliceImpl; +import com.blazebit.persistence.spring.data.base.query.ParameterBinder; +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.AbstractJpaQuery; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; +import org.springframework.data.jpa.repository.query.JpaQueryExecution; +import org.springframework.data.jpa.repository.support.QueryHints; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.criteria.CriteriaBuilder; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.2.0 + */ +public class PartTreeBlazePersistenceQuery extends AbstractPartTreeBlazePersistenceQuery { + + public PartTreeBlazePersistenceQuery(EntityViewAwareJpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider, EscapeCharacter escape, CriteriaBuilderFactory cbf, EntityViewManager evm) { + super(method, em, persistenceProvider, escape, cbf, evm); + } + + @Override + protected ParameterMetadataProvider createParameterMetadataProvider(CriteriaBuilder builder, ParametersParameterAccessor accessor, PersistenceProvider provider, Object escape) { + return new ParameterMetadataProviderImpl(builder, accessor, provider, (EscapeCharacter) escape); + } + + @Override + protected ParameterMetadataProvider createParameterMetadataProvider(CriteriaBuilder builder, JpaParameters parameters, PersistenceProvider provider, Object escape) { + return new ParameterMetadataProviderImpl(builder, parameters, provider, (EscapeCharacter) escape); + } + + @Override + protected JpaQueryExecution getExecution() { + if (getQueryMethod().isSliceQuery()) { + return new PartTreeBlazePersistenceQuery.SlicedExecution(getQueryMethod().getParameters()); + } else if (getQueryMethod().isPageQuery()) { + return new PartTreeBlazePersistenceQuery.PagedExecution(getQueryMethod().getParameters()); + } else if (isDelete()) { + return new PartTreeBlazePersistenceQuery.DeleteExecution(getEntityManager()); + } else if (isExists()) { + return new PartTreeBlazePersistenceQuery.ExistsExecution(); + } else { + return super.getExecution(); + } + } + + /** + * {@link JpaQueryExecution} performing an exists check on the query. + * + * @author Christian Beikov + * @since 1.3.0 + */ + private static class ExistsExecution extends JpaQueryExecution { + + @Override + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return !((PartTreeBlazePersistenceQuery) repositoryQuery).createQuery(jpaParametersParameterAccessor).getResultList().isEmpty(); + } + } + + /** + * Uses the {@link com.blazebit.persistence.PaginatedCriteriaBuilder} API for executing the query. + * + * @author Christian Beikov + * @since 1.2.0 + */ + private static class SlicedExecution extends JpaQueryExecution { + + private final Parameters parameters; + + public SlicedExecution(Parameters parameters) { + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query paginatedCriteriaBuilder = ((PartTreeBlazePersistenceQuery) repositoryQuery).createPaginatedQuery(jpaParametersParameterAccessor.getValues(), false); + PagedList resultList = (PagedList) paginatedCriteriaBuilder.getResultList(); + ParameterAccessor accessor = new ParametersParameterAccessor(parameters, jpaParametersParameterAccessor.getValues()); + Pageable pageable = accessor.getPageable(); + + return new KeysetAwareSliceImpl<>(resultList, pageable); + } + } + + /** + * Uses the {@link com.blazebit.persistence.PaginatedCriteriaBuilder} API for executing the query. + * + * @author Christian Beikov + * @since 1.2.0 + */ + private static class PagedExecution extends JpaQueryExecution { + + private final Parameters parameters; + + public PagedExecution(Parameters parameters) { + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query paginatedCriteriaBuilder = ((PartTreeBlazePersistenceQuery) repositoryQuery).createPaginatedQuery(jpaParametersParameterAccessor.getValues(), true); + PagedList resultList = (PagedList) paginatedCriteriaBuilder.getResultList(); + Long total = resultList.getTotalSize(); + ParameterAccessor accessor = new ParametersParameterAccessor(parameters, jpaParametersParameterAccessor.getValues()); + Pageable pageable = accessor.getPageable(); + + if (total.equals(0L)) { + return new KeysetAwarePageImpl<>(Collections.emptyList(), total, null, pageable); + } + + return new KeysetAwarePageImpl<>(resultList, pageable); + } + } + + /** + * {@link JpaQueryExecution} removing entities matching the query. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ + static class DeleteExecution extends JpaQueryExecution { + + private final EntityManager em; + + public DeleteExecution(EntityManager em) { + this.em = em; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.JpaQueryExecution#doExecute(org.springframework.data.jpa.repository.query.AbstractJpaQuery, java.lang.Object[]) + */ + @Override + protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query query = ((PartTreeBlazePersistenceQuery) jpaQuery).createQuery(jpaParametersParameterAccessor); + List resultList = query.getResultList(); + + for (Object o : resultList) { + em.remove(o); + } + + return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size(); + } + } + + @Override + protected Query doCreateQuery(JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return super.doCreateQuery(jpaParametersParameterAccessor.getValues()); + } + + @Override + protected Query doCreateCountQuery(JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return super.doCreateCountQuery(jpaParametersParameterAccessor.getValues()); + } + + @Override + protected boolean isCountProjection(PartTree tree) { + return tree.isCountProjection(); + } + + @Override + protected boolean isDelete(PartTree tree) { + return tree.isDelete(); + } + + @Override + protected boolean isExists(PartTree tree) { + return tree.isExistsProjection(); + } + + @Override + protected int getOffset(Pageable pageable) { + if (pageable.isPaged()) { + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } + return 0; + } + + @Override + protected int getLimit(Pageable pageable) { + if (pageable.isPaged()) { + return pageable.getPageSize(); + } + return Integer.MAX_VALUE; + } + + @Override + protected ParameterBinder createCriteriaQueryParameterBinder(JpaParameters parameters, Object[] values, List> expressions) { + return new CriteriaQueryParameterBinder(getEntityManager(), evm, parameters, values, expressions); + } + + @Override + protected Map tryGetFetchGraphHints(JpaEntityGraph entityGraph, Class entityType) { + QueryHints fetchGraphHint = Jpa21Utils.getFetchGraphHint( + this.getEntityManager(), + entityGraph, + this.getQueryMethod() + .getEntityInformation() + .getJavaType() + ); + Map map = new HashMap<>(); + fetchGraphHint.forEach(map::put); + return map; + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java new file mode 100644 index 0000000000..ebfc98ec10 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java @@ -0,0 +1,582 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.parser.EntityMetamodel; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareJpaQueryMethod; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.spring.data.base.repository.AbstractEntityViewAwareRepository; +import com.blazebit.persistence.spring.data.base.repository.EntityViewAwareCrudMethodMetadata; +import com.blazebit.persistence.spring.data.base.repository.EntityViewAwareCrudMethodMetadataPostProcessor; +import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryInformation; +import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryMetadataImpl; +import com.blazebit.persistence.spring.data.impl.query.PartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.repository.EntityViewReplacingMethodInterceptor; +import com.blazebit.persistence.view.EntityViewManager; +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.provider.QueryExtractor; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; +import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodInvocationValidator; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; +import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.util.ClassUtils; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.lang.Nullable; +import org.springframework.transaction.interceptor.TransactionalProxy; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +import javax.persistence.EntityManager; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * Partly copied from {@link JpaRepositoryFactory} to retain functionality but mostly original. + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.5.0 + */ +public class BlazePersistenceRepositoryFactory extends JpaRepositoryFactory { + + private static final Constructor IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR; + private static final Constructor QUERY_EXECUTOR_METHOD_INTERCEPTOR; + + static { + Constructor implementationMethodExecutionInterceptor; + Constructor queryExecutorMethodInterceptor; + try { + implementationMethodExecutionInterceptor = (Constructor) Class.forName("org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor") + .getConstructor( + RepositoryInformation.class, + RepositoryComposition.class, + List.class + ); + implementationMethodExecutionInterceptor.setAccessible(true); + queryExecutorMethodInterceptor = (Constructor) Class.forName("org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor") + .getConstructor( + RepositoryInformation.class, + ProjectionFactory.class, + Optional.class, + NamedQueries.class, + List.class, + List.class + ); + queryExecutorMethodInterceptor.setAccessible(true); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR = implementationMethodExecutionInterceptor; + QUERY_EXECUTOR_METHOD_INTERCEPTOR = queryExecutorMethodInterceptor; + } + + private final EntityManager entityManager; + private final CriteriaBuilderFactory cbf; + private final EntityViewManager evm; + private final QueryExtractor extractor; + private final Map repositoryInformationCache; + private final EntityViewReplacingMethodInterceptor entityViewReplacingMethodInterceptor; + private List postProcessors; + private EntityViewAwareCrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; + private Optional> repositoryBaseClass; + private QueryLookupStrategy.Key queryLookupStrategyKey; + private List> queryPostProcessors; + private List methodInvocationListeners; + private NamedQueries namedQueries; + private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private ClassLoader classLoader; + private QueryMethodEvaluationContextProvider evaluationContextProvider; + private BeanFactory beanFactory; + + private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); + + /** + * Creates a new {@link JpaRepositoryFactory}. + * + * @param entityManager must not be {@literal null} + * @param cbf + * @param evm + */ + public BlazePersistenceRepositoryFactory(EntityManager entityManager, CriteriaBuilderFactory cbf, EntityViewManager evm) { + super(entityManager); + this.entityManager = entityManager; + this.extractor = PersistenceProvider.fromEntityManager(entityManager); + this.repositoryInformationCache = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + this.cbf = cbf; + this.evm = evm; + this.namedQueries = PropertiesBasedNamedQueries.EMPTY; + this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT; + this.queryPostProcessors = new ArrayList<>(); + this.queryPostProcessors.add(collectingListener); + this.methodInvocationListeners = new ArrayList<>(); + addRepositoryProxyPostProcessor(this.crudMethodMetadataPostProcessor = new EntityViewAwareCrudMethodMetadataPostProcessor()); + this.repositoryBaseClass = Optional.empty(); + this.entityViewReplacingMethodInterceptor = new EntityViewReplacingMethodInterceptor(entityManager, evm); + } + + @Override + public void setQueryLookupStrategyKey(QueryLookupStrategy.Key key) { + this.queryLookupStrategyKey = key; + } + + @Override + public void setNamedQueries(NamedQueries namedQueries) { + this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries; + } + + @Override + public void addQueryCreationListener(QueryCreationListener listener) { + Assert.notNull(listener, "Listener must not be null!"); + this.queryPostProcessors.add(listener); + } + + @Override + public void addInvocationListener(RepositoryMethodInvocationListener listener) { + Assert.notNull(listener, "Listener must not be null!"); + this.methodInvocationListeners.add(listener); + } + + @Override + public void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor processor) { + if (crudMethodMetadataPostProcessor != null) { + Assert.notNull(processor, "RepositoryProxyPostProcessor must not be null!"); + super.addRepositoryProxyPostProcessor(processor); + if (postProcessors == null) { + this.postProcessors = new ArrayList<>(); + } + this.postProcessors.add(processor); + } + } + + @Override + protected List getQueryMethods() { + return collectingListener.getQueryMethods(); + } + + @Override + public void setEscapeCharacter(EscapeCharacter escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + + protected EntityViewAwareCrudMethodMetadata getCrudMethodMetadata() { + return crudMethodMetadataPostProcessor == null ? null : crudMethodMetadataPostProcessor.getCrudMethodMetadata(); + } + + @Override + protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + return new EntityViewAwareRepositoryMetadataImpl(super.getRepositoryMetadata(repositoryInterface), evm); + } + + @Override + protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryComposition.RepositoryFragments fragments) { + return getRepositoryInformation(metadata, super.getRepositoryInformation(metadata, fragments)); + } + + protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryInformation repositoryInformation) { + return new EntityViewAwareRepositoryInformation((EntityViewAwareRepositoryMetadata) metadata, repositoryInformation); + } + + @Override + protected void validate(RepositoryMetadata repositoryMetadata) { + super.validate(repositoryMetadata); + + if (cbf.getService(EntityMetamodel.class).getEntity(repositoryMetadata.getDomainType()) == null) { + throw new InvalidDataAccessApiUsageException( + String.format("Cannot implement repository %s when using a non-entity domain type %s. Only types annotated with @Entity are supported!", + repositoryMetadata.getRepositoryInterface().getName(), repositoryMetadata.getDomainType().getName())); + } + } + + @Override + protected JpaRepositoryImplementation getTargetRepository(RepositoryInformation information, EntityManager entityManager) { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + JpaEntityInformation entityInformation = getEntityInformation(information.getDomainType()); + AbstractEntityViewAwareRepository entityViewAwareRepository = getTargetRepositoryViaReflection(information, entityInformation, entityManager, cbf, evm, ((EntityViewAwareRepositoryInformation) information).getEntityViewType()); + entityViewAwareRepository.setRepositoryMethodMetadata(getCrudMethodMetadata()); + return (JpaRepositoryImplementation) entityViewAwareRepository; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + return EntityViewAwareRepositoryImpl.class; + } + + @Override + protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { + switch (key != null ? key : QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) { + case CREATE: + return Optional.of(new CreateQueryLookupStrategy(entityManager, extractor, escapeCharacter, cbf, evm)); + case USE_DECLARED_QUERY: + return Optional.of(new DelegateQueryLookupStrategy(super.getQueryLookupStrategy(key, evaluationContextProvider).get())); + case CREATE_IF_NOT_FOUND: + return Optional.of(new CreateIfNotFoundQueryLookupStrategy(entityManager, extractor, new CreateQueryLookupStrategy(entityManager, extractor, escapeCharacter, cbf, evm), + new DelegateQueryLookupStrategy(super.getQueryLookupStrategy(QueryLookupStrategy.Key.USE_DECLARED_QUERY, evaluationContextProvider).get()))); + default: + throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); + } + } + + private static class CreateQueryLookupStrategy implements QueryLookupStrategy { + + private final EntityManager em; + private final QueryExtractor provider; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escapeCharacter; + private final CriteriaBuilderFactory cbf; + private final EntityViewManager evm; + + public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor, EscapeCharacter escapeCharacter, CriteriaBuilderFactory cbf, EntityViewManager evm) { + this.em = em; + this.provider = extractor; + this.persistenceProvider = PersistenceProvider.fromEntityManager(em); + this.escapeCharacter = escapeCharacter; + this.cbf = cbf; + this.evm = evm; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + try { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + return new PartTreeBlazePersistenceQuery(new EntityViewAwareJpaQueryMethod(method, (EntityViewAwareRepositoryMetadata) metadata, factory, provider), em, persistenceProvider, escapeCharacter, cbf, evm); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + String.format("Could not create query metamodel for method %s!", method.toString()), e); + } + } + } + + private static class DelegateQueryLookupStrategy implements QueryLookupStrategy { + + private final QueryLookupStrategy delegate; + + public DelegateQueryLookupStrategy(QueryLookupStrategy delegate) { + this.delegate = delegate; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + return delegate.resolveQuery(method, metadata, factory, namedQueries); + } + } + + private static class CreateIfNotFoundQueryLookupStrategy implements QueryLookupStrategy { + + private final EntityManager em; + private final QueryExtractor provider; + private final DelegateQueryLookupStrategy lookupStrategy; + private final CreateQueryLookupStrategy createStrategy; + + public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor, + CreateQueryLookupStrategy createStrategy, DelegateQueryLookupStrategy lookupStrategy) { + this.em = em; + this.provider = extractor; + this.createStrategy = createStrategy; + this.lookupStrategy = lookupStrategy; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + try { + return lookupStrategy.resolveQuery(method, metadata, factory, namedQueries); + } catch (IllegalStateException e) { + return createStrategy.resolveQuery(method, metadata, factory, namedQueries); + } + } + } + + // Mostly copied from RepositoryFactorySupport to be able to use a custom RepositoryInformation implementation + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + super.setBeanClassLoader(classLoader); + this.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + super.setBeanFactory(beanFactory); + this.beanFactory = beanFactory; + } + + @Override + public void setRepositoryBaseClass(Class repositoryBaseClass) { + super.setRepositoryBaseClass(repositoryBaseClass); + this.repositoryBaseClass = Optional.ofNullable(repositoryBaseClass); + } + + private static final BiFunction REACTIVE_ARGS_CONVERTER = (method, o) -> { + + if (ReactiveWrappers.isAvailable()) { + + Class[] parameterTypes = method.getParameterTypes(); + + Object[] converted = new Object[o.length]; + for (int i = 0; i < parameterTypes.length; i++) { + + Class parameterType = parameterTypes[i]; + Object value = o[i]; + + if (value == null) { + continue; + } + + if (!parameterType.isAssignableFrom(value.getClass()) + && ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) { + + converted[i] = ReactiveWrapperConverters.toWrapper(value, parameterType); + } else { + converted[i] = value; + } + } + + return converted; + } + + return o; + }; + + /** + * Returns a repository instance for the given interface backed by an instance providing implementation logic for + * custom logic. + * + * @param repositoryInterface must not be {@literal null}. + * @param fragments must not be {@literal null}. + * @return + * @since 2.0 + */ + @SuppressWarnings({ "unchecked" }) + public T getRepository(Class repositoryInterface, RepositoryComposition.RepositoryFragments fragments) { + + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); + Assert.notNull(fragments, "RepositoryFragments must not be null!"); + + RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface); + RepositoryComposition composition = getRepositoryComposition(metadata, fragments); + RepositoryInformation information = getRepositoryInformation(metadata, composition); + + validate(information, composition); + + Object target = getTargetRepository(information); + + // Create proxy + ProxyFactory result = new ProxyFactory(); + result.setTarget(target); + result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class); + + if (MethodInvocationValidator.supports(repositoryInterface)) { + result.addAdvice(new MethodInvocationValidator()); + } + + result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); + result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + + postProcessors.forEach(processor -> processor.postProcess(result, information)); + + result.addAdvice(entityViewReplacingMethodInterceptor); + result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); + + ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory); + Optional queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey, + evaluationContextProvider); + result.addAdvice(queryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy, + namedQueries, queryPostProcessors, methodInvocationListeners)); + + composition = composition.append(RepositoryFragment.implemented(target)); + result.addAdvice(implementationMethodExecutionInterceptor(information, composition, methodInvocationListeners)); + + return (T) result.getProxy(classLoader); + } + + private Advice implementationMethodExecutionInterceptor( + RepositoryInformation information, + RepositoryComposition composition, + List methodInvocationListeners) { + try { + return IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR.newInstance( + information, + composition, + methodInvocationListeners + ); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Advice queryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, + ProjectionFactory projectionFactory, Optional queryLookupStrategy, NamedQueries namedQueries, + List> queryPostProcessors, + List methodInvocationListeners) { + try { + return QUERY_EXECUTOR_METHOD_INTERCEPTOR.newInstance( + repositoryInformation, + projectionFactory, + queryLookupStrategy, + namedQueries, + queryPostProcessors, + methodInvocationListeners + ); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Validates the given repository interface as well as the given custom implementation. + * + * @param repositoryInformation + * @param composition + */ + private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) { + + if (repositoryInformation.hasCustomMethod()) { + + if (composition.isEmpty()) { + + throw new IllegalArgumentException( + String.format("You have custom methods in %s but not provided a custom implementation!", + repositoryInformation.getRepositoryInterface())); + } + + composition.validateImplementation(); + } + + validate(repositoryInformation); + } + + private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, + RepositoryComposition composition) { + + RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition); + + return repositoryInformationCache.computeIfAbsent(cacheKey, key -> { + + Class baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata)); + + return getRepositoryInformation(metadata, new DefaultRepositoryInformation(metadata, baseClass, composition)); + }); + } + + private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata, RepositoryComposition.RepositoryFragments fragments) { + + Assert.notNull(metadata, "RepositoryMetadata must not be null!"); + Assert.notNull(fragments, "RepositoryFragments must not be null!"); + + RepositoryComposition composition = getRepositoryComposition(metadata); + RepositoryComposition.RepositoryFragments repositoryAspects = getRepositoryFragments(metadata); + + return composition.append(fragments).append(repositoryAspects); + } + + /** + * Creates {@link RepositoryComposition} based on {@link RepositoryMetadata} for repository-specific method handling. + * + * @param metadata + * @return + */ + private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata) { + + RepositoryComposition composition = RepositoryComposition.empty(); + + if (metadata.isReactiveRepository()) { + return composition.withMethodLookup(MethodLookups.forReactiveTypes(metadata)) + .withArgumentConverter(REACTIVE_ARGS_CONVERTER); + } + + return composition.withMethodLookup(MethodLookups.forRepositoryTypes(metadata)); + } + + private static class RepositoryInformationCacheKey { + + String repositoryInterfaceName; + final long compositionHash; + + /** + * Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and composition. + * + * @param metadata must not be {@literal null}. + * @param composition must not be {@literal null}. + */ + public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryComposition composition) { + + this.repositoryInterfaceName = metadata.getRepositoryInterface().getName(); + this.compositionHash = composition.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RepositoryInformationCacheKey)) { + return false; + } + + RepositoryInformationCacheKey that = (RepositoryInformationCacheKey) o; + + if (compositionHash != that.compositionHash) { + return false; + } + return repositoryInterfaceName != null ? repositoryInterfaceName.equals(that.repositoryInterfaceName) : that.repositoryInterfaceName == null; + } + + @Override + public int hashCode() { + int result = repositoryInterfaceName != null ? repositoryInterfaceName.hashCode() : 0; + result = 31 * result + (int) (compositionHash ^ (compositionHash >>> 32)); + return result; + } + } + +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java new file mode 100644 index 0000000000..605ff63636 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; +import org.springframework.util.Assert; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.io.Serializable; + +/** + * @author Moritz Becker + * @since 1.2.0 + */ +public class BlazePersistenceRepositoryFactoryBean, S, ID extends Serializable> extends + TransactionalRepositoryFactoryBeanSupport { + + private EntityManager entityManager; + + @Autowired + private CriteriaBuilderFactory cbf; + + @Autowired + private EntityViewManager evm; + + /** + * Creates a new {@link BlazePersistenceRepositoryFactoryBean}. + */ + protected BlazePersistenceRepositoryFactoryBean() { + super(null); + } + + /** + * Creates a new {@link BlazePersistenceRepositoryFactoryBean} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + protected BlazePersistenceRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + public boolean isSingleton() { + return true; + } + + /** + * The {@link EntityManager} to be used. + * + * @param entityManager the entityManager to set + */ + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + if (this.entityManager == null) { + this.entityManager = entityManager; + } + } + + /* + * (non-Javadoc) + * @see com.blazebit.persistence.spring.data.impl.repository.BlazeRepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext) + */ + @Override + public void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support. + * BlazeTransactionalRepositoryFactoryBeanSupport#doCreateRepositoryFactory() + */ + @Override + protected BlazePersistenceRepositoryFactory doCreateRepositoryFactory() { + return createRepositoryFactory(entityManager); + } + + /** + * Returns a {@link RepositoryFactorySupport}. + * + * @param entityManager + * @return + */ + protected BlazePersistenceRepositoryFactory createRepositoryFactory(EntityManager entityManager) { + return new BlazePersistenceRepositoryFactory(entityManager, cbf, evm); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + Assert.notNull(entityManager, "EntityManager must not be null!"); + super.afterPropertiesSet(); + } + + public void setEscapeCharacter(char escapeCharacter) { + // Needed to work with Spring Boot 2.1.4 + } + + public char getEscapeCharacter() { + // Needed to work with Spring Boot 2.1.4 + return '\\'; + } + +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java new file mode 100644 index 0000000000..a9d9fcbaf7 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java @@ -0,0 +1,295 @@ +/* + * Copyright 2011-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springframework.data.repository.util.ClassUtils.isGenericRepositoryInterface; +import static org.springframework.util.ReflectionUtils.makeAccessible; + +/** + * Default implementation of {@link RepositoryInformation}. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Mark Paluch + * @author Christoph Strobl + */ +class DefaultRepositoryInformation implements RepositoryInformation { + + private final Map methodCache = new ConcurrentHashMap<>(); + + private final RepositoryMetadata metadata; + private final Class repositoryBaseClass; + private final RepositoryComposition composition; + private final RepositoryComposition baseComposition; + + /** + * Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class. + * + * @param metadata must not be {@literal null}. + * @param repositoryBaseClass must not be {@literal null}. + * @param composition must not be {@literal null}. + */ + public DefaultRepositoryInformation(RepositoryMetadata metadata, Class repositoryBaseClass, + RepositoryComposition composition) { + + Assert.notNull(metadata, "Repository metadata must not be null!"); + Assert.notNull(repositoryBaseClass, "Repository base class must not be null!"); + Assert.notNull(composition, "Repository composition must not be null!"); + + this.metadata = metadata; + this.repositoryBaseClass = repositoryBaseClass; + this.composition = composition; + this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) // + .withArgumentConverter(composition.getArgumentConverter()) // + .withMethodLookup(composition.getMethodLookup()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getDomainClass() + */ + @Override + public Class getDomainType() { + return metadata.getDomainType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getIdClass() + */ + @Override + public Class getIdType() { + return metadata.getIdType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getRepositoryBaseClass() + */ + @Override + public Class getRepositoryBaseClass() { + return this.repositoryBaseClass; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getTargetClassMethod(java.lang.reflect.Method) + */ + @Override + public Method getTargetClassMethod(Method method) { + + if (methodCache.containsKey(method)) { + return methodCache.get(method); + } + + Method result = composition.findMethod(method).orElse(method); + + if (!result.equals(method)) { + return cacheAndReturn(method, result); + } + + return cacheAndReturn(method, baseComposition.findMethod(method).orElse(method)); + } + + private Method cacheAndReturn(Method key, Method value) { + + if (value != null) { + makeAccessible(value); + } + + methodCache.put(key, value); + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getQueryMethods() + */ + @Override + public Streamable getQueryMethods() { + + Set result = new HashSet<>(); + + for (Method method : getRepositoryInterface().getMethods()) { + method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface()); + if (isQueryMethodCandidate(method)) { + result.add(method); + } + } + + return Streamable.of(Collections.unmodifiableSet(result)); + } + + /** + * Checks whether the given method is a query method candidate. + * + * @param method + * @return + */ + private boolean isQueryMethodCandidate(Method method) { + return !method.isBridge() && !method.isDefault() // + && !Modifier.isStatic(method.getModifiers()) // + && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); + } + + /** + * Checks whether the given method contains a custom store specific query annotation annotated with + * {@link QueryAnnotation}. The method-hierarchy is also considered in the search for the annotation. + * + * @param method + * @return + */ + private boolean isQueryAnnotationPresentOn(Method method) { + + return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#isCustomMethod(java.lang.reflect.Method) + */ + @Override + public boolean isCustomMethod(Method method) { + return composition.findMethod(method).isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryInformation#isQueryMethod(java.lang.reflect.Method) + */ + @Override + public boolean isQueryMethod(Method method) { + return getQueryMethods().stream().anyMatch(it -> it.equals(method)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryInformation#isBaseClassMethod(java.lang.reflect.Method) + */ + @Override + public boolean isBaseClassMethod(Method method) { + + Assert.notNull(method, "Method must not be null!"); + return baseComposition.findMethod(method).isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#hasCustomMethod() + */ + @Override + public boolean hasCustomMethod() { + + Class repositoryInterface = getRepositoryInterface(); + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(repositoryInterface)) { + return false; + } + + for (Method method : repositoryInterface.getMethods()) { + if (isCustomMethod(method) && !isBaseClassMethod(method)) { + return true; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getRepositoryInterface() + */ + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method) + */ + @Override + public Class getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(java.lang.reflect.Method) + */ + @Override + public TypeInformation getReturnType(Method method) { + return this.metadata.getReturnType(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getCrudMethods() + */ + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#isPagingRepository() + */ + @Override + public boolean isPagingRepository() { + return metadata.isPagingRepository(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getAlternativeDomainTypes() + */ + @Override + public Set> getAlternativeDomainTypes() { + return metadata.getAlternativeDomainTypes(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#isReactiveRepository() + */ + @Override + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java new file mode 100644 index 0000000000..e8e0a98efe --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spring.data.base.repository.AbstractEntityViewAwareRepository; +import com.blazebit.persistence.spring.data.repository.EntityViewRepository; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.jpa.repository.support.CrudMethodMetadata; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Christian Beikov + * @since 1.3.0 + */ +@Transactional(readOnly = true) +public class EntityViewAwareRepositoryImpl extends AbstractEntityViewAwareRepository implements JpaRepositoryImplementation, EntityViewRepository/*, EntityViewSpecificationExecutor*/ { // Can't implement that interface because of clashing methods + + public EntityViewAwareRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager, CriteriaBuilderFactory cbf, EntityViewManager evm, Class entityViewClass) { + super(entityInformation, entityManager, cbf, evm, (Class) entityViewClass); + } + + @Override + protected Map tryGetFetchGraphHints(EntityManager entityManager, JpaEntityGraph entityGraph, Class entityType) { + Map map = new HashMap<>(); + Jpa21Utils.getFetchGraphHint(entityManager, entityGraph, entityType).forEach( map::put ); + return map; + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findOne(Example example) { + try { + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findOne(Specification spec) { + try { + return Optional.of((E) getQuery(spec, (Sort) null).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findById(ID id) { + return Optional.ofNullable((E) findOne(id)); + } + + public E getById(ID id) { + return (E) getReference(id); + } + + private Object convert(Optional optional, Class targetType) { + return optional.orElse(null); + } + + @Override + public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { + // Ignore the Spring data version of the CrudMethodMetadata + } + + @Override + protected int getOffset(Pageable pageable) { + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java new file mode 100644 index 0000000000..0955aec369 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java @@ -0,0 +1,443 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodLookup; +import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.util.Assert; + +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.springframework.core.GenericTypeResolver.resolveParameterType; + +/** + * Implementations of method lookup functions. + * + * @author Mark Paluch + * @author Oliver Gierke + * @since 2.0 + */ +interface MethodLookups { + + /** + * Direct method lookup filtering on exact method name, parameter count and parameter types. + * + * @return direct method lookup. + */ + public static MethodLookup direct() { + + MethodPredicate direct = (invoked, candidate) -> candidate.getName().equals(invoked.getName()) + && candidate.getParameterCount() == invoked.getParameterCount() + && Arrays.equals(candidate.getParameterTypes(), invoked.getParameterTypes()) + && candidate.getReturnType().equals(invoked.getMethod().getReturnType()); + + return () -> Collections.singletonList(direct); + } + + /** + * Repository type-aware method lookup composed of {@link #direct()} and {@link RepositoryAwareMethodLookup}. + *

+ * Repository-aware lookups resolve generic types from the repository declaration to verify assignability to Id/domain + * types. This lookup also permits assignable method signatures but prefers {@link #direct()} matches. + * + * @param repositoryMetadata must not be {@literal null}. + * @return the composed, repository-aware method lookup. + * @see #direct() + */ + public static MethodLookup forRepositoryTypes(RepositoryMetadata repositoryMetadata) { + return direct().and(new RepositoryAwareMethodLookup(repositoryMetadata)); + } + + /** + * Repository type-aware method lookup composed of {@link #direct()} and {@link ReactiveTypeInteropMethodLookup}. + *

+ * This method lookup considers adaptability of reactive types in method signatures. Repository methods accepting a + * reactive type can be possibly called with a different reactive type if the reactive type can be adopted to the + * target type. This lookup also permits assignable method signatures and resolves repository id/entity types but + * prefers {@link #direct()} matches. + * + * @param repositoryMetadata must not be {@literal null}. + * @return the composed, repository-aware method lookup. + * @see #direct() + * @see #forRepositoryTypes(RepositoryMetadata) + */ + public static MethodLookup forReactiveTypes(RepositoryMetadata repositoryMetadata) { + return direct().and(new ReactiveTypeInteropMethodLookup(repositoryMetadata)); + } + + /** + * Default {@link MethodLookup} considering repository Id and entity types permitting calls to methods with assignable + * arguments. + * + * @author Mark Paluch + */ + static class RepositoryAwareMethodLookup implements MethodLookup { + + @SuppressWarnings("rawtypes") private static final TypeVariable>[] PARAMETERS = Repository.class + .getTypeParameters(); + private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); + private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); + + private final ResolvableType entityType, idType; + private final Class repositoryInterface; + + /** + * Creates a new {@link RepositoryAwareMethodLookup} for the given {@link RepositoryMetadata}. + * + * @param repositoryMetadata must not be {@literal null}. + */ + public RepositoryAwareMethodLookup(RepositoryMetadata repositoryMetadata) { + + Assert.notNull(repositoryMetadata, "Repository metadata must not be null!"); + + this.entityType = ResolvableType.forClass(repositoryMetadata.getDomainType()); + this.idType = ResolvableType.forClass(repositoryMetadata.getIdType()); + this.repositoryInterface = repositoryMetadata.getRepositoryInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.MethodLookup#getLookups() + */ + @Override + public List getLookups() { + + MethodPredicate detailedComparison = (invoked, candidate) -> Optional.of(candidate) + .filter(baseClassMethod -> baseClassMethod.getName().equals(invoked.getName()))// Right name + .filter(baseClassMethod -> baseClassMethod.getParameterCount() == invoked.getParameterCount()) + .filter(baseClassMethod -> parametersMatch(invoked.getMethod(), baseClassMethod))// All parameters match + .isPresent(); + + return Collections.singletonList(detailedComparison); + } + + /** + * Checks whether the given parameter type matches the generic type of the given parameter. Thus when {@literal PK} + * is declared, the method ensures that given method parameter is the primary key type declared in the given + * repository interface e.g. + * + * @param variable must not be {@literal null}. + * @param parameterType must not be {@literal null}. + * @return + */ + protected boolean matchesGenericType(TypeVariable variable, ResolvableType parameterType) { + + GenericDeclaration declaration = variable.getGenericDeclaration(); + + if (declaration instanceof Class) { + + if (ID_TYPE_NAME.equals(variable.getName()) && parameterType.isAssignableFrom(idType)) { + return true; + } + + Type boundType = variable.getBounds()[0]; + String referenceName = boundType instanceof TypeVariable ? boundType.toString() : variable.toString(); + + return DOMAIN_TYPE_NAME.equals(referenceName) && parameterType.isAssignableFrom(entityType); + } + + for (Type type : variable.getBounds()) { + if (ResolvableType.forType(type).isAssignableFrom(parameterType)) { + return true; + } + } + + return false; + } + + /** + * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments + * against the ones bound in the given repository interface. + * + * @param invokedMethod + * @param candidate + * @return + */ + private boolean parametersMatch(Method invokedMethod, Method candidate) { + + Class[] methodParameterTypes = invokedMethod.getParameterTypes(); + Type[] genericTypes = candidate.getGenericParameterTypes(); + Class[] types = candidate.getParameterTypes(); + + for (int i = 0; i < genericTypes.length; i++) { + + Type genericType = genericTypes[i]; + Class type = types[i]; + MethodParameter parameter = new MethodParameter(invokedMethod, i); + Class parameterType = resolveParameterType(parameter, repositoryInterface); + + if (genericType instanceof TypeVariable) { + + if (!matchesGenericType((TypeVariable) genericType, ResolvableType.forMethodParameter(parameter))) { + return false; + } + + continue; + } + + if (types[i].equals(parameterType)) { + continue; + } + + if (!type.isAssignableFrom(parameterType) || !type.equals(methodParameterTypes[i])) { + return false; + } + } + + return true; + } + } + + /** + * Extension to {@link RepositoryAwareMethodLookup} considering reactive type adoption and entity types permitting + * calls to methods with assignable arguments. + * + * @author Mark Paluch + */ + static class ReactiveTypeInteropMethodLookup extends RepositoryAwareMethodLookup { + + private final RepositoryMetadata repositoryMetadata; + + public ReactiveTypeInteropMethodLookup(RepositoryMetadata repositoryMetadata) { + + super(repositoryMetadata); + this.repositoryMetadata = repositoryMetadata; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.MethodLookups.RepositoryAwareMethodLookup#getLookups() + */ + @Override + public List getLookups() { + + MethodPredicate convertibleComparison = (invokedMethod, candidate) -> { + + List>> suppliers = new ArrayList<>(); + + if (usesParametersWithReactiveWrappers(invokedMethod.getMethod())) { + suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, assignableWrapperMatch())); // + suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, wrapperConversionMatch())); + } + + return suppliers.stream().anyMatch(supplier -> supplier.get().isPresent()); + }; + + MethodPredicate detailedComparison = (invokedMethod, candidate) -> getMethodCandidate(invokedMethod, candidate, + matchParameterOrComponentType(repositoryMetadata.getRepositoryInterface())).isPresent(); + + return Arrays.asList(convertibleComparison, detailedComparison); + } + + /** + * {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be + * wrapped but supports unwrapping. Usually types like {@link Optional} or {@link Stream}. + * + * @param repositoryInterface + * @return + * @see QueryExecutionConverters + * @see #matchesGenericType + */ + private Predicate matchParameterOrComponentType(Class repositoryInterface) { + + return (parameterCriteria) -> { + + Class parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface); + Type genericType = parameterCriteria.getGenericBaseType(); + + if (genericType instanceof TypeVariable) { + + if (!matchesGenericType((TypeVariable) genericType, + ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) { + return false; + } + } + + return parameterCriteria.getBaseType().isAssignableFrom(parameterType) + && parameterCriteria.isAssignableFromDeclared(); + }; + } + + /** + * Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped. + * + * @param parameterType must not be {@literal null}. + * @return + */ + private static boolean isNonUnwrappingWrapper(Class parameterType) { + + Assert.notNull(parameterType, "Parameter type must not be null!"); + + return QueryExecutionConverters.supports(parameterType) + && !QueryExecutionConverters.supportsUnwrapping(parameterType); + } + + /** + * Returns whether the given {@link Method} uses a reactive wrapper type as parameter. + * + * @param method must not be {@literal null}. + * @return + */ + private static boolean usesParametersWithReactiveWrappers(Method method) { + + Assert.notNull(method, "Method must not be null!"); + + return Arrays.stream(method.getParameterTypes())// + .anyMatch(ReactiveTypeInteropMethodLookup::isNonUnwrappingWrapper); + } + + /** + * Returns a candidate method from the base class for the given one or the method given in the first place if none + * one the base class matches. + * + * @param method must not be {@literal null}. + * @param baseClass must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @return + */ + private static Optional getMethodCandidate(InvokedMethod invokedMethod, Method candidate, + Predicate predicate) { + + return Optional.of(candidate)// + .filter(it -> invokedMethod.getName().equals(it.getName()))// + .filter(it -> invokedMethod.getParameterCount() == it.getParameterCount())// + .filter(it -> parametersMatch(it, invokedMethod.getMethod(), predicate)); + } + + /** + * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments + * against the ones bound in the given repository interface. + * + * @param baseClassMethod must not be {@literal null}. + * @param declaredMethod must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @return + */ + private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod, + Predicate predicate) { + + return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate); + } + + /** + * {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be + * converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher} + * conversion. + * + * @return + */ + private static Predicate wrapperConversionMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType()); + } + + /** + * {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and + * a declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher} + * conversion. + * + * @return + */ + private static Predicate assignableWrapperMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType()); + } + + private static Stream methodParameters(Method first, Method second) { + + Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!"); + + return IntStream.range(0, first.getParameterCount()) // + .mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index), + new MethodParameter(second, index))); + } + + /** + * Criterion to represent {@link MethodParameter}s from a base method and its declared (overridden) method. Method + * parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same + * parameter count. + */ + static class ParameterOverrideCriteria { + + private final MethodParameter base; + private final MethodParameter declared; + + private ParameterOverrideCriteria(MethodParameter base, MethodParameter declared) { + this.base = base; + this.declared = declared; + } + + public static ParameterOverrideCriteria of(MethodParameter base, MethodParameter declared) { + return new ParameterOverrideCriteria(base, declared); + } + + public MethodParameter getBase() { + return base; + } + + public MethodParameter getDeclared() { + return declared; + } + + /** + * @return base method parameter type. + */ + public Class getBaseType() { + return base.getParameterType(); + } + + /** + * @return generic base method parameter type. + */ + public Type getGenericBaseType() { + return base.getGenericParameterType(); + } + + /** + * @return declared method parameter type. + */ + public Class getDeclaredType() { + return declared.getParameterType(); + } + + public boolean isAssignableFromDeclared() { + return getBaseType().isAssignableFrom(getDeclaredType()); + } + } + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java new file mode 100644 index 0000000000..65572a02f1 --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + + +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.5.0 + */ +public class QueryCollectingQueryCreationListener implements QueryCreationListener { + + private final List queryMethods = new ArrayList<>(); + + @Override + public void onCreation(RepositoryQuery query) { + this.queryMethods.add(query.getQueryMethod()); + } + + public List getQueryMethods() { + return queryMethods; + } +} diff --git a/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java new file mode 100644 index 0000000000..ec5cda6fae --- /dev/null +++ b/integration/spring-data/2.5/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java @@ -0,0 +1,294 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.repository.util.NullableWrapper; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Simple domain service to convert query results into a dedicated type. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Jens Schauder + */ +class QueryExecutionResultHandler { + + private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class); + + private final GenericConversionService conversionService; + + private final Object mutex = new Object(); + + // concurrent access guarded by mutex. + private Map descriptorCache = Collections.emptyMap(); + + /** + * Creates a new {@link QueryExecutionResultHandler}. + */ + public QueryExecutionResultHandler(GenericConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Post-processes the given result of a query invocation to match the return type of the given method. + * + * @param result can be {@literal null}. + * @param method must not be {@literal null}. + * @return + */ + @Nullable + public Object postProcessInvocationResult(@Nullable Object result, Method method) { + + if (!processingRequired(result, method.getReturnType())) { + return result; + } + + ReturnTypeDescriptor descriptor = getOrCreateReturnTypeDescriptor(method); + + return postProcessInvocationResult(result, 0, descriptor); + } + + private ReturnTypeDescriptor getOrCreateReturnTypeDescriptor(Method method) { + + Map descriptorCache = this.descriptorCache; + ReturnTypeDescriptor descriptor = descriptorCache.get(method); + + if (descriptor == null) { + + descriptor = ReturnTypeDescriptor.of(method); + + Map updatedDescriptorCache; + + if (descriptorCache.isEmpty()) { + updatedDescriptorCache = Collections.singletonMap(method, descriptor); + } else { + updatedDescriptorCache = new HashMap<>(descriptorCache.size() + 1, 1); + updatedDescriptorCache.putAll(descriptorCache); + updatedDescriptorCache.put(method, descriptor); + + } + + synchronized (mutex) { + this.descriptorCache = updatedDescriptorCache; + } + } + + return descriptor; + } + + /** + * Post-processes the given result of a query invocation to the given type. + * + * @param result can be {@literal null}. + * @param nestingLevel + * @param descriptor must not be {@literal null}. + * @return + */ + @Nullable + Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, ReturnTypeDescriptor descriptor) { + + TypeDescriptor returnTypeDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel); + + if (returnTypeDescriptor == null) { + return result; + } + + Class expectedReturnType = returnTypeDescriptor.getType(); + + result = unwrapOptional(result); + + if (QueryExecutionConverters.supports(expectedReturnType)) { + + // For a wrapper type, try nested resolution first + result = postProcessInvocationResult(result, nestingLevel + 1, descriptor); + + if (conversionRequired(WRAPPER_TYPE, returnTypeDescriptor)) { + return conversionService.convert(new NullableWrapper(result), returnTypeDescriptor); + } + + if (result != null) { + + TypeDescriptor source = TypeDescriptor.valueOf(result.getClass()); + + if (conversionRequired(source, returnTypeDescriptor)) { + return conversionService.convert(result, returnTypeDescriptor); + } + } + } + + if (result != null) { + + if (ReactiveWrapperConverters.supports(expectedReturnType)) { + return ReactiveWrapperConverters.toWrapper(result, expectedReturnType); + } + + if (result instanceof Collection) { + + TypeDescriptor elementDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel + 1); + boolean requiresConversion = requiresConversion((Collection) result, expectedReturnType, elementDescriptor); + + if (!requiresConversion) { + return result; + } + } + + TypeDescriptor resultDescriptor = TypeDescriptor.forObject(result); + return conversionService.canConvert(resultDescriptor, returnTypeDescriptor) + ? conversionService.convert(result, returnTypeDescriptor) + : result; + } + + return Map.class.equals(expectedReturnType) // + ? CollectionFactory.createMap(expectedReturnType, 0) // + : null; + + } + private boolean requiresConversion(Collection collection, Class expectedReturnType, + @Nullable TypeDescriptor elementDescriptor) { + + if (Streamable.class.isAssignableFrom(expectedReturnType) || !expectedReturnType.isInstance(collection)) { + return true; + } + + if (elementDescriptor == null || !Iterable.class.isAssignableFrom(expectedReturnType)) { + return false; + } + + Class type = elementDescriptor.getType(); + + for (Object o : collection) { + + if (!type.isInstance(o)) { + return true; + } + } + + return false; + } + + /** + * Returns whether the configured {@link ConversionService} can convert between the given {@link TypeDescriptor}s and + * the conversion will not be a no-op. + * + * @param source + * @param target + * @return + */ + private boolean conversionRequired(TypeDescriptor source, TypeDescriptor target) { + + return conversionService.canConvert(source, target) // + && !conversionService.canBypassConvert(source, target); + } + + /** + * Unwraps the given value if it's a JDK 8 {@link Optional}. + * + * @param source can be {@literal null}. + * @return + */ + @Nullable + @SuppressWarnings("unchecked") + private static Object unwrapOptional(@Nullable Object source) { + + if (source == null) { + return null; + } + + return Optional.class.isInstance(source) // + ? Optional.class.cast(source).orElse(null) // + : source; + } + + /** + * Returns whether we have to process the given source object in the first place. + * + * @param source can be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + private static boolean processingRequired(@Nullable Object source, Class targetType) { + + return !targetType.isInstance(source) // + || source == null // + || Collection.class.isInstance(source); + } + + /** + * Value object capturing {@link MethodParameter} and {@link TypeDescriptor}s for top and nested levels. + */ + static class ReturnTypeDescriptor { + + private final MethodParameter methodParameter; + private final TypeDescriptor typeDescriptor; + private final @Nullable TypeDescriptor nestedTypeDescriptor; + + private ReturnTypeDescriptor(Method method) { + this.methodParameter = new MethodParameter(method, -1); + this.typeDescriptor = TypeDescriptor.nested(this.methodParameter, 0); + this.nestedTypeDescriptor = TypeDescriptor.nested(this.methodParameter, 1); + } + + /** + * Create a {@link ReturnTypeDescriptor} from a {@link Method}. + * + * @param method + * @return + */ + public static ReturnTypeDescriptor of(Method method) { + return new ReturnTypeDescriptor(method); + } + + /** + * Return the {@link TypeDescriptor} for a nested type declared within the method parameter described by + * {@code nestingLevel} . + * + * @param nestingLevel the nesting level. {@code 0} is the first level, {@code 1} the next inner one. + * @return the {@link TypeDescriptor} or {@literal null} if it could not be obtained. + * @see TypeDescriptor#nested(MethodParameter, int) + */ + @Nullable + public TypeDescriptor getReturnTypeDescriptor(int nestingLevel) { + + // optimizing for nesting level 0 and 1 (Optional, List) + // nesting level 2 (Optional>) uses the slow path. + + switch (nestingLevel) { + case 0: + return typeDescriptor; + case 1: + return nestedTypeDescriptor; + default: + return TypeDescriptor.nested(this.methodParameter, nestingLevel); + } + } + } +} diff --git a/integration/spring-data/2.6/pom.xml b/integration/spring-data/2.6/pom.xml new file mode 100644 index 0000000000..4058a5e99b --- /dev/null +++ b/integration/spring-data/2.6/pom.xml @@ -0,0 +1,263 @@ + + + + blaze-persistence-integration-spring-data-parent + com.blazebit + 1.6.7-SNAPSHOT + ../pom.xml + + 4.0.0 + + Blazebit Persistence Integration Spring-Data 2.6 + blaze-persistence-integration-spring-data-2.6 + + + com.blazebit.persistence.integration.spring.data.impl + + + ${version.spring-data-2.6-spring} + 1.2.1 + 1.3.6 + 1.8 + 1.8 + + + + + + org.springframework + spring-framework-bom + ${version.spring} + import + pom + + + org.jetbrains.kotlinx + kotlinx-coroutines-bom + ${kotlin-coroutines} + pom + import + + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.6} + provided + + + + io.reactivex + rxjava-reactive-streams + ${rxjava-reactive-streams} + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + true + + + + com.infradna.tool + bridge-method-annotation + ${version.bridge-injector} + provided + + + org.jenkins-ci + annotation-indexer + + + + + + org.jenkins-ci + annotation-indexer + ${version.bridge-injector-indexer} + provided + + + + com.blazebit + blaze-persistence-entity-view-api + + + com.blazebit + blaze-persistence-jpa-criteria-api + + + com.blazebit + blaze-persistence-jpa-criteria-impl + + + jakarta.persistence + jakarta.persistence-api + provided + + + + + com.blazebit + blaze-persistence-integration-spring-data-base + compile + + + com.blazebit + blaze-persistence-integration-entity-view-spring + compile + + + com.blazebit + blaze-persistence-core-impl + runtime + + + com.blazebit + blaze-persistence-entity-view-impl + runtime + + + + + + org.springframework + spring-test + test + + + junit + junit + test + + + + + + + org.moditect + moditect-maven-plugin + + + add-module-infos + package + + add-module-info + + + + + module ${module.name} { + requires transitive com.blazebit.persistence.integration.spring.data; + exports com.blazebit.persistence.spring.data.impl.query; + exports com.blazebit.persistence.spring.data.impl.repository; + } + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-resource + generate-resources + + add-resource + + + + + target/generated/resources + + + + + + + + org.bsc.maven + maven-processor-plugin + + + process + + process + + generate-sources + + + target/generated/resources + + org.jvnet.hudson.annotation_indexer.AnnotationProcessorImpl + + + + + + + com.infradna.tool + bridge-method-annotation + ${version.bridge-injector} + + + + org.jenkins-ci + annotation-indexer + ${version.bridge-injector-indexer} + + + + + com.infradna.tool + bridge-method-injector + ${version.bridge-injector} + + + + process + + + + + + + org.ow2.asm + asm-debug-all + CUSTOM + + + org.ow2.asm + asm + ${version.bridge-injector-asm} + + + org.ow2.asm + asm-commons + ${version.bridge-injector-asm} + + + + + + + \ No newline at end of file diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java new file mode 100644 index 0000000000..356792688f --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.AbstractCriteriaQueryParameterBinder; +import com.blazebit.persistence.spring.data.base.query.JpaParameters; +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; + +/** + * Concrete version for Spring Data 2.x. + * + * @author Christian Beikov + * @since 1.3.0 + */ +public class CriteriaQueryParameterBinder extends AbstractCriteriaQueryParameterBinder { + + public CriteriaQueryParameterBinder(EntityManager em, EntityViewManager evm, JpaParameters parameters, Object[] values, Iterable> expressions) { + super(em, evm, parameters, values, expressions); + } + + @Override + protected int getOffset() { + Pageable pageable = getPageable(); + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java new file mode 100644 index 0000000000..663f00dd26 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityViewAwareRepositoryInformation implements RepositoryInformation, EntityViewAwareRepositoryMetadata { + + private final EntityViewAwareRepositoryMetadata metadata; + private final RepositoryInformation repositoryInformation; + + public EntityViewAwareRepositoryInformation(EntityViewAwareRepositoryMetadata metadata, RepositoryInformation repositoryInformation) { + this.metadata = metadata; + this.repositoryInformation = repositoryInformation; + } + + @Override + public EntityViewManager getEntityViewManager() { + return metadata.getEntityViewManager(); + } + + @Override + public Class getRepositoryBaseClass() { + return repositoryInformation.getRepositoryBaseClass(); + } + + @Override + public boolean hasCustomMethod() { + return repositoryInformation.hasCustomMethod(); + } + + @Override + public boolean isCustomMethod(Method method) { + return repositoryInformation.isCustomMethod(method); + } + + @Override + public boolean isQueryMethod(Method method) { + return repositoryInformation.isQueryMethod(method); + } + + @Override + public boolean isBaseClassMethod(Method method) { + return repositoryInformation.isBaseClassMethod(method); + } + + @Override + public Streamable getQueryMethods() { + return repositoryInformation.getQueryMethods(); + } + + @Override + public Method getTargetClassMethod(Method method) { + return repositoryInformation.getTargetClassMethod(method); + } + + @Override + public Class getIdType() { + return (Class) repositoryInformation.getIdType(); + } + + @Override + public Class getDomainType() { + return repositoryInformation.getDomainType(); + } + + @Override + public Class getEntityViewType() { + return metadata.getEntityViewType(); + } + + @Override + public Class getRepositoryInterface() { + return repositoryInformation.getRepositoryInterface(); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return repositoryInformation.getReturnedDomainClass(method); + } + + @Override + public Class getReturnedEntityViewClass(Method method) { + return metadata.getReturnedEntityViewClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return repositoryInformation.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return repositoryInformation.isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return repositoryInformation.getAlternativeDomainTypes(); + } + + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return repositoryInformation.getReturnType(method); + } + +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java new file mode 100644 index 0000000000..2ffe8fc784 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java @@ -0,0 +1,126 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.metamodel.ManagedViewType; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.util.TypeInformation; + +import java.lang.reflect.Method; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityViewAwareRepositoryMetadataImpl implements EntityViewAwareRepositoryMetadata { + + private final RepositoryMetadata metadata; + private final EntityViewManager evm; + private final Class domainType; + private final Class entityViewType; + + public EntityViewAwareRepositoryMetadataImpl(RepositoryMetadata metadata, EntityViewManager evm) { + this.metadata = metadata; + this.evm = evm; + Class domainType = metadata.getDomainType(); + ManagedViewType managedViewType = evm.getMetamodel().managedView(domainType); + if (managedViewType == null) { + this.domainType = domainType; + this.entityViewType = null; + } else { + this.domainType = managedViewType.getEntityClass(); + this.entityViewType = managedViewType.getJavaType(); + } + } + + @Override + public EntityViewManager getEntityViewManager() { + return evm; + } + + @Override + public Class getIdType() { + return metadata.getIdType(); + } + + @Override + public Class getDomainType() { + return domainType; + } + + @Override + public Class getEntityViewType() { + return entityViewType; + } + + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public Class getReturnedDomainClass(Method method) { + Class returnedDomainClass = metadata.getReturnedDomainClass(method); + ManagedViewType managedViewType = evm.getMetamodel().managedView(returnedDomainClass); + if (managedViewType == null) { + return returnedDomainClass; + } else { + return managedViewType.getEntityClass(); + } + } + + @Override + public TypeInformation getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class getReturnedEntityViewClass(Method method) { + Class returnedDomainClass = metadata.getReturnedDomainClass(method); + ManagedViewType managedViewType = evm.getMetamodel().managedView(returnedDomainClass); + if (managedViewType == null) { + return null; + } else { + return managedViewType.getJavaType(); + } + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return metadata.isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return metadata.getAlternativeDomainTypes(); + } + + @Override + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } + +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java new file mode 100644 index 0000000000..c6f59f065d --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java @@ -0,0 +1,309 @@ +/* + * Copyright 2011-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.expression.Expression; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.ParameterExpression; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Helper class to allow easy creation of {@link ParameterMetadata}s. + * + * Christian Beikov: Copied while implementing the shared interface to be able to share code between Spring Data integrations for 1.x and 2.x. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Mark Paluch + * @author Christoph Strobl + * @author Jens Schauder + * @author Andrey Kovalev + */ +class ParameterMetadataProviderImpl implements ParameterMetadataProvider { + + private final CriteriaBuilder builder; + private final Iterator parameters; + private final List> expressions; + private final Iterator bindableParameterValues; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} and + * {@link ParametersParameterAccessor} with support for parameter value customizations via {@link PersistenceProvider} + * . + * + * @param builder must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + public ParameterMetadataProviderImpl(CriteriaBuilder builder, ParametersParameterAccessor accessor, + PersistenceProvider provider, EscapeCharacter escape) { + this(builder, accessor.iterator(), accessor.getParameters(), provider, escape); + } + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} and {@link Parameters} with + * support for parameter value customizations via {@link PersistenceProvider}. + * + * @param builder must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + public ParameterMetadataProviderImpl(CriteriaBuilder builder, Parameters parameters, PersistenceProvider provider, + EscapeCharacter escape) { + this(builder, null, parameters, provider, escape); + } + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} an {@link Iterable} of all + * bindable parameter values, and {@link Parameters} with support for parameter value customizations via + * {@link PersistenceProvider}. + * + * @param builder must not be {@literal null}. + * @param bindableParameterValues may be {@literal null}. + * @param parameters must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + private ParameterMetadataProviderImpl(CriteriaBuilder builder, Iterator bindableParameterValues, + Parameters parameters, PersistenceProvider provider, EscapeCharacter escape) { + + Assert.notNull(builder, "CriteriaBuilder must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + Assert.notNull(provider, "PesistenceProvider must not be null!"); + + this.builder = builder; + this.parameters = parameters.getBindableParameters().iterator(); + this.expressions = new ArrayList<>(); + this.bindableParameterValues = bindableParameterValues; + this.persistenceProvider = provider; + this.escape = escape; + } + + /** + * Returns all {@link ParameterMetadata}s built. + * + * @return the expressions + */ + public List> getExpressions() { + return expressions; + } + + /** + * Builds a new {@link ParameterMetadata} for given {@link Part} and the next {@link Parameter}. + */ + @SuppressWarnings("unchecked") + public ParameterMetadata next(Part part) { + if (!parameters.hasNext()) { + throw new IllegalArgumentException(String.format("No parameter available for part %s.", part)); + } + + Parameter parameter = parameters.next(); + return (ParameterMetadata) next(part, parameter.getType(), parameter); + } + + /** + * Builds a new {@link ParameterMetadata} of the given {@link Part} and type. Forwards the underlying + * {@link Parameters} as well. + * + * @param is the type parameter of the returend {@link ParameterMetadata}. + * @param type must not be {@literal null}. + * @return ParameterMetadata for the next parameter. + */ + @SuppressWarnings("unchecked") + public ParameterMetadata next(Part part, Class type) { + + Parameter parameter = parameters.next(); + Class typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type; + return (ParameterMetadata) next(part, typeToUse, parameter); + } + + /** + * Builds a new {@link ParameterMetadata} for the given type and name. + * + * @param type parameter for the returned {@link ParameterMetadata}. + * @param part must not be {@literal null}. + * @param type must not be {@literal null}. + * @param parameter providing the name for the returned {@link ParameterMetadata}. + * @return a new {@link ParameterMetadata} for the given type and name. + */ + private ParameterMetadata next(Part part, Class type, Parameter parameter) { + + Assert.notNull(type, "Type must not be null!"); + + /* + * We treat Expression types as Object vales since the real value to be bound as a parameter is determined at query time. + */ + @SuppressWarnings("unchecked") + Class reifiedType = Expression.class.equals(type) ? (Class) Object.class : type; + + Supplier name = () -> parameter.getName() + .orElseThrow(() -> new IllegalArgumentException("o_O Parameter needs to be named")); + + ParameterExpression expression = parameter.isExplicitlyNamed() // + ? builder.parameter(reifiedType, name.get()) // + : builder.parameter(reifiedType); + + Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next(); + + ParameterMetadata metadata = new ParameterMetadataImpl<>(expression, part, value, persistenceProvider, escape); + expressions.add(metadata); + + return metadata; + } + + EscapeCharacter getEscape() { + return escape; + } + + /** + * @author Oliver Gierke + * @author Thomas Darimont + * @author Andrey Kovalev + * @param + */ + static class ParameterMetadataImpl implements ParameterMetadata { + + private final Type type; + private final ParameterExpression expression; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; + private final boolean ignoreCase; + + /** + * Creates a new {@link ParameterMetadata}. + */ + public ParameterMetadataImpl(ParameterExpression expression, Part part, Object value, + PersistenceProvider provider, EscapeCharacter escape) { + + this.expression = expression; + this.persistenceProvider = provider; + this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType(); + this.ignoreCase = IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); + this.escape = escape; + } + + /** + * Returns the {@link ParameterExpression}. + * + * @return the expression + */ + public ParameterExpression getExpression() { + return expression; + } + + /** + * Returns whether the parameter shall be considered an {@literal IS NULL} parameter. + */ + public boolean isIsNullParameter() { + return Type.IS_NULL.equals(type); + } + + /** + * Prepares the object before it's actually bound to the {@link javax.persistence.Query;}. + * + * @param value must not be {@literal null}. + */ + public Object prepare(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + Class expressionType = expression.getJavaType(); + + if (String.class.equals(expressionType)) { + + switch (type) { + case STARTING_WITH: + return String.format("%s%%", escape.escape(value.toString())); + case ENDING_WITH: + return String.format("%%%s", escape.escape(value.toString())); + case CONTAINING: + case NOT_CONTAINING: + return String.format("%%%s%%", escape.escape(value.toString())); + default: + return value; + } + } + + return Collection.class.isAssignableFrom(expressionType) // + ? upperIfIgnoreCase(ignoreCase, toCollection(value)) // + : value; + } + + /** + * Returns the given argument as {@link Collection} which means it will return it as is if it's a + * {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element + * {@link Collections}. + * + * @param value the value to be converted to a {@link Collection}. + * @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value. + */ + private static Collection toCollection(Object value) { + + if (value == null) { + return null; + } + + if (value instanceof Collection) { + return (Collection) value; + } + + if (ObjectUtils.isArray(value)) { + return Arrays.asList(ObjectUtils.toObjectArray(value)); + } + + return Collections.singleton(value); + } + + @SuppressWarnings("unchecked") + private static Collection upperIfIgnoreCase(boolean ignoreCase, Collection collection) { + + if (!ignoreCase || CollectionUtils.isEmpty(collection)) { + return collection; + } + + return ((Collection) collection).stream() // + .map(it -> it == null // + ? null // + : it.toUpperCase()) // + .collect(Collectors.toList()); + } + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java new file mode 100644 index 0000000000..eb31c4cda6 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java @@ -0,0 +1,255 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.PagedList; +import com.blazebit.persistence.spring.data.base.query.AbstractPartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareJpaQueryMethod; +import com.blazebit.persistence.spring.data.base.query.JpaParameters; +import com.blazebit.persistence.spring.data.base.query.KeysetAwarePageImpl; +import com.blazebit.persistence.spring.data.base.query.KeysetAwareSliceImpl; +import com.blazebit.persistence.spring.data.base.query.ParameterBinder; +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.AbstractJpaQuery; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; +import org.springframework.data.jpa.repository.query.JpaQueryExecution; +import org.springframework.data.jpa.repository.support.QueryHints; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.criteria.CriteriaBuilder; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.2.0 + */ +public class PartTreeBlazePersistenceQuery extends AbstractPartTreeBlazePersistenceQuery { + + public PartTreeBlazePersistenceQuery(EntityViewAwareJpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider, EscapeCharacter escape, CriteriaBuilderFactory cbf, EntityViewManager evm) { + super(method, em, persistenceProvider, escape, cbf, evm); + } + + @Override + protected ParameterMetadataProvider createParameterMetadataProvider(CriteriaBuilder builder, ParametersParameterAccessor accessor, PersistenceProvider provider, Object escape) { + return new ParameterMetadataProviderImpl(builder, accessor, provider, (EscapeCharacter) escape); + } + + @Override + protected ParameterMetadataProvider createParameterMetadataProvider(CriteriaBuilder builder, JpaParameters parameters, PersistenceProvider provider, Object escape) { + return new ParameterMetadataProviderImpl(builder, parameters, provider, (EscapeCharacter) escape); + } + + @Override + protected JpaQueryExecution getExecution() { + if (getQueryMethod().isSliceQuery()) { + return new PartTreeBlazePersistenceQuery.SlicedExecution(getQueryMethod().getParameters()); + } else if (getQueryMethod().isPageQuery()) { + return new PartTreeBlazePersistenceQuery.PagedExecution(getQueryMethod().getParameters()); + } else if (isDelete()) { + return new PartTreeBlazePersistenceQuery.DeleteExecution(getEntityManager()); + } else if (isExists()) { + return new PartTreeBlazePersistenceQuery.ExistsExecution(); + } else { + return super.getExecution(); + } + } + + /** + * {@link JpaQueryExecution} performing an exists check on the query. + * + * @author Christian Beikov + * @since 1.3.0 + */ + private static class ExistsExecution extends JpaQueryExecution { + + @Override + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return !((PartTreeBlazePersistenceQuery) repositoryQuery).createQuery(jpaParametersParameterAccessor).getResultList().isEmpty(); + } + } + + /** + * Uses the {@link com.blazebit.persistence.PaginatedCriteriaBuilder} API for executing the query. + * + * @author Christian Beikov + * @since 1.2.0 + */ + private static class SlicedExecution extends JpaQueryExecution { + + private final Parameters parameters; + + public SlicedExecution(Parameters parameters) { + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query paginatedCriteriaBuilder = ((PartTreeBlazePersistenceQuery) repositoryQuery).createPaginatedQuery(jpaParametersParameterAccessor.getValues(), false); + PagedList resultList = (PagedList) paginatedCriteriaBuilder.getResultList(); + ParameterAccessor accessor = new ParametersParameterAccessor(parameters, jpaParametersParameterAccessor.getValues()); + Pageable pageable = accessor.getPageable(); + + return new KeysetAwareSliceImpl<>(resultList, pageable); + } + } + + /** + * Uses the {@link com.blazebit.persistence.PaginatedCriteriaBuilder} API for executing the query. + * + * @author Christian Beikov + * @since 1.2.0 + */ + private static class PagedExecution extends JpaQueryExecution { + + private final Parameters parameters; + + public PagedExecution(Parameters parameters) { + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query paginatedCriteriaBuilder = ((PartTreeBlazePersistenceQuery) repositoryQuery).createPaginatedQuery(jpaParametersParameterAccessor.getValues(), true); + PagedList resultList = (PagedList) paginatedCriteriaBuilder.getResultList(); + Long total = resultList.getTotalSize(); + ParameterAccessor accessor = new ParametersParameterAccessor(parameters, jpaParametersParameterAccessor.getValues()); + Pageable pageable = accessor.getPageable(); + + if (total.equals(0L)) { + return new KeysetAwarePageImpl<>(Collections.emptyList(), total, null, pageable); + } + + return new KeysetAwarePageImpl<>(resultList, pageable); + } + } + + /** + * {@link JpaQueryExecution} removing entities matching the query. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ + static class DeleteExecution extends JpaQueryExecution { + + private final EntityManager em; + + public DeleteExecution(EntityManager em) { + this.em = em; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.JpaQueryExecution#doExecute(org.springframework.data.jpa.repository.query.AbstractJpaQuery, java.lang.Object[]) + */ + @Override + protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query query = ((PartTreeBlazePersistenceQuery) jpaQuery).createQuery(jpaParametersParameterAccessor); + List resultList = query.getResultList(); + + for (Object o : resultList) { + em.remove(o); + } + + return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size(); + } + } + + @Override + protected Query doCreateQuery(JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return super.doCreateQuery(jpaParametersParameterAccessor.getValues()); + } + + @Override + protected Query doCreateCountQuery(JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return super.doCreateCountQuery(jpaParametersParameterAccessor.getValues()); + } + + @Override + protected boolean isCountProjection(PartTree tree) { + return tree.isCountProjection(); + } + + @Override + protected boolean isDelete(PartTree tree) { + return tree.isDelete(); + } + + @Override + protected boolean isExists(PartTree tree) { + return tree.isExistsProjection(); + } + + @Override + protected int getOffset(Pageable pageable) { + if (pageable.isPaged()) { + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } + return 0; + } + + @Override + protected int getLimit(Pageable pageable) { + if (pageable.isPaged()) { + return pageable.getPageSize(); + } + return Integer.MAX_VALUE; + } + + @Override + protected ParameterBinder createCriteriaQueryParameterBinder(JpaParameters parameters, Object[] values, List> expressions) { + return new CriteriaQueryParameterBinder(getEntityManager(), evm, parameters, values, expressions); + } + + @Override + protected Map tryGetFetchGraphHints(JpaEntityGraph entityGraph, Class entityType) { + QueryHints fetchGraphHint = Jpa21Utils.getFetchGraphHint( + this.getEntityManager(), + entityGraph, + this.getQueryMethod() + .getEntityInformation() + .getJavaType() + ); + Map map = new HashMap<>(); + fetchGraphHint.forEach(map::put); + return map; + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java new file mode 100644 index 0000000000..ebfc98ec10 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java @@ -0,0 +1,582 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.parser.EntityMetamodel; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareJpaQueryMethod; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.spring.data.base.repository.AbstractEntityViewAwareRepository; +import com.blazebit.persistence.spring.data.base.repository.EntityViewAwareCrudMethodMetadata; +import com.blazebit.persistence.spring.data.base.repository.EntityViewAwareCrudMethodMetadataPostProcessor; +import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryInformation; +import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryMetadataImpl; +import com.blazebit.persistence.spring.data.impl.query.PartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.repository.EntityViewReplacingMethodInterceptor; +import com.blazebit.persistence.view.EntityViewManager; +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.provider.QueryExtractor; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; +import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodInvocationValidator; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; +import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.util.ClassUtils; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.lang.Nullable; +import org.springframework.transaction.interceptor.TransactionalProxy; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +import javax.persistence.EntityManager; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * Partly copied from {@link JpaRepositoryFactory} to retain functionality but mostly original. + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.5.0 + */ +public class BlazePersistenceRepositoryFactory extends JpaRepositoryFactory { + + private static final Constructor IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR; + private static final Constructor QUERY_EXECUTOR_METHOD_INTERCEPTOR; + + static { + Constructor implementationMethodExecutionInterceptor; + Constructor queryExecutorMethodInterceptor; + try { + implementationMethodExecutionInterceptor = (Constructor) Class.forName("org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor") + .getConstructor( + RepositoryInformation.class, + RepositoryComposition.class, + List.class + ); + implementationMethodExecutionInterceptor.setAccessible(true); + queryExecutorMethodInterceptor = (Constructor) Class.forName("org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor") + .getConstructor( + RepositoryInformation.class, + ProjectionFactory.class, + Optional.class, + NamedQueries.class, + List.class, + List.class + ); + queryExecutorMethodInterceptor.setAccessible(true); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR = implementationMethodExecutionInterceptor; + QUERY_EXECUTOR_METHOD_INTERCEPTOR = queryExecutorMethodInterceptor; + } + + private final EntityManager entityManager; + private final CriteriaBuilderFactory cbf; + private final EntityViewManager evm; + private final QueryExtractor extractor; + private final Map repositoryInformationCache; + private final EntityViewReplacingMethodInterceptor entityViewReplacingMethodInterceptor; + private List postProcessors; + private EntityViewAwareCrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; + private Optional> repositoryBaseClass; + private QueryLookupStrategy.Key queryLookupStrategyKey; + private List> queryPostProcessors; + private List methodInvocationListeners; + private NamedQueries namedQueries; + private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private ClassLoader classLoader; + private QueryMethodEvaluationContextProvider evaluationContextProvider; + private BeanFactory beanFactory; + + private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); + + /** + * Creates a new {@link JpaRepositoryFactory}. + * + * @param entityManager must not be {@literal null} + * @param cbf + * @param evm + */ + public BlazePersistenceRepositoryFactory(EntityManager entityManager, CriteriaBuilderFactory cbf, EntityViewManager evm) { + super(entityManager); + this.entityManager = entityManager; + this.extractor = PersistenceProvider.fromEntityManager(entityManager); + this.repositoryInformationCache = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + this.cbf = cbf; + this.evm = evm; + this.namedQueries = PropertiesBasedNamedQueries.EMPTY; + this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT; + this.queryPostProcessors = new ArrayList<>(); + this.queryPostProcessors.add(collectingListener); + this.methodInvocationListeners = new ArrayList<>(); + addRepositoryProxyPostProcessor(this.crudMethodMetadataPostProcessor = new EntityViewAwareCrudMethodMetadataPostProcessor()); + this.repositoryBaseClass = Optional.empty(); + this.entityViewReplacingMethodInterceptor = new EntityViewReplacingMethodInterceptor(entityManager, evm); + } + + @Override + public void setQueryLookupStrategyKey(QueryLookupStrategy.Key key) { + this.queryLookupStrategyKey = key; + } + + @Override + public void setNamedQueries(NamedQueries namedQueries) { + this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries; + } + + @Override + public void addQueryCreationListener(QueryCreationListener listener) { + Assert.notNull(listener, "Listener must not be null!"); + this.queryPostProcessors.add(listener); + } + + @Override + public void addInvocationListener(RepositoryMethodInvocationListener listener) { + Assert.notNull(listener, "Listener must not be null!"); + this.methodInvocationListeners.add(listener); + } + + @Override + public void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor processor) { + if (crudMethodMetadataPostProcessor != null) { + Assert.notNull(processor, "RepositoryProxyPostProcessor must not be null!"); + super.addRepositoryProxyPostProcessor(processor); + if (postProcessors == null) { + this.postProcessors = new ArrayList<>(); + } + this.postProcessors.add(processor); + } + } + + @Override + protected List getQueryMethods() { + return collectingListener.getQueryMethods(); + } + + @Override + public void setEscapeCharacter(EscapeCharacter escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + + protected EntityViewAwareCrudMethodMetadata getCrudMethodMetadata() { + return crudMethodMetadataPostProcessor == null ? null : crudMethodMetadataPostProcessor.getCrudMethodMetadata(); + } + + @Override + protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + return new EntityViewAwareRepositoryMetadataImpl(super.getRepositoryMetadata(repositoryInterface), evm); + } + + @Override + protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryComposition.RepositoryFragments fragments) { + return getRepositoryInformation(metadata, super.getRepositoryInformation(metadata, fragments)); + } + + protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryInformation repositoryInformation) { + return new EntityViewAwareRepositoryInformation((EntityViewAwareRepositoryMetadata) metadata, repositoryInformation); + } + + @Override + protected void validate(RepositoryMetadata repositoryMetadata) { + super.validate(repositoryMetadata); + + if (cbf.getService(EntityMetamodel.class).getEntity(repositoryMetadata.getDomainType()) == null) { + throw new InvalidDataAccessApiUsageException( + String.format("Cannot implement repository %s when using a non-entity domain type %s. Only types annotated with @Entity are supported!", + repositoryMetadata.getRepositoryInterface().getName(), repositoryMetadata.getDomainType().getName())); + } + } + + @Override + protected JpaRepositoryImplementation getTargetRepository(RepositoryInformation information, EntityManager entityManager) { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + JpaEntityInformation entityInformation = getEntityInformation(information.getDomainType()); + AbstractEntityViewAwareRepository entityViewAwareRepository = getTargetRepositoryViaReflection(information, entityInformation, entityManager, cbf, evm, ((EntityViewAwareRepositoryInformation) information).getEntityViewType()); + entityViewAwareRepository.setRepositoryMethodMetadata(getCrudMethodMetadata()); + return (JpaRepositoryImplementation) entityViewAwareRepository; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + return EntityViewAwareRepositoryImpl.class; + } + + @Override + protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { + switch (key != null ? key : QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) { + case CREATE: + return Optional.of(new CreateQueryLookupStrategy(entityManager, extractor, escapeCharacter, cbf, evm)); + case USE_DECLARED_QUERY: + return Optional.of(new DelegateQueryLookupStrategy(super.getQueryLookupStrategy(key, evaluationContextProvider).get())); + case CREATE_IF_NOT_FOUND: + return Optional.of(new CreateIfNotFoundQueryLookupStrategy(entityManager, extractor, new CreateQueryLookupStrategy(entityManager, extractor, escapeCharacter, cbf, evm), + new DelegateQueryLookupStrategy(super.getQueryLookupStrategy(QueryLookupStrategy.Key.USE_DECLARED_QUERY, evaluationContextProvider).get()))); + default: + throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); + } + } + + private static class CreateQueryLookupStrategy implements QueryLookupStrategy { + + private final EntityManager em; + private final QueryExtractor provider; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escapeCharacter; + private final CriteriaBuilderFactory cbf; + private final EntityViewManager evm; + + public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor, EscapeCharacter escapeCharacter, CriteriaBuilderFactory cbf, EntityViewManager evm) { + this.em = em; + this.provider = extractor; + this.persistenceProvider = PersistenceProvider.fromEntityManager(em); + this.escapeCharacter = escapeCharacter; + this.cbf = cbf; + this.evm = evm; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + try { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + return new PartTreeBlazePersistenceQuery(new EntityViewAwareJpaQueryMethod(method, (EntityViewAwareRepositoryMetadata) metadata, factory, provider), em, persistenceProvider, escapeCharacter, cbf, evm); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + String.format("Could not create query metamodel for method %s!", method.toString()), e); + } + } + } + + private static class DelegateQueryLookupStrategy implements QueryLookupStrategy { + + private final QueryLookupStrategy delegate; + + public DelegateQueryLookupStrategy(QueryLookupStrategy delegate) { + this.delegate = delegate; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + return delegate.resolveQuery(method, metadata, factory, namedQueries); + } + } + + private static class CreateIfNotFoundQueryLookupStrategy implements QueryLookupStrategy { + + private final EntityManager em; + private final QueryExtractor provider; + private final DelegateQueryLookupStrategy lookupStrategy; + private final CreateQueryLookupStrategy createStrategy; + + public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor, + CreateQueryLookupStrategy createStrategy, DelegateQueryLookupStrategy lookupStrategy) { + this.em = em; + this.provider = extractor; + this.createStrategy = createStrategy; + this.lookupStrategy = lookupStrategy; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + try { + return lookupStrategy.resolveQuery(method, metadata, factory, namedQueries); + } catch (IllegalStateException e) { + return createStrategy.resolveQuery(method, metadata, factory, namedQueries); + } + } + } + + // Mostly copied from RepositoryFactorySupport to be able to use a custom RepositoryInformation implementation + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + super.setBeanClassLoader(classLoader); + this.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + super.setBeanFactory(beanFactory); + this.beanFactory = beanFactory; + } + + @Override + public void setRepositoryBaseClass(Class repositoryBaseClass) { + super.setRepositoryBaseClass(repositoryBaseClass); + this.repositoryBaseClass = Optional.ofNullable(repositoryBaseClass); + } + + private static final BiFunction REACTIVE_ARGS_CONVERTER = (method, o) -> { + + if (ReactiveWrappers.isAvailable()) { + + Class[] parameterTypes = method.getParameterTypes(); + + Object[] converted = new Object[o.length]; + for (int i = 0; i < parameterTypes.length; i++) { + + Class parameterType = parameterTypes[i]; + Object value = o[i]; + + if (value == null) { + continue; + } + + if (!parameterType.isAssignableFrom(value.getClass()) + && ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) { + + converted[i] = ReactiveWrapperConverters.toWrapper(value, parameterType); + } else { + converted[i] = value; + } + } + + return converted; + } + + return o; + }; + + /** + * Returns a repository instance for the given interface backed by an instance providing implementation logic for + * custom logic. + * + * @param repositoryInterface must not be {@literal null}. + * @param fragments must not be {@literal null}. + * @return + * @since 2.0 + */ + @SuppressWarnings({ "unchecked" }) + public T getRepository(Class repositoryInterface, RepositoryComposition.RepositoryFragments fragments) { + + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); + Assert.notNull(fragments, "RepositoryFragments must not be null!"); + + RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface); + RepositoryComposition composition = getRepositoryComposition(metadata, fragments); + RepositoryInformation information = getRepositoryInformation(metadata, composition); + + validate(information, composition); + + Object target = getTargetRepository(information); + + // Create proxy + ProxyFactory result = new ProxyFactory(); + result.setTarget(target); + result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class); + + if (MethodInvocationValidator.supports(repositoryInterface)) { + result.addAdvice(new MethodInvocationValidator()); + } + + result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); + result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + + postProcessors.forEach(processor -> processor.postProcess(result, information)); + + result.addAdvice(entityViewReplacingMethodInterceptor); + result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); + + ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory); + Optional queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey, + evaluationContextProvider); + result.addAdvice(queryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy, + namedQueries, queryPostProcessors, methodInvocationListeners)); + + composition = composition.append(RepositoryFragment.implemented(target)); + result.addAdvice(implementationMethodExecutionInterceptor(information, composition, methodInvocationListeners)); + + return (T) result.getProxy(classLoader); + } + + private Advice implementationMethodExecutionInterceptor( + RepositoryInformation information, + RepositoryComposition composition, + List methodInvocationListeners) { + try { + return IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR.newInstance( + information, + composition, + methodInvocationListeners + ); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Advice queryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, + ProjectionFactory projectionFactory, Optional queryLookupStrategy, NamedQueries namedQueries, + List> queryPostProcessors, + List methodInvocationListeners) { + try { + return QUERY_EXECUTOR_METHOD_INTERCEPTOR.newInstance( + repositoryInformation, + projectionFactory, + queryLookupStrategy, + namedQueries, + queryPostProcessors, + methodInvocationListeners + ); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Validates the given repository interface as well as the given custom implementation. + * + * @param repositoryInformation + * @param composition + */ + private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) { + + if (repositoryInformation.hasCustomMethod()) { + + if (composition.isEmpty()) { + + throw new IllegalArgumentException( + String.format("You have custom methods in %s but not provided a custom implementation!", + repositoryInformation.getRepositoryInterface())); + } + + composition.validateImplementation(); + } + + validate(repositoryInformation); + } + + private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, + RepositoryComposition composition) { + + RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition); + + return repositoryInformationCache.computeIfAbsent(cacheKey, key -> { + + Class baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata)); + + return getRepositoryInformation(metadata, new DefaultRepositoryInformation(metadata, baseClass, composition)); + }); + } + + private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata, RepositoryComposition.RepositoryFragments fragments) { + + Assert.notNull(metadata, "RepositoryMetadata must not be null!"); + Assert.notNull(fragments, "RepositoryFragments must not be null!"); + + RepositoryComposition composition = getRepositoryComposition(metadata); + RepositoryComposition.RepositoryFragments repositoryAspects = getRepositoryFragments(metadata); + + return composition.append(fragments).append(repositoryAspects); + } + + /** + * Creates {@link RepositoryComposition} based on {@link RepositoryMetadata} for repository-specific method handling. + * + * @param metadata + * @return + */ + private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata) { + + RepositoryComposition composition = RepositoryComposition.empty(); + + if (metadata.isReactiveRepository()) { + return composition.withMethodLookup(MethodLookups.forReactiveTypes(metadata)) + .withArgumentConverter(REACTIVE_ARGS_CONVERTER); + } + + return composition.withMethodLookup(MethodLookups.forRepositoryTypes(metadata)); + } + + private static class RepositoryInformationCacheKey { + + String repositoryInterfaceName; + final long compositionHash; + + /** + * Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and composition. + * + * @param metadata must not be {@literal null}. + * @param composition must not be {@literal null}. + */ + public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryComposition composition) { + + this.repositoryInterfaceName = metadata.getRepositoryInterface().getName(); + this.compositionHash = composition.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RepositoryInformationCacheKey)) { + return false; + } + + RepositoryInformationCacheKey that = (RepositoryInformationCacheKey) o; + + if (compositionHash != that.compositionHash) { + return false; + } + return repositoryInterfaceName != null ? repositoryInterfaceName.equals(that.repositoryInterfaceName) : that.repositoryInterfaceName == null; + } + + @Override + public int hashCode() { + int result = repositoryInterfaceName != null ? repositoryInterfaceName.hashCode() : 0; + result = 31 * result + (int) (compositionHash ^ (compositionHash >>> 32)); + return result; + } + } + +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java new file mode 100644 index 0000000000..605ff63636 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; +import org.springframework.util.Assert; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.io.Serializable; + +/** + * @author Moritz Becker + * @since 1.2.0 + */ +public class BlazePersistenceRepositoryFactoryBean, S, ID extends Serializable> extends + TransactionalRepositoryFactoryBeanSupport { + + private EntityManager entityManager; + + @Autowired + private CriteriaBuilderFactory cbf; + + @Autowired + private EntityViewManager evm; + + /** + * Creates a new {@link BlazePersistenceRepositoryFactoryBean}. + */ + protected BlazePersistenceRepositoryFactoryBean() { + super(null); + } + + /** + * Creates a new {@link BlazePersistenceRepositoryFactoryBean} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + protected BlazePersistenceRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + public boolean isSingleton() { + return true; + } + + /** + * The {@link EntityManager} to be used. + * + * @param entityManager the entityManager to set + */ + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + if (this.entityManager == null) { + this.entityManager = entityManager; + } + } + + /* + * (non-Javadoc) + * @see com.blazebit.persistence.spring.data.impl.repository.BlazeRepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext) + */ + @Override + public void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support. + * BlazeTransactionalRepositoryFactoryBeanSupport#doCreateRepositoryFactory() + */ + @Override + protected BlazePersistenceRepositoryFactory doCreateRepositoryFactory() { + return createRepositoryFactory(entityManager); + } + + /** + * Returns a {@link RepositoryFactorySupport}. + * + * @param entityManager + * @return + */ + protected BlazePersistenceRepositoryFactory createRepositoryFactory(EntityManager entityManager) { + return new BlazePersistenceRepositoryFactory(entityManager, cbf, evm); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + Assert.notNull(entityManager, "EntityManager must not be null!"); + super.afterPropertiesSet(); + } + + public void setEscapeCharacter(char escapeCharacter) { + // Needed to work with Spring Boot 2.1.4 + } + + public char getEscapeCharacter() { + // Needed to work with Spring Boot 2.1.4 + return '\\'; + } + +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java new file mode 100644 index 0000000000..a9d9fcbaf7 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java @@ -0,0 +1,295 @@ +/* + * Copyright 2011-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springframework.data.repository.util.ClassUtils.isGenericRepositoryInterface; +import static org.springframework.util.ReflectionUtils.makeAccessible; + +/** + * Default implementation of {@link RepositoryInformation}. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Mark Paluch + * @author Christoph Strobl + */ +class DefaultRepositoryInformation implements RepositoryInformation { + + private final Map methodCache = new ConcurrentHashMap<>(); + + private final RepositoryMetadata metadata; + private final Class repositoryBaseClass; + private final RepositoryComposition composition; + private final RepositoryComposition baseComposition; + + /** + * Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class. + * + * @param metadata must not be {@literal null}. + * @param repositoryBaseClass must not be {@literal null}. + * @param composition must not be {@literal null}. + */ + public DefaultRepositoryInformation(RepositoryMetadata metadata, Class repositoryBaseClass, + RepositoryComposition composition) { + + Assert.notNull(metadata, "Repository metadata must not be null!"); + Assert.notNull(repositoryBaseClass, "Repository base class must not be null!"); + Assert.notNull(composition, "Repository composition must not be null!"); + + this.metadata = metadata; + this.repositoryBaseClass = repositoryBaseClass; + this.composition = composition; + this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) // + .withArgumentConverter(composition.getArgumentConverter()) // + .withMethodLookup(composition.getMethodLookup()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getDomainClass() + */ + @Override + public Class getDomainType() { + return metadata.getDomainType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getIdClass() + */ + @Override + public Class getIdType() { + return metadata.getIdType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getRepositoryBaseClass() + */ + @Override + public Class getRepositoryBaseClass() { + return this.repositoryBaseClass; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getTargetClassMethod(java.lang.reflect.Method) + */ + @Override + public Method getTargetClassMethod(Method method) { + + if (methodCache.containsKey(method)) { + return methodCache.get(method); + } + + Method result = composition.findMethod(method).orElse(method); + + if (!result.equals(method)) { + return cacheAndReturn(method, result); + } + + return cacheAndReturn(method, baseComposition.findMethod(method).orElse(method)); + } + + private Method cacheAndReturn(Method key, Method value) { + + if (value != null) { + makeAccessible(value); + } + + methodCache.put(key, value); + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getQueryMethods() + */ + @Override + public Streamable getQueryMethods() { + + Set result = new HashSet<>(); + + for (Method method : getRepositoryInterface().getMethods()) { + method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface()); + if (isQueryMethodCandidate(method)) { + result.add(method); + } + } + + return Streamable.of(Collections.unmodifiableSet(result)); + } + + /** + * Checks whether the given method is a query method candidate. + * + * @param method + * @return + */ + private boolean isQueryMethodCandidate(Method method) { + return !method.isBridge() && !method.isDefault() // + && !Modifier.isStatic(method.getModifiers()) // + && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); + } + + /** + * Checks whether the given method contains a custom store specific query annotation annotated with + * {@link QueryAnnotation}. The method-hierarchy is also considered in the search for the annotation. + * + * @param method + * @return + */ + private boolean isQueryAnnotationPresentOn(Method method) { + + return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#isCustomMethod(java.lang.reflect.Method) + */ + @Override + public boolean isCustomMethod(Method method) { + return composition.findMethod(method).isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryInformation#isQueryMethod(java.lang.reflect.Method) + */ + @Override + public boolean isQueryMethod(Method method) { + return getQueryMethods().stream().anyMatch(it -> it.equals(method)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryInformation#isBaseClassMethod(java.lang.reflect.Method) + */ + @Override + public boolean isBaseClassMethod(Method method) { + + Assert.notNull(method, "Method must not be null!"); + return baseComposition.findMethod(method).isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#hasCustomMethod() + */ + @Override + public boolean hasCustomMethod() { + + Class repositoryInterface = getRepositoryInterface(); + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(repositoryInterface)) { + return false; + } + + for (Method method : repositoryInterface.getMethods()) { + if (isCustomMethod(method) && !isBaseClassMethod(method)) { + return true; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getRepositoryInterface() + */ + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method) + */ + @Override + public Class getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(java.lang.reflect.Method) + */ + @Override + public TypeInformation getReturnType(Method method) { + return this.metadata.getReturnType(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getCrudMethods() + */ + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#isPagingRepository() + */ + @Override + public boolean isPagingRepository() { + return metadata.isPagingRepository(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getAlternativeDomainTypes() + */ + @Override + public Set> getAlternativeDomainTypes() { + return metadata.getAlternativeDomainTypes(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#isReactiveRepository() + */ + @Override + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java new file mode 100644 index 0000000000..1388411d2f --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spring.data.base.repository.AbstractEntityViewAwareRepository; +import com.blazebit.persistence.spring.data.repository.EntityViewRepository; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.jpa.repository.support.CrudMethodMetadata; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.TypedQuery; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * @author Christian Beikov + * @since 1.3.0 + */ +@Transactional(readOnly = true) +public class EntityViewAwareRepositoryImpl extends AbstractEntityViewAwareRepository implements JpaRepositoryImplementation, EntityViewRepository/*, EntityViewSpecificationExecutor*/ { // Can't implement that interface because of clashing methods + + public EntityViewAwareRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager, CriteriaBuilderFactory cbf, EntityViewManager evm, Class entityViewClass) { + super(entityInformation, entityManager, cbf, evm, (Class) entityViewClass); + } + + @Override + protected Map tryGetFetchGraphHints(EntityManager entityManager, JpaEntityGraph entityGraph, Class entityType) { + Map map = new HashMap<>(); + Jpa21Utils.getFetchGraphHint(entityManager, entityGraph, entityType).forEach( map::put ); + return map; + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findOne(Example example) { + try { + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findOne(Specification spec) { + try { + return Optional.of((E) getQuery(spec, (Sort) null).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findById(ID id) { + return Optional.ofNullable((E) findOne(id)); + } + + public E getById(ID id) { + return (E) getReference(id); + } + + private Object convert(Optional optional, Class targetType) { + return optional.orElse(null); + } + + @Override + public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { + // Ignore the Spring data version of the CrudMethodMetadata + } + + @Override + protected int getOffset(Pageable pageable) { + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } + + @Override + public long count(Example example) { + return super.count(example); + } + + @Override + public boolean exists(Example example) { + return super.exists(example); + } + + @Override + public R findBy(Example example, Function, R> queryFunction) { + Assert.notNull(example, "Sample must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + Function> finder = sort -> { + + ExampleSpecification spec = new ExampleSpecification<>(example, escapeCharacter); + Class probeType = example.getProbeType(); + + return getQuery(spec, probeType, sort); + }; + + FluentQuery.FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, finder, this::count, this::exists, getEntityManager(), this.escapeCharacter); + + return queryFunction.apply(fluentQuery); + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java new file mode 100644 index 0000000000..b75aa5f66f --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java @@ -0,0 +1,280 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import javax.persistence.EntityGraph; +import javax.persistence.EntityManager; +import javax.persistence.Subgraph; +import javax.persistence.TypedQuery; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.util.Assert; + +/** + * Immutable implementation of {@link FetchableFluentQuery} based on Query by {@link Example}. All methods that return a + * {@link FetchableFluentQuery} will return a new instance, not the original. + * + * Christian Beikov: Copied to be able to share code between Spring Data integrations for 2.6 and 2.7. + * + * @param Domain type + * @param Result type + * @author Greg Turnquist + * @author Mark Paluch + * @author Jens Schauder + * @author J.R. Onyschak + * @since 2.6 + */ +public class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { + + private final Example example; + private final Function> finder; + private final Function, Long> countOperation; + private final Function, Boolean> existsOperation; + private final EntityManager entityManager; + private final EscapeCharacter escapeCharacter; + + public FetchableFluentQueryByExample(Example example, Function> finder, + Function, Long> countOperation, Function, Boolean> existsOperation, + EntityManager entityManager, EscapeCharacter escapeCharacter) { + this(example, example.getProbeType(), (Class) example.getProbeType(), Sort.unsorted(), Collections.emptySet(), + finder, countOperation, existsOperation, entityManager, escapeCharacter); + } + + private FetchableFluentQueryByExample(Example example, Class entityType, Class returnType, Sort sort, + Collection properties, Function> finder, Function, Long> countOperation, + Function, Boolean> existsOperation, + EntityManager entityManager, EscapeCharacter escapeCharacter) { + + super(returnType, sort, properties, entityType); + this.example = example; + this.finder = finder; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + this.entityManager = entityManager; + this.escapeCharacter = escapeCharacter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) + */ + @Override + public FetchableFluentQuery sortBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return new FetchableFluentQueryByExample<>(example, entityType, resultType, this.sort.and(sort), properties, + finder, countOperation, existsOperation, entityManager, escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) + */ + @Override + public FetchableFluentQuery as(Class resultType) { + + Assert.notNull(resultType, "Projection target type must not be null!"); + if (!resultType.isInterface()) { + throw new UnsupportedOperationException("Class-based DTOs are not yet supported."); + } + + return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder, + countOperation, existsOperation, entityManager, escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) + */ + @Override + public FetchableFluentQuery project(Collection properties) { + + return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties), + finder, countOperation, existsOperation, entityManager, escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Override + public R oneValue() { + + TypedQuery limitedQuery = createSortedAndProjectedQuery(); + limitedQuery.setMaxResults(2); // Never need more than 2 values + + List results = limitedQuery.getResultList(); + + if (results.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + @Override + public R firstValue() { + + TypedQuery limitedQuery = createSortedAndProjectedQuery(); + limitedQuery.setMaxResults(1); // Never need more than 1 value + + List results = limitedQuery.getResultList(); + + return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + + List resultList = createSortedAndProjectedQuery().getResultList(); + + return convert(resultList); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + + return createSortedAndProjectedQuery() // + .getResultStream() // + .map(getConversionFunction()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return countOperation.apply(example); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return existsOperation.apply(example); + } + + private Page readPage(Pageable pageable) { + + TypedQuery pagedQuery = createSortedAndProjectedQuery(); + + if (pageable.isPaged()) { + pagedQuery.setFirstResult((int) pageable.getOffset()); + pagedQuery.setMaxResults(pageable.getPageSize()); + } + + List paginatedResults = convert(pagedQuery.getResultList()); + + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example)); + } + + private TypedQuery createSortedAndProjectedQuery() { + + TypedQuery query = finder.apply(sort); + + if (!properties.isEmpty()) { + query.setHint("javax.persistence.fetchgraph", create(entityManager, entityType, properties)); + } + + return query; + } + + private static EntityGraph create(EntityManager entityManager, Class domainType, Set properties) { + + EntityGraph entityGraph = entityManager.createEntityGraph(domainType); + + for (String property : properties) { + + Subgraph current = null; + + for (PropertyPath path : PropertyPath.from(property, domainType)) { + + if (path.hasNext()) { + current = current == null ? entityGraph.addSubgraph(path.getSegment()) + : current.addSubgraph(path.getSegment()); + continue; + } + + if (current == null) { + entityGraph.addAttributeNodes(path.getSegment()); + } else { + current.addAttributeNodes(path.getSegment()); + + } + } + } + + return entityGraph; + } + + private List convert(List resultList) { + + Function conversionFunction = getConversionFunction(); + List mapped = new ArrayList<>(resultList.size()); + + for (S s : resultList) { + mapped.add(conversionFunction.apply(s)); + } + return mapped; + } + + private Function getConversionFunction() { + return getConversionFunction(example.getProbeType(), resultType); + } + +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java new file mode 100644 index 0000000000..3dfd21d820 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; + +/** + * Supporting class containing some state and convenience methods for building and executing fluent queries. + * + * Christian Beikov: Copied to be able to share code between Spring Data integrations for 2.6 and 2.7. + * + * @param The resulting type of the query. + * @author Greg Turnquist + * @author Jens Schauder + * @since 2.6 + */ +public abstract class FluentQuerySupport { + + protected final Class resultType; + protected final Sort sort; + protected final Set properties; + protected final Class entityType; + + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + + public FluentQuerySupport(Class resultType, Sort sort, Collection properties, Class entityType) { + + this.resultType = resultType; + this.sort = sort; + + if (properties != null) { + this.properties = new HashSet<>(properties); + } else { + this.properties = Collections.emptySet(); + } + + this.entityType = entityType; + } + + final Collection mergeProperties(Collection additionalProperties) { + + Set newProperties = new HashSet<>(); + newProperties.addAll(properties); + newProperties.addAll(additionalProperties); + return Collections.unmodifiableCollection(newProperties); + } + + @SuppressWarnings("unchecked") + final Function getConversionFunction(Class inputType, Class targetType) { + + if (targetType.isAssignableFrom(inputType)) { + return (Function) Function.identity(); + } + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + return o -> DefaultConversionService.getSharedInstance().convert(o, targetType); + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java new file mode 100644 index 0000000000..0955aec369 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java @@ -0,0 +1,443 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodLookup; +import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.util.Assert; + +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.springframework.core.GenericTypeResolver.resolveParameterType; + +/** + * Implementations of method lookup functions. + * + * @author Mark Paluch + * @author Oliver Gierke + * @since 2.0 + */ +interface MethodLookups { + + /** + * Direct method lookup filtering on exact method name, parameter count and parameter types. + * + * @return direct method lookup. + */ + public static MethodLookup direct() { + + MethodPredicate direct = (invoked, candidate) -> candidate.getName().equals(invoked.getName()) + && candidate.getParameterCount() == invoked.getParameterCount() + && Arrays.equals(candidate.getParameterTypes(), invoked.getParameterTypes()) + && candidate.getReturnType().equals(invoked.getMethod().getReturnType()); + + return () -> Collections.singletonList(direct); + } + + /** + * Repository type-aware method lookup composed of {@link #direct()} and {@link RepositoryAwareMethodLookup}. + *

+ * Repository-aware lookups resolve generic types from the repository declaration to verify assignability to Id/domain + * types. This lookup also permits assignable method signatures but prefers {@link #direct()} matches. + * + * @param repositoryMetadata must not be {@literal null}. + * @return the composed, repository-aware method lookup. + * @see #direct() + */ + public static MethodLookup forRepositoryTypes(RepositoryMetadata repositoryMetadata) { + return direct().and(new RepositoryAwareMethodLookup(repositoryMetadata)); + } + + /** + * Repository type-aware method lookup composed of {@link #direct()} and {@link ReactiveTypeInteropMethodLookup}. + *

+ * This method lookup considers adaptability of reactive types in method signatures. Repository methods accepting a + * reactive type can be possibly called with a different reactive type if the reactive type can be adopted to the + * target type. This lookup also permits assignable method signatures and resolves repository id/entity types but + * prefers {@link #direct()} matches. + * + * @param repositoryMetadata must not be {@literal null}. + * @return the composed, repository-aware method lookup. + * @see #direct() + * @see #forRepositoryTypes(RepositoryMetadata) + */ + public static MethodLookup forReactiveTypes(RepositoryMetadata repositoryMetadata) { + return direct().and(new ReactiveTypeInteropMethodLookup(repositoryMetadata)); + } + + /** + * Default {@link MethodLookup} considering repository Id and entity types permitting calls to methods with assignable + * arguments. + * + * @author Mark Paluch + */ + static class RepositoryAwareMethodLookup implements MethodLookup { + + @SuppressWarnings("rawtypes") private static final TypeVariable>[] PARAMETERS = Repository.class + .getTypeParameters(); + private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); + private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); + + private final ResolvableType entityType, idType; + private final Class repositoryInterface; + + /** + * Creates a new {@link RepositoryAwareMethodLookup} for the given {@link RepositoryMetadata}. + * + * @param repositoryMetadata must not be {@literal null}. + */ + public RepositoryAwareMethodLookup(RepositoryMetadata repositoryMetadata) { + + Assert.notNull(repositoryMetadata, "Repository metadata must not be null!"); + + this.entityType = ResolvableType.forClass(repositoryMetadata.getDomainType()); + this.idType = ResolvableType.forClass(repositoryMetadata.getIdType()); + this.repositoryInterface = repositoryMetadata.getRepositoryInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.MethodLookup#getLookups() + */ + @Override + public List getLookups() { + + MethodPredicate detailedComparison = (invoked, candidate) -> Optional.of(candidate) + .filter(baseClassMethod -> baseClassMethod.getName().equals(invoked.getName()))// Right name + .filter(baseClassMethod -> baseClassMethod.getParameterCount() == invoked.getParameterCount()) + .filter(baseClassMethod -> parametersMatch(invoked.getMethod(), baseClassMethod))// All parameters match + .isPresent(); + + return Collections.singletonList(detailedComparison); + } + + /** + * Checks whether the given parameter type matches the generic type of the given parameter. Thus when {@literal PK} + * is declared, the method ensures that given method parameter is the primary key type declared in the given + * repository interface e.g. + * + * @param variable must not be {@literal null}. + * @param parameterType must not be {@literal null}. + * @return + */ + protected boolean matchesGenericType(TypeVariable variable, ResolvableType parameterType) { + + GenericDeclaration declaration = variable.getGenericDeclaration(); + + if (declaration instanceof Class) { + + if (ID_TYPE_NAME.equals(variable.getName()) && parameterType.isAssignableFrom(idType)) { + return true; + } + + Type boundType = variable.getBounds()[0]; + String referenceName = boundType instanceof TypeVariable ? boundType.toString() : variable.toString(); + + return DOMAIN_TYPE_NAME.equals(referenceName) && parameterType.isAssignableFrom(entityType); + } + + for (Type type : variable.getBounds()) { + if (ResolvableType.forType(type).isAssignableFrom(parameterType)) { + return true; + } + } + + return false; + } + + /** + * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments + * against the ones bound in the given repository interface. + * + * @param invokedMethod + * @param candidate + * @return + */ + private boolean parametersMatch(Method invokedMethod, Method candidate) { + + Class[] methodParameterTypes = invokedMethod.getParameterTypes(); + Type[] genericTypes = candidate.getGenericParameterTypes(); + Class[] types = candidate.getParameterTypes(); + + for (int i = 0; i < genericTypes.length; i++) { + + Type genericType = genericTypes[i]; + Class type = types[i]; + MethodParameter parameter = new MethodParameter(invokedMethod, i); + Class parameterType = resolveParameterType(parameter, repositoryInterface); + + if (genericType instanceof TypeVariable) { + + if (!matchesGenericType((TypeVariable) genericType, ResolvableType.forMethodParameter(parameter))) { + return false; + } + + continue; + } + + if (types[i].equals(parameterType)) { + continue; + } + + if (!type.isAssignableFrom(parameterType) || !type.equals(methodParameterTypes[i])) { + return false; + } + } + + return true; + } + } + + /** + * Extension to {@link RepositoryAwareMethodLookup} considering reactive type adoption and entity types permitting + * calls to methods with assignable arguments. + * + * @author Mark Paluch + */ + static class ReactiveTypeInteropMethodLookup extends RepositoryAwareMethodLookup { + + private final RepositoryMetadata repositoryMetadata; + + public ReactiveTypeInteropMethodLookup(RepositoryMetadata repositoryMetadata) { + + super(repositoryMetadata); + this.repositoryMetadata = repositoryMetadata; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.MethodLookups.RepositoryAwareMethodLookup#getLookups() + */ + @Override + public List getLookups() { + + MethodPredicate convertibleComparison = (invokedMethod, candidate) -> { + + List>> suppliers = new ArrayList<>(); + + if (usesParametersWithReactiveWrappers(invokedMethod.getMethod())) { + suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, assignableWrapperMatch())); // + suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, wrapperConversionMatch())); + } + + return suppliers.stream().anyMatch(supplier -> supplier.get().isPresent()); + }; + + MethodPredicate detailedComparison = (invokedMethod, candidate) -> getMethodCandidate(invokedMethod, candidate, + matchParameterOrComponentType(repositoryMetadata.getRepositoryInterface())).isPresent(); + + return Arrays.asList(convertibleComparison, detailedComparison); + } + + /** + * {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be + * wrapped but supports unwrapping. Usually types like {@link Optional} or {@link Stream}. + * + * @param repositoryInterface + * @return + * @see QueryExecutionConverters + * @see #matchesGenericType + */ + private Predicate matchParameterOrComponentType(Class repositoryInterface) { + + return (parameterCriteria) -> { + + Class parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface); + Type genericType = parameterCriteria.getGenericBaseType(); + + if (genericType instanceof TypeVariable) { + + if (!matchesGenericType((TypeVariable) genericType, + ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) { + return false; + } + } + + return parameterCriteria.getBaseType().isAssignableFrom(parameterType) + && parameterCriteria.isAssignableFromDeclared(); + }; + } + + /** + * Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped. + * + * @param parameterType must not be {@literal null}. + * @return + */ + private static boolean isNonUnwrappingWrapper(Class parameterType) { + + Assert.notNull(parameterType, "Parameter type must not be null!"); + + return QueryExecutionConverters.supports(parameterType) + && !QueryExecutionConverters.supportsUnwrapping(parameterType); + } + + /** + * Returns whether the given {@link Method} uses a reactive wrapper type as parameter. + * + * @param method must not be {@literal null}. + * @return + */ + private static boolean usesParametersWithReactiveWrappers(Method method) { + + Assert.notNull(method, "Method must not be null!"); + + return Arrays.stream(method.getParameterTypes())// + .anyMatch(ReactiveTypeInteropMethodLookup::isNonUnwrappingWrapper); + } + + /** + * Returns a candidate method from the base class for the given one or the method given in the first place if none + * one the base class matches. + * + * @param method must not be {@literal null}. + * @param baseClass must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @return + */ + private static Optional getMethodCandidate(InvokedMethod invokedMethod, Method candidate, + Predicate predicate) { + + return Optional.of(candidate)// + .filter(it -> invokedMethod.getName().equals(it.getName()))// + .filter(it -> invokedMethod.getParameterCount() == it.getParameterCount())// + .filter(it -> parametersMatch(it, invokedMethod.getMethod(), predicate)); + } + + /** + * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments + * against the ones bound in the given repository interface. + * + * @param baseClassMethod must not be {@literal null}. + * @param declaredMethod must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @return + */ + private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod, + Predicate predicate) { + + return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate); + } + + /** + * {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be + * converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher} + * conversion. + * + * @return + */ + private static Predicate wrapperConversionMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType()); + } + + /** + * {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and + * a declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher} + * conversion. + * + * @return + */ + private static Predicate assignableWrapperMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType()); + } + + private static Stream methodParameters(Method first, Method second) { + + Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!"); + + return IntStream.range(0, first.getParameterCount()) // + .mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index), + new MethodParameter(second, index))); + } + + /** + * Criterion to represent {@link MethodParameter}s from a base method and its declared (overridden) method. Method + * parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same + * parameter count. + */ + static class ParameterOverrideCriteria { + + private final MethodParameter base; + private final MethodParameter declared; + + private ParameterOverrideCriteria(MethodParameter base, MethodParameter declared) { + this.base = base; + this.declared = declared; + } + + public static ParameterOverrideCriteria of(MethodParameter base, MethodParameter declared) { + return new ParameterOverrideCriteria(base, declared); + } + + public MethodParameter getBase() { + return base; + } + + public MethodParameter getDeclared() { + return declared; + } + + /** + * @return base method parameter type. + */ + public Class getBaseType() { + return base.getParameterType(); + } + + /** + * @return generic base method parameter type. + */ + public Type getGenericBaseType() { + return base.getGenericParameterType(); + } + + /** + * @return declared method parameter type. + */ + public Class getDeclaredType() { + return declared.getParameterType(); + } + + public boolean isAssignableFromDeclared() { + return getBaseType().isAssignableFrom(getDeclaredType()); + } + } + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java new file mode 100644 index 0000000000..65572a02f1 --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + + +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.5.0 + */ +public class QueryCollectingQueryCreationListener implements QueryCreationListener { + + private final List queryMethods = new ArrayList<>(); + + @Override + public void onCreation(RepositoryQuery query) { + this.queryMethods.add(query.getQueryMethod()); + } + + public List getQueryMethods() { + return queryMethods; + } +} diff --git a/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java new file mode 100644 index 0000000000..ec5cda6fae --- /dev/null +++ b/integration/spring-data/2.6/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java @@ -0,0 +1,294 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.repository.util.NullableWrapper; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Simple domain service to convert query results into a dedicated type. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Jens Schauder + */ +class QueryExecutionResultHandler { + + private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class); + + private final GenericConversionService conversionService; + + private final Object mutex = new Object(); + + // concurrent access guarded by mutex. + private Map descriptorCache = Collections.emptyMap(); + + /** + * Creates a new {@link QueryExecutionResultHandler}. + */ + public QueryExecutionResultHandler(GenericConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Post-processes the given result of a query invocation to match the return type of the given method. + * + * @param result can be {@literal null}. + * @param method must not be {@literal null}. + * @return + */ + @Nullable + public Object postProcessInvocationResult(@Nullable Object result, Method method) { + + if (!processingRequired(result, method.getReturnType())) { + return result; + } + + ReturnTypeDescriptor descriptor = getOrCreateReturnTypeDescriptor(method); + + return postProcessInvocationResult(result, 0, descriptor); + } + + private ReturnTypeDescriptor getOrCreateReturnTypeDescriptor(Method method) { + + Map descriptorCache = this.descriptorCache; + ReturnTypeDescriptor descriptor = descriptorCache.get(method); + + if (descriptor == null) { + + descriptor = ReturnTypeDescriptor.of(method); + + Map updatedDescriptorCache; + + if (descriptorCache.isEmpty()) { + updatedDescriptorCache = Collections.singletonMap(method, descriptor); + } else { + updatedDescriptorCache = new HashMap<>(descriptorCache.size() + 1, 1); + updatedDescriptorCache.putAll(descriptorCache); + updatedDescriptorCache.put(method, descriptor); + + } + + synchronized (mutex) { + this.descriptorCache = updatedDescriptorCache; + } + } + + return descriptor; + } + + /** + * Post-processes the given result of a query invocation to the given type. + * + * @param result can be {@literal null}. + * @param nestingLevel + * @param descriptor must not be {@literal null}. + * @return + */ + @Nullable + Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, ReturnTypeDescriptor descriptor) { + + TypeDescriptor returnTypeDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel); + + if (returnTypeDescriptor == null) { + return result; + } + + Class expectedReturnType = returnTypeDescriptor.getType(); + + result = unwrapOptional(result); + + if (QueryExecutionConverters.supports(expectedReturnType)) { + + // For a wrapper type, try nested resolution first + result = postProcessInvocationResult(result, nestingLevel + 1, descriptor); + + if (conversionRequired(WRAPPER_TYPE, returnTypeDescriptor)) { + return conversionService.convert(new NullableWrapper(result), returnTypeDescriptor); + } + + if (result != null) { + + TypeDescriptor source = TypeDescriptor.valueOf(result.getClass()); + + if (conversionRequired(source, returnTypeDescriptor)) { + return conversionService.convert(result, returnTypeDescriptor); + } + } + } + + if (result != null) { + + if (ReactiveWrapperConverters.supports(expectedReturnType)) { + return ReactiveWrapperConverters.toWrapper(result, expectedReturnType); + } + + if (result instanceof Collection) { + + TypeDescriptor elementDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel + 1); + boolean requiresConversion = requiresConversion((Collection) result, expectedReturnType, elementDescriptor); + + if (!requiresConversion) { + return result; + } + } + + TypeDescriptor resultDescriptor = TypeDescriptor.forObject(result); + return conversionService.canConvert(resultDescriptor, returnTypeDescriptor) + ? conversionService.convert(result, returnTypeDescriptor) + : result; + } + + return Map.class.equals(expectedReturnType) // + ? CollectionFactory.createMap(expectedReturnType, 0) // + : null; + + } + private boolean requiresConversion(Collection collection, Class expectedReturnType, + @Nullable TypeDescriptor elementDescriptor) { + + if (Streamable.class.isAssignableFrom(expectedReturnType) || !expectedReturnType.isInstance(collection)) { + return true; + } + + if (elementDescriptor == null || !Iterable.class.isAssignableFrom(expectedReturnType)) { + return false; + } + + Class type = elementDescriptor.getType(); + + for (Object o : collection) { + + if (!type.isInstance(o)) { + return true; + } + } + + return false; + } + + /** + * Returns whether the configured {@link ConversionService} can convert between the given {@link TypeDescriptor}s and + * the conversion will not be a no-op. + * + * @param source + * @param target + * @return + */ + private boolean conversionRequired(TypeDescriptor source, TypeDescriptor target) { + + return conversionService.canConvert(source, target) // + && !conversionService.canBypassConvert(source, target); + } + + /** + * Unwraps the given value if it's a JDK 8 {@link Optional}. + * + * @param source can be {@literal null}. + * @return + */ + @Nullable + @SuppressWarnings("unchecked") + private static Object unwrapOptional(@Nullable Object source) { + + if (source == null) { + return null; + } + + return Optional.class.isInstance(source) // + ? Optional.class.cast(source).orElse(null) // + : source; + } + + /** + * Returns whether we have to process the given source object in the first place. + * + * @param source can be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + private static boolean processingRequired(@Nullable Object source, Class targetType) { + + return !targetType.isInstance(source) // + || source == null // + || Collection.class.isInstance(source); + } + + /** + * Value object capturing {@link MethodParameter} and {@link TypeDescriptor}s for top and nested levels. + */ + static class ReturnTypeDescriptor { + + private final MethodParameter methodParameter; + private final TypeDescriptor typeDescriptor; + private final @Nullable TypeDescriptor nestedTypeDescriptor; + + private ReturnTypeDescriptor(Method method) { + this.methodParameter = new MethodParameter(method, -1); + this.typeDescriptor = TypeDescriptor.nested(this.methodParameter, 0); + this.nestedTypeDescriptor = TypeDescriptor.nested(this.methodParameter, 1); + } + + /** + * Create a {@link ReturnTypeDescriptor} from a {@link Method}. + * + * @param method + * @return + */ + public static ReturnTypeDescriptor of(Method method) { + return new ReturnTypeDescriptor(method); + } + + /** + * Return the {@link TypeDescriptor} for a nested type declared within the method parameter described by + * {@code nestingLevel} . + * + * @param nestingLevel the nesting level. {@code 0} is the first level, {@code 1} the next inner one. + * @return the {@link TypeDescriptor} or {@literal null} if it could not be obtained. + * @see TypeDescriptor#nested(MethodParameter, int) + */ + @Nullable + public TypeDescriptor getReturnTypeDescriptor(int nestingLevel) { + + // optimizing for nesting level 0 and 1 (Optional, List) + // nesting level 2 (Optional>) uses the slow path. + + switch (nestingLevel) { + case 0: + return typeDescriptor; + case 1: + return nestedTypeDescriptor; + default: + return TypeDescriptor.nested(this.methodParameter, nestingLevel); + } + } + } +} diff --git a/integration/spring-data/2.7/pom.xml b/integration/spring-data/2.7/pom.xml new file mode 100644 index 0000000000..5363c03361 --- /dev/null +++ b/integration/spring-data/2.7/pom.xml @@ -0,0 +1,263 @@ + + + + blaze-persistence-integration-spring-data-parent + com.blazebit + 1.6.7-SNAPSHOT + ../pom.xml + + 4.0.0 + + Blazebit Persistence Integration Spring-Data 2.7 + blaze-persistence-integration-spring-data-2.7 + + + com.blazebit.persistence.integration.spring.data.impl + + + ${version.spring-data-2.7-spring} + 1.2.1 + 1.3.6 + 1.8 + 1.8 + + + + + + org.springframework + spring-framework-bom + ${version.spring} + import + pom + + + org.jetbrains.kotlinx + kotlinx-coroutines-bom + ${kotlin-coroutines} + pom + import + + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + provided + + + + io.reactivex + rxjava-reactive-streams + ${rxjava-reactive-streams} + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + true + + + + com.infradna.tool + bridge-method-annotation + ${version.bridge-injector} + provided + + + org.jenkins-ci + annotation-indexer + + + + + + org.jenkins-ci + annotation-indexer + ${version.bridge-injector-indexer} + provided + + + + com.blazebit + blaze-persistence-entity-view-api + + + com.blazebit + blaze-persistence-jpa-criteria-api + + + com.blazebit + blaze-persistence-jpa-criteria-impl + + + jakarta.persistence + jakarta.persistence-api + provided + + + + + com.blazebit + blaze-persistence-integration-spring-data-base + compile + + + com.blazebit + blaze-persistence-integration-entity-view-spring + compile + + + com.blazebit + blaze-persistence-core-impl + runtime + + + com.blazebit + blaze-persistence-entity-view-impl + runtime + + + + + + org.springframework + spring-test + test + + + junit + junit + test + + + + + + + org.moditect + moditect-maven-plugin + + + add-module-infos + package + + add-module-info + + + + + module ${module.name} { + requires transitive com.blazebit.persistence.integration.spring.data; + exports com.blazebit.persistence.spring.data.impl.query; + exports com.blazebit.persistence.spring.data.impl.repository; + } + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-resource + generate-resources + + add-resource + + + + + target/generated/resources + + + + + + + + org.bsc.maven + maven-processor-plugin + + + process + + process + + generate-sources + + + target/generated/resources + + org.jvnet.hudson.annotation_indexer.AnnotationProcessorImpl + + + + + + + com.infradna.tool + bridge-method-annotation + ${version.bridge-injector} + + + + org.jenkins-ci + annotation-indexer + ${version.bridge-injector-indexer} + + + + + com.infradna.tool + bridge-method-injector + ${version.bridge-injector} + + + + process + + + + + + + org.ow2.asm + asm-debug-all + CUSTOM + + + org.ow2.asm + asm + ${version.bridge-injector-asm} + + + org.ow2.asm + asm-commons + ${version.bridge-injector-asm} + + + + + + + \ No newline at end of file diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java new file mode 100644 index 0000000000..356792688f --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/CriteriaQueryParameterBinder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.AbstractCriteriaQueryParameterBinder; +import com.blazebit.persistence.spring.data.base.query.JpaParameters; +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; + +/** + * Concrete version for Spring Data 2.x. + * + * @author Christian Beikov + * @since 1.3.0 + */ +public class CriteriaQueryParameterBinder extends AbstractCriteriaQueryParameterBinder { + + public CriteriaQueryParameterBinder(EntityManager em, EntityViewManager evm, JpaParameters parameters, Object[] values, Iterable> expressions) { + super(em, evm, parameters, values, expressions); + } + + @Override + protected int getOffset() { + Pageable pageable = getPageable(); + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java new file mode 100644 index 0000000000..716167dd96 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryInformation.java @@ -0,0 +1,147 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityViewAwareRepositoryInformation implements RepositoryInformation, EntityViewAwareRepositoryMetadata { + + private final EntityViewAwareRepositoryMetadata metadata; + private final RepositoryInformation repositoryInformation; + + public EntityViewAwareRepositoryInformation(EntityViewAwareRepositoryMetadata metadata, RepositoryInformation repositoryInformation) { + this.metadata = metadata; + this.repositoryInformation = repositoryInformation; + } + + @Override + public EntityViewManager getEntityViewManager() { + return metadata.getEntityViewManager(); + } + + @Override + public Class getRepositoryBaseClass() { + return repositoryInformation.getRepositoryBaseClass(); + } + + @Override + public boolean hasCustomMethod() { + return repositoryInformation.hasCustomMethod(); + } + + @Override + public boolean isCustomMethod(Method method) { + return repositoryInformation.isCustomMethod(method); + } + + @Override + public boolean isQueryMethod(Method method) { + return repositoryInformation.isQueryMethod(method); + } + + @Override + public boolean isBaseClassMethod(Method method) { + return repositoryInformation.isBaseClassMethod(method); + } + + @Override + public Streamable getQueryMethods() { + return repositoryInformation.getQueryMethods(); + } + + @Override + public Method getTargetClassMethod(Method method) { + return repositoryInformation.getTargetClassMethod(method); + } + + @Override + public Class getIdType() { + return (Class) repositoryInformation.getIdType(); + } + + @Override + public Class getDomainType() { + return repositoryInformation.getDomainType(); + } + + @Override + public Class getEntityViewType() { + return metadata.getEntityViewType(); + } + + @Override + public Class getRepositoryInterface() { + return repositoryInformation.getRepositoryInterface(); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return repositoryInformation.getReturnedDomainClass(method); + } + + @Override + public Class getReturnedEntityViewClass(Method method) { + return metadata.getReturnedEntityViewClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return repositoryInformation.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return repositoryInformation.isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return repositoryInformation.getAlternativeDomainTypes(); + } + + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return repositoryInformation.getReturnType(method); + } + + @Override + public TypeInformation getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java new file mode 100644 index 0000000000..1109893ae9 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/EntityViewAwareRepositoryMetadataImpl.java @@ -0,0 +1,135 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.metamodel.ManagedViewType; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.util.TypeInformation; + +import java.lang.reflect.Method; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityViewAwareRepositoryMetadataImpl implements EntityViewAwareRepositoryMetadata { + + private final RepositoryMetadata metadata; + private final EntityViewManager evm; + private final Class domainType; + private final Class entityViewType; + + public EntityViewAwareRepositoryMetadataImpl(RepositoryMetadata metadata, EntityViewManager evm) { + this.metadata = metadata; + this.evm = evm; + Class domainType = metadata.getDomainType(); + ManagedViewType managedViewType = evm.getMetamodel().managedView(domainType); + if (managedViewType == null) { + this.domainType = domainType; + this.entityViewType = null; + } else { + this.domainType = managedViewType.getEntityClass(); + this.entityViewType = managedViewType.getJavaType(); + } + } + + @Override + public EntityViewManager getEntityViewManager() { + return evm; + } + + @Override + public Class getIdType() { + return metadata.getIdType(); + } + + @Override + public Class getDomainType() { + return domainType; + } + + @Override + public Class getEntityViewType() { + return entityViewType; + } + + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public Class getReturnedDomainClass(Method method) { + Class returnedDomainClass = metadata.getReturnedDomainClass(method); + ManagedViewType managedViewType = evm.getMetamodel().managedView(returnedDomainClass); + if (managedViewType == null) { + return returnedDomainClass; + } else { + return managedViewType.getEntityClass(); + } + } + + @Override + public TypeInformation getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class getReturnedEntityViewClass(Method method) { + Class returnedDomainClass = metadata.getReturnedDomainClass(method); + ManagedViewType managedViewType = evm.getMetamodel().managedView(returnedDomainClass); + if (managedViewType == null) { + return null; + } else { + return managedViewType.getJavaType(); + } + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return metadata.isPagingRepository(); + } + + @Override + public Set> getAlternativeDomainTypes() { + return metadata.getAlternativeDomainTypes(); + } + + @Override + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } + + @Override + public TypeInformation getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java new file mode 100644 index 0000000000..c6f59f065d --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java @@ -0,0 +1,309 @@ +/* + * Copyright 2011-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.expression.Expression; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.ParameterExpression; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Helper class to allow easy creation of {@link ParameterMetadata}s. + * + * Christian Beikov: Copied while implementing the shared interface to be able to share code between Spring Data integrations for 1.x and 2.x. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Mark Paluch + * @author Christoph Strobl + * @author Jens Schauder + * @author Andrey Kovalev + */ +class ParameterMetadataProviderImpl implements ParameterMetadataProvider { + + private final CriteriaBuilder builder; + private final Iterator parameters; + private final List> expressions; + private final Iterator bindableParameterValues; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} and + * {@link ParametersParameterAccessor} with support for parameter value customizations via {@link PersistenceProvider} + * . + * + * @param builder must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + public ParameterMetadataProviderImpl(CriteriaBuilder builder, ParametersParameterAccessor accessor, + PersistenceProvider provider, EscapeCharacter escape) { + this(builder, accessor.iterator(), accessor.getParameters(), provider, escape); + } + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} and {@link Parameters} with + * support for parameter value customizations via {@link PersistenceProvider}. + * + * @param builder must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + public ParameterMetadataProviderImpl(CriteriaBuilder builder, Parameters parameters, PersistenceProvider provider, + EscapeCharacter escape) { + this(builder, null, parameters, provider, escape); + } + + /** + * Creates a new {@link ParameterMetadataProviderImpl} from the given {@link CriteriaBuilder} an {@link Iterable} of all + * bindable parameter values, and {@link Parameters} with support for parameter value customizations via + * {@link PersistenceProvider}. + * + * @param builder must not be {@literal null}. + * @param bindableParameterValues may be {@literal null}. + * @param parameters must not be {@literal null}. + * @param provider must not be {@literal null}. + * @param escape + */ + private ParameterMetadataProviderImpl(CriteriaBuilder builder, Iterator bindableParameterValues, + Parameters parameters, PersistenceProvider provider, EscapeCharacter escape) { + + Assert.notNull(builder, "CriteriaBuilder must not be null!"); + Assert.notNull(parameters, "Parameters must not be null!"); + Assert.notNull(provider, "PesistenceProvider must not be null!"); + + this.builder = builder; + this.parameters = parameters.getBindableParameters().iterator(); + this.expressions = new ArrayList<>(); + this.bindableParameterValues = bindableParameterValues; + this.persistenceProvider = provider; + this.escape = escape; + } + + /** + * Returns all {@link ParameterMetadata}s built. + * + * @return the expressions + */ + public List> getExpressions() { + return expressions; + } + + /** + * Builds a new {@link ParameterMetadata} for given {@link Part} and the next {@link Parameter}. + */ + @SuppressWarnings("unchecked") + public ParameterMetadata next(Part part) { + if (!parameters.hasNext()) { + throw new IllegalArgumentException(String.format("No parameter available for part %s.", part)); + } + + Parameter parameter = parameters.next(); + return (ParameterMetadata) next(part, parameter.getType(), parameter); + } + + /** + * Builds a new {@link ParameterMetadata} of the given {@link Part} and type. Forwards the underlying + * {@link Parameters} as well. + * + * @param is the type parameter of the returend {@link ParameterMetadata}. + * @param type must not be {@literal null}. + * @return ParameterMetadata for the next parameter. + */ + @SuppressWarnings("unchecked") + public ParameterMetadata next(Part part, Class type) { + + Parameter parameter = parameters.next(); + Class typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type; + return (ParameterMetadata) next(part, typeToUse, parameter); + } + + /** + * Builds a new {@link ParameterMetadata} for the given type and name. + * + * @param type parameter for the returned {@link ParameterMetadata}. + * @param part must not be {@literal null}. + * @param type must not be {@literal null}. + * @param parameter providing the name for the returned {@link ParameterMetadata}. + * @return a new {@link ParameterMetadata} for the given type and name. + */ + private ParameterMetadata next(Part part, Class type, Parameter parameter) { + + Assert.notNull(type, "Type must not be null!"); + + /* + * We treat Expression types as Object vales since the real value to be bound as a parameter is determined at query time. + */ + @SuppressWarnings("unchecked") + Class reifiedType = Expression.class.equals(type) ? (Class) Object.class : type; + + Supplier name = () -> parameter.getName() + .orElseThrow(() -> new IllegalArgumentException("o_O Parameter needs to be named")); + + ParameterExpression expression = parameter.isExplicitlyNamed() // + ? builder.parameter(reifiedType, name.get()) // + : builder.parameter(reifiedType); + + Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next(); + + ParameterMetadata metadata = new ParameterMetadataImpl<>(expression, part, value, persistenceProvider, escape); + expressions.add(metadata); + + return metadata; + } + + EscapeCharacter getEscape() { + return escape; + } + + /** + * @author Oliver Gierke + * @author Thomas Darimont + * @author Andrey Kovalev + * @param + */ + static class ParameterMetadataImpl implements ParameterMetadata { + + private final Type type; + private final ParameterExpression expression; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; + private final boolean ignoreCase; + + /** + * Creates a new {@link ParameterMetadata}. + */ + public ParameterMetadataImpl(ParameterExpression expression, Part part, Object value, + PersistenceProvider provider, EscapeCharacter escape) { + + this.expression = expression; + this.persistenceProvider = provider; + this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType(); + this.ignoreCase = IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); + this.escape = escape; + } + + /** + * Returns the {@link ParameterExpression}. + * + * @return the expression + */ + public ParameterExpression getExpression() { + return expression; + } + + /** + * Returns whether the parameter shall be considered an {@literal IS NULL} parameter. + */ + public boolean isIsNullParameter() { + return Type.IS_NULL.equals(type); + } + + /** + * Prepares the object before it's actually bound to the {@link javax.persistence.Query;}. + * + * @param value must not be {@literal null}. + */ + public Object prepare(Object value) { + + Assert.notNull(value, "Value must not be null!"); + + Class expressionType = expression.getJavaType(); + + if (String.class.equals(expressionType)) { + + switch (type) { + case STARTING_WITH: + return String.format("%s%%", escape.escape(value.toString())); + case ENDING_WITH: + return String.format("%%%s", escape.escape(value.toString())); + case CONTAINING: + case NOT_CONTAINING: + return String.format("%%%s%%", escape.escape(value.toString())); + default: + return value; + } + } + + return Collection.class.isAssignableFrom(expressionType) // + ? upperIfIgnoreCase(ignoreCase, toCollection(value)) // + : value; + } + + /** + * Returns the given argument as {@link Collection} which means it will return it as is if it's a + * {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element + * {@link Collections}. + * + * @param value the value to be converted to a {@link Collection}. + * @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value. + */ + private static Collection toCollection(Object value) { + + if (value == null) { + return null; + } + + if (value instanceof Collection) { + return (Collection) value; + } + + if (ObjectUtils.isArray(value)) { + return Arrays.asList(ObjectUtils.toObjectArray(value)); + } + + return Collections.singleton(value); + } + + @SuppressWarnings("unchecked") + private static Collection upperIfIgnoreCase(boolean ignoreCase, Collection collection) { + + if (!ignoreCase || CollectionUtils.isEmpty(collection)) { + return collection; + } + + return ((Collection) collection).stream() // + .map(it -> it == null // + ? null // + : it.toUpperCase()) // + .collect(Collectors.toList()); + } + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java new file mode 100644 index 0000000000..eb31c4cda6 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/query/PartTreeBlazePersistenceQuery.java @@ -0,0 +1,255 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.query; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.PagedList; +import com.blazebit.persistence.spring.data.base.query.AbstractPartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareJpaQueryMethod; +import com.blazebit.persistence.spring.data.base.query.JpaParameters; +import com.blazebit.persistence.spring.data.base.query.KeysetAwarePageImpl; +import com.blazebit.persistence.spring.data.base.query.KeysetAwareSliceImpl; +import com.blazebit.persistence.spring.data.base.query.ParameterBinder; +import com.blazebit.persistence.spring.data.base.query.ParameterMetadataProvider; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.AbstractJpaQuery; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; +import org.springframework.data.jpa.repository.query.JpaQueryExecution; +import org.springframework.data.jpa.repository.support.QueryHints; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.criteria.CriteriaBuilder; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.2.0 + */ +public class PartTreeBlazePersistenceQuery extends AbstractPartTreeBlazePersistenceQuery { + + public PartTreeBlazePersistenceQuery(EntityViewAwareJpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider, EscapeCharacter escape, CriteriaBuilderFactory cbf, EntityViewManager evm) { + super(method, em, persistenceProvider, escape, cbf, evm); + } + + @Override + protected ParameterMetadataProvider createParameterMetadataProvider(CriteriaBuilder builder, ParametersParameterAccessor accessor, PersistenceProvider provider, Object escape) { + return new ParameterMetadataProviderImpl(builder, accessor, provider, (EscapeCharacter) escape); + } + + @Override + protected ParameterMetadataProvider createParameterMetadataProvider(CriteriaBuilder builder, JpaParameters parameters, PersistenceProvider provider, Object escape) { + return new ParameterMetadataProviderImpl(builder, parameters, provider, (EscapeCharacter) escape); + } + + @Override + protected JpaQueryExecution getExecution() { + if (getQueryMethod().isSliceQuery()) { + return new PartTreeBlazePersistenceQuery.SlicedExecution(getQueryMethod().getParameters()); + } else if (getQueryMethod().isPageQuery()) { + return new PartTreeBlazePersistenceQuery.PagedExecution(getQueryMethod().getParameters()); + } else if (isDelete()) { + return new PartTreeBlazePersistenceQuery.DeleteExecution(getEntityManager()); + } else if (isExists()) { + return new PartTreeBlazePersistenceQuery.ExistsExecution(); + } else { + return super.getExecution(); + } + } + + /** + * {@link JpaQueryExecution} performing an exists check on the query. + * + * @author Christian Beikov + * @since 1.3.0 + */ + private static class ExistsExecution extends JpaQueryExecution { + + @Override + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return !((PartTreeBlazePersistenceQuery) repositoryQuery).createQuery(jpaParametersParameterAccessor).getResultList().isEmpty(); + } + } + + /** + * Uses the {@link com.blazebit.persistence.PaginatedCriteriaBuilder} API for executing the query. + * + * @author Christian Beikov + * @since 1.2.0 + */ + private static class SlicedExecution extends JpaQueryExecution { + + private final Parameters parameters; + + public SlicedExecution(Parameters parameters) { + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query paginatedCriteriaBuilder = ((PartTreeBlazePersistenceQuery) repositoryQuery).createPaginatedQuery(jpaParametersParameterAccessor.getValues(), false); + PagedList resultList = (PagedList) paginatedCriteriaBuilder.getResultList(); + ParameterAccessor accessor = new ParametersParameterAccessor(parameters, jpaParametersParameterAccessor.getValues()); + Pageable pageable = accessor.getPageable(); + + return new KeysetAwareSliceImpl<>(resultList, pageable); + } + } + + /** + * Uses the {@link com.blazebit.persistence.PaginatedCriteriaBuilder} API for executing the query. + * + * @author Christian Beikov + * @since 1.2.0 + */ + private static class PagedExecution extends JpaQueryExecution { + + private final Parameters parameters; + + public PagedExecution(Parameters parameters) { + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query paginatedCriteriaBuilder = ((PartTreeBlazePersistenceQuery) repositoryQuery).createPaginatedQuery(jpaParametersParameterAccessor.getValues(), true); + PagedList resultList = (PagedList) paginatedCriteriaBuilder.getResultList(); + Long total = resultList.getTotalSize(); + ParameterAccessor accessor = new ParametersParameterAccessor(parameters, jpaParametersParameterAccessor.getValues()); + Pageable pageable = accessor.getPageable(); + + if (total.equals(0L)) { + return new KeysetAwarePageImpl<>(Collections.emptyList(), total, null, pageable); + } + + return new KeysetAwarePageImpl<>(resultList, pageable); + } + } + + /** + * {@link JpaQueryExecution} removing entities matching the query. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ + static class DeleteExecution extends JpaQueryExecution { + + private final EntityManager em; + + public DeleteExecution(EntityManager em) { + this.em = em; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.JpaQueryExecution#doExecute(org.springframework.data.jpa.repository.query.AbstractJpaQuery, java.lang.Object[]) + */ + @Override + protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor jpaParametersParameterAccessor) { + Query query = ((PartTreeBlazePersistenceQuery) jpaQuery).createQuery(jpaParametersParameterAccessor); + List resultList = query.getResultList(); + + for (Object o : resultList) { + em.remove(o); + } + + return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size(); + } + } + + @Override + protected Query doCreateQuery(JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return super.doCreateQuery(jpaParametersParameterAccessor.getValues()); + } + + @Override + protected Query doCreateCountQuery(JpaParametersParameterAccessor jpaParametersParameterAccessor) { + return super.doCreateCountQuery(jpaParametersParameterAccessor.getValues()); + } + + @Override + protected boolean isCountProjection(PartTree tree) { + return tree.isCountProjection(); + } + + @Override + protected boolean isDelete(PartTree tree) { + return tree.isDelete(); + } + + @Override + protected boolean isExists(PartTree tree) { + return tree.isExistsProjection(); + } + + @Override + protected int getOffset(Pageable pageable) { + if (pageable.isPaged()) { + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } + return 0; + } + + @Override + protected int getLimit(Pageable pageable) { + if (pageable.isPaged()) { + return pageable.getPageSize(); + } + return Integer.MAX_VALUE; + } + + @Override + protected ParameterBinder createCriteriaQueryParameterBinder(JpaParameters parameters, Object[] values, List> expressions) { + return new CriteriaQueryParameterBinder(getEntityManager(), evm, parameters, values, expressions); + } + + @Override + protected Map tryGetFetchGraphHints(JpaEntityGraph entityGraph, Class entityType) { + QueryHints fetchGraphHint = Jpa21Utils.getFetchGraphHint( + this.getEntityManager(), + entityGraph, + this.getQueryMethod() + .getEntityInformation() + .getJavaType() + ); + Map map = new HashMap<>(); + fetchGraphHint.forEach(map::put); + return map; + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java new file mode 100644 index 0000000000..caa862fcb3 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java @@ -0,0 +1,590 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.parser.EntityMetamodel; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareJpaQueryMethod; +import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; +import com.blazebit.persistence.spring.data.base.repository.AbstractEntityViewAwareRepository; +import com.blazebit.persistence.spring.data.base.repository.EntityViewAwareCrudMethodMetadata; +import com.blazebit.persistence.spring.data.base.repository.EntityViewAwareCrudMethodMetadataPostProcessor; +import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryInformation; +import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryMetadataImpl; +import com.blazebit.persistence.spring.data.impl.query.PartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.repository.EntityViewReplacingMethodInterceptor; +import com.blazebit.persistence.view.EntityViewManager; +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.provider.QueryExtractor; +import org.springframework.data.jpa.repository.query.AbstractJpaQuery; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; +import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodInvocationValidator; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; +import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.util.ClassUtils; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.lang.Nullable; +import org.springframework.transaction.interceptor.TransactionalProxy; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +import javax.persistence.EntityManager; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * Partly copied from {@link JpaRepositoryFactory} to retain functionality but mostly original. + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.5.0 + */ +public class BlazePersistenceRepositoryFactory extends JpaRepositoryFactory { + + private static final Constructor IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR; + private static final Constructor QUERY_EXECUTOR_METHOD_INTERCEPTOR; + + static { + Constructor implementationMethodExecutionInterceptor; + Constructor queryExecutorMethodInterceptor; + try { + implementationMethodExecutionInterceptor = (Constructor) Class.forName("org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor") + .getConstructor( + RepositoryInformation.class, + RepositoryComposition.class, + List.class + ); + implementationMethodExecutionInterceptor.setAccessible(true); + queryExecutorMethodInterceptor = (Constructor) Class.forName("org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor") + .getConstructor( + RepositoryInformation.class, + ProjectionFactory.class, + Optional.class, + NamedQueries.class, + List.class, + List.class + ); + queryExecutorMethodInterceptor.setAccessible(true); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR = implementationMethodExecutionInterceptor; + QUERY_EXECUTOR_METHOD_INTERCEPTOR = queryExecutorMethodInterceptor; + } + + private final EntityManager entityManager; + private final CriteriaBuilderFactory cbf; + private final EntityViewManager evm; + private final QueryExtractor extractor; + private final Map repositoryInformationCache; + private final EntityViewReplacingMethodInterceptor entityViewReplacingMethodInterceptor; + private List postProcessors; + private EntityViewAwareCrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; + private Optional> repositoryBaseClass; + private QueryLookupStrategy.Key queryLookupStrategyKey; + private List> queryPostProcessors; + private List methodInvocationListeners; + private NamedQueries namedQueries; + private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private ClassLoader classLoader; + private QueryMethodEvaluationContextProvider evaluationContextProvider; + private BeanFactory beanFactory; + + private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener(); + + /** + * Creates a new {@link JpaRepositoryFactory}. + * + * @param entityManager must not be {@literal null} + * @param cbf + * @param evm + */ + public BlazePersistenceRepositoryFactory(EntityManager entityManager, CriteriaBuilderFactory cbf, EntityViewManager evm) { + super(entityManager); + this.entityManager = entityManager; + this.extractor = PersistenceProvider.fromEntityManager(entityManager); + this.repositoryInformationCache = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + this.cbf = cbf; + this.evm = evm; + this.namedQueries = PropertiesBasedNamedQueries.EMPTY; + this.evaluationContextProvider = QueryMethodEvaluationContextProvider.DEFAULT; + this.queryPostProcessors = new ArrayList<>(); + this.queryPostProcessors.add(collectingListener); + this.methodInvocationListeners = new ArrayList<>(); + addRepositoryProxyPostProcessor(this.crudMethodMetadataPostProcessor = new EntityViewAwareCrudMethodMetadataPostProcessor()); + this.repositoryBaseClass = Optional.empty(); + this.entityViewReplacingMethodInterceptor = new EntityViewReplacingMethodInterceptor(entityManager, evm); + } + + @Override + public void setQueryLookupStrategyKey(QueryLookupStrategy.Key key) { + this.queryLookupStrategyKey = key; + } + + @Override + public void setNamedQueries(NamedQueries namedQueries) { + this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries; + } + + @Override + public void addQueryCreationListener(QueryCreationListener listener) { + Assert.notNull(listener, "Listener must not be null!"); + this.queryPostProcessors.add(listener); + } + + @Override + public void addInvocationListener(RepositoryMethodInvocationListener listener) { + Assert.notNull(listener, "Listener must not be null!"); + this.methodInvocationListeners.add(listener); + } + + @Override + public void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor processor) { + if (crudMethodMetadataPostProcessor != null) { + Assert.notNull(processor, "RepositoryProxyPostProcessor must not be null!"); + super.addRepositoryProxyPostProcessor(processor); + if (postProcessors == null) { + this.postProcessors = new ArrayList<>(); + } + this.postProcessors.add(processor); + } + } + + @Override + protected List getQueryMethods() { + return collectingListener.getQueryMethods(); + } + + @Override + public void setEscapeCharacter(EscapeCharacter escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + + protected EntityViewAwareCrudMethodMetadata getCrudMethodMetadata() { + return crudMethodMetadataPostProcessor == null ? null : crudMethodMetadataPostProcessor.getCrudMethodMetadata(); + } + + @Override + protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + return new EntityViewAwareRepositoryMetadataImpl(super.getRepositoryMetadata(repositoryInterface), evm); + } + + @Override + protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryComposition.RepositoryFragments fragments) { + return getRepositoryInformation(metadata, super.getRepositoryInformation(metadata, fragments)); + } + + protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryInformation repositoryInformation) { + return new EntityViewAwareRepositoryInformation((EntityViewAwareRepositoryMetadata) metadata, repositoryInformation); + } + + @Override + protected void validate(RepositoryMetadata repositoryMetadata) { + super.validate(repositoryMetadata); + + if (cbf.getService(EntityMetamodel.class).getEntity(repositoryMetadata.getDomainType()) == null) { + throw new InvalidDataAccessApiUsageException( + String.format("Cannot implement repository %s when using a non-entity domain type %s. Only types annotated with @Entity are supported!", + repositoryMetadata.getRepositoryInterface().getName(), repositoryMetadata.getDomainType().getName())); + } + } + + @Override + protected JpaRepositoryImplementation getTargetRepository(RepositoryInformation information, EntityManager entityManager) { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + JpaEntityInformation entityInformation = getEntityInformation(information.getDomainType()); + AbstractEntityViewAwareRepository entityViewAwareRepository = getTargetRepositoryViaReflection(information, entityInformation, entityManager, cbf, evm, ((EntityViewAwareRepositoryInformation) information).getEntityViewType()); + entityViewAwareRepository.setRepositoryMethodMetadata(getCrudMethodMetadata()); + return (JpaRepositoryImplementation) entityViewAwareRepository; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + return EntityViewAwareRepositoryImpl.class; + } + + @Override + protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { + switch (key != null ? key : QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) { + case CREATE: + return Optional.of(new CreateQueryLookupStrategy(entityManager, extractor, escapeCharacter, cbf, evm)); + case USE_DECLARED_QUERY: + return Optional.of(new DelegateQueryLookupStrategy(super.getQueryLookupStrategy(key, evaluationContextProvider).get())); + case CREATE_IF_NOT_FOUND: + return Optional.of(new CreateIfNotFoundQueryLookupStrategy(entityManager, extractor, new CreateQueryLookupStrategy(entityManager, extractor, escapeCharacter, cbf, evm), + new DelegateQueryLookupStrategy(super.getQueryLookupStrategy(QueryLookupStrategy.Key.USE_DECLARED_QUERY, evaluationContextProvider).get()))); + default: + throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); + } + } + + private static class CreateQueryLookupStrategy implements QueryLookupStrategy { + + private final EntityManager em; + private final QueryExtractor provider; + private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escapeCharacter; + private final CriteriaBuilderFactory cbf; + private final EntityViewManager evm; + + public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor, EscapeCharacter escapeCharacter, CriteriaBuilderFactory cbf, EntityViewManager evm) { + this.em = em; + this.provider = extractor; + this.persistenceProvider = PersistenceProvider.fromEntityManager(em); + this.escapeCharacter = escapeCharacter; + this.cbf = cbf; + this.evm = evm; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + try { + // TODO: at some point, we might want to switch to the default if the repository doesn't contain entity views or keyset pagination + return new PartTreeBlazePersistenceQuery(new EntityViewAwareJpaQueryMethod(method, (EntityViewAwareRepositoryMetadata) metadata, factory, provider), em, persistenceProvider, escapeCharacter, cbf, evm); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + String.format("Could not create query metamodel for method %s!", method.toString()), e); + } + } + } + + private static class DelegateQueryLookupStrategy implements QueryLookupStrategy { + + private final QueryLookupStrategy delegate; + + public DelegateQueryLookupStrategy(QueryLookupStrategy delegate) { + this.delegate = delegate; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + return delegate.resolveQuery(method, metadata, factory, namedQueries); + } + } + + private static class CreateIfNotFoundQueryLookupStrategy implements QueryLookupStrategy { + + private final EntityManager em; + private final QueryExtractor provider; + private final DelegateQueryLookupStrategy lookupStrategy; + private final CreateQueryLookupStrategy createStrategy; + + public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor, + CreateQueryLookupStrategy createStrategy, DelegateQueryLookupStrategy lookupStrategy) { + this.em = em; + this.provider = extractor; + this.createStrategy = createStrategy; + this.lookupStrategy = lookupStrategy; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + try { + RepositoryQuery repositoryQuery = lookupStrategy.resolveQuery(method, metadata, factory, namedQueries); + // Only return something if the RepositoryQuery is not an instance of org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.NoQuery + // Since we can't refer to the class though, we instead check if the returned class is an instance of AbstractJpaQuery, + // because we know that NoQuery is not matching that + if (repositoryQuery instanceof AbstractJpaQuery) { + return repositoryQuery; + } + } catch (IllegalStateException e) { + // Ignore + } + return createStrategy.resolveQuery(method, metadata, factory, namedQueries); + } + } + + // Mostly copied from RepositoryFactorySupport to be able to use a custom RepositoryInformation implementation + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + super.setBeanClassLoader(classLoader); + this.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + super.setBeanFactory(beanFactory); + this.beanFactory = beanFactory; + } + + @Override + public void setRepositoryBaseClass(Class repositoryBaseClass) { + super.setRepositoryBaseClass(repositoryBaseClass); + this.repositoryBaseClass = Optional.ofNullable(repositoryBaseClass); + } + + private static final BiFunction REACTIVE_ARGS_CONVERTER = (method, o) -> { + + if (ReactiveWrappers.isAvailable()) { + + Class[] parameterTypes = method.getParameterTypes(); + + Object[] converted = new Object[o.length]; + for (int i = 0; i < parameterTypes.length; i++) { + + Class parameterType = parameterTypes[i]; + Object value = o[i]; + + if (value == null) { + continue; + } + + if (!parameterType.isAssignableFrom(value.getClass()) + && ReactiveWrapperConverters.canConvert(value.getClass(), parameterType)) { + + converted[i] = ReactiveWrapperConverters.toWrapper(value, parameterType); + } else { + converted[i] = value; + } + } + + return converted; + } + + return o; + }; + + /** + * Returns a repository instance for the given interface backed by an instance providing implementation logic for + * custom logic. + * + * @param repositoryInterface must not be {@literal null}. + * @param fragments must not be {@literal null}. + * @return + * @since 2.0 + */ + @SuppressWarnings({ "unchecked" }) + public T getRepository(Class repositoryInterface, RepositoryComposition.RepositoryFragments fragments) { + + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); + Assert.notNull(fragments, "RepositoryFragments must not be null!"); + + RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface); + RepositoryComposition composition = getRepositoryComposition(metadata, fragments); + RepositoryInformation information = getRepositoryInformation(metadata, composition); + + validate(information, composition); + + Object target = getTargetRepository(information); + + // Create proxy + ProxyFactory result = new ProxyFactory(); + result.setTarget(target); + result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class); + + if (MethodInvocationValidator.supports(repositoryInterface)) { + result.addAdvice(new MethodInvocationValidator()); + } + + result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); + result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + + postProcessors.forEach(processor -> processor.postProcess(result, information)); + + result.addAdvice(entityViewReplacingMethodInterceptor); + result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); + + ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory); + Optional queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey, + evaluationContextProvider); + result.addAdvice(queryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy, + namedQueries, queryPostProcessors, methodInvocationListeners)); + + composition = composition.append(RepositoryFragment.implemented(target)); + result.addAdvice(implementationMethodExecutionInterceptor(information, composition, methodInvocationListeners)); + + return (T) result.getProxy(classLoader); + } + + private Advice implementationMethodExecutionInterceptor( + RepositoryInformation information, + RepositoryComposition composition, + List methodInvocationListeners) { + try { + return IMPLEMENTATION_METHOD_EXECUTION_INTERCEPTOR.newInstance( + information, + composition, + methodInvocationListeners + ); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Advice queryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, + ProjectionFactory projectionFactory, Optional queryLookupStrategy, NamedQueries namedQueries, + List> queryPostProcessors, + List methodInvocationListeners) { + try { + return QUERY_EXECUTOR_METHOD_INTERCEPTOR.newInstance( + repositoryInformation, + projectionFactory, + queryLookupStrategy, + namedQueries, + queryPostProcessors, + methodInvocationListeners + ); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Validates the given repository interface as well as the given custom implementation. + * + * @param repositoryInformation + * @param composition + */ + private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) { + + if (repositoryInformation.hasCustomMethod()) { + + if (composition.isEmpty()) { + + throw new IllegalArgumentException( + String.format("You have custom methods in %s but not provided a custom implementation!", + repositoryInformation.getRepositoryInterface())); + } + + composition.validateImplementation(); + } + + validate(repositoryInformation); + } + + private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, + RepositoryComposition composition) { + + RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition); + + return repositoryInformationCache.computeIfAbsent(cacheKey, key -> { + + Class baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata)); + + return getRepositoryInformation(metadata, new DefaultRepositoryInformation(metadata, baseClass, composition)); + }); + } + + private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata, RepositoryComposition.RepositoryFragments fragments) { + + Assert.notNull(metadata, "RepositoryMetadata must not be null!"); + Assert.notNull(fragments, "RepositoryFragments must not be null!"); + + RepositoryComposition composition = getRepositoryComposition(metadata); + RepositoryComposition.RepositoryFragments repositoryAspects = getRepositoryFragments(metadata); + + return composition.append(fragments).append(repositoryAspects); + } + + /** + * Creates {@link RepositoryComposition} based on {@link RepositoryMetadata} for repository-specific method handling. + * + * @param metadata + * @return + */ + private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata) { + + RepositoryComposition composition = RepositoryComposition.empty(); + + if (metadata.isReactiveRepository()) { + return composition.withMethodLookup(MethodLookups.forReactiveTypes(metadata)) + .withArgumentConverter(REACTIVE_ARGS_CONVERTER); + } + + return composition.withMethodLookup(MethodLookups.forRepositoryTypes(metadata)); + } + + private static class RepositoryInformationCacheKey { + + String repositoryInterfaceName; + final long compositionHash; + + /** + * Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and composition. + * + * @param metadata must not be {@literal null}. + * @param composition must not be {@literal null}. + */ + public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryComposition composition) { + + this.repositoryInterfaceName = metadata.getRepositoryInterface().getName(); + this.compositionHash = composition.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RepositoryInformationCacheKey)) { + return false; + } + + RepositoryInformationCacheKey that = (RepositoryInformationCacheKey) o; + + if (compositionHash != that.compositionHash) { + return false; + } + return repositoryInterfaceName != null ? repositoryInterfaceName.equals(that.repositoryInterfaceName) : that.repositoryInterfaceName == null; + } + + @Override + public int hashCode() { + int result = repositoryInterfaceName != null ? repositoryInterfaceName.hashCode() : 0; + result = 31 * result + (int) (compositionHash ^ (compositionHash >>> 32)); + return result; + } + } + +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java new file mode 100644 index 0000000000..605ff63636 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactoryBean.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.view.EntityViewManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; +import org.springframework.util.Assert; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.io.Serializable; + +/** + * @author Moritz Becker + * @since 1.2.0 + */ +public class BlazePersistenceRepositoryFactoryBean, S, ID extends Serializable> extends + TransactionalRepositoryFactoryBeanSupport { + + private EntityManager entityManager; + + @Autowired + private CriteriaBuilderFactory cbf; + + @Autowired + private EntityViewManager evm; + + /** + * Creates a new {@link BlazePersistenceRepositoryFactoryBean}. + */ + protected BlazePersistenceRepositoryFactoryBean() { + super(null); + } + + /** + * Creates a new {@link BlazePersistenceRepositoryFactoryBean} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + protected BlazePersistenceRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + public boolean isSingleton() { + return true; + } + + /** + * The {@link EntityManager} to be used. + * + * @param entityManager the entityManager to set + */ + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + if (this.entityManager == null) { + this.entityManager = entityManager; + } + } + + /* + * (non-Javadoc) + * @see com.blazebit.persistence.spring.data.impl.repository.BlazeRepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext) + */ + @Override + public void setMappingContext(MappingContext mappingContext) { + super.setMappingContext(mappingContext); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support. + * BlazeTransactionalRepositoryFactoryBeanSupport#doCreateRepositoryFactory() + */ + @Override + protected BlazePersistenceRepositoryFactory doCreateRepositoryFactory() { + return createRepositoryFactory(entityManager); + } + + /** + * Returns a {@link RepositoryFactorySupport}. + * + * @param entityManager + * @return + */ + protected BlazePersistenceRepositoryFactory createRepositoryFactory(EntityManager entityManager) { + return new BlazePersistenceRepositoryFactory(entityManager, cbf, evm); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + Assert.notNull(entityManager, "EntityManager must not be null!"); + super.afterPropertiesSet(); + } + + public void setEscapeCharacter(char escapeCharacter) { + // Needed to work with Spring Boot 2.1.4 + } + + public char getEscapeCharacter() { + // Needed to work with Spring Boot 2.1.4 + return '\\'; + } + +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java new file mode 100644 index 0000000000..444ab04dfd --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java @@ -0,0 +1,313 @@ +/* + * Copyright 2011-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springframework.data.repository.util.ClassUtils.isGenericRepositoryInterface; +import static org.springframework.util.ReflectionUtils.makeAccessible; + +/** + * Default implementation of {@link RepositoryInformation}. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Mark Paluch + * @author Christoph Strobl + */ +class DefaultRepositoryInformation implements RepositoryInformation { + + private final Map methodCache = new ConcurrentHashMap<>(); + + private final RepositoryMetadata metadata; + private final Class repositoryBaseClass; + private final RepositoryComposition composition; + private final RepositoryComposition baseComposition; + + /** + * Creates a new {@link DefaultRepositoryMetadata} for the given repository interface and repository base class. + * + * @param metadata must not be {@literal null}. + * @param repositoryBaseClass must not be {@literal null}. + * @param composition must not be {@literal null}. + */ + public DefaultRepositoryInformation(RepositoryMetadata metadata, Class repositoryBaseClass, + RepositoryComposition composition) { + + Assert.notNull(metadata, "Repository metadata must not be null!"); + Assert.notNull(repositoryBaseClass, "Repository base class must not be null!"); + Assert.notNull(composition, "Repository composition must not be null!"); + + this.metadata = metadata; + this.repositoryBaseClass = repositoryBaseClass; + this.composition = composition; + this.baseComposition = RepositoryComposition.of(RepositoryFragment.structural(repositoryBaseClass)) // + .withArgumentConverter(composition.getArgumentConverter()) // + .withMethodLookup(composition.getMethodLookup()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getDomainClass() + */ + @Override + public Class getDomainType() { + return metadata.getDomainType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getIdClass() + */ + @Override + public Class getIdType() { + return metadata.getIdType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getRepositoryBaseClass() + */ + @Override + public Class getRepositoryBaseClass() { + return this.repositoryBaseClass; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getTargetClassMethod(java.lang.reflect.Method) + */ + @Override + public Method getTargetClassMethod(Method method) { + + if (methodCache.containsKey(method)) { + return methodCache.get(method); + } + + Method result = composition.findMethod(method).orElse(method); + + if (!result.equals(method)) { + return cacheAndReturn(method, result); + } + + return cacheAndReturn(method, baseComposition.findMethod(method).orElse(method)); + } + + private Method cacheAndReturn(Method key, Method value) { + + if (value != null) { + makeAccessible(value); + } + + methodCache.put(key, value); + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#getQueryMethods() + */ + @Override + public Streamable getQueryMethods() { + + Set result = new HashSet<>(); + + for (Method method : getRepositoryInterface().getMethods()) { + method = ClassUtils.getMostSpecificMethod(method, getRepositoryInterface()); + if (isQueryMethodCandidate(method)) { + result.add(method); + } + } + + return Streamable.of(Collections.unmodifiableSet(result)); + } + + /** + * Checks whether the given method is a query method candidate. + * + * @param method + * @return + */ + private boolean isQueryMethodCandidate(Method method) { + return !method.isBridge() && !method.isDefault() // + && !Modifier.isStatic(method.getModifiers()) // + && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); + } + + /** + * Checks whether the given method contains a custom store specific query annotation annotated with + * {@link QueryAnnotation}. The method-hierarchy is also considered in the search for the annotation. + * + * @param method + * @return + */ + private boolean isQueryAnnotationPresentOn(Method method) { + + return AnnotationUtils.findAnnotation(method, QueryAnnotation.class) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#isCustomMethod(java.lang.reflect.Method) + */ + @Override + public boolean isCustomMethod(Method method) { + return composition.findMethod(method).isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryInformation#isQueryMethod(java.lang.reflect.Method) + */ + @Override + public boolean isQueryMethod(Method method) { + return getQueryMethods().stream().anyMatch(it -> it.equals(method)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryInformation#isBaseClassMethod(java.lang.reflect.Method) + */ + @Override + public boolean isBaseClassMethod(Method method) { + + Assert.notNull(method, "Method must not be null!"); + return baseComposition.findMethod(method).isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryInformation#hasCustomMethod() + */ + @Override + public boolean hasCustomMethod() { + + Class repositoryInterface = getRepositoryInterface(); + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(repositoryInterface)) { + return false; + } + + for (Method method : repositoryInterface.getMethods()) { + if (isCustomMethod(method) && !isBaseClassMethod(method)) { + return true; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getRepositoryInterface() + */ + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method) + */ + @Override + public Class getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(java.lang.reflect.Method) + */ + @Override + public TypeInformation getReturnType(Method method) { + return this.metadata.getReturnType(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getIdTypeInformation() + */ + @Override + public TypeInformation getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getDomainTypeInformation() + */ + @Override + public TypeInformation getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getCrudMethods() + */ + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#isPagingRepository() + */ + @Override + public boolean isPagingRepository() { + return metadata.isPagingRepository(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#getAlternativeDomainTypes() + */ + @Override + public Set> getAlternativeDomainTypes() { + return metadata.getAlternativeDomainTypes(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.RepositoryMetadata#isReactiveRepository() + */ + @Override + public boolean isReactiveRepository() { + return metadata.isReactiveRepository(); + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java new file mode 100644 index 0000000000..1388411d2f --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/EntityViewAwareRepositoryImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spring.data.base.repository.AbstractEntityViewAwareRepository; +import com.blazebit.persistence.spring.data.repository.EntityViewRepository; +import com.blazebit.persistence.spring.data.repository.KeysetPageable; +import com.blazebit.persistence.view.EntityViewManager; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.jpa.repository.support.CrudMethodMetadata; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.TypedQuery; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * @author Christian Beikov + * @since 1.3.0 + */ +@Transactional(readOnly = true) +public class EntityViewAwareRepositoryImpl extends AbstractEntityViewAwareRepository implements JpaRepositoryImplementation, EntityViewRepository/*, EntityViewSpecificationExecutor*/ { // Can't implement that interface because of clashing methods + + public EntityViewAwareRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager, CriteriaBuilderFactory cbf, EntityViewManager evm, Class entityViewClass) { + super(entityInformation, entityManager, cbf, evm, (Class) entityViewClass); + } + + @Override + protected Map tryGetFetchGraphHints(EntityManager entityManager, JpaEntityGraph entityGraph, Class entityType) { + Map map = new HashMap<>(); + Jpa21Utils.getFetchGraphHint(entityManager, entityGraph, entityType).forEach( map::put ); + return map; + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findOne(Example example) { + try { + return Optional.of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findOne(Specification spec) { + try { + return Optional.of((E) getQuery(spec, (Sort) null).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Override + @WithBridgeMethods(value = Object.class, adapterMethod = "convert") + public Optional findById(ID id) { + return Optional.ofNullable((E) findOne(id)); + } + + public E getById(ID id) { + return (E) getReference(id); + } + + private Object convert(Optional optional, Class targetType) { + return optional.orElse(null); + } + + @Override + public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { + // Ignore the Spring data version of the CrudMethodMetadata + } + + @Override + protected int getOffset(Pageable pageable) { + if (pageable instanceof KeysetPageable) { + return ((KeysetPageable) pageable).getIntOffset(); + } else { + return (int) pageable.getOffset(); + } + } + + @Override + public long count(Example example) { + return super.count(example); + } + + @Override + public boolean exists(Example example) { + return super.exists(example); + } + + @Override + public R findBy(Example example, Function, R> queryFunction) { + Assert.notNull(example, "Sample must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + Function> finder = sort -> { + + ExampleSpecification spec = new ExampleSpecification<>(example, escapeCharacter); + Class probeType = example.getProbeType(); + + return getQuery(spec, probeType, sort); + }; + + FluentQuery.FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, finder, this::count, this::exists, getEntityManager(), this.escapeCharacter); + + return queryFunction.apply(fluentQuery); + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java new file mode 100644 index 0000000000..dcc5c03e15 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java @@ -0,0 +1,279 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.util.Assert; + +import javax.persistence.EntityGraph; +import javax.persistence.EntityManager; +import javax.persistence.Subgraph; +import javax.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Immutable implementation of {@link FetchableFluentQuery} based on Query by {@link Example}. All methods that return a + * {@link FetchableFluentQuery} will return a new instance, not the original. + * + * Christian Beikov: Copied to be able to share code between Spring Data integrations for 2.6 and 2.7. + * + * @param Domain type + * @param Result type + * @author Greg Turnquist + * @author Mark Paluch + * @author Jens Schauder + * @author J.R. Onyschak + * @since 2.6 + */ +public class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { + + private final Example example; + private final Function> finder; + private final Function, Long> countOperation; + private final Function, Boolean> existsOperation; + private final EntityManager entityManager; + private final EscapeCharacter escapeCharacter; + + public FetchableFluentQueryByExample(Example example, Function> finder, + Function, Long> countOperation, Function, Boolean> existsOperation, + EntityManager entityManager, EscapeCharacter escapeCharacter) { + this(example, example.getProbeType(), (Class) example.getProbeType(), Sort.unsorted(), Collections.emptySet(), + finder, countOperation, existsOperation, entityManager, escapeCharacter); + } + + private FetchableFluentQueryByExample(Example example, Class entityType, Class returnType, Sort sort, + Collection properties, Function> finder, Function, Long> countOperation, + Function, Boolean> existsOperation, + EntityManager entityManager, EscapeCharacter escapeCharacter) { + + super(returnType, sort, properties, entityType); + this.example = example; + this.finder = finder; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + this.entityManager = entityManager; + this.escapeCharacter = escapeCharacter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort) + */ + @Override + public FetchableFluentQuery sortBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return new FetchableFluentQueryByExample<>(example, entityType, resultType, this.sort.and(sort), properties, + finder, countOperation, existsOperation, entityManager, escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) + */ + @Override + public FetchableFluentQuery as(Class resultType) { + + Assert.notNull(resultType, "Projection target type must not be null!"); + if (!resultType.isInterface()) { + throw new UnsupportedOperationException("Class-based DTOs are not yet supported."); + } + + return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, properties, finder, + countOperation, existsOperation, entityManager, escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) + */ + @Override + public FetchableFluentQuery project(Collection properties) { + + return new FetchableFluentQueryByExample<>(example, entityType, resultType, sort, mergeProperties(properties), + finder, countOperation, existsOperation, entityManager, escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Override + public R oneValue() { + + TypedQuery limitedQuery = createSortedAndProjectedQuery(); + limitedQuery.setMaxResults(2); // Never need more than 2 values + + List results = limitedQuery.getResultList(); + + if (results.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + @Override + public R firstValue() { + + TypedQuery limitedQuery = createSortedAndProjectedQuery(); + limitedQuery.setMaxResults(1); // Never need more than 1 value + + List results = limitedQuery.getResultList(); + + return results.isEmpty() ? null : getConversionFunction().apply(results.get(0)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + + List resultList = createSortedAndProjectedQuery().getResultList(); + + return convert(resultList); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + + return createSortedAndProjectedQuery() // + .getResultStream() // + .map(getConversionFunction()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return countOperation.apply(example); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return existsOperation.apply(example); + } + + private Page readPage(Pageable pageable) { + + TypedQuery pagedQuery = createSortedAndProjectedQuery(); + + if (pageable.isPaged()) { + pagedQuery.setFirstResult((int) pageable.getOffset()); + pagedQuery.setMaxResults(pageable.getPageSize()); + } + + List paginatedResults = convert(pagedQuery.getResultList()); + + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(example)); + } + + private TypedQuery createSortedAndProjectedQuery() { + + TypedQuery query = finder.apply(sort); + + if (!properties.isEmpty()) { + query.setHint("javax.persistence.fetchgraph", create(entityManager, entityType, properties)); + } + + return query; + } + + private static EntityGraph create(EntityManager entityManager, Class domainType, Set properties) { + + EntityGraph entityGraph = entityManager.createEntityGraph(domainType); + + for (String property : properties) { + + Subgraph current = null; + + for (PropertyPath path : PropertyPath.from(property, domainType)) { + + if (path.hasNext()) { + current = current == null ? entityGraph.addSubgraph(path.getSegment()) + : current.addSubgraph(path.getSegment()); + continue; + } + + if (current == null) { + entityGraph.addAttributeNodes(path.getSegment()); + } else { + current.addAttributeNodes(path.getSegment()); + + } + } + } + + return entityGraph; + } + + private List convert(List resultList) { + + Function conversionFunction = getConversionFunction(); + List mapped = new ArrayList<>(resultList.size()); + + for (S s : resultList) { + mapped.add(conversionFunction.apply(s)); + } + return mapped; + } + + private Function getConversionFunction() { + return getConversionFunction(example.getProbeType(), resultType); + } + +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java new file mode 100644 index 0000000000..527f5becef --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +/** + * Supporting class containing some state and convenience methods for building and executing fluent queries. + * + * Christian Beikov: Copied to be able to share code between Spring Data integrations for 2.6 and 2.7. + * + * @param The resulting type of the query. + * @author Greg Turnquist + * @author Jens Schauder + * @since 2.6 + */ +public abstract class FluentQuerySupport { + + protected final Class resultType; + protected final Sort sort; + protected final Set properties; + protected final Class entityType; + + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + + public FluentQuerySupport(Class resultType, Sort sort, Collection properties, Class entityType) { + + this.resultType = resultType; + this.sort = sort; + + if (properties != null) { + this.properties = new HashSet<>(properties); + } else { + this.properties = Collections.emptySet(); + } + + this.entityType = entityType; + } + + final Collection mergeProperties(Collection additionalProperties) { + + Set newProperties = new HashSet<>(); + newProperties.addAll(properties); + newProperties.addAll(additionalProperties); + return Collections.unmodifiableCollection(newProperties); + } + + @SuppressWarnings("unchecked") + final Function getConversionFunction(Class inputType, Class targetType) { + + if (targetType.isAssignableFrom(inputType)) { + return (Function) Function.identity(); + } + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + return o -> DefaultConversionService.getSharedInstance().convert(o, targetType); + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java new file mode 100644 index 0000000000..0955aec369 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/MethodLookups.java @@ -0,0 +1,443 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodLookup; +import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.util.Assert; + +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.springframework.core.GenericTypeResolver.resolveParameterType; + +/** + * Implementations of method lookup functions. + * + * @author Mark Paluch + * @author Oliver Gierke + * @since 2.0 + */ +interface MethodLookups { + + /** + * Direct method lookup filtering on exact method name, parameter count and parameter types. + * + * @return direct method lookup. + */ + public static MethodLookup direct() { + + MethodPredicate direct = (invoked, candidate) -> candidate.getName().equals(invoked.getName()) + && candidate.getParameterCount() == invoked.getParameterCount() + && Arrays.equals(candidate.getParameterTypes(), invoked.getParameterTypes()) + && candidate.getReturnType().equals(invoked.getMethod().getReturnType()); + + return () -> Collections.singletonList(direct); + } + + /** + * Repository type-aware method lookup composed of {@link #direct()} and {@link RepositoryAwareMethodLookup}. + *

+ * Repository-aware lookups resolve generic types from the repository declaration to verify assignability to Id/domain + * types. This lookup also permits assignable method signatures but prefers {@link #direct()} matches. + * + * @param repositoryMetadata must not be {@literal null}. + * @return the composed, repository-aware method lookup. + * @see #direct() + */ + public static MethodLookup forRepositoryTypes(RepositoryMetadata repositoryMetadata) { + return direct().and(new RepositoryAwareMethodLookup(repositoryMetadata)); + } + + /** + * Repository type-aware method lookup composed of {@link #direct()} and {@link ReactiveTypeInteropMethodLookup}. + *

+ * This method lookup considers adaptability of reactive types in method signatures. Repository methods accepting a + * reactive type can be possibly called with a different reactive type if the reactive type can be adopted to the + * target type. This lookup also permits assignable method signatures and resolves repository id/entity types but + * prefers {@link #direct()} matches. + * + * @param repositoryMetadata must not be {@literal null}. + * @return the composed, repository-aware method lookup. + * @see #direct() + * @see #forRepositoryTypes(RepositoryMetadata) + */ + public static MethodLookup forReactiveTypes(RepositoryMetadata repositoryMetadata) { + return direct().and(new ReactiveTypeInteropMethodLookup(repositoryMetadata)); + } + + /** + * Default {@link MethodLookup} considering repository Id and entity types permitting calls to methods with assignable + * arguments. + * + * @author Mark Paluch + */ + static class RepositoryAwareMethodLookup implements MethodLookup { + + @SuppressWarnings("rawtypes") private static final TypeVariable>[] PARAMETERS = Repository.class + .getTypeParameters(); + private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); + private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); + + private final ResolvableType entityType, idType; + private final Class repositoryInterface; + + /** + * Creates a new {@link RepositoryAwareMethodLookup} for the given {@link RepositoryMetadata}. + * + * @param repositoryMetadata must not be {@literal null}. + */ + public RepositoryAwareMethodLookup(RepositoryMetadata repositoryMetadata) { + + Assert.notNull(repositoryMetadata, "Repository metadata must not be null!"); + + this.entityType = ResolvableType.forClass(repositoryMetadata.getDomainType()); + this.idType = ResolvableType.forClass(repositoryMetadata.getIdType()); + this.repositoryInterface = repositoryMetadata.getRepositoryInterface(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.MethodLookup#getLookups() + */ + @Override + public List getLookups() { + + MethodPredicate detailedComparison = (invoked, candidate) -> Optional.of(candidate) + .filter(baseClassMethod -> baseClassMethod.getName().equals(invoked.getName()))// Right name + .filter(baseClassMethod -> baseClassMethod.getParameterCount() == invoked.getParameterCount()) + .filter(baseClassMethod -> parametersMatch(invoked.getMethod(), baseClassMethod))// All parameters match + .isPresent(); + + return Collections.singletonList(detailedComparison); + } + + /** + * Checks whether the given parameter type matches the generic type of the given parameter. Thus when {@literal PK} + * is declared, the method ensures that given method parameter is the primary key type declared in the given + * repository interface e.g. + * + * @param variable must not be {@literal null}. + * @param parameterType must not be {@literal null}. + * @return + */ + protected boolean matchesGenericType(TypeVariable variable, ResolvableType parameterType) { + + GenericDeclaration declaration = variable.getGenericDeclaration(); + + if (declaration instanceof Class) { + + if (ID_TYPE_NAME.equals(variable.getName()) && parameterType.isAssignableFrom(idType)) { + return true; + } + + Type boundType = variable.getBounds()[0]; + String referenceName = boundType instanceof TypeVariable ? boundType.toString() : variable.toString(); + + return DOMAIN_TYPE_NAME.equals(referenceName) && parameterType.isAssignableFrom(entityType); + } + + for (Type type : variable.getBounds()) { + if (ResolvableType.forType(type).isAssignableFrom(parameterType)) { + return true; + } + } + + return false; + } + + /** + * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments + * against the ones bound in the given repository interface. + * + * @param invokedMethod + * @param candidate + * @return + */ + private boolean parametersMatch(Method invokedMethod, Method candidate) { + + Class[] methodParameterTypes = invokedMethod.getParameterTypes(); + Type[] genericTypes = candidate.getGenericParameterTypes(); + Class[] types = candidate.getParameterTypes(); + + for (int i = 0; i < genericTypes.length; i++) { + + Type genericType = genericTypes[i]; + Class type = types[i]; + MethodParameter parameter = new MethodParameter(invokedMethod, i); + Class parameterType = resolveParameterType(parameter, repositoryInterface); + + if (genericType instanceof TypeVariable) { + + if (!matchesGenericType((TypeVariable) genericType, ResolvableType.forMethodParameter(parameter))) { + return false; + } + + continue; + } + + if (types[i].equals(parameterType)) { + continue; + } + + if (!type.isAssignableFrom(parameterType) || !type.equals(methodParameterTypes[i])) { + return false; + } + } + + return true; + } + } + + /** + * Extension to {@link RepositoryAwareMethodLookup} considering reactive type adoption and entity types permitting + * calls to methods with assignable arguments. + * + * @author Mark Paluch + */ + static class ReactiveTypeInteropMethodLookup extends RepositoryAwareMethodLookup { + + private final RepositoryMetadata repositoryMetadata; + + public ReactiveTypeInteropMethodLookup(RepositoryMetadata repositoryMetadata) { + + super(repositoryMetadata); + this.repositoryMetadata = repositoryMetadata; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.MethodLookups.RepositoryAwareMethodLookup#getLookups() + */ + @Override + public List getLookups() { + + MethodPredicate convertibleComparison = (invokedMethod, candidate) -> { + + List>> suppliers = new ArrayList<>(); + + if (usesParametersWithReactiveWrappers(invokedMethod.getMethod())) { + suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, assignableWrapperMatch())); // + suppliers.add(() -> getMethodCandidate(invokedMethod, candidate, wrapperConversionMatch())); + } + + return suppliers.stream().anyMatch(supplier -> supplier.get().isPresent()); + }; + + MethodPredicate detailedComparison = (invokedMethod, candidate) -> getMethodCandidate(invokedMethod, candidate, + matchParameterOrComponentType(repositoryMetadata.getRepositoryInterface())).isPresent(); + + return Arrays.asList(convertibleComparison, detailedComparison); + } + + /** + * {@link Predicate} to check parameter assignability between a parameters in which the declared parameter may be + * wrapped but supports unwrapping. Usually types like {@link Optional} or {@link Stream}. + * + * @param repositoryInterface + * @return + * @see QueryExecutionConverters + * @see #matchesGenericType + */ + private Predicate matchParameterOrComponentType(Class repositoryInterface) { + + return (parameterCriteria) -> { + + Class parameterType = resolveParameterType(parameterCriteria.getDeclared(), repositoryInterface); + Type genericType = parameterCriteria.getGenericBaseType(); + + if (genericType instanceof TypeVariable) { + + if (!matchesGenericType((TypeVariable) genericType, + ResolvableType.forMethodParameter(parameterCriteria.getDeclared()))) { + return false; + } + } + + return parameterCriteria.getBaseType().isAssignableFrom(parameterType) + && parameterCriteria.isAssignableFromDeclared(); + }; + } + + /** + * Checks whether the type is a wrapper without unwrapping support. Reactive wrappers don't like to be unwrapped. + * + * @param parameterType must not be {@literal null}. + * @return + */ + private static boolean isNonUnwrappingWrapper(Class parameterType) { + + Assert.notNull(parameterType, "Parameter type must not be null!"); + + return QueryExecutionConverters.supports(parameterType) + && !QueryExecutionConverters.supportsUnwrapping(parameterType); + } + + /** + * Returns whether the given {@link Method} uses a reactive wrapper type as parameter. + * + * @param method must not be {@literal null}. + * @return + */ + private static boolean usesParametersWithReactiveWrappers(Method method) { + + Assert.notNull(method, "Method must not be null!"); + + return Arrays.stream(method.getParameterTypes())// + .anyMatch(ReactiveTypeInteropMethodLookup::isNonUnwrappingWrapper); + } + + /** + * Returns a candidate method from the base class for the given one or the method given in the first place if none + * one the base class matches. + * + * @param method must not be {@literal null}. + * @param baseClass must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @return + */ + private static Optional getMethodCandidate(InvokedMethod invokedMethod, Method candidate, + Predicate predicate) { + + return Optional.of(candidate)// + .filter(it -> invokedMethod.getName().equals(it.getName()))// + .filter(it -> invokedMethod.getParameterCount() == it.getParameterCount())// + .filter(it -> parametersMatch(it, invokedMethod.getMethod(), predicate)); + } + + /** + * Checks the given method's parameters to match the ones of the given base class method. Matches generic arguments + * against the ones bound in the given repository interface. + * + * @param baseClassMethod must not be {@literal null}. + * @param declaredMethod must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @return + */ + private static boolean parametersMatch(Method baseClassMethod, Method declaredMethod, + Predicate predicate) { + + return methodParameters(baseClassMethod, declaredMethod).allMatch(predicate); + } + + /** + * {@link Predicate} to check whether a method parameter is a {@link #isNonUnwrappingWrapper(Class)} and can be + * converted into a different wrapper. Usually {@link rx.Observable} to {@link org.reactivestreams.Publisher} + * conversion. + * + * @return + */ + private static Predicate wrapperConversionMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && ReactiveWrapperConverters.canConvert(parameterCriteria.getDeclaredType(), parameterCriteria.getBaseType()); + } + + /** + * {@link Predicate} to check parameter assignability between a {@link #isNonUnwrappingWrapper(Class)} parameter and + * a declared parameter. Usually {@link reactor.core.publisher.Flux} vs. {@link org.reactivestreams.Publisher} + * conversion. + * + * @return + */ + private static Predicate assignableWrapperMatch() { + + return (parameterCriteria) -> isNonUnwrappingWrapper(parameterCriteria.getBaseType()) // + && isNonUnwrappingWrapper(parameterCriteria.getDeclaredType()) // + && parameterCriteria.getBaseType().isAssignableFrom(parameterCriteria.getDeclaredType()); + } + + private static Stream methodParameters(Method first, Method second) { + + Assert.isTrue(first.getParameterCount() == second.getParameterCount(), "Method parameter count must be equal!"); + + return IntStream.range(0, first.getParameterCount()) // + .mapToObj(index -> ParameterOverrideCriteria.of(new MethodParameter(first, index), + new MethodParameter(second, index))); + } + + /** + * Criterion to represent {@link MethodParameter}s from a base method and its declared (overridden) method. Method + * parameters indexes are correlated so {@link ParameterOverrideCriteria} applies only to methods with same + * parameter count. + */ + static class ParameterOverrideCriteria { + + private final MethodParameter base; + private final MethodParameter declared; + + private ParameterOverrideCriteria(MethodParameter base, MethodParameter declared) { + this.base = base; + this.declared = declared; + } + + public static ParameterOverrideCriteria of(MethodParameter base, MethodParameter declared) { + return new ParameterOverrideCriteria(base, declared); + } + + public MethodParameter getBase() { + return base; + } + + public MethodParameter getDeclared() { + return declared; + } + + /** + * @return base method parameter type. + */ + public Class getBaseType() { + return base.getParameterType(); + } + + /** + * @return generic base method parameter type. + */ + public Type getGenericBaseType() { + return base.getGenericParameterType(); + } + + /** + * @return declared method parameter type. + */ + public Class getDeclaredType() { + return declared.getParameterType(); + } + + public boolean isAssignableFromDeclared() { + return getBaseType().isAssignableFrom(getDeclaredType()); + } + } + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java new file mode 100644 index 0000000000..65572a02f1 --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryCollectingQueryCreationListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 - 2022 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.spring.data.impl.repository; + + +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Moritz Becker + * @author Christian Beikov + * @since 1.5.0 + */ +public class QueryCollectingQueryCreationListener implements QueryCreationListener { + + private final List queryMethods = new ArrayList<>(); + + @Override + public void onCreation(RepositoryQuery query) { + this.queryMethods.add(query.getQueryMethod()); + } + + public List getQueryMethods() { + return queryMethods; + } +} diff --git a/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java new file mode 100644 index 0000000000..ec5cda6fae --- /dev/null +++ b/integration/spring-data/2.7/src/main/java/com/blazebit/persistence/spring/data/impl/repository/QueryExecutionResultHandler.java @@ -0,0 +1,294 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.spring.data.impl.repository; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.repository.util.NullableWrapper; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Simple domain service to convert query results into a dedicated type. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Jens Schauder + */ +class QueryExecutionResultHandler { + + private static final TypeDescriptor WRAPPER_TYPE = TypeDescriptor.valueOf(NullableWrapper.class); + + private final GenericConversionService conversionService; + + private final Object mutex = new Object(); + + // concurrent access guarded by mutex. + private Map descriptorCache = Collections.emptyMap(); + + /** + * Creates a new {@link QueryExecutionResultHandler}. + */ + public QueryExecutionResultHandler(GenericConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Post-processes the given result of a query invocation to match the return type of the given method. + * + * @param result can be {@literal null}. + * @param method must not be {@literal null}. + * @return + */ + @Nullable + public Object postProcessInvocationResult(@Nullable Object result, Method method) { + + if (!processingRequired(result, method.getReturnType())) { + return result; + } + + ReturnTypeDescriptor descriptor = getOrCreateReturnTypeDescriptor(method); + + return postProcessInvocationResult(result, 0, descriptor); + } + + private ReturnTypeDescriptor getOrCreateReturnTypeDescriptor(Method method) { + + Map descriptorCache = this.descriptorCache; + ReturnTypeDescriptor descriptor = descriptorCache.get(method); + + if (descriptor == null) { + + descriptor = ReturnTypeDescriptor.of(method); + + Map updatedDescriptorCache; + + if (descriptorCache.isEmpty()) { + updatedDescriptorCache = Collections.singletonMap(method, descriptor); + } else { + updatedDescriptorCache = new HashMap<>(descriptorCache.size() + 1, 1); + updatedDescriptorCache.putAll(descriptorCache); + updatedDescriptorCache.put(method, descriptor); + + } + + synchronized (mutex) { + this.descriptorCache = updatedDescriptorCache; + } + } + + return descriptor; + } + + /** + * Post-processes the given result of a query invocation to the given type. + * + * @param result can be {@literal null}. + * @param nestingLevel + * @param descriptor must not be {@literal null}. + * @return + */ + @Nullable + Object postProcessInvocationResult(@Nullable Object result, int nestingLevel, ReturnTypeDescriptor descriptor) { + + TypeDescriptor returnTypeDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel); + + if (returnTypeDescriptor == null) { + return result; + } + + Class expectedReturnType = returnTypeDescriptor.getType(); + + result = unwrapOptional(result); + + if (QueryExecutionConverters.supports(expectedReturnType)) { + + // For a wrapper type, try nested resolution first + result = postProcessInvocationResult(result, nestingLevel + 1, descriptor); + + if (conversionRequired(WRAPPER_TYPE, returnTypeDescriptor)) { + return conversionService.convert(new NullableWrapper(result), returnTypeDescriptor); + } + + if (result != null) { + + TypeDescriptor source = TypeDescriptor.valueOf(result.getClass()); + + if (conversionRequired(source, returnTypeDescriptor)) { + return conversionService.convert(result, returnTypeDescriptor); + } + } + } + + if (result != null) { + + if (ReactiveWrapperConverters.supports(expectedReturnType)) { + return ReactiveWrapperConverters.toWrapper(result, expectedReturnType); + } + + if (result instanceof Collection) { + + TypeDescriptor elementDescriptor = descriptor.getReturnTypeDescriptor(nestingLevel + 1); + boolean requiresConversion = requiresConversion((Collection) result, expectedReturnType, elementDescriptor); + + if (!requiresConversion) { + return result; + } + } + + TypeDescriptor resultDescriptor = TypeDescriptor.forObject(result); + return conversionService.canConvert(resultDescriptor, returnTypeDescriptor) + ? conversionService.convert(result, returnTypeDescriptor) + : result; + } + + return Map.class.equals(expectedReturnType) // + ? CollectionFactory.createMap(expectedReturnType, 0) // + : null; + + } + private boolean requiresConversion(Collection collection, Class expectedReturnType, + @Nullable TypeDescriptor elementDescriptor) { + + if (Streamable.class.isAssignableFrom(expectedReturnType) || !expectedReturnType.isInstance(collection)) { + return true; + } + + if (elementDescriptor == null || !Iterable.class.isAssignableFrom(expectedReturnType)) { + return false; + } + + Class type = elementDescriptor.getType(); + + for (Object o : collection) { + + if (!type.isInstance(o)) { + return true; + } + } + + return false; + } + + /** + * Returns whether the configured {@link ConversionService} can convert between the given {@link TypeDescriptor}s and + * the conversion will not be a no-op. + * + * @param source + * @param target + * @return + */ + private boolean conversionRequired(TypeDescriptor source, TypeDescriptor target) { + + return conversionService.canConvert(source, target) // + && !conversionService.canBypassConvert(source, target); + } + + /** + * Unwraps the given value if it's a JDK 8 {@link Optional}. + * + * @param source can be {@literal null}. + * @return + */ + @Nullable + @SuppressWarnings("unchecked") + private static Object unwrapOptional(@Nullable Object source) { + + if (source == null) { + return null; + } + + return Optional.class.isInstance(source) // + ? Optional.class.cast(source).orElse(null) // + : source; + } + + /** + * Returns whether we have to process the given source object in the first place. + * + * @param source can be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + private static boolean processingRequired(@Nullable Object source, Class targetType) { + + return !targetType.isInstance(source) // + || source == null // + || Collection.class.isInstance(source); + } + + /** + * Value object capturing {@link MethodParameter} and {@link TypeDescriptor}s for top and nested levels. + */ + static class ReturnTypeDescriptor { + + private final MethodParameter methodParameter; + private final TypeDescriptor typeDescriptor; + private final @Nullable TypeDescriptor nestedTypeDescriptor; + + private ReturnTypeDescriptor(Method method) { + this.methodParameter = new MethodParameter(method, -1); + this.typeDescriptor = TypeDescriptor.nested(this.methodParameter, 0); + this.nestedTypeDescriptor = TypeDescriptor.nested(this.methodParameter, 1); + } + + /** + * Create a {@link ReturnTypeDescriptor} from a {@link Method}. + * + * @param method + * @return + */ + public static ReturnTypeDescriptor of(Method method) { + return new ReturnTypeDescriptor(method); + } + + /** + * Return the {@link TypeDescriptor} for a nested type declared within the method parameter described by + * {@code nestingLevel} . + * + * @param nestingLevel the nesting level. {@code 0} is the first level, {@code 1} the next inner one. + * @return the {@link TypeDescriptor} or {@literal null} if it could not be obtained. + * @see TypeDescriptor#nested(MethodParameter, int) + */ + @Nullable + public TypeDescriptor getReturnTypeDescriptor(int nestingLevel) { + + // optimizing for nesting level 0 and 1 (Optional, List) + // nesting level 2 (Optional>) uses the slow path. + + switch (nestingLevel) { + case 0: + return typeDescriptor; + case 1: + return nestedTypeDescriptor; + default: + return TypeDescriptor.nested(this.methodParameter, nestingLevel); + } + } + } +} diff --git a/integration/spring-data/base/pom.xml b/integration/spring-data/base/pom.xml index 3e19d8831e..beb5dd0290 100644 --- a/integration/spring-data/base/pom.xml +++ b/integration/spring-data/base/pom.xml @@ -56,9 +56,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided @@ -385,6 +384,44 @@ + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + 1.8 + 1.8 + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + provided + + + + + + + com.infradna.tool + bridge-method-injector + ${version.bridge-injector} + + + bridge-injector + NONE + + + + + + \ No newline at end of file diff --git a/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/base/repository/AbstractEntityViewAwareRepository.java b/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/base/repository/AbstractEntityViewAwareRepository.java index 619819fd54..42cfdce4c3 100644 --- a/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/base/repository/AbstractEntityViewAwareRepository.java +++ b/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/base/repository/AbstractEntityViewAwareRepository.java @@ -40,6 +40,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaEntityGraph; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.data.jpa.repository.support.JpaEntityInformation; @@ -56,6 +57,7 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import java.io.Serializable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -77,6 +79,9 @@ public abstract class AbstractEntityViewAwareRepository entityInformation; private final EntityManager entityManager; @@ -100,6 +105,10 @@ public void setRepositoryMethodMetadata(EntityViewAwareCrudMethodMetadata crudMe this.metadata = crudMethodMetadata; } + public void setEscapeCharacter(EscapeCharacter escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + protected EntityViewAwareCrudMethodMetadata getRepositoryMethodMetadata() { return metadata; } @@ -108,6 +117,10 @@ protected Class getDomainClass() { return entityInformation.getJavaType(); } + protected EntityManager getEntityManager() { + return entityManager; + } + protected abstract Map tryGetFetchGraphHints(EntityManager entityManager, JpaEntityGraph entityGraph, Class entityType); protected Map getQueryHints(boolean applyFetchGraph) { @@ -252,6 +265,11 @@ public void deleteAllInBatch(Iterable entities) { deleteInBatch(entities); } + @Transactional + public void deleteAllById(Iterable ids) { + deleteAllByIdInBatch((Iterable) ids); + } + @Transactional public void deleteAllByIdInBatch(Iterable ids) { @@ -288,26 +306,30 @@ public E getReferenceById(ID id) { } public long count(Example example) { - return executeCountQuery(getCountQuery(new ExampleSpecification<>(example), example.getProbeType())); + return executeCountQuery(getCountQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType())); } public boolean exists(Example example) { - return !getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getResultList() + return !getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getResultList() .isEmpty(); } + public boolean exists(Specification spec) { + return !getQuery(spec, getDomainClass(), (Sort) null).getResultList().isEmpty(); + } + public List findAll(Example example) { - return getQuery(new ExampleSpecification<>(example), example.getProbeType(), (Sort) null).getResultList(); + return getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), (Sort) null).getResultList(); } public List findAll(Example example, Sort sort) { - return getQuery(new ExampleSpecification<>(example), example.getProbeType(), sort).getResultList(); + return getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), sort).getResultList(); } public Page findAll(Example example, Pageable pageable) { - ExampleSpecification spec = new ExampleSpecification<>(example); + ExampleSpecification spec = new ExampleSpecification<>(example, escapeCharacter); Class probeType = example.getProbeType(); - TypedQuery query = getQuery(new ExampleSpecification<>(example), probeType, pageable); + TypedQuery query = getQuery(new ExampleSpecification<>(example, escapeCharacter), probeType, pageable); return pageable == null ? new KeysetAwarePageImpl<>(query.getResultList()) : new KeysetAwarePageImpl<>((PagedList) query.getResultList(), pageable); } @@ -330,15 +352,39 @@ public Page findAll(Pageable pageable) { */ protected static class ExampleSpecification implements Specification { + private static final Method GET_PREDICATE_NEW; + + static { + Method getPredicate = null; + try { + getPredicate = QueryByExamplePredicateBuilder.class.getMethod("getPredicate", Root.class, javax.persistence.criteria.CriteriaBuilder.class, Example.class, EscapeCharacter.class); + } catch (NoSuchMethodException e) { + // Ignore + } + GET_PREDICATE_NEW = getPredicate; + } + private final Example example; + private final EscapeCharacter escapeCharacter; - public ExampleSpecification(Example example) { + public ExampleSpecification(Example example, EscapeCharacter escapeCharacter) { Assert.notNull(example, "Example must not be null!"); + Assert.notNull(escapeCharacter, "EscapeCharacter must not be null!"); this.example = example; + this.escapeCharacter = escapeCharacter; } @Override public Predicate toPredicate(Root root, CriteriaQuery query, javax.persistence.criteria.CriteriaBuilder cb) { + if ( GET_PREDICATE_NEW != null ) { + try { + return (Predicate) GET_PREDICATE_NEW.invoke(null, cb, example, escapeCharacter); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } return QueryByExamplePredicateBuilder.getPredicate(root, cb, example); } } diff --git a/integration/spring-data/pom.xml b/integration/spring-data/pom.xml index 3fb8cea600..93bbeaaf10 100644 --- a/integration/spring-data/pom.xml +++ b/integration/spring-data/pom.xml @@ -38,6 +38,9 @@ 2.2 2.3 2.4 + 2.5 + 2.6 + 2.7 webmvc webflux testsuite diff --git a/integration/spring-data/testsuite/webflux/pom.xml b/integration/spring-data/testsuite/webflux/pom.xml index 845ccc6b5b..bdbe3cf6ed 100644 --- a/integration/spring-data/testsuite/webflux/pom.xml +++ b/integration/spring-data/testsuite/webflux/pom.xml @@ -2535,7 +2535,7 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 test @@ -2555,7 +2555,27 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + test + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + test + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 test diff --git a/integration/spring-data/testsuite/webmvc/pom.xml b/integration/spring-data/testsuite/webmvc/pom.xml index 8da1176a70..28babaae32 100644 --- a/integration/spring-data/testsuite/webmvc/pom.xml +++ b/integration/spring-data/testsuite/webmvc/pom.xml @@ -45,9 +45,8 @@ blaze-persistence-jpa-criteria-impl - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided @@ -2493,7 +2492,7 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 test @@ -2512,7 +2511,26 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + test + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + + + + org.springframework.data + spring-data-jpa + ${version.spring-data-2.7} + test + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 test diff --git a/integration/spring-data/webflux/pom.xml b/integration/spring-data/webflux/pom.xml index 75b43d6262..6ae304a9fa 100644 --- a/integration/spring-data/webflux/pom.xml +++ b/integration/spring-data/webflux/pom.xml @@ -205,7 +205,7 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 provided @@ -219,7 +219,21 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + provided + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7} + + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 provided diff --git a/integration/spring-data/webmvc/pom.xml b/integration/spring-data/webmvc/pom.xml index 6ae0fd4fd9..27f56b5bfe 100644 --- a/integration/spring-data/webmvc/pom.xml +++ b/integration/spring-data/webmvc/pom.xml @@ -257,7 +257,7 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 provided @@ -276,7 +276,26 @@ com.blazebit - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + provided + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + + + + org.springframework.data + spring-data-commons + ${version.spring-data-2.7} + true + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 provided diff --git a/integration/spring-hateoas/webmvc/pom.xml b/integration/spring-hateoas/webmvc/pom.xml index abd7de7636..74013e2b0e 100644 --- a/integration/spring-hateoas/webmvc/pom.xml +++ b/integration/spring-hateoas/webmvc/pom.xml @@ -77,9 +77,8 @@ provided - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.0.Final + jakarta.persistence + jakarta.persistence-api provided @@ -386,7 +385,7 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.5 test @@ -402,7 +401,23 @@ ${project.groupId} - blaze-persistence-integration-spring-data-2.4 + blaze-persistence-integration-spring-data-2.6 + test + + + + + spring-data-2.7.x + + ${version.spring-data-2.7-spring} + ${version.spring-data-2.7-spring-boot} + ${version.spring-data-2.7} + 1.5.1 + + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 test diff --git a/parent/pom.xml b/parent/pom.xml index 686fedbc45..0bcefc36e2 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -93,10 +93,10 @@ 1.8.1 1.9.6 - 1.11.10.RELEASE + 1.11.20.RELEASE 4.3.9.RELEASE 1.5.4.RELEASE - 2.0.4.RELEASE + 2.0.14.RELEASE 5.0.4.RELEASE 2.0.0.RELEASE 2.1.8.RELEASE @@ -117,6 +117,9 @@ 2.6.1 5.3.15 2.6.3 + 2.7.2 + 5.3.22 + 2.7.1 4.7.5 6.0-6 @@ -498,6 +501,21 @@ blaze-persistence-integration-spring-data-2.4 ${project.version} + + ${project.groupId} + blaze-persistence-integration-spring-data-2.5 + ${project.version} + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.6 + ${project.version} + + + ${project.groupId} + blaze-persistence-integration-spring-data-2.7 + ${project.version} + com.blazebit blaze-persistence-integration-spring-data-webmvc diff --git a/pom.xml b/pom.xml index 3a44cf4949..05b458e539 100644 --- a/pom.xml +++ b/pom.xml @@ -314,6 +314,8 @@ **/src/main/java/com/blazebit/persistence/spring/data/base/query/JpaParameters.java **/src/main/java/com/blazebit/persistence/spring/data/base/query/AbstractPartTreeBlazePersistenceQuery.java **/src/main/java/com/blazebit/persistence/spring/data/base/repository/EntityViewAwareCrudMethodMetadataPostProcessor.java + **/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FluentQuerySupport.java + **/src/main/java/com/blazebit/persistence/spring/data/impl/repository/FetchableFluentQueryByExample.java **/src/main/java/com/blazebit/persistence/spring/data/base/SharedEntityManagerCreator.java **/src/main/java/com/blazebit/persistence/spring/data/impl/query/ParameterMetadataProviderImpl.java **/src/main/java/com/blazebit/persistence/spring/data/impl/repository/DefaultRepositoryInformation.java diff --git a/website/src/main/jbake/content/downloads.adoc b/website/src/main/jbake/content/downloads.adoc index 980d51f46f..1c3ea259f8 100644 --- a/website/src/main/jbake/content/downloads.adoc +++ b/website/src/main/jbake/content/downloads.adoc @@ -100,6 +100,27 @@ Older releases can be found on https://github.com/Blazebit/blaze-persistence/rel runtime + + + com.blazebit + blaze-persistence-integration-spring-data-2.7 + ${blaze-persistence.version} + + + + + com.blazebit + blaze-persistence-integration-spring-data-2.6 + ${blaze-persistence.version} + + + + + com.blazebit + blaze-persistence-integration-spring-data-2.5 + ${blaze-persistence.version} + + com.blazebit