From b6f047da0f709a1ae1ec97c6711f382cca4570b5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 May 2025 14:56:47 +0200 Subject: [PATCH] Add support for `RepositoryFragmentsContributor`. Repository fragments contributors can contribute repository fragments such as Querydsl or Cypherdsl fragments based on the repository interface declaration. Fragment contributors can be enabled for configuration to allow external contributions and provide metadata to describe fragments without actually creating instances for AOT description purposes. --- .../support/BuiltinContributor.java | 83 ++++++++++ .../support/Neo4jRepositoryFactory.java | 51 +------ .../support/Neo4jRepositoryFactoryBean.java | 14 +- .../Neo4jRepositoryFactoryCdiBean.java | 3 +- .../Neo4jRepositoryFragmentsContributor.java | 83 ++++++++++ .../support/ReactiveBuiltinContributor.java | 83 ++++++++++ .../ReactiveNeo4jRepositoryFactory.java | 49 +----- .../ReactiveNeo4jRepositoryFactoryBean.java | 14 +- ...veNeo4jRepositoryFragmentsContributor.java | 85 +++++++++++ .../support/Neo4jRepositoryFactoryTest.java | 4 +- ...positoryFragmentsContributorUnitTests.java | 142 +++++++++++++++++ .../ReactiveNeo4jRepositoryFactoryTest.java | 2 +- ...positoryFragmentsContributorUnitTests.java | 143 ++++++++++++++++++ 13 files changed, 664 insertions(+), 92 deletions(-) create mode 100644 src/main/java/org/springframework/data/neo4j/repository/support/BuiltinContributor.java create mode 100644 src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributor.java create mode 100644 src/main/java/org/springframework/data/neo4j/repository/support/ReactiveBuiltinContributor.java create mode 100644 src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFragmentsContributor.java create mode 100644 src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributorUnitTests.java create mode 100644 src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFragmentsContributorUnitTests.java diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/BuiltinContributor.java b/src/main/java/org/springframework/data/neo4j/repository/support/BuiltinContributor.java new file mode 100644 index 0000000000..eb98d218f0 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/support/BuiltinContributor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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 org.springframework.data.neo4j.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.query.CypherdslConditionExecutorImpl; +import org.springframework.data.neo4j.repository.query.QuerydslNeo4jPredicateExecutor; +import org.springframework.data.neo4j.repository.query.SimpleQueryByExampleExecutor; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * Built-in {@link RepositoryFragmentsContributor} contributing Query by Example, Querydsl, and Cypher condition + * fragments if a repository implements the corresponding interfaces. + * + * @author Mark Paluch + * @since 8.0 + */ +enum BuiltinContributor implements Neo4jRepositoryFragmentsContributor { + + INSTANCE; + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public RepositoryFragments contribute(RepositoryMetadata metadata, Neo4jEntityInformation entityInformation, + Neo4jOperations operations, Neo4jMappingContext mappingContext) { + + RepositoryFragments fragments = RepositoryFragments + .of(RepositoryFragment.implemented(new SimpleQueryByExampleExecutor(operations, mappingContext))); + + if (isQuerydslRepository(metadata)) { + fragments = fragments.append(RepositoryFragment + .implemented(new QuerydslNeo4jPredicateExecutor(mappingContext, entityInformation, operations))); + } + + if (CypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments + .append(RepositoryFragment.implemented(new CypherdslConditionExecutorImpl(entityInformation, operations))); + } + + return fragments; + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + + RepositoryFragments fragments = RepositoryFragments + .of(RepositoryFragment.structural(SimpleQueryByExampleExecutor.class)); + + if (isQuerydslRepository(metadata)) { + fragments = fragments.append(RepositoryFragment.structural(QuerydslNeo4jPredicateExecutor.class)); + } + + if (CypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append(RepositoryFragment.structural(CypherdslConditionExecutorImpl.class)); + } + + return fragments; + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 41243f6a65..4f67cdd6f9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -24,19 +24,13 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.query.CypherdslConditionExecutorImpl; import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy; -import org.springframework.data.neo4j.repository.query.QuerydslNeo4jPredicateExecutor; -import org.springframework.data.neo4j.repository.query.SimpleQueryByExampleExecutor; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; -import org.springframework.data.querydsl.QuerydslUtils; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -46,6 +40,7 @@ * * @author Gerrit Meier * @author Michael J. Simons + * @author Mark Paluch * @since 6.0 */ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { @@ -54,12 +49,15 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { private final Neo4jMappingContext mappingContext; + private final Neo4jRepositoryFragmentsContributor fragmentsContributor; + private Configuration cypherDSLConfiguration = Configuration.defaultConfig(); - Neo4jRepositoryFactory(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext) { + Neo4jRepositoryFactory(Neo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, Neo4jRepositoryFragmentsContributor fragmentsContributor) { this.neo4jOperations = neo4jOperations; this.mappingContext = mappingContext; + this.fragmentsContributor = fragmentsContributor; } @Override @@ -79,44 +77,7 @@ protected Object getTargetRepository(RepositoryInformation metadata) { @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - - RepositoryFragments fragments = RepositoryFragments.empty(); - - Object byExampleExecutor = instantiateClass(SimpleQueryByExampleExecutor.class, neo4jOperations, - mappingContext); - - fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); - - boolean isQueryDslRepository = QuerydslUtils.QUERY_DSL_PRESENT - && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class)); - } - - if (CypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { - - fragments = fragments.append(createDSLExecutorFragment(metadata, CypherdslConditionExecutorImpl.class)); - } - - return fragments; - } - - private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class implementor) { - - Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations); - - return RepositoryFragment.implemented(querydslFragment); - } - - private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { - - Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); - - return RepositoryFragment.implemented(querydslFragment); + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), neo4jOperations, mappingContext); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java index 73f01686c0..fbb1a0eb35 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryBean.java @@ -30,6 +30,7 @@ * * @author Michael J. Simons * @author Gerrit Meier + * @author Mark Paluch * @param the type of the repository * @param type of the domain class to map * @param identifier type in the domain class @@ -43,6 +44,8 @@ public final class Neo4jRepositoryFactoryBean, S, ID private Neo4jMappingContext neo4jMappingContext; + private Neo4jRepositoryFragmentsContributor repositoryFragmentsContributor = Neo4jRepositoryFragmentsContributor.DEFAULT; + /** * Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface. * @@ -61,8 +64,17 @@ public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { this.neo4jMappingContext = neo4jMappingContext; } + @Override + public Neo4jRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + + public void setRepositoryFragmentsContributor(Neo4jRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new Neo4jRepositoryFactory(neo4jOperations, neo4jMappingContext); + return new Neo4jRepositoryFactory(neo4jOperations, neo4jMappingContext, repositoryFragmentsContributor); } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java index 260200e6ff..13819c18d2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryCdiBean.java @@ -35,6 +35,7 @@ * The CDI pendant to the {@link Neo4jRepositoryFactoryBean}. It creates instances of {@link Neo4jRepositoryFactory}. * * @author Michael J. Simons + * @author Mark Paluch * @param The type of the repository being created * @soundtrack Various - TRON Legacy R3conf1gur3d * @since 6.0 @@ -57,7 +58,7 @@ protected T create(CreationalContext creationalContext, Class repositoryTy Neo4jOperations neo4jOperations = getReference(Neo4jOperations.class, creationalContext); Neo4jMappingContext mappingContext = getReference(Neo4jMappingContext.class, creationalContext); - return create(() -> new Neo4jRepositoryFactory(neo4jOperations, mappingContext), repositoryType); + return create(() -> new Neo4jRepositoryFactory(neo4jOperations, mappingContext, Neo4jRepositoryFragmentsContributor.DEFAULT), repositoryType); } private RT getReference(Class clazz, CreationalContext creationalContext) { diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributor.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributor.java new file mode 100644 index 0000000000..17e0d18c6f --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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 org.springframework.data.neo4j.repository.support; + +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * Neo4j-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. Typically, + * contributes Query by Example Executor, Querydsl, and Cypher condition DSL fragments. + *

+ * Implementations must define a no-args constructor. + * + * @author Mark Paluch + * @since 8.0 + * @see org.springframework.data.repository.query.QueryByExampleExecutor + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor + * @see CypherdslConditionExecutor + */ +public interface Neo4jRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + Neo4jRepositoryFragmentsContributor DEFAULT = BuiltinContributor.INSTANCE; + + /** + * Returns a composed {@code Neo4jRepositoryFragmentsContributor} that first applies this contributor to its inputs, + * and then applies the {@code after} contributor concatenating effectively both results. If evaluation of either + * contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default Neo4jRepositoryFragmentsContributor andThen(Neo4jRepositoryFragmentsContributor after) { + + Assert.notNull(after, "Neo4jRepositoryFragmentsContributor must not be null"); + + return new Neo4jRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + Neo4jEntityInformation entityInformation, Neo4jOperations operations, + Neo4jMappingContext mappingContext) { + return Neo4jRepositoryFragmentsContributor.this + .contribute(metadata, entityInformation, operations, mappingContext) + .append(after.contribute(metadata, entityInformation, operations, mappingContext)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return Neo4jRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add Neo4j-specific + * extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + Neo4jEntityInformation entityInformation, Neo4jOperations operations, Neo4jMappingContext mappingContext); +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveBuiltinContributor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveBuiltinContributor.java new file mode 100644 index 0000000000..e4b5849dff --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveBuiltinContributor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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 org.springframework.data.neo4j.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.query.ReactiveCypherdslConditionExecutorImpl; +import org.springframework.data.neo4j.repository.query.ReactiveQuerydslNeo4jPredicateExecutor; +import org.springframework.data.neo4j.repository.query.SimpleReactiveQueryByExampleExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Reactive Built-in {@link ReactiveNeo4jRepositoryFragmentsContributor} contributing Query by Example, Querydsl, and + * Cypher condition fragments if a repository implements the corresponding interfaces. + * + * @author Mark Paluch + * @since 8.0 + */ +enum ReactiveBuiltinContributor implements ReactiveNeo4jRepositoryFragmentsContributor { + + INSTANCE; + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public RepositoryFragments contribute(RepositoryMetadata metadata, Neo4jEntityInformation entityInformation, + ReactiveNeo4jOperations operations, Neo4jMappingContext mappingContext) { + + RepositoryFragments fragments = RepositoryFragments + .of(RepositoryFragment.implemented(new SimpleReactiveQueryByExampleExecutor(operations, mappingContext))); + + if (isQuerydslRepository(metadata)) { + fragments = fragments.append(RepositoryFragment + .implemented(new ReactiveQuerydslNeo4jPredicateExecutor(mappingContext, entityInformation, operations))); + } + + if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append( + RepositoryFragment.implemented(new ReactiveCypherdslConditionExecutorImpl(entityInformation, operations))); + } + + return fragments; + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + + RepositoryFragments fragments = RepositoryFragments + .of(RepositoryFragment.structural(SimpleReactiveQueryByExampleExecutor.class)); + + if (isQuerydslRepository(metadata)) { + fragments = fragments.append(RepositoryFragment.structural(ReactiveQuerydslNeo4jPredicateExecutor.class)); + } + + if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append(RepositoryFragment.structural(ReactiveCypherdslConditionExecutorImpl.class)); + } + + return fragments; + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index f56e701856..07983a8685 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -48,6 +48,7 @@ * @author Gerrit Meier * @author Michael J. Simons * @author Niklas Krieger + * @author Mark Paluch * @since 6.0 */ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -56,12 +57,15 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp private final Neo4jMappingContext mappingContext; + private final ReactiveNeo4jRepositoryFragmentsContributor fragmentsContributor; + private Configuration cypherDSLConfiguration = Configuration.defaultConfig(); - ReactiveNeo4jRepositoryFactory(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext) { + ReactiveNeo4jRepositoryFactory(ReactiveNeo4jOperations neo4jOperations, Neo4jMappingContext mappingContext, ReactiveNeo4jRepositoryFragmentsContributor fragmentsContributor) { this.neo4jOperations = neo4jOperations; this.mappingContext = mappingContext; + this.fragmentsContributor = fragmentsContributor; } @Override @@ -81,44 +85,7 @@ protected Object getTargetRepository(RepositoryInformation metadata) { @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - - RepositoryFragments fragments = RepositoryFragments.empty(); - - SimpleReactiveQueryByExampleExecutor byExampleExecutor = instantiateClass( - SimpleReactiveQueryByExampleExecutor.class, neo4jOperations, mappingContext); - - fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); - - boolean isQueryDslRepository = QuerydslUtils.QUERY_DSL_PRESENT - && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); - } - - if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { - - fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveCypherdslConditionExecutorImpl.class)); - } - - return fragments; - } - - private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class implementor) { - - Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations); - - return RepositoryFragment.implemented(querydslFragment); - } - - private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { - - Neo4jEntityInformation entityInformation = getEntityInformation(metadata); - Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); - - return RepositoryFragment.implemented(querydslFragment); + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), neo4jOperations, mappingContext); } @Override @@ -126,8 +93,8 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleReactiveNeo4jRepository.class; } - - @Override protected Optional getQueryLookupStrategy(Key key, + @Override + protected Optional getQueryLookupStrategy(Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional .of(new ReactiveNeo4jQueryLookupStrategy(neo4jOperations, mappingContext, valueExpressionDelegate, cypherDSLConfiguration)); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java index ba503cf58a..1a0c31ea79 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryBean.java @@ -30,6 +30,7 @@ * * @author Gerrit Meier * @author Michael J. Simons + * @author Mark Paluch * @param the type of the repository * @param type of the domain class to map * @param identifier type in the domain class @@ -43,6 +44,8 @@ public final class ReactiveNeo4jRepositoryFactoryBean + * Implementations must define a no-args constructor. + * + * @author Mark Paluch + * @since 8.0 + * @see org.springframework.data.repository.query.QueryByExampleExecutor + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor + * @see CypherdslConditionExecutor + */ +public interface ReactiveNeo4jRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + ReactiveNeo4jRepositoryFragmentsContributor DEFAULT = ReactiveBuiltinContributor.INSTANCE; + + /** + * Returns a composed {@code ReactiveNeo4jRepositoryFragmentsContributor} that first applies this contributor to its + * inputs, and then applies the {@code after} contributor concatenating effectively both results. If evaluation of + * either contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default ReactiveNeo4jRepositoryFragmentsContributor andThen(ReactiveNeo4jRepositoryFragmentsContributor after) { + + Assert.notNull(after, "Neo4jRepositoryFragmentsContributor must not be null"); + + return new ReactiveNeo4jRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + Neo4jEntityInformation entityInformation, ReactiveNeo4jOperations operations, + Neo4jMappingContext mappingContext) { + return ReactiveNeo4jRepositoryFragmentsContributor.this + .contribute(metadata, entityInformation, operations, mappingContext) + .append(after.contribute(metadata, entityInformation, operations, mappingContext)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return ReactiveNeo4jRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add Neo4j-specific + * extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + Neo4jEntityInformation entityInformation, ReactiveNeo4jOperations operations, + Neo4jMappingContext mappingContext); + +} diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java index 68b4b55914..01f3617f23 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactoryTest.java @@ -58,7 +58,7 @@ class Neo4jRepositoryFactoryTest { */ @Nested class IdentifierTypeCheck { - @Spy private Neo4jRepositoryFactory neo4jRepositoryFactory = new Neo4jRepositoryFactory(null, null); + @Spy private Neo4jRepositoryFactory neo4jRepositoryFactory = new Neo4jRepositoryFactory(null, null, null); private Neo4jEntityInformation entityInformation; private RepositoryInformation metadata; @@ -109,7 +109,7 @@ void prepareContext() { Arrays.asList(ThingWithAllAdditionalTypes.class, ThingWithAllCypherTypes.class, ThingWithCompositeProperties.class))); - repositoryFactory = new Neo4jRepositoryFactory(Mockito.mock(Neo4jTemplate.class), mappingContext); + repositoryFactory = new Neo4jRepositoryFactory(Mockito.mock(Neo4jTemplate.class), mappingContext, Neo4jRepositoryFragmentsContributor.DEFAULT); } @Test diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributorUnitTests.java b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..7ed4439ad0 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2025 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 org.springframework.data.neo4j.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes; +import org.springframework.data.neo4j.repository.query.CypherdslConditionExecutorImpl; +import org.springframework.data.neo4j.repository.query.QuerydslNeo4jPredicateExecutor; +import org.springframework.data.neo4j.repository.query.SimpleQueryByExampleExecutor; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Unit tests for {@link Neo4jRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class Neo4jRepositoryFragmentsContributorUnitTests { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + Neo4jOperations operations = mock(Neo4jOperations.class); + + @Test + void builtInContributorShouldCreateFragments() { + + RepositoryComposition.RepositoryFragments fragments = Neo4jRepositoryFragmentsContributor.DEFAULT.contribute( + AbstractRepositoryMetadata.getMetadata(CypherdslRepository.class), + new DefaultNeo4jEntityInformation<>(mappingContext.getPersistentEntity(ThingWithAllAdditionalTypes.class)), + operations, mappingContext); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment queryByExample = iterator.next(); + assertThat(queryByExample.getImplementationClass()).contains(SimpleQueryByExampleExecutor.class); + + RepositoryFragment cypherdsl = iterator.next(); + assertThat(cypherdsl.getImplementationClass()).contains(CypherdslConditionExecutorImpl.class); + } + + @Test + void builtInContributorShouldDescribeFragments() { + + RepositoryComposition.RepositoryFragments fragments = Neo4jRepositoryFragmentsContributor.DEFAULT + .describe(AbstractRepositoryMetadata.getMetadata(ComposedRepository.class)); + + assertThat(fragments).hasSize(3); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment queryByExample = iterator.next(); + assertThat(queryByExample.getImplementationClass()).contains(SimpleQueryByExampleExecutor.class); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(QuerydslNeo4jPredicateExecutor.class); + + RepositoryFragment cypherdsl = iterator.next(); + assertThat(cypherdsl.getImplementationClass()).contains(CypherdslConditionExecutorImpl.class); + } + + @Test + void composedContributorShouldCreateFragments() { + + Neo4jRepositoryFragmentsContributor contributor = Neo4jRepositoryFragmentsContributor.DEFAULT + .andThen(MyNeo4jRepositoryFragmentsContributor.INSTANCE); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslRepository.class), + new DefaultNeo4jEntityInformation<>(mappingContext.getPersistentEntity(ThingWithAllAdditionalTypes.class)), + operations, mappingContext); + + assertThat(fragments).hasSize(3); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment queryByExample = iterator.next(); + assertThat(queryByExample.getImplementationClass()).contains(SimpleQueryByExampleExecutor.class); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(QuerydslNeo4jPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyNeo4jRepositoryFragmentsContributor implements Neo4jRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + Neo4jEntityInformation entityInformation, Neo4jOperations operations, + Neo4jMappingContext mappingContext) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslRepository + extends Repository, QuerydslPredicateExecutor {} + + interface CypherdslRepository + extends Repository, CypherdslConditionExecutor {} + + interface ComposedRepository extends Repository, + QuerydslPredicateExecutor, CypherdslConditionExecutor {} + +} diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java b/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java index a0dc56fa5a..ca3174d7b7 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactoryTest.java @@ -44,7 +44,7 @@ class ReactiveNeo4jRepositoryFactoryTest { @Nested class IdentifierTypeCheck { - @Spy private ReactiveNeo4jRepositoryFactory neo4jRepositoryFactory = new ReactiveNeo4jRepositoryFactory(null, null); + @Spy private ReactiveNeo4jRepositoryFactory neo4jRepositoryFactory = new ReactiveNeo4jRepositoryFactory(null, null, null); private Neo4jEntityInformation entityInformation; private RepositoryInformation metadata; diff --git a/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFragmentsContributorUnitTests.java b/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..28b3bdaeb0 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2025 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 org.springframework.data.neo4j.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes; +import org.springframework.data.neo4j.repository.query.ReactiveCypherdslConditionExecutorImpl; +import org.springframework.data.neo4j.repository.query.ReactiveQuerydslNeo4jPredicateExecutor; +import org.springframework.data.neo4j.repository.query.SimpleReactiveQueryByExampleExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Unit tests for {@link ReactiveNeo4jRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class ReactiveNeo4jRepositoryFragmentsContributorUnitTests { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + ReactiveNeo4jOperations operations = mock(ReactiveNeo4jOperations.class); + + @Test + void builtInContributorShouldCreateFragments() { + + RepositoryComposition.RepositoryFragments fragments = ReactiveNeo4jRepositoryFragmentsContributor.DEFAULT + .contribute(AbstractRepositoryMetadata.getMetadata(CypherdslRepository.class), + new DefaultNeo4jEntityInformation<>(mappingContext.getPersistentEntity(ThingWithAllAdditionalTypes.class)), + operations, mappingContext); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment queryByExample = iterator.next(); + assertThat(queryByExample.getImplementationClass()).contains(SimpleReactiveQueryByExampleExecutor.class); + + RepositoryFragment cypherdsl = iterator.next(); + assertThat(cypherdsl.getImplementationClass()).contains(ReactiveCypherdslConditionExecutorImpl.class); + } + + @Test + void builtInContributorShouldDescribeFragments() { + + RepositoryComposition.RepositoryFragments fragments = ReactiveNeo4jRepositoryFragmentsContributor.DEFAULT + .describe(AbstractRepositoryMetadata.getMetadata(ComposedRepository.class)); + + assertThat(fragments).hasSize(3); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment queryByExample = iterator.next(); + assertThat(queryByExample.getImplementationClass()).contains(SimpleReactiveQueryByExampleExecutor.class); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(ReactiveQuerydslNeo4jPredicateExecutor.class); + + RepositoryFragment cypherdsl = iterator.next(); + assertThat(cypherdsl.getImplementationClass()).contains(ReactiveCypherdslConditionExecutorImpl.class); + } + + @Test + void composedContributorShouldCreateFragments() { + + ReactiveNeo4jRepositoryFragmentsContributor contributor = ReactiveNeo4jRepositoryFragmentsContributor.DEFAULT + .andThen(MyNeo4jRepositoryFragmentsContributor.INSTANCE); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslRepository.class), + new DefaultNeo4jEntityInformation<>(mappingContext.getPersistentEntity(ThingWithAllAdditionalTypes.class)), + operations, mappingContext); + + assertThat(fragments).hasSize(3); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment queryByExample = iterator.next(); + assertThat(queryByExample.getImplementationClass()).contains(SimpleReactiveQueryByExampleExecutor.class); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(ReactiveQuerydslNeo4jPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyNeo4jRepositoryFragmentsContributor implements ReactiveNeo4jRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + Neo4jEntityInformation entityInformation, ReactiveNeo4jOperations operations, + Neo4jMappingContext mappingContext) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslRepository extends Repository, + ReactiveQuerydslPredicateExecutor {} + + interface CypherdslRepository extends Repository, + ReactiveCypherdslConditionExecutor {} + + interface ComposedRepository extends Repository, + ReactiveQuerydslPredicateExecutor, + ReactiveCypherdslConditionExecutor {} + +}