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 000000000..eb98d218f --- /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 41243f6a6..4f67cdd6f 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 73f01686c..fbb1a0eb3 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 260200e6f..13819c18d 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 000000000..17e0d18c6 --- /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 000000000..e4b5849df --- /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 f56e70185..07983a868 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 ba503cf58..1a0c31ea7 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 68b4b5591..01f3617f2 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 000000000..7ed4439ad --- /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 a0dc56fa5..ca3174d7b 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 000000000..28b3bdaeb --- /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 {} + +}