diff --git a/pom.xml b/pom.xml index 9d2f70be2..44e56605d 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-cassandra-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-1566-SNAPSHOT pom Spring Data for Apache Cassandra diff --git a/spring-data-cassandra-distribution/pom.xml b/spring-data-cassandra-distribution/pom.xml index cf545591f..59d0e59f5 100644 --- a/spring-data-cassandra-distribution/pom.xml +++ b/spring-data-cassandra-distribution/pom.xml @@ -8,7 +8,7 @@ org.springframework.data spring-data-cassandra-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-1566-SNAPSHOT ../pom.xml diff --git a/spring-data-cassandra/pom.xml b/spring-data-cassandra/pom.xml index 7f28c7f2b..b1c356104 100644 --- a/spring-data-cassandra/pom.xml +++ b/spring-data-cassandra/pom.xml @@ -8,7 +8,7 @@ org.springframework.data spring-data-cassandra-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-1566-SNAPSHOT ../pom.xml diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/CassandraRepositoryConfigurationExtension.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/CassandraRepositoryConfigurationExtension.java index 0595f5751..87c8bee44 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/CassandraRepositoryConfigurationExtension.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/CassandraRepositoryConfigurationExtension.java @@ -25,6 +25,7 @@ import org.springframework.data.cassandra.core.mapping.Table; import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.data.cassandra.repository.support.CassandraRepositoryFactoryBean; +import org.springframework.data.cassandra.repository.support.SimpleCassandraRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; @@ -40,6 +41,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Mateusz Szymczak + * @author Chris Bono */ public class CassandraRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { @@ -55,6 +57,11 @@ protected String getModulePrefix() { return "cassandra"; } + @Override + public String getRepositoryBaseClassName() { + return SimpleCassandraRepository.class.getName(); + } + @Override public String getRepositoryFactoryBeanClassName() { return CassandraRepositoryFactoryBean.class.getName(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/ReactiveCassandraRepositoryConfigurationExtension.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/ReactiveCassandraRepositoryConfigurationExtension.java index ab45eb8c5..c1f2fb332 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/ReactiveCassandraRepositoryConfigurationExtension.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/config/ReactiveCassandraRepositoryConfigurationExtension.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.data.cassandra.repository.ReactiveCassandraRepository; import org.springframework.data.cassandra.repository.support.ReactiveCassandraRepositoryFactoryBean; +import org.springframework.data.cassandra.repository.support.SimpleReactiveCassandraRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.core.RepositoryMetadata; @@ -30,6 +31,7 @@ * {@link RepositoryConfigurationExtension} for Cassandra. * * @author Mark Paluch + * @author Chris Bono * @since 2.0 */ public class ReactiveCassandraRepositoryConfigurationExtension extends CassandraRepositoryConfigurationExtension { @@ -39,6 +41,11 @@ public String getModuleName() { return "Reactive Cassandra"; } + @Override + public String getRepositoryBaseClassName() { + return SimpleReactiveCassandraRepository.class.getName(); + } + @Override public String getRepositoryFactoryBeanClassName() { return ReactiveCassandraRepositoryFactoryBean.class.getName(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java index 02593f75e..89427db29 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java @@ -34,6 +34,7 @@ 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.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -50,6 +51,7 @@ * @author Thomas Darimont * @author Mark Paluch * @author John Blum + * @author Chris Bono */ public class CassandraRepositoryFactory extends RepositoryFactorySupport { @@ -57,6 +59,8 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { private final CassandraOperations operations; + private CassandraRepositoryFragmentsContributor fragmentsContributor = CassandraRepositoryFragmentsContributor.DEFAULT; + /** * Create a new {@link CassandraRepositoryFactory} with the given {@link CassandraOperations}. * @@ -70,6 +74,17 @@ public CassandraRepositoryFactory(CassandraOperations operations) { this.mappingContext = operations.getConverter().getMappingContext(); } + /** + * Configures the {@link CassandraRepositoryFragmentsContributor} to be used. Defaults to + * {@link CassandraRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 5.0 + */ + public void setFragmentsContributor(CassandraRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + @Override protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { @@ -105,6 +120,23 @@ protected Optional getQueryLookupStrategy(@Nullable Key key new CachingValueExpressionDelegate(valueExpressionDelegate), mappingContext)); } + @Override + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + return getRepositoryFragments(metadata, operations); + } + + /** + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Cassandra-specific extensions. + * Built-in fragment contribution can be customized by configuring {@link CassandraRepositoryFragmentsContributor}. + * + * @param metadata repository metadata. + * @param operations the Cassandra operations manager. + * @return {@link RepositoryFragments} to be added to the repository. + * @since 5.0 + */ + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, CassandraOperations operations) { + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata.getDomainType()), operations); + } private record CassandraQueryLookupStrategy(CassandraOperations operations, ValueExpressionDelegate valueExpressionDelegate, @@ -132,4 +164,3 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, } } - diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryBean.java index 921a325cc..afbccbded 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryBean.java @@ -19,6 +19,7 @@ import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.CassandraTemplate; import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -31,12 +32,17 @@ * @author John Blum * @author Oliver Gierke * @author Mark Paluch + * @author Chris Bono */ public class CassandraRepositoryFactoryBean, S, ID> extends RepositoryFactoryBeanSupport { + private boolean mappingContextConfigured = false; + private @Nullable CassandraOperations cassandraOperations; + private CassandraRepositoryFragmentsContributor repositoryFragmentsContributor = CassandraRepositoryFragmentsContributor.DEFAULT; + /** * Create a new {@link CassandraRepositoryFactoryBean} for the given repository interface. * @@ -51,7 +57,35 @@ protected RepositoryFactorySupport createRepositoryFactory() { Assert.state(cassandraOperations != null, "CassandraOperations must not be null"); - return new CassandraRepositoryFactory(cassandraOperations); + CassandraRepositoryFactory factory = getFactoryInstance(cassandraOperations); + factory.setFragmentsContributor(repositoryFragmentsContributor); + return factory; + } + + /** + * Creates and initializes a {@link CassandraRepositoryFactory} instance. + * + * @param operations the Cassandra operations + * @return new {@link CassandraRepositoryFactory} instance + */ + protected CassandraRepositoryFactory getFactoryInstance(CassandraOperations operations) { + return new CassandraRepositoryFactory(operations); + } + + @Override + public CassandraRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return this.repositoryFragmentsContributor; + } + + /** + * Configures the {@link CassandraRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 5.0 + */ + public void setRepositoryFragmentsContributor(CassandraRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; } /** @@ -61,9 +95,15 @@ protected RepositoryFactorySupport createRepositoryFactory() { * on Apache Cassandra. */ public void setCassandraTemplate(CassandraTemplate cassandraTemplate) { - this.cassandraOperations = cassandraTemplate; - setMappingContext(cassandraTemplate.getConverter().getMappingContext()); + } + + @Override + protected void setMappingContext(MappingContext mappingContext) { + + super.setMappingContext(mappingContext); + + this.mappingContextConfigured = true; } @Override @@ -72,5 +112,9 @@ public void afterPropertiesSet() { super.afterPropertiesSet(); Assert.notNull(cassandraOperations, "CassandraOperations must not be null"); + + if (!mappingContextConfigured) { + setMappingContext(cassandraOperations.getConverter().getMappingContext()); + } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFragmentsContributor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFragmentsContributor.java new file mode 100644 index 000000000..27963e4ea --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFragmentsContributor.java @@ -0,0 +1,77 @@ +/* + * 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.cassandra.repository.support; + +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * Cassandra-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + * + * @author Chris Bono + * @since 5.0 + */ +public interface CassandraRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + CassandraRepositoryFragmentsContributor DEFAULT = EmptyFragmentsContributor.INSTANCE; + + /** + * Returns a composed {@code CassandraRepositoryFragmentsContributor} 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 CassandraRepositoryFragmentsContributor andThen(CassandraRepositoryFragmentsContributor after) { + + Assert.notNull(after, "CassandraRepositoryFragmentsContributor must not be null"); + + return new CassandraRepositoryFragmentsContributor() { + + @Override + public RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, CassandraOperations operations) { + return CassandraRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations) + .append(after.contribute(metadata, entityInformation, operations)); + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + return CassandraRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add + * Cassandra-specific extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @return {@link RepositoryFragments} to be added to the repository. + */ + RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, CassandraOperations operations); + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/EmptyFragmentsContributor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/EmptyFragmentsContributor.java new file mode 100644 index 000000000..3bf69f572 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/EmptyFragmentsContributor.java @@ -0,0 +1,43 @@ +/* + * 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.cassandra.repository.support; + +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; + +/** + * Implementation of {@link CassandraRepositoryFragmentsContributor} that contributes empty fragments by default. + * + * @author Chris Bono + * @since 5.0 + */ +enum EmptyFragmentsContributor implements CassandraRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, CassandraOperations operations) { + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.empty(); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java index 65dc0f6d1..fea55f9d6 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactory.java @@ -34,6 +34,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; @@ -46,6 +47,7 @@ * * @author Mark Paluch * @author Marcin Grzejszczak + * @author Chris Bono * @since 2.0 */ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -54,6 +56,8 @@ public class ReactiveCassandraRepositoryFactory extends ReactiveRepositoryFactor private final MappingContext, ? extends CassandraPersistentProperty> mappingContext; + private ReactiveCassandraRepositoryFragmentsContributor fragmentsContributor = ReactiveCassandraRepositoryFragmentsContributor.DEFAULT; + /** * Create a new {@link ReactiveCassandraRepositoryFactory} with the given {@link ReactiveCassandraOperations}. * @@ -67,6 +71,17 @@ public ReactiveCassandraRepositoryFactory(ReactiveCassandraOperations cassandraO this.mappingContext = cassandraOperations.getConverter().getMappingContext(); } + /** + * Configures the {@link ReactiveCassandraRepositoryFragmentsContributor} to be used. Defaults to + * {@link ReactiveCassandraRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 5.0 + */ + public void setFragmentsContributor(ReactiveCassandraRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + @Override protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { @@ -101,6 +116,24 @@ public CassandraEntityInformation getEntityInformation(Class d return new MappingCassandraEntityInformation<>((CassandraPersistentEntity) entity, operations.getConverter()); } + @Override + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + return getRepositoryFragments(metadata, operations); + } + + /** + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Cassandra-specific extensions. + * Built-in fragment contribution can be customized by configuring {@link CassandraRepositoryFragmentsContributor}. + * + * @param metadata repository metadata. + * @param operations the Cassandra operations manager. + * @return {@link RepositoryFragments} to be added to the repository. + * @since 5.0 + */ + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, ReactiveCassandraOperations operations) { + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata.getDomainType()), operations); + } + /** * {@link QueryLookupStrategy} to create * {@link org.springframework.data.cassandra.repository.query.PartTreeCassandraQuery} instances. diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java index c2dba1dc8..5aa9401f3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFactoryBean.java @@ -29,6 +29,7 @@ * {@link org.springframework.data.cassandra.repository.ReactiveCassandraRepository} instances. * * @author Mark Paluch + * @author Chris Bono * @since 2.0 * @see org.springframework.data.repository.reactive.ReactiveSortingRepository */ @@ -39,6 +40,8 @@ public class ReactiveCassandraRepositoryFactoryBean, private @Nullable ReactiveCassandraOperations operations; + private ReactiveCassandraRepositoryFragmentsContributor repositoryFragmentsContributor = ReactiveCassandraRepositoryFragmentsContributor.DEFAULT; + /** * Create a new {@link ReactiveCassandraRepositoryFactoryBean} for the given repository interface. * @@ -71,7 +74,9 @@ protected final RepositoryFactorySupport createRepositoryFactory() { Assert.state(operations != null, "ReactiveCassandraOperations must not be null"); - return getFactoryInstance(operations); + ReactiveCassandraRepositoryFactory factory = getFactoryInstance(operations); + factory.setFragmentsContributor(repositoryFragmentsContributor); + return factory; } /** @@ -80,10 +85,26 @@ protected final RepositoryFactorySupport createRepositoryFactory() { * @param operations * @return */ - protected RepositoryFactorySupport getFactoryInstance(ReactiveCassandraOperations operations) { + protected ReactiveCassandraRepositoryFactory getFactoryInstance(ReactiveCassandraOperations operations) { return new ReactiveCassandraRepositoryFactory(operations); } + @Override + public ReactiveCassandraRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return this.repositoryFragmentsContributor; + } + + /** + * Configures the {@link ReactiveCassandraRepositoryFragmentsContributor} to contribute built-in fragment + * functionality to the repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 5.0 + */ + public void setRepositoryFragmentsContributor(ReactiveCassandraRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + @Override public void afterPropertiesSet() { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFragmentsContributor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFragmentsContributor.java new file mode 100644 index 000000000..6715b1a03 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveCassandraRepositoryFragmentsContributor.java @@ -0,0 +1,77 @@ +/* + * 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.cassandra.repository.support; + +import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * Reactive Cassandra-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + * + * @author Chris Bono + * @since 5.0 + */ +public interface ReactiveCassandraRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + ReactiveCassandraRepositoryFragmentsContributor DEFAULT = ReactiveEmptyFragmentsContributor.INSTANCE; + + /** + * Returns a composed {@code ReactiveCassandraRepositoryFragmentsContributor} 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 ReactiveCassandraRepositoryFragmentsContributor andThen(ReactiveCassandraRepositoryFragmentsContributor after) { + + Assert.notNull(after, "ReactiveCassandraRepositoryFragmentsContributor must not be null"); + + return new ReactiveCassandraRepositoryFragmentsContributor() { + + @Override + public RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, ReactiveCassandraOperations operations) { + return ReactiveCassandraRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations) + .append(after.contribute(metadata, entityInformation, operations)); + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + return ReactiveCassandraRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add + * Cassandra-specific extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @return {@link RepositoryFragments} to be added to the repository. + */ + RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, ReactiveCassandraOperations operations); + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveEmptyFragmentsContributor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveEmptyFragmentsContributor.java new file mode 100644 index 000000000..76426b560 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/ReactiveEmptyFragmentsContributor.java @@ -0,0 +1,44 @@ +/* + * 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.cassandra.repository.support; + +import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; + +/** + * Implementation of {@link ReactiveCassandraRepositoryFragmentsContributor} that contributes empty fragments by + * default. + * + * @author Chris Bono + * @since 5.0 + */ +enum ReactiveEmptyFragmentsContributor implements ReactiveCassandraRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, ReactiveCassandraOperations operations) { + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.empty(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraRepositoryFragmentsContributorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 000000000..2a457d37e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,107 @@ +/* + * 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.cassandra.support; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.domain.User; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.cassandra.repository.support.CassandraRepositoryFragmentsContributor; +import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; +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.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragment; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link CassandraRepositoryFragmentsContributor}. + * + * @author Chris Bono + */ +class CassandraRepositoryFragmentsContributorUnitTests { + + @Test // GH-3279 + void composedContributorShouldCreateFragments() { + + var mappingContext = new CassandraMappingContext(); + var converter = new MappingCassandraConverter(mappingContext); + CassandraOperations operations = mock(); + when(operations.getConverter()).thenReturn(converter); + + var contributor = CassandraRepositoryFragmentsContributor.DEFAULT + .andThen(MyCassandraRepositoryFragmentsContributor.INSTANCE) + .andThen(MyOtherCassandraRepositoryFragmentsContributor.INSTANCE); + + var fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(MyUserRepo.class), + new MappingCassandraEntityInformation<>(mappingContext.getPersistentEntity(User.class), converter), + operations); + + assertThat(fragments.stream()) + .extracting(RepositoryFragment::getImplementationClass) + .containsExactly(Optional.of(MyFragment.class), Optional.of(MyOtherFragment.class)); + } + + enum MyCassandraRepositoryFragmentsContributor implements CassandraRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, CassandraOperations operations) { + return RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryFragments.just(new MyFragment()); + } + } + + enum MyOtherCassandraRepositoryFragmentsContributor implements CassandraRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, CassandraOperations operations) { + return RepositoryFragments.just(new MyOtherFragment()); + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryFragments.just(new MyOtherFragment()); + } + } + + static class MyFragment { + + } + + static class MyOtherFragment { + + } + + interface MyUserRepo extends Repository {} + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/ReactiveCassandraRepositoryFragmentsContributorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/ReactiveCassandraRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 000000000..ef8f836be --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/ReactiveCassandraRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,107 @@ +/* + * 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.cassandra.support; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.domain.User; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; +import org.springframework.data.cassandra.repository.support.ReactiveCassandraRepositoryFragmentsContributor; +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.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragment; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link ReactiveCassandraRepositoryFragmentsContributor}. + * + * @author Chris Bono + */ +class ReactiveCassandraRepositoryFragmentsContributorUnitTests { + + @Test // GH-3279 + void composedContributorShouldCreateFragments() { + + var mappingContext = new CassandraMappingContext(); + var converter = new MappingCassandraConverter(mappingContext); + ReactiveCassandraOperations operations = mock(); + when(operations.getConverter()).thenReturn(converter); + + var contributor = ReactiveCassandraRepositoryFragmentsContributor.DEFAULT + .andThen(MyReactiveCassandraRepositoryFragmentsContributor.INSTANCE) + .andThen(MyOtherReactiveCassandraRepositoryFragmentsContributor.INSTANCE); + + var fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(MyUserRepo.class), + new MappingCassandraEntityInformation<>(mappingContext.getPersistentEntity(User.class), converter), + operations); + + assertThat(fragments.stream()) + .extracting(RepositoryFragment::getImplementationClass) + .containsExactly(Optional.of(MyReactiveFragment.class), Optional.of(MyOtherReactiveFragment.class)); + } + + enum MyReactiveCassandraRepositoryFragmentsContributor implements ReactiveCassandraRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, ReactiveCassandraOperations operations) { + return RepositoryFragments.just(new MyReactiveFragment()); + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryFragments.just(new MyReactiveFragment()); + } + } + + enum MyOtherReactiveCassandraRepositoryFragmentsContributor implements ReactiveCassandraRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryFragments contribute(RepositoryMetadata metadata, + CassandraEntityInformation entityInformation, ReactiveCassandraOperations operations) { + return RepositoryFragments.just(new MyOtherReactiveFragment()); + } + + @Override + public RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryFragments.just(new MyOtherReactiveFragment()); + } + } + + static class MyReactiveFragment { + + } + + static class MyOtherReactiveFragment { + + } + + interface MyUserRepo extends Repository {} + +}