diff --git a/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java b/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java index 9aa12bf3..345c1cc6 100644 --- a/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java +++ b/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java @@ -32,7 +32,7 @@ * @author Christoph Strobl * @since 3.3 */ -class PredicateQueryEngine extends QueryEngine, Comparator> { +public class PredicateQueryEngine extends QueryEngine, Comparator> { /** * Creates a new {@link PredicateQueryEngine}. diff --git a/src/main/java/org/springframework/data/keyvalue/core/QueryEngineFactory.java b/src/main/java/org/springframework/data/keyvalue/core/QueryEngineFactory.java new file mode 100644 index 00000000..333d61af --- /dev/null +++ b/src/main/java/org/springframework/data/keyvalue/core/QueryEngineFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.keyvalue.core; + +/** + * Interface for {@code QueryEngineFactory} implementations that provide a {@link QueryEngine} object as part of the + * configuration. + *

+ * The factory is used during configuration to supply the query engine to be used. When configured, a + * {@code QueryEngineFactory} can be instantiated by accepting a {@link SortAccessor} in its constructor. Otherwise, + * implementations are expected to declare a no-args constructor. + * + * @author Mark Paluch + * @since 3.3.1 + */ +public interface QueryEngineFactory { + + /** + * Factory method for creating a {@link QueryEngine}. + * + * @return the query engine. + */ + QueryEngine create(); +} diff --git a/src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java b/src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java index 76552899..5b80155c 100644 --- a/src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java +++ b/src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java @@ -34,9 +34,8 @@ * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch - * @param */ -class SpelQueryEngine extends QueryEngine> { +public class SpelQueryEngine extends QueryEngine> { private static final SpelExpressionParser PARSER = new SpelExpressionParser(); diff --git a/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java index f73eb224..d1c0fa79 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.repository.KeyValueRepository; @@ -36,7 +37,6 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; /** * {@link RepositoryConfigurationExtension} for {@link KeyValueRepository}. @@ -90,17 +90,15 @@ public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfi */ private static Class getQueryCreatorType(AnnotationRepositoryConfigurationSource config) { - AnnotationMetadata metadata = config.getEnableAnnotationMetadata(); + AnnotationMetadata amd = (AnnotationMetadata) config.getSource(); + MergedAnnotation queryCreator = amd.getAnnotations().get(QueryCreatorType.class); + Class queryCreatorType = queryCreator.isPresent() ? queryCreator.getClass("value") : Class.class; - Map queryCreatorAnnotationAttributes = metadata - .getAnnotationAttributes(QueryCreatorType.class.getName()); - - if (CollectionUtils.isEmpty(queryCreatorAnnotationAttributes)) { + if (queryCreatorType == Class.class) { return SpelQueryCreator.class; } - AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes(queryCreatorAnnotationAttributes); - return queryCreatorAttributes.getClass("value"); + return queryCreatorType; } /** @@ -132,10 +130,10 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf registerIfNotAlreadyRegistered(() -> { - RootBeanDefinition definitionefinition = new RootBeanDefinition(KeyValueMappingContext.class); - definitionefinition.setSource(configurationSource.getSource()); + RootBeanDefinition mappingContext = new RootBeanDefinition(KeyValueMappingContext.class); + mappingContext.setSource(configurationSource.getSource()); - return definitionefinition; + return mappingContext; }, registry, getMappingContextBeanRef(), configurationSource); diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java b/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java index bffef1bd..1305a97d 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java @@ -89,7 +89,7 @@ public KeyValuePartTreeQuery(QueryMethod queryMethod, QueryMethodEvaluationConte QueryCreatorFactory, ?>> queryCreatorFactory) { Assert.notNull(queryMethod, "Query method must not be null"); - Assert.notNull(evaluationContextProvider, "EvaluationContextprovider must not be null"); + Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); Assert.notNull(keyValueOperations, "KeyValueOperations must not be null"); Assert.notNull(queryCreatorFactory, "QueryCreatorFactory type must not be null"); diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java b/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java index 52076fe1..43314dc6 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java @@ -38,6 +38,8 @@ import org.springframework.util.comparator.NullSafeComparator; /** + * {@link AbstractQueryCreator} to create {@link Predicate}-based {@link KeyValueQuery}s. + * * @author Christoph Strobl * @since 3.3 */ diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java b/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java index e2b4cc2c..9e3dafed 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java @@ -32,7 +32,7 @@ import org.springframework.util.StringUtils; /** - * {@link AbstractQueryCreator} to create {@link SpelExpression} based {@link KeyValueQuery}s. + * {@link AbstractQueryCreator} to create {@link SpelExpression}-based {@link KeyValueQuery}s. * * @author Christoph Strobl * @author Oliver Gierke @@ -180,8 +180,8 @@ protected SpelExpression toPredicateExpression(PartTree tree) { case NEGATING_SIMPLE_PROPERTY: case EXISTS: default: - throw new InvalidDataAccessApiUsageException(String.format("Found invalid part '%s' in query", - part.getType())); + throw new InvalidDataAccessApiUsageException( + String.format("Found invalid part '%s' in query", part.getType())); } if (partIter.hasNext()) { diff --git a/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java b/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java index b352f99f..3153edcb 100644 --- a/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java +++ b/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java @@ -28,16 +28,18 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.repository.config.QueryCreatorType; -import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; /** * Annotation to activate Map repositories. If no base package is configured through either {@link #value()}, @@ -45,13 +47,14 @@ * * @author Christoph Strobl * @author Oliver Gierke + * @author Mark Paluch */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(MapRepositoriesRegistrar.class) -@QueryCreatorType(value = PredicateQueryCreator.class, repositoryQueryType = KeyValuePartTreeQuery.class) +@QueryCreatorType(PredicateQueryCreator.class) public @interface EnableMapRepositories { /** @@ -145,10 +148,29 @@ @SuppressWarnings("rawtypes") Class mapType() default ConcurrentHashMap.class; + /** + * Configures the {@link QueryEngineFactory} to create the QueryEngine. When both, the query engine and sort accessors + * are configured, the query engine is instantiated using the configured sort accessor. + * + * @return {@link QueryEngineFactory} to configure the QueryEngine. + * @since 3.3.1 + */ + Class queryEngineFactory() default QueryEngineFactory.class; + + /** + * Configures the {@code QueryCreator} to create Part-Tree queries. The QueryCreator must create queries supported by + * the underlying {@code QueryEngine}. + * + * @return {@link AbstractQueryCreator} + * @since 3.3.1 + */ + @AliasFor(annotation = QueryCreatorType.class, value = "value") + Class> queryCreator() default PredicateQueryCreator.class; + /** * Configures the {@link SortAccessor accessor} for sorting results. * - * @return {@link SortAccessor} to indicate usage of default implementation. + * @return the configured {@link SortAccessor}. * @since 3.1.10 */ Class sortAccessor() default SortAccessor.class; diff --git a/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java index c8a63166..5ec1df51 100644 --- a/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java @@ -15,6 +15,7 @@ */ package org.springframework.data.map.repository.config; +import java.lang.reflect.Constructor; import java.util.Map; import org.springframework.beans.BeanUtils; @@ -24,15 +25,23 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.config.ParsingUtils; import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.keyvalue.core.QueryEngine; +import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; import org.springframework.data.map.MapKeyValueAdapter; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** + * {@link RepositoryConfigurationExtension} for Map-based repositories. + * * @author Christoph Strobl + * @author Mark Paluch */ +@SuppressWarnings("unchecked") public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension { @Override @@ -58,7 +67,11 @@ protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource)); SortAccessor sortAccessor = getSortAccessor(configurationSource); - if(sortAccessor != null) { + QueryEngine queryEngine = getQueryEngine(sortAccessor, configurationSource); + + if (queryEngine != null) { + adapterBuilder.addConstructorArgValue(queryEngine); + } else if (sortAccessor != null) { adapterBuilder.addConstructorArgValue(sortAccessor); } @@ -73,20 +86,60 @@ protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( @SuppressWarnings({ "unchecked", "rawtypes" }) private static Class getMapTypeToUse(RepositoryConfigurationSource source) { - return (Class) ((AnnotationMetadata) source.getSource()).getAnnotationAttributes( - EnableMapRepositories.class.getName()).get("mapType"); + return (Class) getAnnotationAttributes(source).get("mapType"); } @Nullable private static SortAccessor getSortAccessor(RepositoryConfigurationSource source) { - Class> sortAccessorType = (Class>) ((AnnotationMetadata) source.getSource()).getAnnotationAttributes( - EnableMapRepositories.class.getName()).get("sortAccessor"); + Class> sortAccessorType = (Class>) getAnnotationAttributes( + source).get("sortAccessor"); - if(sortAccessorType != null && !sortAccessorType.isInterface()) { + if (sortAccessorType != null && !sortAccessorType.isInterface()) { return BeanUtils.instantiateClass(sortAccessorType); } return null; } + + @Nullable + private static QueryEngine getQueryEngine(@Nullable SortAccessor sortAccessor, + RepositoryConfigurationSource source) { + + Class queryEngineFactoryType = (Class) getAnnotationAttributes( + source).get("queryEngineFactory"); + + if (queryEngineFactoryType != null && !queryEngineFactoryType.isInterface()) { + + if (sortAccessor != null) { + Constructor constructor = ClassUtils + .getConstructorIfAvailable(queryEngineFactoryType, SortAccessor.class); + if (constructor != null) { + return BeanUtils.instantiateClass(constructor, sortAccessor).create(); + } + } + + return BeanUtils.instantiateClass(queryEngineFactoryType).create(); + } + + return null; + } + + private static Map getAnnotationAttributes(RepositoryConfigurationSource source) { + + AnnotationMetadata annotationSource = (AnnotationMetadata) source.getSource(); + + if (annotationSource == null) { + throw new IllegalArgumentException("AnnotationSource not available"); + } + + Map annotationAttributes = annotationSource + .getAnnotationAttributes(EnableMapRepositories.class.getName()); + + if (annotationAttributes == null) { + throw new IllegalStateException("No annotation attributes for @EnableMapRepositories"); + } + + return annotationAttributes; + } } diff --git a/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java b/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java index b6acc7b0..7c768249 100644 --- a/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java +++ b/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java @@ -19,8 +19,11 @@ import static org.mockito.Mockito.*; import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.Predicate; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; @@ -32,12 +35,24 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.keyvalue.core.PathSortAccessor; +import org.springframework.data.keyvalue.core.QueryEngine; +import org.springframework.data.keyvalue.core.QueryEngineFactory; +import org.springframework.data.keyvalue.core.SortAccessor; +import org.springframework.data.keyvalue.core.SpelQueryEngine; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.keyvalue.repository.KeyValueRepository; +import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; import org.springframework.data.map.MapKeyValueAdapter; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; import org.springframework.test.util.ReflectionTestUtils; /** @@ -78,7 +93,8 @@ void shouldUseCustomAdapter() { PersonRepository repository = context.getBean(PersonRepository.class); - assertThatThrownBy(() -> repository.findById("foo")).hasRootCauseInstanceOf(IllegalStateException.class).hasMessageContaining("Mock"); + assertThatThrownBy(() -> repository.findById("foo")).hasRootCauseInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mock"); context.close(); } @@ -101,6 +117,41 @@ void considersSortAccessorConfiguredOnAnnotation() { new AnnotationConfigApplicationContext(ConfigWithCustomizedSortAccessor.class)); } + @Test // GH-576 + void considersQueryEngineConfiguration() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithQueryEngine.class); + + KeyValueTemplate template = context.getBean(KeyValueTemplate.class); + Object adapter = ReflectionTestUtils.getField(template, "adapter"); + + assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); + + Object engine = ReflectionTestUtils.getField(adapter, "engine"); + + assertThat(engine).isInstanceOf(SpelQueryEngine.class); + } + + @Test // GH-576 + void considersQueryEngineAndSortAccessorConfiguration() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + ConfigWithQueryEngineAndCustomizedSortAccessor.class); + + KeyValueTemplate template = context.getBean(KeyValueTemplate.class); + Object adapter = ReflectionTestUtils.getField(template, "adapter"); + + assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); + + Object engine = ReflectionTestUtils.getField(adapter, "engine"); + + assertThat(engine).isInstanceOf(SpelQueryEngine.class); + Object sortAccessor = ReflectionTestUtils.getField(engine, "sortAccessor"); + + assertThat(sortAccessor).asInstanceOf(InstanceOfAssertFactories.OPTIONAL) + .containsInstanceOf(PathSortAccessor.class); + } + private static void assertKeyValueTemplateWithAdapterFor(Class mapType, ApplicationContext context) { KeyValueTemplate template = context.getBean(KeyValueTemplate.class); @@ -123,8 +174,32 @@ private static void assertKeyValueTemplateWithSortAccessorFor(Class sortAcces assertThat(sortAccessor).asInstanceOf(InstanceOfAssertFactories.OPTIONAL).containsInstanceOf(sortAccessorType); } + @Test // GH-576 + void considersDefaultQueryCreator() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + + KeyValueRepositoryFactoryBean factoryBean = context.getBean(KeyValueRepositoryFactoryBean.class); + Object queryCreator = ReflectionTestUtils.getField(factoryBean, "queryCreator"); + + assertThat(queryCreator).isEqualTo(PredicateQueryCreator.class); + } + + @Test // GH-576 + void considersCustomQueryCreator() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + ConfigWithCustomQueryCreator.class); + + KeyValueRepositoryFactoryBean factoryBean = context.getBean(KeyValueRepositoryFactoryBean.class); + Object queryCreator = ReflectionTestUtils.getField(factoryBean, "queryCreator"); + + assertThat(queryCreator).isEqualTo(MyQueryCreator.class); + } + @Configuration - @EnableMapRepositories + @EnableMapRepositories(considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(value = PersonRepository.class, type = FilterType.ASSIGNABLE_TYPE)) static class Config {} @Configuration @@ -167,9 +242,74 @@ public KeyValueTemplate mapKeyValueTemplate() { } } + @EnableMapRepositories(queryEngineFactory = JustSpelQueryEngineFactory.class) + static class ConfigWithQueryEngine {} + + static class JustSpelQueryEngineFactory implements QueryEngineFactory { + + @Override + public QueryEngine create() { + return new SpelQueryEngine(); + } + + } + @EnableMapRepositories(sortAccessor = PathSortAccessor.class) static class ConfigWithCustomizedSortAccessor {} + @EnableMapRepositories(sortAccessor = PathSortAccessor.class, queryEngineFactory = SpelQueryEngineFactory.class) + static class ConfigWithQueryEngineAndCustomizedSortAccessor {} + + @EnableMapRepositories(queryCreator = MyQueryCreator.class, considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PersonRepository.class)) + static class ConfigWithCustomQueryCreator {} + + static class SpelQueryEngineFactory implements QueryEngineFactory { + + private final SortAccessor> sortAccessor; + + public SpelQueryEngineFactory(SortAccessor> sortAccessor) { + this.sortAccessor = sortAccessor; + } + + @Override + public QueryEngine create() { + return new SpelQueryEngine(sortAccessor); + } + + } + + static class MyQueryCreator extends AbstractQueryCreator>, Predicate> { + + public MyQueryCreator(PartTree tree) { + super(tree); + } + + public MyQueryCreator(PartTree tree, ParameterAccessor parameters) { + super(tree, parameters); + } + + @Override + protected Predicate create(Part part, Iterator iterator) { + return null; + } + + @Override + protected Predicate and(Part part, Predicate base, Iterator iterator) { + return null; + } + + @Override + protected Predicate or(Predicate base, Predicate criteria) { + return null; + } + + @Override + protected KeyValueQuery> complete(Predicate criteria, Sort sort) { + return null; + } + } + interface PersonRepository extends KeyValueRepository { }