From 35bd627fd898b3d2bd2ab0c3d416208195968bd2 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 16 Feb 2018 16:08:40 -0800 Subject: [PATCH] SGF-713 - Override generated OQL from Repository query methods. --- src/main/asciidoc/reference/repositories.adoc | 200 ++++++- .../mapping/GemfirePersistentEntity.java | 17 +- .../mapping/GemfirePersistentProperty.java | 35 +- .../data/gemfire/mapping/Regions.java | 3 +- .../gemfire/mapping/annotation/Region.java | 4 +- .../data/gemfire/repository/Query.java | 7 +- ...mfireRepositoryConfigurationExtension.java | 45 +- .../repository/query/GemfireQueryCreator.java | 12 +- .../repository/query/GemfireQueryMethod.java | 34 +- .../query/GemfireRepositoryQuery.java | 53 +- .../query/PartTreeGemfireRepositoryQuery.java | 51 +- .../repository/query/QueryBuilder.java | 29 +- .../repository/query/QueryPostProcessor.java | 122 ++++ .../gemfire/repository/query/QueryString.java | 221 ++++--- .../StringBasedGemfireRepositoryQuery.java | 221 +++++-- .../repository/query/support/OqlKeyword.java | 1 + .../support/GemfireRepositoryFactory.java | 226 +++++-- .../support/GemfireRepositoryFactoryBean.java | 198 ++++++- .../support/SimpleGemfireRepository.java | 53 +- .../data/gemfire/util/CollectionUtils.java | 174 ++++-- ...AndEntityIdRepositoryIntegrationTests.java | 142 +++++ .../query/QueryBuilderUnitTests.java | 36 +- .../QueryPostProcessorIntegrationTests.java | 227 +++++++ .../query/QueryPostProcessorUnitTests.java | 123 ++++ .../query/QueryStringUnitTests.java | 201 +++++-- ...StringBasedGemfireRepositoryQueryTest.java | 46 +- .../gemfire/repository/sample/Customer.java | 39 +- ...RegionKeyEntityIdAnimalRepositoryTest.java | 72 --- .../data/gemfire/repository/sample/Plant.java | 17 +- .../sample/PlantRepositoryTest.java | 64 -- .../data/gemfire/repository/sample/User.java | 7 +- .../GemfireRepositoryFactoryUnitTests.java | 554 ++++++++++++------ .../util/CollectionUtilsUnitTests.java | 311 ++++++++-- ...eyEntityIdAnimalRepositoryTest-context.xml | 34 -- .../sample/PlantRepositoryTest-context.xml | 34 -- 35 files changed, 2736 insertions(+), 877 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java create mode 100644 src/test/java/org/springframework/data/gemfire/repository/config/IncompatibleRegionKeyRepositoryIdAndEntityIdRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java delete mode 100644 src/test/java/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest.java delete mode 100644 src/test/java/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest.java delete mode 100644 src/test/resources/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest-context.xml delete mode 100644 src/test/resources/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest-context.xml diff --git a/src/main/asciidoc/reference/repositories.adoc b/src/main/asciidoc/reference/repositories.adoc index fd52affa..27a5f83f 100644 --- a/src/main/asciidoc/reference/repositories.adoc +++ b/src/main/asciidoc/reference/repositories.adoc @@ -45,7 +45,7 @@ https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html# Using this approach, it is a simple matter to bootstrap _Spring Data Repositories_ using the SDG `@EnableGemfireRepositories` annotation: -.Bootstrap _Spring Data Geode Repositories_ with `@EnableGemfireRepositories` +.Bootstrap _Spring Data GemFire Repositories_ with `@EnableGemfireRepositories` ==== [source, java] ---- @@ -93,7 +93,7 @@ Many other aspects of the SDG's _Repository_ infrastructure extension maybe cust https://docs.spring.io/spring-data/gemfire/docs/current/api/org/springframework/data/gemfire/repository/config/EnableGemfireRepositories.html[`@EnableGemfireRepositories` _Javadoc_] for more details on all configuration settings. -[[gemfire-repositories.executing-queries]] +[[gemfire-repositories.queries.executing]] == Executing OQL Queries _Spring Data GemFire Repositories_ enable the definition of query methods to easily execute GemFire OQL Queries @@ -195,7 +195,7 @@ becomes too verbose, you can annotate the query methods with `@Query` as seen fo | `x.active = false` |=== -[[gemfire-repositories:oql-extensions]] +[[gemfire-repositories.queries.oql-extensions]] == OQL Query Extensions using Annotations Many query languages, such as Pivotal GemFire's OQL (Object Query Language), have extensions that are not directly @@ -334,3 +334,197 @@ indexes and other optimizations wisely as an improper or poorly choosen index ca performance given the overhead in maintaining the index. The "ReputationIdx" was only used to serve the purpose of the example. ==== + +[[gemfire-repositories.queries.post-processing]] +== Query Post Processing + +Using the Spring Data _Repository_ abstraction, query method convention for defining data store specific queries +(e.g. OQL) is easy and convenient. However, it is sometimes desirable to still want to inspect or even possibly +modify the query "generated" from the _Repository_ query method. + +Since 2.0.x, _Spring Data GemFire_ introduces the `o.s.d.gemfire.repository.query.QueryPostProcessor` +functional interface. The interface is loosely defined as follows... + +.QueryPostProcessor +==== +[source,java] +---- +package org.springframework.data.gemfire.repository.query; + +import org.springframework.core.Ordered; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryMethod; +import ...; + +@FunctionalInterface +interface QueryPostProcessor extends Ordered { + + QUERY postProcess(QueryMethod queryMethod, QUERY query, Object... arguments); + +} +---- +==== + +There are additional default methods provided to allow users to compose instances of `QueryPostProcessor` very similar +to how https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html#compose-java.util.function.Function-[java.util.function.Function.andThen(:Function)] +and https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html#compose-java.util.function.Function-[java.util.function.Function.compose(:Function)] +work. + +Additionally, you will notice that the `QueryPostProcessor` interface implements the +https://docs.spring.io/spring/docs/5.0.2.RELEASE/javadoc-api/org/springframework/core/Ordered.html[`org.springframework.core.Ordered`] +interface, which is useful when multiple `QueryPostProcessors` are declared and registered in the Spring context +and used to create a pipeline of processing for a group of generated query method queries. + +Finally, the `QueryPostProcessor` accepts type arguments corresponding to the type parameters, `T` and `QUERY`, +respectively. Type of `T` extends the _Spring Data Commons_ marker interface, +https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/Repository.html[`org.springframework.data.repository.Repository`]. +We will discuss this further below. All `QUERY` type parameter arguments in _Spring Data GemFire's_ case +will be of type `java.lang.String`. + +NOTE: It is useful to define the query as type `QUERY` since this `QueryPostProcessor` interface maybe ported to +_Spring Data Commons_ and therefore must handle all forms of queries by different data stores (e.g. JPA, MongoDB, +or Redis). + +As user may implement this interface to receive a callback with the query that was generated from the application +`Repository` interface method when the method is called. + +For example, I might want to log all queries from all application _Repository_ interface definitions. I could do so +using the following `QueryPostProcessor` implementation... + +.LoggingQueryPostProcessor +==== +[source,java] +---- +package example; + +import ...; + +class LoggingQueryPostProcessor implements QueryPostProcessor { + + private Logger logger = Logger.getLogger("someLoggerName"); + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + String message = String.format("Executing query [%s] with arguments [%s]", query, Arrays.toString(arguments)); + + this.logger.info(message); + } +} +---- +==== + +The `LoggingQueryPostProcessor` was typed to the Spring Data `org.springframework.data.repository.Repository` +marker interface, and therefore, will log all application _Repository_ interface query method "generated" queries. + +You could limit the scope of this logging to queries only from certain types of application _Repository_ interfaces, +such as, say, an `CustomerRepository`... + +.CustomerRepository +==== +[source,java] +---- +interface CustomerRepository extends CrudRepository { + + Customer findByAccountNumber(String accountNumber); + + List findByLastNameLike(String lastName); + +} +---- +==== + +Then, I could have typed the `LoggingQueryPostProcessor` specifically to the `CustomerRepository`, like so... + +.CustomerLoggingQueryPostProcessor +==== +[source,java] +---- +class LoggingQueryPostProcessor implements QueryPostProcessor { .. } +---- +==== + +As result, only queries defined in the `CustomerRepository` interface (e.g. `findByAccountNumber`) would be logged. + +I might want to create a `QueryPostProcessor` for a specific query defined by a _Repository_ query method. For example, +say I want to "`LIMIT`" the OQL query generated from the `CustomerRepository.findByLastNameLike(:String)` query method +to only return 5 results and I want to order the `Customers` by `firstName`, ascending. Well, then, I can define +a custom `QueryPostProcessor` like so... + +.OrderedLimitedCustomerByLastNameQueryPostProcessor +==== +[source,java] +---- +class OrderedLimitedCustomerByLastNameQueryPostProcessor implements QueryPostProcessor { + + private final int limit; + + public OrderedLimitedCustomerByLastNameQueryPostProcessor(int limit) { + this.limit = limit; + } + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + return "findByLastNameLike".equals(queryMethod.getName()) + ? query.trim() + .replace("SELECT", "SELECT DISTINCT") + .concat(" ORDER BY firstName ASC") + .concat(String.format(" LIMIT %d", this.limit)) + : query; + } +} +---- +==== + +While this works, it possible to achieve the same affect just using the Spring Data _Repository_ convention and extensions +provided by _Spring Data GemFire_. For instance, the same query could be defined as... + +.CustomerRepository using the convention +==== +[source,java] +---- +interface CustomerRepository extends CrudRepository { + + @Limit(5) + List findDistinctByLastNameLikeOrderByFirstNameDesc(String lastName); + +} +---- +==== + +However, if you do not have control over the application `CustomerRepository` interface definition, +then the `QueryPostProcessor` (i.e. `OrderedLimitedCustomerByLastNameQueryPostProcessor`) is convenient. + +If I want to ensure the `LoggingQueryPostProcessor` always comes after the other application-defined `QueryPostProcessors` +that I may have declared and registered in the Spring `ApplicationContext`, then I can set the `order` property +by overriding the `o.s.core.Ordered.getOrder()` method. + +.Defining the `order` property +==== +[source,java] +---- +class LoggingQueryPostProcessor implements QueryPostProcessor { + + @Override + int getOrder() { + return 1; + } +} + +class CustomerQueryPostProcessor implements QueryPostProcessor { + + @Override + int getOrder() { + return 0; + } +} +---- +==== + +This ensures that I will always see the affects of the post processing applied by my other `QueryPostProcessors` +before my `LoggingQueryPostProcessor` logs the query. + +You can define as many `QueryPostProcessors` in the Spring `ApplicationContext` as you like and apply them in any +order, to all or specific application _Repository_ interfaces, and be a granular as yuo like using the provided +arguments to the `postProcess(..)` method callback. diff --git a/src/main/java/org/springframework/data/gemfire/mapping/GemfirePersistentEntity.java b/src/main/java/org/springframework/data/gemfire/mapping/GemfirePersistentEntity.java index 4622aa98..2143c36f 100644 --- a/src/main/java/org/springframework/data/gemfire/mapping/GemfirePersistentEntity.java +++ b/src/main/java/org/springframework/data/gemfire/mapping/GemfirePersistentEntity.java @@ -28,6 +28,7 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.NonNull; import org.springframework.util.StringUtils; /** @@ -67,9 +68,10 @@ public class GemfirePersistentEntity extends BasicPersistentEntity persistentEntityType, Annotation regionAnnotation) { Optional regionName = Optional.ofNullable(regionAnnotation) - .map((annotation) -> getAnnotationAttributeStringValue(annotation, "value")); + .map(annotation -> getAnnotationAttributeStringValue(annotation, "value")) + .filter(StringUtils::hasText); - return regionName.filter(StringUtils::hasText).orElse(persistentEntityType.getSimpleName()); + return regionName.orElse(persistentEntityType.getSimpleName()); } /* (non-Javadoc) */ @@ -131,13 +133,14 @@ public class GemfirePersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity Modifier.isTransient(field.getModifiers())).isPresent(); } + /** + * Returns the {@link String name} of this {@link GemfirePersistentProperty's} {@link Class type}. + * + * @return the {@link String name} of this {@link GemfirePersistentProperty's} {@link Class type}. + * @see java.lang.Class#getName() + * @see #getType() + */ + public String getTypeName() { + return getType().getName(); + } + /** * @inheritDoc */ diff --git a/src/main/java/org/springframework/data/gemfire/mapping/Regions.java b/src/main/java/org/springframework/data/gemfire/mapping/Regions.java index 32310624..219285f1 100644 --- a/src/main/java/org/springframework/data/gemfire/mapping/Regions.java +++ b/src/main/java/org/springframework/data/gemfire/mapping/Regions.java @@ -77,7 +77,8 @@ public class Regions implements Iterable> { Assert.notNull(entityType, "Entity type must not be null"); String regionName = Optional.ofNullable(this.mappingContext.getPersistentEntity(entityType)) - .map(entity -> entity.getRegionName()).orElseGet(entityType::getSimpleName); + .map(entity -> entity.getRegionName()) + .orElseGet(entityType::getSimpleName); return (Region) this.regions.get(regionName); } diff --git a/src/main/java/org/springframework/data/gemfire/mapping/annotation/Region.java b/src/main/java/org/springframework/data/gemfire/mapping/annotation/Region.java index a832fb5f..ad964c4e 100644 --- a/src/main/java/org/springframework/data/gemfire/mapping/annotation/Region.java +++ b/src/main/java/org/springframework/data/gemfire/mapping/annotation/Region.java @@ -44,8 +44,8 @@ import org.springframework.core.annotation.AliasFor; public @interface Region { @SuppressWarnings("unchecked") - List> REGION_ANNOTATION_TYPES = Arrays.asList( - ClientRegion.class, LocalRegion.class, PartitionRegion.class, ReplicateRegion.class, Region.class); + List> REGION_ANNOTATION_TYPES = + Arrays.asList(ClientRegion.class, LocalRegion.class, PartitionRegion.class, ReplicateRegion.class, Region.class); /** * Name, or fully-qualified bean name of the {@link org.apache.geode.cache.Region} diff --git a/src/main/java/org/springframework/data/gemfire/repository/Query.java b/src/main/java/org/springframework/data/gemfire/repository/Query.java index 71164b86..7de75d49 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/Query.java +++ b/src/main/java/org/springframework/data/gemfire/repository/Query.java @@ -21,13 +21,16 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.data.annotation.QueryAnnotation; + /** * * @author Oliver Gierke */ -@Documented -@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@QueryAnnotation +@Documented public @interface Query { String value() default ""; diff --git a/src/main/java/org/springframework/data/gemfire/repository/config/GemfireRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/gemfire/repository/config/GemfireRepositoryConfigurationExtension.java index 8374bf44..a323cf71 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/config/GemfireRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/gemfire/repository/config/GemfireRepositoryConfigurationExtension.java @@ -27,6 +27,8 @@ import org.springframework.data.gemfire.mapping.GemfireMappingContext; import org.springframework.data.gemfire.mapping.annotation.Region; import org.springframework.data.gemfire.repository.GemfireRepository; import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; @@ -34,12 +36,13 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; /** - * {@link RepositoryConfigurationExtension} implementation handling GemFire specific extensions to the Repository XML - * namespace and annotation-based configuration meta-data. + * {@link RepositoryConfigurationExtension} implementation handling Apache Geode and Pivotal GemFire specific extensions + * in the Repository XML namespace and Annotation-based configuration meta-data. * * @author Oliver Gierke * @author John Blum * @see org.springframework.data.repository.config.RepositoryConfigurationExtension + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport */ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { @@ -47,8 +50,8 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura private static final String MAPPING_CONTEXT_PROPERTY_NAME = "gemfireMappingContext"; private static final String MAPPING_CONTEXT_REF_ATTRIBUTE_NAME = "mappingContextRef"; - static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = String.format("%1$s.%2$s", - GemfireMappingContext.class.getName(), "DEFAULT"); + static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = + String.format("%1$s.%2$s", GemfireMappingContext.class.getName(), "DEFAULT"); /* * (non-Javadoc) @@ -92,9 +95,7 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura */ @Override public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource configurationSource) { - builder.addPropertyReference(MAPPING_CONTEXT_PROPERTY_NAME, - configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME) - .orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); + addMappingContextPropertyReference(builder, configurationSource); } /* @@ -103,6 +104,22 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura */ @Override public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource configurationSource) { + addMappingContextPropertyReference(builder, configurationSource); + } + + /** + * Adds a property reference to the store-specific {@link MappingContext} + * in the given {@link BeanDefinitionBuilder bean definition}. + * + * @param builder {@link BeanDefinitionBuilder} used to build the target bean definition. + * @param configurationSource {@link RepositoryConfigurationSource} containing {@link Repository} + * configuration meta-data. + * @see org.springframework.beans.factory.support.BeanDefinitionBuilder + * @see org.springframework.data.repository.config.RepositoryConfigurationSource + */ + private void addMappingContextPropertyReference(BeanDefinitionBuilder builder, + RepositoryConfigurationSource configurationSource) { + builder.addPropertyReference(MAPPING_CONTEXT_PROPERTY_NAME, configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME) .orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME)); @@ -117,9 +134,21 @@ public class GemfireRepositoryConfigurationExtension extends RepositoryConfigura super.registerBeansForRoot(registry, configurationSource); - if (!configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME).isPresent()) { + if (noMappingContextIsConfigured(configurationSource)) { registry.registerBeanDefinition(DEFAULT_MAPPING_CONTEXT_BEAN_NAME, new RootBeanDefinition(GemfireMappingContext.class)); } } + + /** + * Determines whether a {@link GemfireMappingContext mapping context} has already been configured. + * + * @param configurationSource {@link RepositoryConfigurationSource} used to check for the presence + * of an existing {@link MappingContext} configuration. + * @return a boolean value indicating whether a {@link GemfireMappingContext mapping context} + * has already been configured. + */ + private boolean noMappingContextIsConfigured(RepositoryConfigurationSource configurationSource) { + return !configurationSource.getAttribute(MAPPING_CONTEXT_REF_ATTRIBUTE_NAME).isPresent(); + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java index ba438f5b..cc52e9b6 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java @@ -27,10 +27,14 @@ import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; /** - * Query creator to create {@link QueryString} instances. + * {@link AbstractQueryCreator} to create {@link QueryString} instances. * * @author Oliver Gierke * @author John Blum + * @see org.springframework.data.gemfire.repository.query.QueryBuilder + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator + * @see org.springframework.data.repository.query.parser.Part + * @see org.springframework.data.repository.query.parser.PartTree */ class GemfireQueryCreator extends AbstractQueryCreator { @@ -47,6 +51,7 @@ class GemfireQueryCreator extends AbstractQueryCreator * @param entity must not be {@literal null}. */ public GemfireQueryCreator(PartTree tree, GemfirePersistentEntity entity) { + super(tree); this.queryBuilder = new QueryBuilder(entity, tree); @@ -59,7 +64,9 @@ class GemfireQueryCreator extends AbstractQueryCreator */ @Override public QueryString createQuery(Sort dynamicSort) { + this.indexes = new IndexProvider(); + return super.createQuery(dynamicSort); } @@ -96,7 +103,8 @@ class GemfireQueryCreator extends AbstractQueryCreator */ @Override protected QueryString complete(Predicates criteria, Sort sort) { - QueryString query = queryBuilder.create(criteria).orderBy(sort); + + QueryString query = this.queryBuilder.create(criteria).orderBy(sort); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Created Query [%s]", query.toString())); diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java index 4bce167f..a70467cf 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java @@ -67,7 +67,6 @@ public class GemfireQueryMethod extends QueryMethod { assertNonPagingQueryMethod(method); this.method = method; - this.entity = mappingContext.getPersistentEntity(getDomainClass()); } @@ -81,6 +80,7 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#getParameterTypes() */ private void assertNonPagingQueryMethod(Method method) { + for (Class type : method.getParameterTypes()) { if (Pageable.class.isAssignableFrom(type)) { throw new IllegalStateException(String.format("Pagination is not supported by GemFire Repositories;" @@ -95,7 +95,7 @@ public class GemfireQueryMethod extends QueryMethod { * @return the {@link GemfirePersistentEntity} the method deals with. */ public GemfirePersistentEntity getPersistentEntity() { - return entity; + return this.entity; } /** @@ -117,9 +117,12 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#getAnnotation(Class) */ String getAnnotatedQuery() { - Query query = method.getAnnotation(Query.class); - String queryString = (query != null ? (String) AnnotationUtils.getValue(query) : null); - return (StringUtils.hasText(queryString) ? queryString : null); + + Query query = this.method.getAnnotation(Query.class); + + String queryString = query != null ? (String) AnnotationUtils.getValue(query) : null; + + return StringUtils.hasText(queryString) ? queryString : null; } /** @@ -131,7 +134,7 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#isAnnotationPresent(Class) */ public boolean hasHint() { - return method.isAnnotationPresent(Hint.class); + return this.method.isAnnotationPresent(Hint.class); } /** @@ -142,8 +145,10 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#getAnnotation(Class) */ public String[] getHints() { + Hint hint = method.getAnnotation(Hint.class); - return (hint != null ? hint.value() : EMPTY_STRING_ARRAY); + + return hint != null ? hint.value() : EMPTY_STRING_ARRAY; } /** @@ -155,7 +160,7 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#isAnnotationPresent(Class) */ public boolean hasImport() { - return method.isAnnotationPresent(Import.class); + return this.method.isAnnotationPresent(Import.class); } /** @@ -166,8 +171,10 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#getAnnotation(Class) */ public String getImport() { + Import importStatement = method.getAnnotation(Import.class); - return (importStatement != null ? importStatement.value() : null); + + return importStatement != null ? importStatement.value() : null; } /** @@ -179,7 +186,7 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#isAnnotationPresent(Class) */ public boolean hasLimit() { - return method.isAnnotationPresent(Limit.class); + return this.method.isAnnotationPresent(Limit.class); } /** @@ -190,8 +197,10 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#getAnnotation(Class) */ public int getLimit() { + Limit limit = method.getAnnotation(Limit.class); - return (limit != null ? limit.value() : Integer.MAX_VALUE); + + return limit != null ? limit.value() : Integer.MAX_VALUE; } /** @@ -202,7 +211,6 @@ public class GemfireQueryMethod extends QueryMethod { * @see java.lang.reflect.Method#isAnnotationPresent(Class) */ public boolean hasTrace() { - return method.isAnnotationPresent(Trace.class); + return this.method.isAnnotationPresent(Trace.class); } - } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java index 4d99fc01..93a8e883 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java @@ -15,6 +15,7 @@ */ package org.springframework.data.gemfire.repository.query; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; @@ -28,25 +29,33 @@ import org.springframework.util.Assert; * @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod * @see org.springframework.data.repository.query.RepositoryQuery */ -abstract class GemfireRepositoryQuery implements RepositoryQuery { +public abstract class GemfireRepositoryQuery implements RepositoryQuery { private final GemfireQueryMethod queryMethod; + private QueryPostProcessor queryPostProcessor = ProvidedQueryPostProcessor.IDENTITY; + /* * (non-Javadoc) * Constructor used for testing purposes only! */ GemfireRepositoryQuery() { - queryMethod = null; + this.queryMethod = null; } /** - * Creates a new {@link GemfireRepositoryQuery} using the given {@link GemfireQueryMethod}. + * Constructs a new instance of {@link GemfireRepositoryQuery} initialized with + * the given {@link GemfireQueryMethod}. * - * @param queryMethod must not be {@literal null}. + * @param queryMethod {@link GemfireQueryMethod} capturing the meta-data + * for the {@link Repository} {@link QueryMethod}; must not be {@literal null}. + * @throws IllegalArgumentException if {@link GemfireQueryMethod query method} is {@literal null}. + * @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod */ public GemfireRepositoryQuery(GemfireQueryMethod queryMethod) { - Assert.notNull(queryMethod); + + Assert.notNull(queryMethod, "QueryMethod must not be null"); + this.queryMethod = queryMethod; } @@ -59,4 +68,38 @@ abstract class GemfireRepositoryQuery implements RepositoryQuery { return this.queryMethod; } + /** + * Returns a reference to the composed {@link QueryPostProcessor QueryPostProcessors}, which gets applied + * to the OQL query prior to execution. + * + * @return a reference to the composed {@link QueryPostProcessor QueryPostProcessors}. + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor + */ + protected QueryPostProcessor getQueryPostProcessor() { + return this.queryPostProcessor; + } + + /** + * Registers the given {@link QueryPostProcessor} used to post process OQL queries + * generated from {@link Repository} {@link QueryMethod query methods}. + * + * @param queryPostProcessor {@link QueryPostProcessor} to register. + * @return this {@link GemfireRepositoryQuery}. + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor + */ + public GemfireRepositoryQuery register(QueryPostProcessor queryPostProcessor) { + this.queryPostProcessor = this.queryPostProcessor.processBefore(queryPostProcessor); + return this; + } + + enum ProvidedQueryPostProcessor implements QueryPostProcessor { + + IDENTITY { + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + return query; + } + } + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java index 2c9776a4..425d6581 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java @@ -22,22 +22,25 @@ import java.util.List; import org.springframework.data.domain.Sort; import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; /** - * {@link GemfireRepositoryQuery} backed by a {@link PartTree} and thus, deriving an OQL query from the backing query - * method's name. + * {@link GemfireRepositoryQuery} backed by a {@link PartTree}, deriving an OQL query + * from the backing query method's name/signature. * * @author Oliver Gierke + * @author John Blum + * @see org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery */ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { private final GemfireQueryMethod method; - private final PartTree tree; + private final GemfireTemplate template; + private final PartTree tree; + /** * Creates a new {@link PartTreeGemfireRepositoryQuery} using the given {@link GemfireQueryMethod} and * {@link GemfireTemplate}. @@ -51,9 +54,9 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { Class domainClass = method.getEntityInformation().getJavaType(); - this.tree = new PartTree(method.getName(), domainClass); this.method = method; this.template = template; + this.tree = new PartTree(method.getName(), domainClass); } /* @@ -61,20 +64,41 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) */ @Override - public Object execute(Object[] parameters) { - ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor(method.getParameters(), parameters); + public Object execute(Object[] arguments) { - QueryString query = new GemfireQueryCreator(tree, method.getPersistentEntity()) - .createQuery(parameterAccessor.getSort()); + QueryString query = createQuery(this.method, this.tree, arguments); - RepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(query.toString(), method, template); + GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(query, this.method, this.template); - return repositoryQuery.execute(prepareStringParameters(parameters)); + return repositoryQuery.execute(prepareStringParameters(arguments)); + } + + private QueryString createQuery(GemfireQueryMethod queryMethod, PartTree tree, Object[] arguments) { + + ParametersParameterAccessor parameterAccessor = + new ParametersParameterAccessor(queryMethod.getParameters(), arguments); + + GemfireQueryCreator queryCreator = new GemfireQueryCreator(tree, queryMethod.getPersistentEntity()); + + return queryCreator.createQuery(parameterAccessor.getSort()); + } + + private GemfireRepositoryQuery newRepositoryQuery(QueryString query, + GemfireQueryMethod queryMethod, GemfireTemplate template) { + + GemfireRepositoryQuery repositoryQuery = + new StringBasedGemfireRepositoryQuery(query.toString(), queryMethod, template); + + repositoryQuery.register(getQueryPostProcessor()); + + return repositoryQuery; } private Object[] prepareStringParameters(Object[] parameters) { - Iterator partsIterator = tree.getParts().iterator(); - List stringParameters = new ArrayList(parameters.length); + + Iterator partsIterator = this.tree.getParts().iterator(); + + List stringParameters = new ArrayList<>(parameters.length); for (Object parameter : parameters) { if (parameter == null || parameter instanceof Sort) { @@ -99,5 +123,4 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { return stringParameters.toArray(); } - } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java index 1602e979..4c1e2484 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java @@ -16,6 +16,7 @@ package org.springframework.data.gemfire.repository.query; +import org.apache.geode.cache.Region; import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; import org.springframework.data.gemfire.repository.query.support.OqlKeyword; import org.springframework.data.repository.query.parser.PartTree; @@ -39,20 +40,21 @@ class QueryBuilder { /* (non-Javadoc) */ static String asQuery(GemfirePersistentEntity entity, PartTree tree) { - return String.format(SELECT_OQL_TEMPLATE, (tree.isDistinct() ? OqlKeyword.DISTINCT : ""), + + return String.format(SELECT_OQL_TEMPLATE, tree.isDistinct() ? OqlKeyword.DISTINCT : "", entity.getRegionName(), DEFAULT_ALIAS).replaceAll("\\s{2,}", " "); } /* (non-Javadoc) */ static String validateQuery(String query) { - Assert.hasText(query, "An OQL Query must be specified"); + Assert.hasText(query, "Query is required"); return query; } /** - * Constructs an instance of {@link QueryBuilder} initialized with the given query {@link String}. + * Constructs a new instance of {@link QueryBuilder} initialized with the given {@link String query}. * - * @param query {@link String} containing the base OQL query. + * @param query {@link String} containing the base {@link String OQL query}. * @see #validateQuery(String) */ public QueryBuilder(String query) { @@ -60,16 +62,15 @@ class QueryBuilder { } /** - * Constructs an instance of {@link QueryBuilder} with the given {@link GemfirePersistentEntity} - * and {@link PartTree} that determines the GemFire {@link org.apache.geode.cache.Region} - * to query and whether the query should capture unique results. + * Constructs a new instance of {@link QueryBuilder} initialized with the given {@link GemfirePersistentEntity} + * and {@link PartTree} used to determine the {@link Region} to query and whether the query + * should capture unique results. * - * @param entity {@link GemfirePersistentEntity} used to determine the GemFire - * {@link org.apache.geode.cache.Region} to query. - * @param tree {@link PartTree} containing parts of the OQL Query for determining things - * like uniqueness. + * @param entity {@link GemfirePersistentEntity} used to determine the {@link Region} to query. + * @param tree {@link PartTree} containing parts of the OQL Query for determining things like uniqueness. * @see org.springframework.data.gemfire.mapping.GemfirePersistentEntity * @see org.springframework.data.repository.query.parser.PartTree + * @see #QueryBuilder(String) */ public QueryBuilder(GemfirePersistentEntity entity, PartTree tree) { this(asQuery(entity, tree)); @@ -97,8 +98,10 @@ class QueryBuilder { * @see org.springframework.data.gemfire.repository.query.Predicate */ protected String withPredicate(String query, Predicate predicate) { - return (predicate == null ? query - : String.format(WHERE_CLAUSE_TEMPLATE, query, predicate.toString(DEFAULT_ALIAS))); + + return predicate != null + ? String.format(WHERE_CLAUSE_TEMPLATE, query, predicate.toString(DEFAULT_ALIAS)) + : query; } /* diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java new file mode 100644 index 00000000..ee261e80 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java @@ -0,0 +1,122 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.gemfire.repository.query; + +import org.springframework.core.Ordered; +import org.springframework.data.gemfire.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * {@link QueryPostProcessor} defines a contract for implementations to post process a given {@link String OQL query} + * and possibly return a new or modified version of the same {@link String OQL query}. + * + * {@link QueryPostProcessor QueryPostProcessors} are particularly useful for {@link Repository} + * {@link QueryMethod query method} generated {@link String queries}, giving the user a chance + * via the callback to further process the generated {@link String query}. + * + * @author John Blum + * @param {@link Class type} identifying the {@link Repository Repositories} to match on for registration. + * @param {@link Class type} of the query to process. + * @see org.springframework.data.repository.Repository + * @see org.springframework.data.repository.query.QueryMethod + * @since 2.1.0 + */ +@FunctionalInterface +@SuppressWarnings("unused") +public interface QueryPostProcessor extends Ordered { + + Object[] EMPTY_ARGUMENTS = new Object[0]; + + /** + * Defines the {@link Integer order} of this {@link QueryPostProcessor} relative to + * other {@link QueryPostProcessor QueryPostProcessors} in a sort. + * + * Defaults to the {@link Ordered#LOWEST_PRECEDENCE}. + * + * @return an {@link Integer} value specifying the {@link Integer order} of this {@link QueryPostProcessor} + * relative to other {@link QueryPostProcessor QueryPostProcessors} in a sort. + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + default int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + /** + * Callback used to post process the given {@link QUERY OQL query} and return a possibly new + * or modified {@link String OQL query}. + * + * This callback is invoked for OQL queries generated from the SD Repository {@link QueryMethod} signature + * as well as OQL queries specified using the {@link Query @Query} annotation and OQL queries + * defined in a application properties file. + * + * @param query {@link QUERY OQL query} to process. + * @return a possibly new or modified version of the same {@link String OQL query}. + * @see org.springframework.data.repository.query.QueryMethod + */ + default QUERY postProcess(@NonNull QueryMethod queryMethod, QUERY query) { + return postProcess(queryMethod, query, EMPTY_ARGUMENTS); + } + + /** + * Callback used to post process the given {@link QUERY OQL query} and return a possibly new + * or modified {@link String OQL query}. + * + * This callback is invoked for OQL queries generated from the SD Repository {@link QueryMethod} signature + * as well as OQL queries specified using the {@link Query @Query} annotation and OQL queries + * defined in a application properties file. + * + * @param query {@link QUERY OQL query} to process. + * @param arguments array of {@link Object Objects} containing the arguments to the query parameters. + * @return a possibly new or modified version of the same {@link String OQL query}. + * @see org.springframework.data.repository.query.QueryMethod + */ + QUERY postProcess(@NonNull QueryMethod queryMethod, QUERY query, Object... arguments); + + /** + * Builder method used to compose 2 {@link QueryPostProcessor QueryPostProcessors} + * with this {@link QueryPostProcessor} preceding the given {@link QueryPostProcessor}. + * + * @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}. + * @return a composed {@link QueryPostProcessor} consisting of this {@link QueryPostProcessor} + * followed by the given {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor} + * if given {@link QueryPostProcessor} is {@literal null}. + */ + @NonNull + default QueryPostProcessor processBefore(@Nullable QueryPostProcessor queryPostProcessor) { + return queryPostProcessor == null ? this : (queryMethod, query, arguments) -> + queryPostProcessor.postProcess(queryMethod, this.postProcess(queryMethod, query, arguments), arguments); + } + + /** + * Builder method used to compose 2 {@link QueryPostProcessor QueryPostProcessors} + * with the given {@link QueryPostProcessor} preceding this {@link QueryPostProcessor}. + * + * @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}. + * @return a composed {@link QueryPostProcessor} consisting of the given {@link QueryPostProcessor} + * followed by this {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor} + * if given {@link QueryPostProcessor} is {@literal null}. + */ + @NonNull + default QueryPostProcessor processAfter(@Nullable QueryPostProcessor queryPostProcessor) { + return queryPostProcessor == null ? this : (queryMethod, query, arguments) -> + this.postProcess(queryMethod, queryPostProcessor.postProcess(queryMethod, query, arguments), arguments); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java index d152ab76..0f5e29bd 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java @@ -16,6 +16,8 @@ package org.springframework.data.gemfire.repository.query; +import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeIsEmpty; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -30,7 +32,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * {@link QueryString} is a utility class used to construct GemFire OQL query statement syntax. + * {@link QueryString} is a utility class used to construct GemFire OQL query statements. * * @author Oliver Gierke * @author David Turanski @@ -42,6 +44,7 @@ import org.springframework.util.StringUtils; */ public class QueryString { + // OQL Query Patterns protected static final Pattern HINT_PATTERN = Pattern.compile(""); protected static final Pattern IMPORT_PATTERN = Pattern.compile("IMPORT .+;"); protected static final Pattern LIMIT_PATTERN = Pattern.compile("LIMIT \\d+"); @@ -59,40 +62,82 @@ public class QueryString { private static final String IN_PARAMETER_PATTERN = "(?<=IN (SET|LIST) \\$)\\d"; private static final String REGION_PATTERN = "\\/(\\/?\\w)+"; - private final String query; + private static final String COUNT_QUERY = "count(*)"; + private static final String STAR_QUERY = "*"; - /* (non-Javadoc) */ - static String asQuery(Class domainType, boolean isCountQuery) { - return String.format(SELECT_OQL_TEMPLATE, (isCountQuery ? "count(*)" : "*"), - validateDomainType(domainType).getSimpleName()); - } - - /* (non-Javadoc) */ - static Class validateDomainType(Class domainType) { - Assert.notNull(domainType, "domainType must not be null"); - return domainType; - } - - /* (non-Javadoc) */ - static String validateQuery(String query) { - Assert.hasText(query, "An OQL Query must be specified"); - return query; + /** + * Factory method used to construct an instance of {@link QueryString} initialized with the given {@link String query}. + * + * @param query {@link String} containing the OQL query. + * @return a new {@link QueryString} initialized with the given {@link String query}. + * @throws IllegalArgumentException if {@link String query} is not specified. + * @see #QueryString(String) + */ + public static QueryString of(String query) { + return new QueryString(query); } /** - * Constructs an instance of {@link QueryString} initialized with the given GemFire OQL Query {@link String}. + * Factory method used to construct an instance of {@link QueryString} initialized with + * the given {@link Class domain type} for which the query will be created. * - * @param query {@link String} specifying the GemFire OQL Query. - * @throws IllegalArgumentException if the query string is unspecified (null or empty). + * @param domainType {@link Class domain object type} for which the query will be created. + * @return a new {@link QueryString} initialized with the given {@link String query}. + * @throws IllegalArgumentException if {@link Class domain type} is {@literal null}. + * @see #QueryString(Class) + */ + public static QueryString from(Class domainType) { + return new QueryString(domainType); + } + + /** + * Factory method used to construct an instance of {@link QueryString} initialized with + * the given {@link Class domain type} for which a count query will be created. + * + * @param domainType {@link Class domain object type} for which the query will be created. + * @return a new {@link QueryString} initialized with the given {@link String query}. + * @throws IllegalArgumentException if {@link Class domain type} is {@literal null}. + * @see #QueryString(Class) + */ + public static QueryString count(Class domainType) { + return new QueryString(domainType, true); + } + + static String asQuery(Class domainType, boolean isCountQuery) { + return String.format(SELECT_OQL_TEMPLATE, isCountQuery ? COUNT_QUERY : STAR_QUERY, + validateDomainType(domainType).getSimpleName()); + } + + static Class validateDomainType(Class domainType) { + Assert.notNull(domainType, "Domain type is required"); + return domainType; + } + + static String validateQuery(String query) { + Assert.hasText(query, String.format("Query [%s] is required", query)); + return query; + } + + private final String query; + + /** + * Constructs a new instance of {@link QueryString} initialized with the given {@link String OQL query}. + * + * @param query {@link String} containing the OQL query. + * @throws IllegalArgumentException if {@link String query} is {@literal null} or empty. + * @see #validateQuery(String) + * @see java.lang.String */ public QueryString(String query) { this.query = validateQuery(query); } /** - * Constructs a GemFire OQL {@literal SELECT} Query for the given domain class. + * Constructs a new instance of {@link QueryString} initialized from the given {@link Class domain type}, + * which is used to construct an OQL {@literal SELECT} query statement. * - * @param domainType application domain object type to query; must not be {@literal null}. + * @param domainType {@link Class application domain type} to query; must not be {@literal null}. + * @throws IllegalArgumentException if {@link Class domain type} is {@literal null}. * @see #QueryString(Class, boolean) */ @SuppressWarnings("unused") @@ -101,16 +146,49 @@ public class QueryString { } /** - * Constructs a GemFire OQL {@literal SELECT} Query for the given domain class. {@code isCountQuery} indicates - * whether to select a count or select the contents of the objects of the given domain object type. + * Constructs a new instance of {@link QueryString} initialized from the given {@link Class domain type}, + * which is used to construct an OQL {@literal SELECT} query statement. * - * @param domainType application domain object type to query; must not be {@literal null}. - * @param isCountQuery boolean value to indicate if this is a count query. - * @throws IllegalArgumentException if {@code domainType} is null. + * {@code asCountQuery} is a {@link Boolean} flag indicating whether to select a count or select the contents + * of the objects for the given {@link Class domain type}. + * + * @param domainType {@link Class application domain type} to query; must not be {@literal null}. + * @param asCountQuery boolean value to indicate if this is a count query. + * @throws IllegalArgumentException if {@link Class domain type} is {@literal null}. + * @see #asQuery(Class, boolean) * @see #QueryString(String) */ - public QueryString(Class domainType, boolean isCountQuery) { - this(asQuery(domainType, isCountQuery)); + public QueryString(Class domainType, boolean asCountQuery) { + this(asQuery(domainType, asCountQuery)); + } + + /** + * Replaces the {@literal SELECT query} with a {@literal SELECT DISTINCT query} if the {@link String query} + * is not already distinct; i.e. does not contain the {@literal DISTINCT} keyword. + * + * @return a {@literal SELECT DISTINCT query} if {@link String query} does not contain + * the {@literal DISTINCT} keyword. + * @see java.lang.String#replaceFirst(String, String) + * @see #asDistinct(String) + */ + public QueryString asDistinct() { + return QueryString.of(asDistinct(this.query)); + } + + /** + * Replaces the {@literal SELECT query} with a {@literal SELECT DISTINCT query} if the {@link String query} + * is not already distinct; i.e. does not contain the {@literal DISTINCT} keyword. + * + * @param query {@link String} containing the {@link String query} to evaluate. + * @return a {@literal SELECT DISTINCT query} if {@link String query} does not contain + * the {@literal DISTINCT} keyword. + * @see java.lang.String#replaceFirst(String, String) + */ + String asDistinct(String query) { + + return query.contains(OqlKeyword.DISTINCT.getKeyword()) ? query + : query.replaceFirst(OqlKeyword.SELECT.getKeyword(), + String.format("%1$s %2$s", OqlKeyword.SELECT.getKeyword(), OqlKeyword.DISTINCT.getKeyword())); } /** @@ -121,8 +199,9 @@ public class QueryString { * @return a Query String having "in" parameters bound with values. */ public QueryString bindIn(Collection values) { - if (values != null) { - return new QueryString(this.query.replaceFirst(IN_PATTERN, String.format("(%s)", + + if (!nullSafeIsEmpty(values)) { + return QueryString.of(this.query.replaceFirst(IN_PATTERN, String.format("(%s)", StringUtils.collectionToDelimitedString(values, ", ", "'", "'")))); } @@ -130,35 +209,40 @@ public class QueryString { } /** - * Replaces the domain classes referenced inside the current query with the given {@link Region}. + * Replaces the {@link Class domain classes} referenced inside the current {@link String query} + * with the given {@link Region}. * - * @param domainClass the class type of the GemFire persistent entity to query; must not be {@literal null}. - * @param region the GemFire Region in which to query; must not be {@literal null}. - * @return a Query String with the FROM clause in the OQL statement evaluated and replaced with - * the fully-qualified Region to query. + * @param domainClass {@link Class type} of the persistent entity to query; must not be {@literal null}. + * @param region {@link Region} to query; must not be {@literal null}. + * @return a new {@link QueryString} with an OQL {@literal SELECT statement} having a {@literal FROM clause} + * based on the selected {@link Region}. * @see org.apache.geode.cache.Region + * @see java.lang.Class */ @SuppressWarnings("unused") - public QueryString forRegion(Class domainClass, Region region) { - return new QueryString(this.query.replaceAll(REGION_PATTERN, region.getFullPath())); + public QueryString fromRegion(Class domainClass, Region region) { + return QueryString.of(this.query.replaceAll(REGION_PATTERN, region.getFullPath())); } - /** + /** * Returns the parameter indexes used in this query. * * @return the parameter indexes used in this query or an empty {@link Iterable} if none are used. * @see java.lang.Iterable */ public Iterable getInParameterIndexes() { + Pattern pattern = Pattern.compile(IN_PARAMETER_PATTERN); - Matcher matcher = pattern.matcher(query); - List result = new ArrayList<>(); + + Matcher matcher = pattern.matcher(this.query); + + List indexes = new ArrayList<>(); while (matcher.find()) { - result.add(Integer.parseInt(matcher.group())); + indexes.add(Integer.parseInt(matcher.group())); } - return result; + return indexes; } /** @@ -171,36 +255,26 @@ public class QueryString { * @see org.springframework.data.gemfire.repository.query.QueryString */ public QueryString orderBy(Sort sort) { + if (hasSort(sort)) { - StringBuilder orderClause = new StringBuilder("ORDER BY "); + + StringBuilder orderByClause = new StringBuilder("ORDER BY "); + int count = 0; for (Sort.Order order : sort) { - orderClause.append(count++ > 0 ? ", " : ""); - orderClause.append(String.format("%1$s %2$s", order.getProperty(), order.getDirection())); + orderByClause.append(count++ > 0 ? ", " : ""); + orderByClause.append(String.format("%1$s %2$s", order.getProperty(), order.getDirection())); } - return new QueryString(String.format("%1$s %2$s", makeDistinct(this.query), orderClause.toString())); + return new QueryString(String.format("%1$s %2$s", asDistinct(this.query), orderByClause.toString())); } return this; } - /* (non-Javadoc) */ private boolean hasSort(Sort sort) { - return (sort != null && sort.iterator().hasNext()); - } - - /** - * Replaces the SELECT query with a SELECT DISTINCT query if the query does not contain the DISTINCT OQL keyword. - * - * @param query {@link String} containing the query to evaluate. - * @return a SELECT DISTINCT query if {@code query} does not contain the DISTINCT OQL keyword. - */ - String makeDistinct(String query) { - return (query.contains(OqlKeyword.DISTINCT.getKeyword()) ? query - : query.replaceFirst(OqlKeyword.SELECT.getKeyword(), - String.format("%1$s %2$s", OqlKeyword.SELECT.getKeyword(), OqlKeyword.DISTINCT.getKeyword()))); + return sort != null && sort.iterator().hasNext(); } /** @@ -210,15 +284,17 @@ public class QueryString { * @return a new {@link QueryString} if hints are not null or empty, or return this {@link QueryString}. */ public QueryString withHints(String... hints) { + if (!ObjectUtils.isEmpty(hints)) { + StringBuilder builder = new StringBuilder(); for (String hint : hints) { builder.append(builder.length() > 0 ? ", " : ""); - builder.append(String.format("'%1$s'", hint)); + builder.append(String.format("'%s'", hint)); } - return new QueryString(String.format(HINTS_OQL_TEMPLATE, builder.toString(), query)); + return QueryString.of(String.format(HINTS_OQL_TEMPLATE, builder.toString(), this.query)); } return this; @@ -231,8 +307,8 @@ public class QueryString { * @return a new {@link QueryString} if an import was declared, or return this {@link QueryString}. */ public QueryString withImport(String importExpression) { - return (StringUtils.hasText(importExpression) ? - new QueryString(String.format(IMPORT_OQL_TEMPLATE, importExpression, query)) : this); + return StringUtils.hasText(importExpression) ? + QueryString.of(String.format(IMPORT_OQL_TEMPLATE, importExpression, this.query)) : this; } /** @@ -242,7 +318,7 @@ public class QueryString { * @return a new {@link QueryString} if a limit was specified, or return this {@link QueryString}. */ public QueryString withLimit(Integer limit) { - return (limit != null ? new QueryString(String.format(LIMIT_OQL_TEMPLATE, query, limit)) : this); + return limit != null ? QueryString.of(String.format(LIMIT_OQL_TEMPLATE, this.query, limit)) : this; } /** @@ -251,15 +327,18 @@ public class QueryString { * @return a new {@link QueryString} with tracing enabled. */ public QueryString withTrace() { - return new QueryString(String.format(TRACE_OQL_TEMPLATE, query)); + return QueryString.of(String.format(TRACE_OQL_TEMPLATE, this.query)); } - /* - * (non-Javadoc) + /** + * Returns a {@link String} representation of this {@link QueryString}. + * + * @return a {@link String} representation of this {@link QueryString}. * @see java.lang.Object#toString() + * @see java.lang.String */ @Override public String toString() { - return query; + return this.query; } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java index 0215cea7..fcc1d300 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java @@ -15,14 +15,19 @@ */ package org.springframework.data.gemfire.repository.query; +import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeSize; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + import java.util.Collection; import java.util.Collections; import org.apache.geode.cache.query.SelectResults; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -49,8 +54,14 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * Constructor used for testing purposes only! */ StringBasedGemfireRepositoryQuery() { - query = null; - template = null; + + this.query = null; + this.template = null; + + register(ProvidedQueryPostProcessors.LIMIT + .processBefore(ProvidedQueryPostProcessors.IMPORT) + .processBefore(ProvidedQueryPostProcessors.HINT) + .processBefore(ProvidedQueryPostProcessors.TRACE)); } /** @@ -73,32 +84,61 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @param template must not be {@literal null}. */ public StringBasedGemfireRepositoryQuery(String query, GemfireQueryMethod queryMethod, GemfireTemplate template) { + super(queryMethod); Assert.notNull(template, "GemfireTemplate must not be null"); + Assert.state(!(queryMethod.isModifyingQuery() || queryMethod.isPageQuery()), INVALID_QUERY); this.userDefinedQuery |= !StringUtils.hasText(query); - this.query = new QueryString(StringUtils.hasText(query) ? query : queryMethod.getAnnotatedQuery()); + this.query = QueryString.of(StringUtils.hasText(query) ? query : queryMethod.getAnnotatedQuery()); this.template = template; - if (queryMethod.isModifyingQuery() || queryMethod.isPageQuery()) { - throw new IllegalStateException(INVALID_QUERY); - } + register(ProvidedQueryPostProcessors.LIMIT + .processBefore(ProvidedQueryPostProcessors.IMPORT) + .processBefore(ProvidedQueryPostProcessors.HINT) + .processBefore(ProvidedQueryPostProcessors.TRACE)); } - /* - * (non-Javadoc) + /** + * Sets this {@link RepositoryQuery} to be user-defined. + * + * @return this {@link RepositoryQuery}. */ public StringBasedGemfireRepositoryQuery asUserDefinedQuery() { this.userDefinedQuery = true; return this; } - /* - * (non-Javadoc) + /** + * Determines whether the query represented by this {@link RepositoryQuery} is user-defined or was generated by + * the Spring Data {@link Repository} infrastructure. + * + * @return a boolean value indicating whether this {@link RepositoryQuery} is user-defined or was generated by + * the Spring Data {@link Repository} infrastructure. */ public boolean isUserDefinedQuery() { - return userDefinedQuery; + return this.userDefinedQuery; + } + + /** + * Returns a reference to the {@link QueryString managed query}. + * + * @return a reference to the {@link QueryString managed query}. + * @see org.springframework.data.gemfire.repository.query.QueryString + */ + protected QueryString getQuery() { + return this.query; + } + + /** + * Returns a reference to the {@link GemfireTemplate} used to perform all data access and query operations. + * + * @return a reference to the {@link GemfireTemplate} used to perform all data access and query operations. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + protected GemfireTemplate getTemplate() { + return this.template; } /* @@ -106,72 +146,69 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) */ @Override - public Object execute(Object[] parameters) { - QueryMethod localQueryMethod = getQueryMethod(); + public Object execute(Object[] arguments) { - QueryString query = (isUserDefinedQuery() ? this.query - : this.query.forRegion(localQueryMethod.getEntityInformation().getJavaType(), template.getRegion())); + QueryMethod queryMethod = getQueryMethod(); + + QueryString query = preProcess(queryMethod, getQuery(), arguments); + + String queryString = query.toString(); + String processedQueryString = getQueryPostProcessor().postProcess(queryMethod, queryString, arguments); + + SelectResults selectResults = getTemplate().find(processedQueryString, arguments); + + return postProcess(queryMethod, selectResults); + } + + QueryString preProcess(QueryMethod queryMethod, QueryString query, Object[] arguments) { + + query = isUserDefinedQuery() ? query + : query.fromRegion(queryMethod.getEntityInformation().getJavaType(), getTemplate().getRegion()); ParametersParameterAccessor parameterAccessor = - new ParametersParameterAccessor(localQueryMethod.getParameters(), parameters); + new ParametersParameterAccessor(queryMethod.getParameters(), arguments); for (Integer index : query.getInParameterIndexes()) { query = query.bindIn(toCollection(parameterAccessor.getBindableValue(index - 1))); } - query = applyQueryAnnotationExtensions(localQueryMethod, query); + return query; + } - Collection result = toCollection(template.find(query.toString(), parameters)); + Object postProcess(QueryMethod queryMethod, SelectResults selectResults) { - if (localQueryMethod.isCollectionQuery()) { - return result; + Collection collection = toCollection(selectResults); + + if (queryMethod.isCollectionQuery()) { + return collection; } - else if (localQueryMethod.isQueryForEntity()) { - if (result.isEmpty()) { + else if (queryMethod.isQueryForEntity()) { + if (collection.isEmpty()) { return null; } - else if (result.size() == 1) { - return result.iterator().next(); + else if (collection.size() == 1) { + return collection.iterator().next(); } else { - throw new IncorrectResultSizeDataAccessException(1, result.size()); + throw new IncorrectResultSizeDataAccessException(1, collection.size()); } } - else if (isSingleResultNonEntityQuery(localQueryMethod, result)) { - return result.iterator().next(); + else if (isSingleNonEntityResult(queryMethod, collection)) { + return collection.iterator().next(); } else { - throw new IllegalStateException("Unsupported query: " + query.toString()); + throw newIllegalStateException("Unsupported query: %s", query.toString()); } } - QueryString applyQueryAnnotationExtensions(QueryMethod queryMethod, QueryString queryString) { - QueryString resolvedQueryString = queryString; + @SuppressWarnings("all") + boolean isSingleNonEntityResult(QueryMethod method, Collection result) { - if (queryMethod instanceof GemfireQueryMethod) { - GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; - String query = queryString.toString().toUpperCase(); + Class methodReturnType = method.getReturnedObjectType(); - if (gemfireQueryMethod.hasImport() && !QueryString.IMPORT_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withImport(gemfireQueryMethod.getImport()); - } - if (gemfireQueryMethod.hasHint() && !QueryString.HINT_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withHints(gemfireQueryMethod.getHints()); - } - if (gemfireQueryMethod.hasLimit() && !QueryString.LIMIT_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withLimit(gemfireQueryMethod.getLimit()); - } - if (gemfireQueryMethod.hasTrace() && !QueryString.TRACE_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withTrace(); - } - } + methodReturnType = methodReturnType != null ? methodReturnType : Void.class; - return resolvedQueryString; - } - - boolean isSingleResultNonEntityQuery(QueryMethod method, Collection result) { - return (!method.isCollectionQuery() && method.getReturnedObjectType() != null - && !Void.TYPE.equals(method.getReturnedObjectType()) && result != null && result.size() == 1); + return nullSafeSize(result) == 1 && !Void.TYPE.equals(methodReturnType) && !method.isCollectionQuery(); } /** @@ -185,7 +222,8 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @see org.springframework.util.CollectionUtils#arrayToList(Object) * @see org.apache.geode.cache.query.SelectResults */ - Collection toCollection(final Object source) { + Collection toCollection(Object source) { + if (source instanceof SelectResults) { return ((SelectResults) source).asList(); } @@ -198,6 +236,81 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { return Collections.emptyList(); } - return (source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singletonList(source)); + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singletonList(source); + } + + enum ProvidedQueryPostProcessors implements QueryPostProcessor { + + HINT { + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasHint() && !QueryString.HINT_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withHints(gemfireQueryMethod.getHints()).toString(); + } + } + + return query; + } + }, + + IMPORT { + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasImport() && !QueryString.IMPORT_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withImport(gemfireQueryMethod.getImport()).toString(); + } + } + + return query; + } + }, + + LIMIT { + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasLimit() && !QueryString.LIMIT_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withLimit(gemfireQueryMethod.getLimit()).toString(); + } + } + + return query; + } + }, + + TRACE { + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasTrace() && !QueryString.TRACE_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withTrace().toString(); + } + } + + return query; + } + } } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java b/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java index fb10c331..b27fa17b 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java @@ -28,6 +28,7 @@ import org.springframework.util.StringUtils; * @since 1.0.0 */ public enum OqlKeyword { + AND, AS, COUNT, diff --git a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java index 8d10db5a..87b0b4ae 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java +++ b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.util.Optional; import org.apache.geode.cache.Region; +import org.springframework.context.ApplicationContext; import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; import org.springframework.data.gemfire.mapping.GemfirePersistentProperty; @@ -34,6 +35,7 @@ import org.springframework.data.gemfire.repository.query.PartTreeGemfireReposito import org.springframework.data.gemfire.repository.query.StringBasedGemfireRepositoryQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -41,6 +43,9 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -54,25 +59,65 @@ import org.springframework.util.StringUtils; */ public class GemfireRepositoryFactory extends RepositoryFactorySupport { + private static final Class REGION_ANNOTATION = + org.springframework.data.gemfire.mapping.annotation.Region.class; + + static final String REGION_NOT_FOUND = "Region [%1$s] for Domain Type [%2$s] using Repository [%3$s] was not found;" + + " You must configure a Region with name [%1$s] in the application context"; + + static final String REGION_REPOSITORY_ID_TYPE_MISMATCH = + "Region [%1$s] requires keys of type [%2$s], but Repository [%3$s] declared an id of type [%4$s]"; + + static final String REPOSITORY_ENTITY_ID_TYPE_MISMATCH = + "Repository [%1$s] declared an id of type [%2$s], but entity [%3$s] has an id of type [%4$s]"; + private final MappingContext, GemfirePersistentProperty> mappingContext; private final Regions regions; /** - * Creates a new {@link GemfireRepositoryFactory}. + * Constructs a new instance of {@link GemfireRepositoryFactory} initialized with the given collection + * of configured {@link Region Regions} and the {@link MappingContext}. * - * @param regions must not be {@literal null}. - * @param mappingContext the {@link MappingContext} used by the constructed Repository for mapping entities - * to the underlying data store, must not be {@literal null}. + * @param regions {@link Iterable} collection of configured {@link Region Regions} used by this application; + * must not be {@literal null}. + * @param mappingContext {@link MappingContext} used to map entities to the underlying data store, + * must not be {@literal null}. + * @throws IllegalArgumentException if either {@link Regions} or the {@link MappingContext} is {@literal null}. + * @see org.springframework.data.gemfire.mapping.GemfireMappingContext + * @see org.springframework.data.gemfire.mapping.Regions + * @see org.springframework.data.mapping.context.MappingContext */ - public GemfireRepositoryFactory(Iterable> regions, - MappingContext, GemfirePersistentProperty> mappingContext) { + public GemfireRepositoryFactory(@NonNull Iterable> regions, + @NonNull MappingContext, GemfirePersistentProperty> mappingContext) { - Assert.notNull(regions, "Regions must not be null"); - Assert.notNull(mappingContext, "MappingContext must not be null"); + Assert.notNull(regions, "Regions are required"); + Assert.notNull(mappingContext, "MappingContext is required"); + this.regions = new Regions(regions, mappingContext); this.mappingContext = mappingContext; - this.regions = new Regions(regions, this.mappingContext); + } + + /** + * Returns a reference to the GemFire {@link MappingContext} used to provide mapping meta-data + * between {@link Class entity types} and the data store. + * + * @return a reference to the GemFire {@link MappingContext}. + * @see org.springframework.data.gemfire.mapping.GemfireMappingContext + * @see org.springframework.data.mapping.context.MappingContext + */ + protected MappingContext, GemfirePersistentProperty> getMappingContext() { + return this.mappingContext; + } + + /** + * Returns a reference to the configured, application-defined {@link Region Regions}. + * + * @return a reference to the configured, application-defined {@link Region Regions}. + * @see org.springframework.data.gemfire.mapping.Regions + */ + protected Regions getRegions() { + return this.regions; } /* @@ -80,13 +125,18 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport { * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) */ @Override - @SuppressWarnings("unchecked") public GemfireEntityInformation getEntityInformation(Class domainClass) { + return new DefaultGemfireEntityInformation<>(resolvePersistentEntity(domainClass)); + } - GemfirePersistentEntity entity = - (GemfirePersistentEntity) mappingContext.getPersistentEntity(domainClass); - - return new DefaultGemfireEntityInformation<>(entity); + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport + * #getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) + */ + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return SimpleGemfireRepository.class; } /* @@ -99,53 +149,96 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport { GemfireEntityInformation entityInformation = getEntityInformation(repositoryInformation.getDomainType()); - GemfireTemplate gemfireTemplate = getTemplate(repositoryInformation); + GemfireTemplate gemfireTemplate = newTemplate(repositoryInformation); return getTargetRepositoryViaReflection(repositoryInformation, gemfireTemplate, entityInformation); } - GemfireTemplate getTemplate(RepositoryMetadata metadata) { - - GemfirePersistentEntity entity = mappingContext.getPersistentEntity(metadata.getDomainType()); - - String entityRegionName = entity.getRegionName(); - String repositoryRegionName = getRepositoryRegionName(metadata.getRepositoryInterface()); - String resolvedRegionName = StringUtils.hasText(repositoryRegionName) ? repositoryRegionName : entityRegionName; - - Region region = regions.getRegion(resolvedRegionName); - - if (region == null) { - throw newIllegalStateException("No Region [%1$s] was found for domain class [%2$s];" - + " Make sure you have configured a GemFire Region of that name in your application context", - resolvedRegionName, metadata.getDomainType().getName()); - } - - Class regionKeyType = region.getAttributes().getKeyConstraint(); - Class entityIdType = metadata.getIdType(); - - if (regionKeyType != null && entity.getIdProperty() != null) { - Assert.isTrue(regionKeyType.isAssignableFrom(entityIdType), String.format( - "The Region referenced only supports keys of type [%1$s], but the entity to be stored has an id of type [%2$s]", - regionKeyType.getName(), entityIdType.getName())); - } - - return new GemfireTemplate(region); - } - - String getRepositoryRegionName(Class repositoryInterface) { - return (repositoryInterface.isAnnotationPresent(org.springframework.data.gemfire.mapping.annotation.Region.class) ? - repositoryInterface.getAnnotation(org.springframework.data.gemfire.mapping.annotation.Region.class).value() : null); - } - - /* - * (non-Javadoc) + /** + * Constructs a new instance of {@link GemfireTemplate} initialized with the identified {@link Region} + * used to back all persistent, data access operations defined by the {@link Repository}. * - * @see org.springframework.data.repository.core.support.RepositoryFactorySupport - * #getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) + * @param repositoryMetadata {@link RepositoryMetadata} containing meta-data about the {@link Repository}. + * @return a new instance of {@link GemfireTemplate} initialized with the identified {@link Region}. + * @see #resolvePersistentEntity(Class) + * @see #resolveRegion(RepositoryMetadata, GemfirePersistentEntity) + * @see #validate(RepositoryMetadata, GemfirePersistentEntity, Region) + * @see org.springframework.data.repository.core.RepositoryMetadata + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.apache.geode.cache.Region */ - @Override - protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { - return SimpleGemfireRepository.class; + protected GemfireTemplate newTemplate(RepositoryMetadata repositoryMetadata) { + + GemfirePersistentEntity entity = resolvePersistentEntity(repositoryMetadata.getDomainType()); + + return new GemfireTemplate(validate(repositoryMetadata, entity, resolveRegion(repositoryMetadata, entity))); + } + + @Nullable + @SuppressWarnings("unchecked") + private GemfirePersistentEntity resolvePersistentEntity(Class domainType) { + return (GemfirePersistentEntity) getMappingContext().getPersistentEntity(domainType); + } + + private Region resolveRegion(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity entity) { + + String resolvedRegionName = getRepositoryRegionName(repositoryMetadata) + .orElseGet(() -> getEntityRegionName(repositoryMetadata, entity)); + + return resolveRegion(repositoryMetadata, resolvedRegionName); + } + + private Region validate(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity entity, + Region region) { + + Assert.notNull(region, "Region is required"); + + Class repositoryIdType = repositoryMetadata.getIdType(); + + Optional.ofNullable(region.getAttributes().getKeyConstraint()) + .ifPresent(regionKeyType -> Assert.isTrue(regionKeyType.isAssignableFrom(repositoryIdType), + () -> String.format(REGION_REPOSITORY_ID_TYPE_MISMATCH, region.getFullPath(), regionKeyType.getName(), + repositoryMetadata.getRepositoryInterface().getName(), repositoryIdType.getName()))); + + Optional.ofNullable(entity) + .map(GemfirePersistentEntity::getIdProperty) + .ifPresent(entityIdProperty -> Assert.isTrue(repositoryIdType.isAssignableFrom(entityIdProperty.getType()), + () -> String.format(REPOSITORY_ENTITY_ID_TYPE_MISMATCH, repositoryMetadata.getRepositoryInterface().getName(), + repositoryIdType.getName(), entityIdProperty.getOwner().getName(), entityIdProperty.getTypeName()))); + + return region; + } + + String getEntityRegionName(@NonNull RepositoryMetadata repositoryMetadata, + @Nullable GemfirePersistentEntity entity) { + + Optional optionalEntity = Optional.ofNullable(entity); + + return optionalEntity + .map(GemfirePersistentEntity::getRegionName) + .filter(StringUtils::hasText) + .orElseGet(() -> optionalEntity + .map(GemfirePersistentEntity::getType) + .map(Class::getSimpleName) + .orElseGet(() -> repositoryMetadata.getDomainType().getSimpleName())); + } + + Optional getRepositoryRegionName(@NonNull RepositoryMetadata repositoryMetadata) { + + return Optional.ofNullable(repositoryMetadata) + .map(RepositoryMetadata::getRepositoryInterface) + .filter(repositoryInterface -> repositoryInterface.isAnnotationPresent(REGION_ANNOTATION)) + .map(repositoryInterface -> repositoryInterface.getAnnotation(REGION_ANNOTATION)) + .map(regionAnnotation -> regionAnnotation.value()) + .filter(StringUtils::hasText); + } + + Region resolveRegion(@NonNull RepositoryMetadata repositoryMetadata, String regionNamePath) { + + return Optional.ofNullable(getRegions().getRegion(regionNamePath)) + .orElseThrow(() -> newIllegalStateException(REGION_NOT_FOUND, + regionNamePath, repositoryMetadata.getDomainType().getName(), + repositoryMetadata.getRepositoryInterface().getName())); } /* @@ -158,22 +251,33 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport { protected Optional getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) { - return Optional.of( - (Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) -> { + return Optional.of((Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory projectionFactory, + NamedQueries namedQueries) -> { - GemfireQueryMethod queryMethod = new GemfireQueryMethod(method, metadata, factory, mappingContext); - GemfireTemplate template = getTemplate(metadata); + GemfireQueryMethod queryMethod = + newQueryMethod(method, repositoryMetadata, projectionFactory, evaluationContextProvider); + + GemfireTemplate template = newTemplate(repositoryMetadata); if (queryMethod.hasAnnotatedQuery()) { return new StringBasedGemfireRepositoryQuery(queryMethod, template).asUserDefinedQuery(); } - if (namedQueries.hasQuery(queryMethod.getNamedQueryName())) { - return new StringBasedGemfireRepositoryQuery(namedQueries.getQuery(queryMethod.getNamedQueryName()), + String namedQueryName = queryMethod.getNamedQueryName(); + + if (namedQueries.hasQuery(namedQueryName)) { + return new StringBasedGemfireRepositoryQuery(namedQueries.getQuery(namedQueryName), queryMethod, template).asUserDefinedQuery(); } return new PartTreeGemfireRepositoryQuery(queryMethod, template); }); } + + @SuppressWarnings({ "unchecked", "unused" }) + protected T newQueryMethod(Method method, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory, EvaluationContextProvider evaluationContextProvider) { + + return (T) new GemfireQueryMethod(method, repositoryMetadata, projectionFactory, getMappingContext()); + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java index 2034210c..a41c4bd2 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java @@ -16,23 +16,41 @@ package org.springframework.data.gemfire.repository.support; -import java.io.Serializable; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.geode.cache.Region; import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.OrderComparator; import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; import org.springframework.data.gemfire.mapping.GemfirePersistentProperty; +import org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery; +import org.springframework.data.gemfire.repository.query.QueryPostProcessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.RepositoryDefinition; +import org.springframework.data.repository.core.support.QueryCreationListener; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.NonNull; import org.springframework.util.Assert; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + /** * {@link FactoryBean} adapter for {@link GemfireRepositoryFactory}. * @@ -50,9 +68,11 @@ import org.springframework.util.Assert; * @see org.springframework.data.repository.core.support.RepositoryFactorySupport * @see org.apache.geode.cache.Region */ -public class GemfireRepositoryFactoryBean, S, ID extends Serializable> +public class GemfireRepositoryFactoryBean, S, ID> extends RepositoryFactoryBeanSupport implements ApplicationContextAware { + private ApplicationContext applicationContext; + private Iterable> regions; private MappingContext, GemfirePersistentProperty> mappingContext; @@ -67,33 +87,49 @@ public class GemfireRepositoryFactoryBean, S, ID ext } /** - * Sets a reference to the Spring {@link ApplicationContext} in which this object runs. + * Sets a reference to the Spring {@link ApplicationContext}. * - * @param applicationContext the Spring {@link ApplicationContext} reference. + * @param applicationContext reference to the Spring {@link ApplicationContext}. * @see org.springframework.context.ApplicationContextAware#setApplicationContext(ApplicationContext) * @see org.springframework.context.ApplicationContext */ @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({ "rawtypes", "unchecked" }) public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - Collection regions = applicationContext.getBeansOfType(Region.class).values(); - this.regions = (Iterable) Collections.unmodifiableCollection(regions); + + this.applicationContext = applicationContext; + + this.regions = Collections.unmodifiableSet(applicationContext.getBeansOfType(Region.class).entrySet().stream() + .>map(Map.Entry::getValue) + .collect(Collectors.toSet())); } /** - * Configures the {@link MappingContext} used to perform domain object type to store mappings. + * Returns an {@link Optional} reference to the configured Spring {@link ApplicationContext}. * - * @param mappingContext the {@link MappingContext} to set. + * @return an {@link Optional} reference to the configured Spring {@link ApplicationContext}. + * @see org.springframework.context.ApplicationContext + * @see java.util.Optional + */ + protected Optional getApplicationContext() { + return Optional.ofNullable(this.applicationContext); + } + + /** + * Configures the {@link MappingContext} used to perform application domain object type to data store mappings. + * + * @param mappingContext {@link MappingContext} to configure. * @see org.springframework.data.gemfire.mapping.GemfireMappingContext * @see org.springframework.data.mapping.context.MappingContext */ + @Autowired(required = false) public void setGemfireMappingContext(MappingContext, GemfirePersistentProperty> mappingContext) { setMappingContext(mappingContext); this.mappingContext = mappingContext; } /** - * Returns a reference to the Spring Data {@link MappingContext} used to perform domain object type + * Returns a reference to the Spring Data {@link MappingContext} used to perform application domain object type * to data store mappings. * * @return a reference to the {@link MappingContext}. @@ -106,10 +142,31 @@ public class GemfireRepositoryFactoryBean, S, ID ext } /** - * Returns an {@link Iterable} reference to the GemFire {@link Region}s defined - * in the Spring {@link ApplicationContext}. + * Attempts to resolve the {@link MappingContext} used to map {@link GemfirePersistentEntity entities} + * to Pivotal GemFire. * - * @return a reference to all GemFire {@link Region}s defined in the Spring {@link ApplicationContext}. + * @return a reference to the resolved {@link MappingContext}. + * @throws IllegalStateException if the {@link MappingContext} cannot be resolved. + * @see org.springframework.data.gemfire.mapping.GemfireMappingContext + * @see org.springframework.data.mapping.context.MappingContext + */ + private MappingContext, GemfirePersistentProperty> resolveGemfireMappingContext() { + + MappingContext, GemfirePersistentProperty> mappingContext = + getGemfireMappingContext(); + + Assert.state(mappingContext != null, "GemfireMappingContext must not be null"); + + return mappingContext; + } + + /** + * Returns an {@link Iterable} reference to the {@link Region Regions} + * defined in the Spring {@link ApplicationContext}. + * + * @return a reference to all {@link Region Regions} defined in the Spring {@link ApplicationContext}. + * @see org.apache.geode.cache.Region + * @see java.lang.Iterable */ protected Iterable> getRegions() { return this.regions; @@ -123,7 +180,15 @@ public class GemfireRepositoryFactoryBean, S, ID ext */ @Override protected RepositoryFactorySupport createRepositoryFactory() { - return new GemfireRepositoryFactory(getRegions(), getGemfireMappingContext()); + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(getRegions(), getGemfireMappingContext()); + + getApplicationContext() + .map(applicationContext -> new QueryPostProcessorRegistrationOnQueryCreationListener(applicationContext)) + .ifPresent(repositoryFactory::addQueryCreationListener); + + return repositoryFactory; } /* @@ -132,7 +197,108 @@ public class GemfireRepositoryFactoryBean, S, ID ext */ @Override public void afterPropertiesSet() { - Assert.state(getGemfireMappingContext() != null, "GemfireMappingContext must not be null"); + resolveGemfireMappingContext(); super.afterPropertiesSet(); } + + protected class QueryPostProcessorRegistrationOnQueryCreationListener + implements QueryCreationListener { + + private Iterable queryPostProcessorsMetadata; + + public QueryPostProcessorRegistrationOnQueryCreationListener(ApplicationContext applicationContext) { + + Assert.notNull(applicationContext, "ApplicationContext must not be null"); + + List queryPostProcessors = + new ArrayList<>(applicationContext.getBeansOfType(QueryPostProcessor.class).values()); + + queryPostProcessors.sort(OrderComparator.INSTANCE); + + this.queryPostProcessorsMetadata = queryPostProcessors.stream() + .map(QueryPostProcessorMetadata::from) + .collect(Collectors.toList()); + } + + protected Iterable getQueryPostProcessorsMetadata() { + return this.queryPostProcessorsMetadata; + } + + @Override + public void onCreation(GemfireRepositoryQuery repositoryQuery) { + + Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); + + StreamSupport.stream(getQueryPostProcessorsMetadata().spliterator(), false) + .filter(queryPostProcessorMetadata -> queryPostProcessorMetadata.isMatch(repositoryInterface)) + .forEach(queryPostProcessorMetadata -> queryPostProcessorMetadata.register(repositoryQuery)); + } + } + + static class QueryPostProcessorMetadata { + + private static final Map cache = new WeakHashMap<>(); + + private final Class declaredRepositoryType; + + private final QueryPostProcessor queryPostProcessor; + + static QueryPostProcessorMetadata from(@NonNull QueryPostProcessor queryPostProcessor) { + + return cache.computeIfAbsent(QueryPostProcessorKey.of(queryPostProcessor), + key -> new QueryPostProcessorMetadata(key.getQueryPostProcessor())); + } + + @SuppressWarnings("unchecked") + QueryPostProcessorMetadata(@NonNull QueryPostProcessor queryPostProcessor) { + + Assert.notNull(queryPostProcessor, "QueryPostProcessor must not be null"); + + this.queryPostProcessor = queryPostProcessor; + + List> typeArguments = ClassTypeInformation.from(queryPostProcessor.getClass()) + .getRequiredSuperTypeInformation(QueryPostProcessor.class) + .getTypeArguments(); + + this.declaredRepositoryType = Optional.of(typeArguments) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0)) + .map(typeInfo -> typeInfo.getType()) + .orElse((Class) Repository.class); + } + + @NonNull + Class getDeclaredRepositoryType() { + return this.declaredRepositoryType; + } + + @NonNull + @SuppressWarnings("unchecked") + QueryPostProcessor getQueryPostProcessor() { + return (QueryPostProcessor) this.queryPostProcessor; + } + + boolean isMatch(Class repositoryInterface) { + + return repositoryInterface != null + && (getDeclaredRepositoryType().isAssignableFrom(repositoryInterface) + || repositoryInterface.isAnnotationPresent(RepositoryDefinition.class)); + } + + GemfireRepositoryQuery register(GemfireRepositoryQuery repositoryQuery) { + + repositoryQuery.register(getQueryPostProcessor()); + + return repositoryQuery; + } + + @EqualsAndHashCode + @RequiredArgsConstructor(staticName = "of") + private static class QueryPostProcessorKey { + + @lombok.NonNull @Getter + QueryPostProcessor queryPostProcessor; + + } + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java b/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java index 4e765936..a59de2bd 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java +++ b/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java @@ -66,6 +66,7 @@ public class SimpleGemfireRepository implements GemfireRepository * @param entityInformation must not be {@literal null}. */ public SimpleGemfireRepository(GemfireTemplate template, EntityInformation entityInformation) { + Assert.notNull(template, "Template must not be null"); Assert.notNull(entityInformation, "EntityInformation must not be null"); @@ -79,9 +80,10 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public U save(U entity) { - ID id = entityInformation.getRequiredId(entity); - template.put(id, entity); + ID id = this.entityInformation.getRequiredId(entity); + + this.template.put(id, entity); return entity; } @@ -92,11 +94,12 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public Iterable saveAll(Iterable entities) { + Map entitiesToSave = new HashMap<>(); - entities.forEach(entity -> entitiesToSave.put(entityInformation.getRequiredId(entity), entity)); + entities.forEach(entity -> entitiesToSave.put(this.entityInformation.getRequiredId(entity), entity)); - template.putAll(entitiesToSave); + this.template.putAll(entitiesToSave); return entitiesToSave.values(); } @@ -107,9 +110,10 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public T save(Wrapper wrapper) { + T entity = wrapper.getEntity(); - template.put(wrapper.getKey(), entity); + this.template.put(wrapper.getKey(), entity); return entity; } @@ -120,8 +124,9 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public long count() { + SelectResults results = - template.find(String.format("SELECT count(*) FROM %s", template.getRegion().getFullPath())); + this.template.find(String.format("SELECT count(*) FROM %s", this.template.getRegion().getFullPath())); return Long.valueOf(results.iterator().next()); } @@ -141,7 +146,7 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public Optional findById(ID id) { - return Optional.ofNullable(template.get(id)); + return Optional.ofNullable(this.template.get(id)); } /* @@ -150,8 +155,9 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public Collection findAll() { + SelectResults results = - template.find(String.format("SELECT * FROM %s", template.getRegion().getFullPath())); + this.template.find(String.format("SELECT * FROM %s", this.template.getRegion().getFullPath())); return results.asList(); } @@ -162,11 +168,12 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public Iterable findAll(Sort sort) { - QueryString query = new QueryString("SELECT * FROM /RegionPlaceholder") - .forRegion(entityInformation.getJavaType(), template.getRegion()) + + QueryString query = QueryString.of("SELECT * FROM /RegionPlaceholder") + .fromRegion(this.entityInformation.getJavaType(), this.template.getRegion()) .orderBy(sort); - SelectResults selectResults = template.find(query.toString()); + SelectResults selectResults = this.template.find(query.toString()); return selectResults.asList(); } @@ -177,9 +184,10 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public Collection findAllById(Iterable ids) { + List keys = Streamable.of(ids).stream().collect(StreamUtils.toUnmodifiableList()); - return CollectionUtils.nullSafeMap(template.getAll(keys)).values().stream() + return CollectionUtils.nullSafeMap(this.template.getAll(keys)).values().stream() .filter(Objects::nonNull).collect(Collectors.toList()); } @@ -189,7 +197,7 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public void deleteById(ID id) { - template.remove(id); + this.template.remove(id); } /* @@ -198,7 +206,7 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public void delete(T entity) { - deleteById(entityInformation.getRequiredId(entity)); + deleteById(this.entityInformation.getRequiredId(entity)); } /* @@ -216,8 +224,9 @@ public class SimpleGemfireRepository implements GemfireRepository * @see org.apache.geode.cache.RegionAttributes#getDataPolicy() */ boolean isPartitioned(Region region) { - return (region != null && region.getAttributes() != null - && isPartitioned(region.getAttributes().getDataPolicy())); + + return region != null && region.getAttributes() != null + && isPartitioned(region.getAttributes().getDataPolicy()); } /* @@ -225,7 +234,7 @@ public class SimpleGemfireRepository implements GemfireRepository * @see org.apache.geode.cache.DataPolicy#withPartitioning() */ boolean isPartitioned(DataPolicy dataPolicy) { - return (dataPolicy != null && dataPolicy.withPartitioning()); + return dataPolicy != null && dataPolicy.withPartitioning(); } /* @@ -234,8 +243,9 @@ public class SimpleGemfireRepository implements GemfireRepository * @see org.apache.geode.cache.Cache#getCacheTransactionManager() */ boolean isTransactionPresent(Region region) { - return (region.getRegionService() instanceof Cache - && isTransactionPresent(((Cache) region.getRegionService()).getCacheTransactionManager())); + + return region.getRegionService() instanceof Cache + && isTransactionPresent(((Cache) region.getRegionService()).getCacheTransactionManager()); } /* @@ -243,7 +253,7 @@ public class SimpleGemfireRepository implements GemfireRepository * @see org.apache.geode.cache.CacheTransactionManager#exists() */ boolean isTransactionPresent(CacheTransactionManager cacheTransactionManager) { - return (cacheTransactionManager != null && cacheTransactionManager.exists()); + return cacheTransactionManager != null && cacheTransactionManager.exists(); } /* (non-Javadoc) */ @@ -257,7 +267,8 @@ public class SimpleGemfireRepository implements GemfireRepository */ @Override public void deleteAll() { - template.execute((GemfireCallback) region -> { + this.template.execute((GemfireCallback) region -> { + if (isPartitioned(region) || isTransactionPresent(region)) { doRegionClear(region); } diff --git a/src/main/java/org/springframework/data/gemfire/util/CollectionUtils.java b/src/main/java/org/springframework/data/gemfire/util/CollectionUtils.java index ac388610..4a71a36b 100644 --- a/src/main/java/org/springframework/data/gemfire/util/CollectionUtils.java +++ b/src/main/java/org/springframework/data/gemfire/util/CollectionUtils.java @@ -20,6 +20,7 @@ import static java.util.stream.StreamSupport.stream; import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; @@ -29,12 +30,16 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.SortedMap; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * The CollectionUtils class is a utility class for working with Java Collections Framework and classes. + * {@link CollectionUtils} is a utility class for working with the Java Collections Framework and classes. * * @author John Blum * @see java.util.Collection @@ -62,7 +67,7 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.lang.Iterable * @see java.util.Collection */ - public static > T addAll(T collection, Iterable iterable) { + public static > T addAll(@NonNull T collection, @Nullable Iterable iterable) { Assert.notNull(collection, "Collection is required"); @@ -79,7 +84,7 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @return an unmodifiable {@link Set} containing the elements from the given object array. */ @SafeVarargs - public static Set asSet(T... elements) { + public static Set asSet(@NonNull T... elements) { Set set = new HashSet<>(elements.length); @@ -96,17 +101,10 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @return a boolean value indicating whether the collection contains at least 1 element from the given array. * @see java.util.Collection#contains(Object) */ - public static boolean containsAny(Collection collection, Object... elements) { + public static boolean containsAny(@Nullable Collection collection, @Nullable Object... elements) { - if (collection != null) { - for (Object element : nullSafeArray(elements, Object.class)) { - if (collection.contains(element)) { - return true; - } - } - } - - return false; + return Arrays.asList(nullSafeArray(elements, Object.class)).stream() + .anyMatch(element -> nullSafeCollection(collection).contains(element)); } /** @@ -117,8 +115,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.lang.Iterable * @see #nullSafeIterable(Iterable) */ + @NonNull public static Iterable emptyIterable() { - return nullSafeIterable(null); + return Collections::emptyIterator; } /** @@ -130,8 +129,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.lang.Iterable * @see java.util.Enumeration */ - public static Iterable iterable(Enumeration enumeration) { - return () -> toIterator(enumeration); + @NonNull + public static Iterable iterable(@Nullable Enumeration enumeration) { + return () -> toIterator(nullSafeEnumeration(enumeration)); } /** @@ -143,8 +143,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.lang.Iterable * @see java.util.Iterator */ - public static Iterable iterable(Iterator iterator) { - return () -> iterator; + @NonNull + public static Iterable iterable(@Nullable Iterator iterator) { + return () -> nullSafeIterator(iterator); } /** @@ -158,10 +159,27 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.util.Collections#emptyList() * @see java.util.Collection */ - public static Collection nullSafeCollection(Collection collection) { + @NonNull + public static Collection nullSafeCollection(@Nullable Collection collection) { return collection != null ? collection : Collections.emptyList(); } + /** + * Null-safe operation returning the given {@link Enumeration} if not {@literal null} + * or an {@link Collections#emptyEnumeration() empty Enumeration} if {@literal null}. + * + * @param {@link Class type} of elements contained in the {@link Enumeration}. + * @param enumeration {@link Enumeration} to evaluate. + * @return the given {@link Enumeration} if not {@literal null} + * or an {@link Collections#emptyEnumeration() empty Enumeration}. + * @see java.util.Collections#emptyEnumeration() + * @see java.util.Enumeration + */ + @NonNull + public static Enumeration nullSafeEnumeration(@Nullable Enumeration enumeration) { + return enumeration != null ? enumeration : Collections.emptyEnumeration(); + } + /** * A null-safe operation returning the original Iterable object if non-null or a default, empty Iterable * implementation if null. @@ -169,11 +187,12 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @param the class type of the iterable elements. * @param iterable the Iterable object evaluated for a null reference. * @return the Iterable object if not null or a default, empty Iterable implementation otherwise. + * @see #emptyIterable() * @see java.lang.Iterable - * @see java.util.Iterator */ - public static Iterable nullSafeIterable(Iterable iterable) { - return iterable != null ? iterable : Collections::emptyIterator; + @NonNull + public static Iterable nullSafeIterable(@Nullable Iterable iterable) { + return iterable != null ? iterable : emptyIterable(); } /** @@ -186,10 +205,27 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @return {@code iterable} if not {@literal null} or empty otherwise return {@code defaultIterable}. * @see java.lang.Iterable */ - public static > T nullSafeIterable(T iterable, T defaultIterable) { + @Nullable + public static > T nullSafeIterable(@Nullable T iterable, @Nullable T defaultIterable) { return Optional.ofNullable(iterable).filter(it -> it.iterator().hasNext()).orElse(defaultIterable); } + /** + * Null-safe operation returning the given {@link Iterator} if not {@literal null} + * or an {@link Collections#emptyIterator() empty Iterator} if {@literal null}. + * + * @param {@link Class type} of elements contained in the {@link Iterator}. + * @param iterator {@link Iterator} to evaluate. + * @return the given {@link Iterator} if not {@literal null} + * or an {@link Collections#emptyIterator() empty Iterator}. + * @see java.util.Collections#emptyIterator() + * @see java.util.Iterator + */ + @NonNull + public static Iterator nullSafeIterator(@Nullable Iterator iterator) { + return iterator != null ? iterator : Collections.emptyIterator(); + } + /** * Null-safe operation returning the given {@link List} if not {@literal null} * or an empty {@link List} if {@literal null}. @@ -200,7 +236,8 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.util.Collections#emptyList() * @see java.util.List */ - public static List nullSafeList(List list) { + @NonNull + public static List nullSafeList(@Nullable List list) { return list != null ? list : Collections.emptyList(); } @@ -216,7 +253,8 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.util.Map */ @SuppressWarnings("all") - public static Map nullSafeMap(Map map) { + @NonNull + public static Map nullSafeMap(@Nullable Map map) { return map != null ? map : Collections.emptyMap(); } @@ -230,10 +268,59 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.util.Collections#emptySet() * @see java.util.Set */ - public static Set nullSafeSet(Set set) { + @NonNull + public static Set nullSafeSet(@Nullable Set set) { return set != null ? set : Collections.emptySet(); } + /** + * Determines whether the given {@link Collection} is {@link Collection#isEmpty() empty}. + * + * @param collection {@link Collection} to evaluate. + * @return a boolean value indicating whether the given {@link Collection} is {@link Collection#isEmpty() empty}. + * @see #nullSafeCollection(Collection) + * @see java.util.Collection#isEmpty() + */ + public static boolean nullSafeIsEmpty(@Nullable Collection collection) { + return nullSafeCollection(collection).isEmpty(); + } + + /** + * Determines whether the given {@link Map} is {@link Map#isEmpty() empty}. + * + * @param map {@link Map} to evaluate. + * @return a boolean value indicating whether the given {@link Map} is {@link Map#isEmpty() empty}. + * @see #nullSafeMap(Map) + * @see java.util.Map#isEmpty() + */ + public static boolean nullSafeIsEmpty(@Nullable Map map) { + return nullSafeMap(map).isEmpty(); + } + + /** + * Determines the {@link Collection#size()} of the given {@link Collection}. + * + * @param collection {@link Collection} to evaluate. + * @return the {@link Collection#size()} of the given {@link Collection}. + * @see #nullSafeCollection(Collection) + * @see java.util.Collection#size() + */ + public static int nullSafeSize(@Nullable Collection collection) { + return nullSafeCollection(collection).size(); + } + + /** + * Determines the {@link Map#size()} of the given {@link Map}. + * + * @param map {@link Map} to evaluate. + * @return the {@link Map#size()} of the given {@link Map}. + * @see #nullSafeMap(Map) + * @see java.util.Map#size() + */ + public static int nullSafeSize(@Nullable Map map) { + return nullSafeMap(map).size(); + } + /** * Sors the elements of the given {@link List} by their natural, {@link Comparable} ordering. * @@ -243,7 +330,9 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @see java.util.Collections#sort(List) * @see java.util.List */ - public static > List sort(List list) { + public static > List sort(@NonNull List list) { + + Assert.notNull(list, "List is required"); Collections.sort(list); @@ -263,7 +352,7 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio * @throws NullPointerException if either the list or indexes are null. * @see java.util.List */ - public static List subList(List source, int... indices) { + public static List subList(@NonNull List source, int... indices) { Assert.notNull(source, "List is required"); @@ -276,23 +365,36 @@ public abstract class CollectionUtils extends org.springframework.util.Collectio return result; } - /* (non-Javadoc) */ - public static String toString(Map map) { + /** + * Returns a {@link String} representation of the given {@link Map}. + * + * @param map {@link Map} represent as a {@link String}. + * @return a {@link String} describing the given {@link Map}. + * @see #newSortedMap(Map) + * @see java.util.Map + */ + @NonNull + public static String toString(@Nullable Map map) { StringBuilder builder = new StringBuilder("{\n"); - int count = 0; + AtomicInteger count = new AtomicInteger(0); - for (Map.Entry entry : new TreeMap<>(map).entrySet()) { - builder.append(++count > 1 ? ",\n" : ""); + newSortedMap(map).forEach((key, value) -> { + builder.append(count.incrementAndGet() > 1 ? ",\n" : ""); builder.append("\t"); - builder.append(entry.getKey()); + builder.append(key); builder.append(" = "); - builder.append(entry.getValue()); - } + builder.append(value); + }); builder.append("\n}"); return builder.toString(); } + + @NonNull + private static SortedMap newSortedMap(@Nullable Map map) { + return new TreeMap<>(nullSafeMap(map)); + } } diff --git a/src/test/java/org/springframework/data/gemfire/repository/config/IncompatibleRegionKeyRepositoryIdAndEntityIdRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/repository/config/IncompatibleRegionKeyRepositoryIdAndEntityIdRepositoryIntegrationTests.java new file mode 100644 index 00000000..ba6a740f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/config/IncompatibleRegionKeyRepositoryIdAndEntityIdRepositoryIntegrationTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.gemfire.repository.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionAttributes; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.repository.sample.Animal; +import org.springframework.data.gemfire.repository.sample.Plant; +import org.springframework.data.gemfire.repository.sample.PlantRepository; +import org.springframework.data.gemfire.repository.sample.RabbitRepository; +import org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.Repository; + +/** + * Integration tests for Spring Data [GemFire] Repositories testing compatibility of {@link Region} + * {@link RegionAttributes#getKeyConstraint() key type}, {@link Repository} {@link Class ID type} + * and {@link PersistentEntity entity} {@link Class ID type}. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.context.ConfigurableApplicationContext + * @since 1.4.0 + * @link https://github.com/spring-projects/spring-data-gemfire/pull/55 + */ +public class IncompatibleRegionKeyRepositoryIdAndEntityIdRepositoryIntegrationTests { + + private static ConfigurableApplicationContext newApplicationContext(Class testConfiguration) { + return new AnnotationConfigApplicationContext(testConfiguration); + } + + private void withTestConfigurationExpectIllegalArgumentExceptionWithMessage(Class testConfiguration, + Class repositoryType, Supplier exceptionMessage) { + + try { + + ConfigurableApplicationContext applicationContext = newApplicationContext(testConfiguration); + + assertThat(applicationContext.getBean(repositoryType)).isNotNull(); + } + catch (BeanCreationException expected) { + + assertThat(expected).hasCauseInstanceOf(IllegalArgumentException.class); + assertThat(expected.getCause()).hasMessage(exceptionMessage.get()); + + throw (IllegalArgumentException) expected.getCause(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void withRegionUsingStringKeyAndRepositoryUsingLongId() { + + withTestConfigurationExpectIllegalArgumentExceptionWithMessage( + TestIncompatibleRegionKeyRepositoryIdTypeConfiguration.class, + RabbitRepository.class, + () -> String.format("Region [/Rabbits] requires keys of type [%1$s], but Repository [%2$s] declared an id of type [%3$s]", + String.class.getName(), RabbitRepository.class.getName(), Long.class.getName())); + } + + @Test(expected = IllegalArgumentException.class) + public void withRepositoryUsingStringIdAndEntityUsingLongId() { + + withTestConfigurationExpectIllegalArgumentExceptionWithMessage( + TestIncompatibleRepositoryIdEntityIdTypeConfiguration.class, + PlantRepository.class, + () -> String.format("Repository [%1$s] declared an id of type [%2$s], but entity [%3$s] has an id of type [%4$s]", + PlantRepository.class.getName(), String.class.getName(), Plant.class.getName(), Long.class.getName())); + } + + @ClientCacheApplication + @EnableGemFireMockObjects + @EnableGemfireRepositories(basePackageClasses = RabbitRepository.class, + includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = RabbitRepository.class) + ) + @SuppressWarnings("unused") + static class TestIncompatibleRegionKeyRepositoryIdTypeConfiguration { + + @Bean("Rabbits") + public ClientRegionFactoryBean clientRegion(GemFireCache gemfireCache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + + clientRegion.setCache(gemfireCache); + clientRegion.setClose(false); + clientRegion.setKeyConstraint(String.class); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + clientRegion.setValueConstraint(Animal.class); + + return clientRegion; + } + } + + @ClientCacheApplication + @EnableGemFireMockObjects + @EnableGemfireRepositories(basePackageClasses = PlantRepository.class, + includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PlantRepository.class) + ) + @SuppressWarnings("unused") + static class TestIncompatibleRepositoryIdEntityIdTypeConfiguration { + + @Bean("Plants") + public ClientRegionFactoryBean clientRegion(GemFireCache gemfireCache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + + clientRegion.setCache(gemfireCache); + clientRegion.setClose(false); + clientRegion.setShortcut(ClientRegionShortcut.PROXY); + + return clientRegion; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/QueryBuilderUnitTests.java b/src/test/java/org/springframework/data/gemfire/repository/query/QueryBuilderUnitTests.java index 4ad3187c..b5b1658a 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/query/QueryBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/repository/query/QueryBuilderUnitTests.java @@ -17,23 +17,18 @@ package org.springframework.data.gemfire.repository.query; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; import org.springframework.data.repository.query.parser.PartTree; /** - * Test suite of test cases testing the contract and functionality of the {@link QueryBuilder} class. + * Unit tests for {@link QueryBuilder} class. * * @author John Blum * @see org.junit.Test @@ -43,12 +38,11 @@ import org.springframework.data.repository.query.parser.PartTree; */ public class QueryBuilderUnitTests { - @Rule - public ExpectedException exception = ExpectedException.none(); - @Test public void createQueryBuilderWithDistinctQuery() { + GemfirePersistentEntity mockPersistentEntity = mock(GemfirePersistentEntity.class); + PartTree mockPartTree = mock(PartTree.class); when(mockPersistentEntity.getRegionName()).thenReturn("Example"); @@ -64,7 +58,9 @@ public class QueryBuilderUnitTests { @Test public void createQueryBuilderWithNonDistinctQuery() { + GemfirePersistentEntity mockPersistentEntity = mock(GemfirePersistentEntity.class); + PartTree mockPartTree = mock(PartTree.class); when(mockPersistentEntity.getRegionName()).thenReturn("Example"); @@ -78,24 +74,31 @@ public class QueryBuilderUnitTests { verify(mockPartTree, times(1)).isDistinct(); } - @Test + @Test(expected = IllegalArgumentException.class) public void createQueryBuilderWithNullQueryString() { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage(is(equalTo("An OQL Query must be specified"))); - new QueryBuilder(null); + try { + new QueryBuilder(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Query is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } } @Test @SuppressWarnings("all") public void createWithPredicate() { + Predicate mockPredicate = mock(Predicate.class); when(mockPredicate.toString(eq(QueryBuilder.DEFAULT_ALIAS))).thenReturn("x.id = 1"); - QueryBuilder queryBuilder = new QueryBuilder( - String.format("SELECT * FROM /Example %s", QueryBuilder.DEFAULT_ALIAS)); + QueryBuilder queryBuilder = + new QueryBuilder(String.format("SELECT * FROM /Example %s", QueryBuilder.DEFAULT_ALIAS)); QueryString queryString = queryBuilder.create(mockPredicate); @@ -107,6 +110,7 @@ public class QueryBuilderUnitTests { @Test public void createWithNullPredicate() { + QueryBuilder queryBuilder = new QueryBuilder("SELECT * FROM /Example"); QueryString queryString = queryBuilder.create(null); diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorIntegrationTests.java new file mode 100644 index 00000000..97c1de00 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorIntegrationTests.java @@ -0,0 +1,227 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.gemfire.repository.query; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.mapping.GemfireMappingContext; +import org.springframework.data.gemfire.repository.sample.Person; +import org.springframework.data.gemfire.repository.sample.PersonRepository; +import org.springframework.data.gemfire.repository.sample.User; +import org.springframework.data.gemfire.repository.sample.UserRepository; +import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * The QueryPostProcessorIntegrationTests class... + * + * @author John Blum + * @since 1.0.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class QueryPostProcessorIntegrationTests { + + private static final AtomicLong idSequence = new AtomicLong(0L); + + @Autowired + private FindOrderedLimitedPeopleByFirstNameQueryPostProcessor peopleQueryPostProcessor; + + @Autowired + private PersonRepository personRepository; + + @Autowired + private RecordingQueryPostProcessor recordingQueryPostProcessor; + + @Autowired + private UserRepository userRepository; + + private static Person newPerson(String firstName, String lastName) { + + Person person = new Person(firstName, lastName); + + person.id = idSequence.incrementAndGet(); + + return person; + } + + @Before + public void setup() { + + this.personRepository.save(newPerson("Jon", "Doe")); + this.personRepository.save(newPerson("Cookie", "Doe")); + this.personRepository.save(newPerson("Pie", "Doe")); + this.personRepository.save(newPerson("Sour", "Doe")); + this.personRepository.save(newPerson("Jack", "BeNimble")); + this.personRepository.save(newPerson("Jack", "BeQuick")); + this.personRepository.save(newPerson("Jack", "Black")); + this.personRepository.save(newPerson("Jack", "JumpedOverTheCandleStick")); + this.personRepository.save(newPerson("Jack", "Handy")); + this.personRepository.save(newPerson("Jack", "Sparrow")); + this.personRepository.save(newPerson("Agent", "Smith")); + + this.userRepository.save(new User("abuser")); + this.userRepository.save(new User("jdoe")); + this.userRepository.save(new User("root")); + } + + @Test + public void queryPostProcessingProcessesGeneratedOqlQueries() { + + assertThat(this.personRepository.count()).isEqualTo(11); + assertThat(this.userRepository.count()).isEqualTo(3); + assertThat(this.recordingQueryPostProcessor.queries).isEmpty(); + + List users = this.userRepository.findDistinctByUsernameLike("%doe"); + + assertThat(users).hasSize(1); + assertThat(this.recordingQueryPostProcessor.queries).hasSize(1); + assertThat(this.recordingQueryPostProcessor.queries) + .containsExactly("SELECT DISTINCT * FROM /Users x WHERE x.username LIKE $1"); + + Collection bakingDoes = this.personRepository.findByFirstnameIn("Cookie", "Pie", "Sour"); + + assertThat(bakingDoes).hasSize(3); + assertThat(this.recordingQueryPostProcessor.queries).hasSize(2); + assertThat(this.recordingQueryPostProcessor.queries).containsExactly( + "SELECT DISTINCT * FROM /Users x WHERE x.username LIKE $1", + "SELECT * FROM /simple x WHERE x.firstname IN SET ('Cookie', 'Pie', 'Sour')" + ); + + Collection jacks = this.personRepository.findByFirstname("Jack"); + + assertThat(jacks).hasSize(1); + assertThat(jacks.stream().findFirst().map(Person::getName).orElse(null)).isEqualTo("Jack Sparrow"); + + assertThat(this.recordingQueryPostProcessor.queries).hasSize(3); + assertThat(this.recordingQueryPostProcessor.queries).containsExactly( + "SELECT DISTINCT * FROM /Users x WHERE x.username LIKE $1", + "SELECT * FROM /simple x WHERE x.firstname IN SET ('Cookie', 'Pie', 'Sour')", + "SELECT DISTINCT * FROM /simple x WHERE x.firstname = $1 ORDER BY lastname DESC LIMIT 1" + ); + } + + @ClientCacheApplication + @SuppressWarnings("unused") + static class TestConfiguration { + + @Bean("simple") + public ClientRegionFactoryBean peopleRegion(GemFireCache gemfireCache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + + clientRegion.setCache(gemfireCache); + clientRegion.setClose(false); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + + return clientRegion; + } + + @Bean("Users") + public ClientRegionFactoryBean usersRegion(GemFireCache gemfireCache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>(); + + clientRegion.setCache(gemfireCache); + clientRegion.setClose(false); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + + return clientRegion; + } + + @Bean + GemfireMappingContext mappingContext() { + return new GemfireMappingContext(); + } + + @Bean + GemfireRepositoryFactoryBean personRepository() { + return new GemfireRepositoryFactoryBean<>(PersonRepository.class); + } + + @Bean + GemfireRepositoryFactoryBean userRepository() { + return new GemfireRepositoryFactoryBean<>(UserRepository.class); + } + + @Bean + FindOrderedLimitedPeopleByFirstNameQueryPostProcessor personQueryPostProcess() { + return new FindOrderedLimitedPeopleByFirstNameQueryPostProcessor(1); + } + + @Bean + RecordingQueryPostProcessor recordingQueryPostProcessor() { + return new RecordingQueryPostProcessor(); + } + } + + static class FindOrderedLimitedPeopleByFirstNameQueryPostProcessor implements QueryPostProcessor { + + private final int limit; + + FindOrderedLimitedPeopleByFirstNameQueryPostProcessor(int limit) { + this.limit = limit; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + return "findByFirstname".equals(queryMethod.getName()) + ? query.trim().replace("SELECT", "SELECT DISTINCT") + .concat(" ORDER BY lastname DESC").concat(String.format(" LIMIT %d", this.limit)) + : query; + } + } + + static class RecordingQueryPostProcessor implements QueryPostProcessor { + + List queries = new CopyOnWriteArrayList<>(); + + @Override + public int getOrder() { + return 1; + } + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + this.queries.add(query); + return query; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java b/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java new file mode 100644 index 00000000..e3964cef --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.gemfire.repository.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.InOrder; +import org.springframework.data.repository.query.QueryMethod; + +/** + * Unit tests for {@link QueryPostProcessor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor + * @since 1.0.0 + */ +public class QueryPostProcessorUnitTests { + + @Test + @SuppressWarnings("unchecked") + public void processAfterReturnsCompositeQueryPostProcessorAndPostProcessesInOrder() { + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + String query = "SELECT * FROM /Test"; + + QueryPostProcessor mockQueryPostProcessorOne = mock(QueryPostProcessor.class); + QueryPostProcessor mockQueryPostProcessorTwo = mock(QueryPostProcessor.class); + + when(mockQueryPostProcessorOne.processAfter(any())).thenCallRealMethod(); + when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query); + when(mockQueryPostProcessorTwo.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query); + + QueryPostProcessor composite = mockQueryPostProcessorOne.processAfter(mockQueryPostProcessorTwo); + + assertThat(composite).isNotNull(); + assertThat(composite).isNotSameAs(mockQueryPostProcessorOne); + assertThat(composite).isNotSameAs(mockQueryPostProcessorTwo); + assertThat(composite.postProcess(mockQueryMethod, query)).isEqualTo(query); + + InOrder inOrder = inOrder(mockQueryPostProcessorOne, mockQueryPostProcessorTwo); + + inOrder.verify(mockQueryPostProcessorTwo, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), any()); + + inOrder.verify(mockQueryPostProcessorOne, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), any()); + } + + @Test + public void processAfterReturnsThis() { + + QueryPostProcessor mockQueryPostProcessor = mock(QueryPostProcessor.class); + + when(mockQueryPostProcessor.processAfter(any())).thenCallRealMethod(); + + assertThat(mockQueryPostProcessor.processAfter(null)).isSameAs(mockQueryPostProcessor); + } + @Test + @SuppressWarnings("unchecked") + public void processBeforeReturnsCompositeQueryPostProcessorAndPostProcessesInOrder() { + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + String query = "SELECT * FROM /Test"; + + QueryPostProcessor mockQueryPostProcessorOne = mock(QueryPostProcessor.class); + QueryPostProcessor mockQueryPostProcessorTwo = mock(QueryPostProcessor.class); + + when(mockQueryPostProcessorOne.processBefore(any())).thenCallRealMethod(); + when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query); + when(mockQueryPostProcessorTwo.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query); + + QueryPostProcessor composite = mockQueryPostProcessorOne.processBefore(mockQueryPostProcessorTwo); + + assertThat(composite).isNotNull(); + assertThat(composite).isNotSameAs(mockQueryPostProcessorOne); + assertThat(composite).isNotSameAs(mockQueryPostProcessorTwo); + assertThat(composite.postProcess(mockQueryMethod, query)).isEqualTo(query); + + InOrder inOrder = inOrder(mockQueryPostProcessorOne, mockQueryPostProcessorTwo); + + inOrder.verify(mockQueryPostProcessorOne, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), any()); + + inOrder.verify(mockQueryPostProcessorTwo, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), any()); + } + + @Test + public void processBeforeReturnsThis() { + + QueryPostProcessor mockQueryPostProcessor = mock(QueryPostProcessor.class); + + when(mockQueryPostProcessor.processBefore(any())).thenCallRealMethod(); + + assertThat(mockQueryPostProcessor.processBefore(null)).isSameAs(mockQueryPostProcessor); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java b/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java index 5fb10d66..aa32c240 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java @@ -17,9 +17,6 @@ package org.springframework.data.gemfire.repository.query; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -61,61 +58,110 @@ public class QueryStringUnitTests { @SuppressWarnings("rawtypes") Region region; - protected Sort.Order newSortOrder(String property) { + private Sort.Order newSortOrder(String property) { return newSortOrder(property, Sort.Direction.ASC); } - protected Sort.Order newSortOrder(String property, Sort.Direction direction) { + private Sort.Order newSortOrder(String property, Sort.Direction direction) { return new Sort.Order(direction, property); } - protected Sort newSort(Sort.Order... orders) { - return new Sort(orders); + private Sort newSort(Sort.Order... orders) { + return Sort.by(orders); } @Test - public void createQueryStringWithDomainType() { + public void constructQueryStringWithDomainType() { assertThat(new QueryString(Person.class).toString()).isEqualTo("SELECT * FROM /Person"); } @Test - public void createQueryStringWithDomainTypeHavingCount() { + public void constructQueryStringWithDomainTypeAsCount() { assertThat(new QueryString(Person.class, true).toString()).isEqualTo("SELECT count(*) FROM /Person"); } - @Test - public void createQueryStringWithNullDomainType() { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage(is(equalTo("domainType must not be null"))); + @Test(expected = IllegalArgumentException.class) + public void constructQueryStringWithNullDomainType() { - new QueryString((Class) null); + try { + new QueryString((Class) null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Domain type is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } } @Test - public void createQueryStringWithQuery() { + public void constructQueryStringWithQuery() { + String query = "SELECT * FROM /Example"; assertThat(new QueryString(query).toString()).isEqualTo(query); } - @Test - public void createQueryStringWithUnspecifiedQueryThrowsIllegalArgumentException() { - assertUnspecifiedQueryThrowsIllegalArgumentException(""); + @Test(expected = IllegalArgumentException.class) + public void constructQueryStringWithBlankQueryThrowsIllegalArgumentException() { assertUnspecifiedQueryThrowsIllegalArgumentException(" "); + } + + @Test(expected = IllegalArgumentException.class) + public void constructQueryStringWithEmptyQueryThrowsIllegalArgumentException() { + assertUnspecifiedQueryThrowsIllegalArgumentException(""); + } + + @Test(expected = IllegalArgumentException.class) + public void constructQueryStringWithNullQueryThrowsIllegalArgumentException() { assertUnspecifiedQueryThrowsIllegalArgumentException(null); } - protected void assertUnspecifiedQueryThrowsIllegalArgumentException(String query) { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage(is(equalTo("An OQL Query must be specified"))); + private void assertUnspecifiedQueryThrowsIllegalArgumentException(String query) { - new QueryString(query); + try { + new QueryString(query); + } + catch (Exception expected) { + + assertThat(expected).hasMessage("Query [%s] is required", query); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void queryStringOfQuery() { + + QueryString query = QueryString.of("SELECT * FROM /Test"); + + assertThat(query).isNotNull(); + assertThat(query.toString()).isEqualTo("SELECT * FROM /Test"); + } + + @Test + public void queryStringFromDomainType() { + + QueryString query = QueryString.from(Person.class); + + assertThat(query).isNotNull(); + assertThat(query.toString()).isEqualTo("SELECT * FROM /Person"); + } + + @Test + public void queryStringCountingObjectsOfDomainType() { + + QueryString query = QueryString.count(Person.class); + + assertThat(query).isNotNull(); + assertThat(query.toString()).isEqualTo("SELECT count(*) FROM /Person"); } @Test public void hintPatternMatches() { + assertThat(matches(HINT_PATTERN, "")).isTrue(); assertThat(matches(HINT_PATTERN, " SELECT * FROM /Example WHERE id = $1")).isTrue(); assertThat(matches(HINT_PATTERN, " SELECT * FROM /Person WHERE lastName = $1 AND birthDate = $2")).isTrue(); @@ -123,6 +169,7 @@ public class QueryStringUnitTests { @Test public void hintPatternNoMatches() { + assertThat(matches(HINT_PATTERN, "HINT")).isFalse(); assertThat(matches(HINT_PATTERN, "")).isFalse(); assertThat(matches(HINT_PATTERN, "")).isFalse(); @@ -136,6 +183,7 @@ public class QueryStringUnitTests { @Test public void importPatternMatches() { + assertThat(matches(IMPORT_PATTERN, "IMPORT *;")).isTrue(); assertThat(matches(IMPORT_PATTERN, "IMPORT org.example.*;")).isTrue(); assertThat(matches(IMPORT_PATTERN, "IMPORT org.example.Type;")).isTrue(); @@ -144,6 +192,7 @@ public class QueryStringUnitTests { @Test public void importPatternNoMatches() { + assertThat(matches(IMPORT_PATTERN, "IMPORT")).isFalse(); assertThat(matches(IMPORT_PATTERN, "IMPORT ;")).isFalse(); assertThat(matches(IMPORT_PATTERN, "IMPORT *")).isFalse(); @@ -153,6 +202,7 @@ public class QueryStringUnitTests { @Test public void limitPatternMatches() { + assertThat(matches(LIMIT_PATTERN, "LIMIT 0")).isTrue(); assertThat(matches(LIMIT_PATTERN, "LIMIT 1")).isTrue(); assertThat(matches(LIMIT_PATTERN, "LIMIT 10")).isTrue(); @@ -162,6 +212,7 @@ public class QueryStringUnitTests { @Test public void limitPatternNoMatches() { + assertThat(matches(LIMIT_PATTERN, "LIMIT")).isFalse(); assertThat(matches(LIMIT_PATTERN, "LIMIT lO")).isFalse(); assertThat(matches(LIMIT_PATTERN, "LIMIT AF")).isFalse(); @@ -172,6 +223,7 @@ public class QueryStringUnitTests { @Test public void tracePatternMatches() { + assertThat(matches(TRACE_PATTERN, "")).isTrue(); assertThat(matches(TRACE_PATTERN, " SELECT * FROM /Example")).isTrue(); assertThat(matches(TRACE_PATTERN, "SELECT * FROM /Example")).isTrue(); @@ -180,56 +232,78 @@ public class QueryStringUnitTests { @Test public void tracePatternNoMatches() { + assertThat(matches(TRACE_PATTERN, "TRACE")).isFalse(); assertThat(matches(TRACE_PATTERN, "TRACE SELECT * FROM /Example")).isFalse(); assertThat(matches(TRACE_PATTERN, "")).isFalse(); } - protected boolean matches(Pattern pattern, String value) { + private boolean matches(Pattern pattern, String value) { return pattern.matcher(value).find(); } + @Test + public void asDistinctQuery() { + + QueryString query = QueryString.of("SELECT * FROM /Test"); + + assertThat(query.asDistinct().toString()).isEqualTo("SELECT DISTINCT * FROM /Test"); + } + + @Test + public void asDistinctWithDistinctQuery() { + + QueryString query = QueryString.of("SELECT DISTINCT * FROM /Test"); + + assertThat(query.asDistinct().toString()).isEqualTo("SELECT DISTINCT * FROM /Test"); + } + // SGF-251 @Test public void replacesDomainTypeSimpleNameWithRegionPathCorrectly() { - QueryString query = new QueryString(Person.class); - when(region.getFullPath()).thenReturn("/foo/bar"); + QueryString query = QueryString.from(Person.class); - assertThat(query.forRegion(Person.class, region).toString()).isEqualTo("SELECT * FROM /foo/bar"); + when(this.region.getFullPath()).thenReturn("/foo/bar"); - verify(region, times(1)).getFullPath(); + assertThat(query.toString()).isEqualTo("SELECT * FROM /Person"); + assertThat(query.fromRegion(Person.class, this.region).toString()).isEqualTo("SELECT * FROM /foo/bar"); + + verify(this.region, times(1)).getFullPath(); } // SGF-156, SGF-251 @Test public void replacesFromClauseWithRegionPathCorrectly() { - QueryString query = new QueryString("SELECT * FROM /Persons p WHERE p.lastname = $1"); - when(region.getFullPath()).thenReturn("/People"); + QueryString query = QueryString.of("SELECT * FROM /Persons p WHERE p.lastname = $1"); - assertThat(query.forRegion(Person.class, region).toString()) + when(this.region.getFullPath()).thenReturn("/People"); + + assertThat(query.fromRegion(Person.class, this.region).toString()) .isEqualTo("SELECT * FROM /People p WHERE p.lastname = $1"); - verify(region, times(1)).getFullPath(); + verify(this.region, times(1)).getFullPath(); } // SGF-252 @Test public void replacesFullyQualifiedSubRegionPathWithRegionPathCorrectly() { - QueryString query = new QueryString("SELECT * FROM //Local/Root/Users u WHERE u.username = $1"); - when(region.getFullPath()).thenReturn("/Remote/Root/Users"); + QueryString query = QueryString.of("SELECT * FROM //Local/Root/Users u WHERE u.username = $1"); - assertThat(query.forRegion(RootUser.class, region).toString()) + when(this.region.getFullPath()).thenReturn("/Remote/Root/Users"); + + assertThat(query.fromRegion(RootUser.class, this.region).toString()) .isEqualTo("SELECT * FROM /Remote/Root/Users u WHERE u.username = $1"); - verify(region, times(1)).getFullPath(); + verify(this.region, times(1)).getFullPath(); } @Test public void bindsInValuesCorrectly() { - QueryString query = new QueryString("SELECT * FROM /Collection WHERE elements IN SET $1"); + + QueryString query = QueryString.of("SELECT * FROM /Collection WHERE elements IN SET $1"); assertThat(query.bindIn(Arrays.asList(1, 2, 3)).toString()) .isEqualTo("SELECT * FROM /Collection WHERE elements IN SET ('1', '2', '3')"); @@ -237,22 +311,26 @@ public class QueryStringUnitTests { @Test public void detectsInParameterIndexesCorrectly() { - QueryString query = new QueryString("SELECT * FROM /Example WHERE values IN SET $1 OR IN SET $2"); + + QueryString query = QueryString.of("SELECT * FROM /Example WHERE values IN SET $1 OR IN SET $2"); assertThat(query.getInParameterIndexes()).isEqualTo(Arrays.asList(1, 2)); } @Test public void addsNoOrderByClauseCorrectly() { - QueryString query = new QueryString("SELECT * FROM /People p").orderBy(null); + + QueryString query = QueryString.of("SELECT * FROM /People p").orderBy(null); assertThat(query.toString()).isEqualTo("SELECT * FROM /People p"); } @Test public void addsOrderByClauseCorrectly() { - QueryString query = new QueryString("SELECT * FROM /People p WHERE p.lastName = $1") - .orderBy(newSort(newSortOrder("lastName", Sort.Direction.DESC), newSortOrder("firstName"))); + + QueryString query = QueryString.of("SELECT * FROM /People p WHERE p.lastName = $1") + .orderBy(newSort(newSortOrder("lastName", Sort.Direction.DESC), + newSortOrder("firstName"))); assertThat(query.toString()) .isEqualTo("SELECT DISTINCT * FROM /People p WHERE p.lastName = $1 ORDER BY lastName DESC, firstName ASC"); @@ -260,7 +338,8 @@ public class QueryStringUnitTests { @Test public void addsSingleOrderByClauseCorrectly() { - QueryString query = new QueryString("SELECT DISTINCT p.lastName FROM /People p WHERE p.firstName = $1") + + QueryString query = QueryString.of("SELECT DISTINCT p.lastName FROM /People p WHERE p.firstName = $1") .orderBy(newSort(newSortOrder("lastName"))); assertThat(query.toString()) @@ -269,16 +348,18 @@ public class QueryStringUnitTests { @Test public void withHints() { - assertThat(new QueryString("SELECT * FROM /Example").withHints("IdIdx").toString()) + + assertThat(QueryString.of("SELECT * FROM /Example").withHints("IdIdx").toString()) .isEqualTo(" SELECT * FROM /Example"); - assertThat(new QueryString("SELECT * FROM /Example").withHints("IdIdx", "SpatialIdx", "TxDateIdx").toString()) + assertThat(QueryString.of("SELECT * FROM /Example").withHints("IdIdx", "SpatialIdx", "TxDateIdx").toString()) .isEqualTo(" SELECT * FROM /Example"); } @Test public void withoutHints() { - QueryString expectedQueryString = new QueryString("SELECT * FROM /Example"); + + QueryString expectedQueryString = QueryString.of("SELECT * FROM /Example"); assertThat(expectedQueryString.withHints()).isSameAs(expectedQueryString); assertThat(expectedQueryString.withHints((String[]) null)).isSameAs(expectedQueryString); @@ -286,13 +367,14 @@ public class QueryStringUnitTests { @Test public void withImport() { - assertThat(new QueryString("SELECT * FROM /People").withImport("org.example.app.domain.Person").toString()) + assertThat(QueryString.of("SELECT * FROM /People").withImport("org.example.app.domain.Person").toString()) .isEqualTo("IMPORT org.example.app.domain.Person; SELECT * FROM /People"); } @Test public void withoutImport() { - QueryString expectedQueryString = new QueryString("SELECT * FROM /Example"); + + QueryString expectedQueryString = QueryString.of("SELECT * FROM /Example"); assertThat(expectedQueryString.withImport(null)).isSameAs(expectedQueryString); assertThat(expectedQueryString.withImport("")).isSameAs(expectedQueryString); @@ -301,40 +383,47 @@ public class QueryStringUnitTests { @Test public void withLimit() { - assertThat(new QueryString("SELECT * FROM /Example").withLimit(10).toString()) + + assertThat(QueryString.of("SELECT * FROM /Example").withLimit(10).toString()) .isEqualTo("SELECT * FROM /Example LIMIT 10"); - assertThat(new QueryString("SELECT * FROM /Example").withLimit(0).toString()) + assertThat(QueryString.of("SELECT * FROM /Example").withLimit(0).toString()) .isEqualTo("SELECT * FROM /Example LIMIT 0"); - assertThat(new QueryString("SELECT * FROM /Example").withLimit(-5).toString()) + assertThat(QueryString.of("SELECT * FROM /Example").withLimit(-5).toString()) .isEqualTo("SELECT * FROM /Example LIMIT -5"); } @Test public void withoutLimit() { - QueryString expectedQueryString = new QueryString("SELECT * FROM /Example"); + + QueryString expectedQueryString = QueryString.of("SELECT * FROM /Example"); assertThat(expectedQueryString.withLimit(null)).isSameAs(expectedQueryString); } @Test public void withTrace() { - assertThat(new QueryString("SELECT * FROM /Example").withTrace().toString()) + assertThat(QueryString.of("SELECT * FROM /Example").withTrace().toString()) .isEqualTo(" SELECT * FROM /Example"); } @Test public void withHintAndTrace() { - assertThat(new QueryString("SELECT * FROM /Example").withHints("IdIdx").withTrace().toString()) + assertThat(QueryString.of("SELECT * FROM /Example").withHints("IdIdx").withTrace().toString()) .isEqualTo(" SELECT * FROM /Example"); } @Test public void withHintImportLimitAndTrace() { - QueryString query = new QueryString("SELECT * FROM /Example"); - assertThat(query.withImport("org.example.domain.Type").withHints("IdIdx", "NameIdx").withLimit(20).withTrace().toString()) + QueryString query = QueryString.of("SELECT * FROM /Example") + .withImport("org.example.domain.Type") + .withHints("IdIdx", "NameIdx") + .withLimit(20) + .withTrace(); + + assertThat(query.toString()) .isEqualTo(" IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 20"); } } diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java b/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java index a87d1ae5..cc93bf92 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java +++ b/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java @@ -18,9 +18,7 @@ package org.springframework.data.gemfire.repository.query; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.sameInstance; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -59,7 +57,9 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void testToCollectionWithSelectResults() { + SelectResults mockSelectResults = mock(SelectResults.class, "testToCollectionWithSelectResults.SelectResults"); + List expectedList = Arrays.asList("one", "two", "three"); when(mockSelectResults.asList()).thenReturn(expectedList); @@ -71,7 +71,9 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void testToCollectionWithResultsBag() { + ResultsBag mockResultsBag = mock(ResultsBag.class, "testToCollectionWithResultsBag.ResultsBag"); + List expectedList = Arrays.asList("a", "b", "c"); when(mockResultsBag.asList()).thenReturn(expectedList); @@ -83,7 +85,9 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void testToCollectionWithCollection() { + List expectedList = Arrays.asList("x", "y", "z"); + Collection actualList = repositoryQuery.toCollection(expectedList); assertSame(expectedList, actualList); @@ -91,7 +95,9 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void testToCollectionWithArray() { + Object[] array = { 1, 2, 3 }; + Collection list = repositoryQuery.toCollection(array); assertNotNull(list); @@ -103,6 +109,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void testToCollectionWithSingleObject() { + Collection list = repositoryQuery.toCollection("test"); assertTrue(list instanceof List); @@ -113,6 +120,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void testToCollectionWithNull() { + Collection list = repositoryQuery.toCollection(null); assertNotNull(list); @@ -121,6 +129,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void applyAllQueryAnnotationExtensions() { + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class, "MockGemfireQueryMethod"); when(mockQueryMethod.hasHint()).thenReturn(true); @@ -131,18 +140,18 @@ public class StringBasedGemfireRepositoryQueryTest { when(mockQueryMethod.getLimit()).thenReturn(10); when(mockQueryMethod.hasTrace()).thenReturn(true); - QueryString queryString = new QueryString("SELECT * FROM /Example"); + QueryString queryString = QueryString.of("SELECT * FROM /Example"); assertThat(queryString.toString(), is(equalTo("SELECT * FROM /Example"))); StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(); - QueryString actualQueryString = repositoryQuery.applyQueryAnnotationExtensions(mockQueryMethod, queryString); + String postProcessedQueryString = repositoryQuery.getQueryPostProcessor() + .postProcess(mockQueryMethod, queryString.toString()); - assertThat(actualQueryString, is(notNullValue())); - assertThat(actualQueryString, is(not(sameInstance(queryString)))); - assertThat(actualQueryString.toString(), is(equalTo( - " IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 10"))); + assertThat(postProcessedQueryString, is(notNullValue())); + assertThat(postProcessedQueryString, + is(equalTo(" IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 10"))); verify(mockQueryMethod, times(1)).hasHint(); verify(mockQueryMethod, times(1)).getHints(); @@ -155,6 +164,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void applyHintLimitAndTraceQueryAnnotationExtensionsWithExistingHintAndLimit() { + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class, "MockGemfireQueryMethod"); when(mockQueryMethod.hasHint()).thenReturn(true); @@ -170,12 +180,11 @@ public class StringBasedGemfireRepositoryQueryTest { StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(); - QueryString actualQueryString = repositoryQuery.applyQueryAnnotationExtensions(mockQueryMethod, queryString); + String postProcessedQueryString = repositoryQuery.getQueryPostProcessor().postProcess(mockQueryMethod, queryString.toString()); - assertThat(actualQueryString, is(notNullValue())); - assertThat(actualQueryString, is(not(sameInstance(queryString)))); - assertThat(actualQueryString.toString(), is(equalTo( - " SELECT * FROM /Example LIMIT 25"))); + assertThat(postProcessedQueryString, is(notNullValue())); + assertThat(postProcessedQueryString, + is(equalTo(" SELECT * FROM /Example LIMIT 25"))); verify(mockQueryMethod, times(1)).hasHint(); verify(mockQueryMethod, never()).getHints(); @@ -188,6 +197,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void applyImportAndTraceQueryAnnotationExtensionsWithExistingTrace() { + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class, "MockGemfireQueryMethod"); when(mockQueryMethod.hasHint()).thenReturn(false); @@ -202,12 +212,11 @@ public class StringBasedGemfireRepositoryQueryTest { StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(); - QueryString actualQueryString = repositoryQuery.applyQueryAnnotationExtensions(mockQueryMethod, queryString); + String postProcessedQueryString = repositoryQuery.getQueryPostProcessor().postProcess(mockQueryMethod, queryString.toString()); - assertThat(actualQueryString, is(notNullValue())); - assertThat(actualQueryString, is(not(sameInstance(queryString)))); - assertThat(actualQueryString.toString(), is(equalTo( - "IMPORT org.example.domain.Type; SELECT * FROM /Example"))); + assertThat(postProcessedQueryString, is(notNullValue())); + assertThat(postProcessedQueryString, + is(equalTo("IMPORT org.example.domain.Type; SELECT * FROM /Example"))); verify(mockQueryMethod, times(1)).hasHint(); verify(mockQueryMethod, never()).getHints(); @@ -217,5 +226,4 @@ public class StringBasedGemfireRepositoryQueryTest { verify(mockQueryMethod, never()).getLimit(); verify(mockQueryMethod, times(1)).hasTrace(); } - } diff --git a/src/test/java/org/springframework/data/gemfire/repository/sample/Customer.java b/src/test/java/org/springframework/data/gemfire/repository/sample/Customer.java index 48ac55ae..7a6ba7e0 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/sample/Customer.java +++ b/src/test/java/org/springframework/data/gemfire/repository/sample/Customer.java @@ -21,6 +21,8 @@ import org.springframework.data.gemfire.mapping.annotation.Region; import org.springframework.data.gemfire.mapping.annotation.ReplicateRegion; import org.springframework.util.ObjectUtils; +import lombok.Data; + /** * The Customer class is a class abstraction modeling a Customer. * @@ -29,6 +31,7 @@ import org.springframework.util.ObjectUtils; * @see Region * @since 1.0.0 */ +@Data @ReplicateRegion("Customers") @SuppressWarnings("unused") public class Customer { @@ -42,39 +45,15 @@ public class Customer { public Customer() { } - public Customer(final Long id) { + public Customer(Long id) { this.id = id; } - public Customer(final String firstName, final String lastName) { + public Customer(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } - public Long getId() { - return id; - } - - public void setId(final Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(final String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(final String lastName) { - this.lastName = lastName; - } - public String getName() { return String.format("%1$s %2$s", getFirstName(), getLastName()); } @@ -85,7 +64,8 @@ public class Customer { @Override public boolean equals(final Object obj) { - if (obj == this) { + + if (this == obj) { return true; } @@ -100,7 +80,7 @@ public class Customer { && ObjectUtils.nullSafeEquals(this.getLastName(), that.getLastName()); } - protected static int hashCodeIgnoreNull(final Object obj) { + protected static int hashCodeIgnoreNull(Object obj) { return (obj != null ? obj.hashCode() : 0); } @@ -115,7 +95,6 @@ public class Customer { @Override public String toString() { - return String.format("%1$s %2$s", getFirstName(), getLastName()); + return getName(); } - } diff --git a/src/test/java/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest.java b/src/test/java/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest.java deleted file mode 100644 index 6bf17a2c..00000000 --- a/src/test/java/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2010-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.gemfire.repository.sample; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.File; - -import org.junit.Test; -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -/** - * The IncompatibleRegionKeyEntityIdAnimalRepositoryTest class is a test suite of test cases testing the functionality - * behind PR #55 involving persisting application domain object/entities to multiple Regions in GemFire's Cache. - * - * @author John Blum - * @see org.junit.Test - * @see org.springframework.context.ConfigurableApplicationContext - * @since 1.4.0 - * @link https://github.com/spring-projects/spring-data-gemfire/pull/55 - */ -public class IncompatibleRegionKeyEntityIdAnimalRepositoryTest { - - private static final String APPLICATION_CONTEXT_CONFIG_LOCATION = String.format("%1$s%2$s%1$s%3$s", - File.separator, AnimalRepositoryTest.class.getPackage().getName().replace('.', File.separatorChar), - String.format("%s-context.xml", IncompatibleRegionKeyEntityIdAnimalRepositoryTest.class.getSimpleName())); - - @Test(expected = IllegalArgumentException.class) - public void storeAnimalHavingLongIdInRabbitsRegionWithStringKey() { - try { - ConfigurableApplicationContext applicationContext = - new ClassPathXmlApplicationContext(APPLICATION_CONTEXT_CONFIG_LOCATION); - - applicationContext.getBean(RabbitRepository.class); - } - // NOTE the ClassCastException thrown from GemFire is unexpected; this is not correct and the identifying type - // mismatch should be caught and handled by GemfireRepositoryFactory.getTemplate(..) method on line 129 - // (appropriately throwing an IllegalArgumentException) after satisfying the condition on line 128, - // which always occurs with the @Region annotation set on the domain class/entity! - catch (ClassCastException unexpected) { - //unexpected.printStackTrace(System.err); - //assertTrue(unexpected.getMessage().contains("key ( java.lang.Long ) does not satisfy keyConstraint ( java.lang.String )")); - fail(unexpected.getMessage()); - } - catch (BeanCreationException expected) { - //expected.printStackTrace(System.err); - assertTrue(expected.getCause() instanceof IllegalArgumentException); - assertEquals(String.format("The Region referenced only supports keys of type [%1$s], but the entity to be stored has an id of type [%2$s]", - String.class.getName(), Long.class.getName()), expected.getCause().getMessage()); - - throw (IllegalArgumentException) expected.getCause(); - } - } -} diff --git a/src/test/java/org/springframework/data/gemfire/repository/sample/Plant.java b/src/test/java/org/springframework/data/gemfire/repository/sample/Plant.java index b5c56844..b5a6da9a 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/sample/Plant.java +++ b/src/test/java/org/springframework/data/gemfire/repository/sample/Plant.java @@ -33,20 +33,20 @@ import org.springframework.util.ObjectUtils; public class Plant { @Id - private String id; + private Long id; private String name; - public String getId() { - return id; + public Long getId() { + return this.id; } - public void setId(String id) { + public void setId(Long id) { this.id = id; } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -55,7 +55,8 @@ public class Plant { @Override public boolean equals(final Object obj) { - if (obj == this) { + + if (this == obj) { return true; } @@ -71,9 +72,12 @@ public class Plant { @Override public int hashCode() { + int hashValue = 17; + hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getId()); hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getName()); + return hashValue; } @@ -81,5 +85,4 @@ public class Plant { public String toString() { return String.format("{ @type = %1$s, id = %2$s, name = %3$s }", getClass().getSimpleName(), getId(), getName()); } - } diff --git a/src/test/java/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest.java b/src/test/java/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest.java deleted file mode 100644 index 2b1e5ba4..00000000 --- a/src/test/java/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2010-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.gemfire.repository.sample; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.io.File; - -import org.junit.Test; -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -/** - * The PlantRepositoryTest class is a test suite of test cases testing the functionality behind PR #55 involving - * persisting application domain object/entities to multiple Regions in GemFire's Cache. - * - * @author John Blum - * @see org.junit.Test - * @see org.springframework.context.ConfigurableApplicationContext - * @since 1.4.0 - * @link https://github.com/spring-projects/spring-data-gemfire/pull/55 - */ -public class PlantRepositoryTest { - - private static final String APPLICATION_CONTEXT_CONFIG_LOCATION = String.format("%1$s%2$s%1$s%3$s", - File.separator, PlantRepositoryTest.class.getPackage().getName().replace('.', File.separatorChar), - "PlantRepositoryTest-context.xml"); - - @Test(expected = IllegalArgumentException.class) - public void storePlantHavingStringIdInPlantsRegionWithLongKey() { - try { - ConfigurableApplicationContext context = - new ClassPathXmlApplicationContext(APPLICATION_CONTEXT_CONFIG_LOCATION); - - context.getBean(PlantRepository.class); - } - // NOTE technically, the IllegalArgumentException for incompatible Region 'Key' and Entity ID is thrown - // when the Spring container starts up and the Repository beans are created. - catch (BeanCreationException expected) { - //expected.printStackTrace(System.err); - assertTrue(expected.getCause() instanceof IllegalArgumentException); - assertEquals(String.format("The Region referenced only supports keys of type [%1$s], but the entity to be stored has an id of type [%2$s]", - Long.class.getName(), String.class.getName()), expected.getCause().getMessage()); - - throw (IllegalArgumentException) expected.getCause(); - } - } -} diff --git a/src/test/java/org/springframework/data/gemfire/repository/sample/User.java b/src/test/java/org/springframework/data/gemfire/repository/sample/User.java index 1f1e793b..165af4c7 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/sample/User.java +++ b/src/test/java/org/springframework/data/gemfire/repository/sample/User.java @@ -93,7 +93,8 @@ public class User implements Comparable { @Override public boolean equals(final Object obj) { - if (obj == this) { + + if (this == obj) { return true; } @@ -113,9 +114,12 @@ public class User implements Comparable { @Override public int hashCode() { + int hashValue = 17; + hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getEmail()); hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getUsername()); + return hashValue; } @@ -123,5 +127,4 @@ public class User implements Comparable { public String toString() { return getUsername(); } - } diff --git a/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java index 488af207..d84736ac 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java @@ -16,18 +16,16 @@ package org.springframework.data.gemfire.repository.support; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; import java.io.Serializable; import java.util.Arrays; @@ -36,285 +34,509 @@ import java.util.Collections; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionAttributes; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import org.springframework.aop.framework.Advised; import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.gemfire.mapping.GemfireMappingContext; +import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; import org.springframework.data.gemfire.repository.GemfireRepository; import org.springframework.data.gemfire.repository.sample.Person; +import org.springframework.data.gemfire.util.RegionUtils; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; /** * Unit tests for {@link GemfireRepositoryFactory}. * * @author Oliver Gierke * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito * @see org.springframework.data.gemfire.repository.support.GemfireRepositoryFactory */ @RunWith(MockitoJUnitRunner.class) -@SuppressWarnings("unused") +@SuppressWarnings("rawtypes") public class GemfireRepositoryFactoryUnitTests { - @Rule - public ExpectedException exception = ExpectedException.none(); - - private GemfireMappingContext gemfireMappingContext = new GemfireMappingContext(); + private GemfireMappingContext mappingContext; @Mock - private Region mockRegion; + private Region mockRegion; @Mock - @SuppressWarnings("rawtypes") private RegionAttributes mockRegionAttributes; - @SuppressWarnings("unchecked") - protected Region configureMockRegion(Region mockRegion, String name, - Class keyType, Class valueType) { - - when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); - when(mockRegion.getFullPath()).thenReturn(String.format("%1$s%2$s", Region.SEPARATOR, name)); - when(mockRegion.getName()).thenReturn(name); - when(mockRegionAttributes.getKeyConstraint()).thenReturn(keyType); - when(mockRegionAttributes.getValueConstraint()).thenReturn(valueType); - - return mockRegion; - } - - @SuppressWarnings("unchecked") - protected Region mockRegion(String name, Class keyType, Class valueType) { - return configureMockRegion(mock(Region.class, name), name, keyType, valueType); - } - - protected RepositoryMetadata mockRepositoryMetadata(final Class domainType, final Class idType, - final Class repositoryInterface) { - - RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); - - when(mockRepositoryMetadata.getDomainType()).then(new Answer>() { - @Override public Class answer(InvocationOnMock invocation) throws Throwable { - return domainType; - } - }); - - when(mockRepositoryMetadata.getIdType()).then(new Answer>() { - @Override public Class answer(InvocationOnMock invocation) throws Throwable { - return idType; - } - }); - - when(mockRepositoryMetadata.getRepositoryInterface()).then(new Answer>() { - @Override public Class answer(InvocationOnMock invocation) throws Throwable { - return repositoryInterface; - } - }); - - return mockRepositoryMetadata; - } - @Before @SuppressWarnings("unchecked") public void setup() { - configureMockRegion(mockRegion, "simple", Object.class, Object.class); + this.mappingContext = new GemfireMappingContext(); + configureMockRegion(this.mockRegion, "simple", Object.class, Object.class); } - @Test - public void constructGemfireRepositoryFactoryWithNullMappingContextThrowsIllegalArgumentException() { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage("MappingContext must not be null"); - - new GemfireRepositoryFactory(Collections.>emptyList(), null); + @SuppressWarnings("unchecked") + private Region mockRegion(String name, Class keyType, Class valueType) { + return configureMockRegion(this.mockRegion, name, keyType, valueType); } - @Test - public void constructGemfireRepositoryFactoryWithNullRegionsThrowsIllegalArgumentException() { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage("Regions must not be null"); + @SuppressWarnings("unchecked") + private Region configureMockRegion(Region mockRegion, String name, + Class keyType, Class valueType) { - new GemfireRepositoryFactory(null, gemfireMappingContext); + when(mockRegion.getAttributes()).thenReturn(this.mockRegionAttributes); + when(mockRegion.getFullPath()).thenReturn(RegionUtils.toRegionPath(name)); + when(mockRegion.getName()).thenReturn(name); + when(this.mockRegionAttributes.getKeyConstraint()).thenReturn(keyType); + when(this.mockRegionAttributes.getValueConstraint()).thenReturn(valueType); + + return mockRegion; } - @Test - public void getRepositoryRegionNameFromRepositoryInterfaceWithRegionAnnotation() { - GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory( - Collections.>emptyList(), gemfireMappingContext); + private RepositoryMetadata mockRepositoryMetadata(Class domainType, Class idType, + Class repositoryInterface) { - assertThat(gemfireRepositoryFactory.getRepositoryRegionName(PersonRepository.class), - is(equalTo("People"))); - } + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); - @Test - public void getRepositoryRegionNameFromRepositoryInterfaceWithoutRegionAnnotation() { - GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory( - Collections.>emptyList(), gemfireMappingContext); + when(mockRepositoryMetadata.getDomainType()).thenAnswer(invocation -> domainType); + when(mockRepositoryMetadata.getIdType()).thenAnswer(invocation -> idType); + when(mockRepositoryMetadata.getRepositoryInterface()).thenAnswer(invocation -> repositoryInterface); - assertThat(gemfireRepositoryFactory.getRepositoryRegionName(SampleCustomGemfireRepository.class), - is(nullValue(String.class))); + return mockRepositoryMetadata; } @Test @SuppressWarnings("unchecked") - public void getTemplateReturnsGemfireTemplateForPeopleRegion() { - RepositoryMetadata mockRepositoryMetadata = mockRepositoryMetadata(Person.class, Long.class, - PersonRepository.class); + public void constructGemfireRepositoryFactorySuccessfully() { + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.singletonList(this.mockRegion), this.mappingContext); + + assertThat(repositoryFactory).isNotNull(); + assertThat(repositoryFactory.getMappingContext()).isEqualTo(this.mappingContext); + assertThat(repositoryFactory.getRegions()).contains(this.mockRegion); + } + + @Test(expected = IllegalArgumentException.class) + public void constructGemfireRepositoryFactoryWithNullMappingContextThrowsIllegalArgumentException() { + + try { + new GemfireRepositoryFactory(Collections.emptyList(), null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("MappingContext is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void constructGemfireRepositoryFactoryWithNullRegionsThrowsIllegalArgumentException() { + + try { + new GemfireRepositoryFactory(null, this.mappingContext); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Regions are required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void getEntityRegionNameFromEntityRegionName() { + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + GemfirePersistentEntity mockPersistentEntity = mock(GemfirePersistentEntity.class); + + when(mockPersistentEntity.getRegionName()).thenReturn("MockRegionName"); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + assertThat(repositoryFactory.getEntityRegionName(mockRepositoryMetadata, mockPersistentEntity)) + .isEqualTo("MockRegionName"); + + verify(mockPersistentEntity, times(1)).getRegionName(); + verify(mockPersistentEntity, never()).getType(); + verifyZeroInteractions(mockRepositoryMetadata); + } + + @Test + public void getEntityRegionNameFromEntityDomainType() { + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + GemfirePersistentEntity mockPersistentEntity = mock(GemfirePersistentEntity.class); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + when(mockPersistentEntity.getRegionName()).thenReturn(" "); + when(mockPersistentEntity.getType()).thenAnswer(invocation -> Person.class); + + assertThat(repositoryFactory.getEntityRegionName(mockRepositoryMetadata, mockPersistentEntity)) + .isEqualTo(Person.class.getSimpleName()); + + verify(mockPersistentEntity, times(1)).getRegionName(); + verify(mockPersistentEntity, times(1)).getType(); + verifyZeroInteractions(mockRepositoryMetadata); + } + + @Test + public void getEntityRegionNameFromRepositoryMetadata() { + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + GemfirePersistentEntity mockPersistentEntity = mock(GemfirePersistentEntity.class); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + when(mockPersistentEntity.getRegionName()).thenReturn(" "); + when(mockPersistentEntity.getType()).thenReturn(null); + when(mockRepositoryMetadata.getDomainType()).thenAnswer(invocation -> Person.class); + + assertThat(repositoryFactory.getEntityRegionName(mockRepositoryMetadata, mockPersistentEntity)) + .isEqualTo(Person.class.getSimpleName()); + + verify(mockPersistentEntity, times(1)).getRegionName(); + verify(mockPersistentEntity, times(1)).getType(); + verify(mockRepositoryMetadata, times(1)).getDomainType(); + } + + @Test + public void getEntityRegionNameFromRepositoryMetadataWhenEntityIsNull() { + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + when(mockRepositoryMetadata.getDomainType()).thenAnswer(invocation -> Person.class); + + assertThat(repositoryFactory.getEntityRegionName(mockRepositoryMetadata, null)) + .isEqualTo(Person.class.getSimpleName()); + + verify(mockRepositoryMetadata, times(1)).getDomainType(); + } + + @Test + public void getRepositoryRegionNameFromRegionAnnotatedRepositoryInterface() { + + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + when(mockRepositoryMetadata.getRepositoryInterface()).thenAnswer(invocation -> PeopleRepository.class); + + assertThat(gemfireRepositoryFactory.getRepositoryRegionName(mockRepositoryMetadata).orElse(null)) + .isEqualTo("People"); + + verify(mockRepositoryMetadata, times(1)).getRepositoryInterface(); + } + + @Test + public void getRepositoryRegionNameFromRegionAnnotatedRepositoryInterfaceHavingNoValue() { + + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + when(mockRepositoryMetadata.getRepositoryInterface()).thenAnswer(invocation -> NonQualifiedRegionAnnotatedRepository.class); + + assertThat(gemfireRepositoryFactory.getRepositoryRegionName(mockRepositoryMetadata).isPresent()).isFalse(); + + verify(mockRepositoryMetadata, times(1)).getRepositoryInterface(); + } + + @Test + public void getRepositoryRegionNameFromNonRegionAnnotatedRepositoryInterface() { + + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + RepositoryMetadata mockRepositoryMetadata = mock(RepositoryMetadata.class); + + when(mockRepositoryMetadata.getRepositoryInterface()) + .thenAnswer(invocation -> TestGemfireRepository.class); + + assertThat(gemfireRepositoryFactory.getRepositoryRegionName(mockRepositoryMetadata).isPresent()).isFalse(); + + verify(mockRepositoryMetadata, times(1)).getRepositoryInterface(); + } + + @Test + public void resolveRegionWithRepositoryMetadataAndRegionNamePath() { + + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Long.class, PeopleRepository.class); + + Region mockRegionOne = mockRegion("RegionOne", Person.class, Long.class); + Region mockRegionTwo = mockRegion("RegionTwo", Object.class, Long.class); + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Arrays.asList(mockRegionOne, mockRegionTwo), this.mappingContext); + + assertThat(repositoryFactory.resolveRegion(mockRepositoryMetadata, mockRegionTwo.getName())) + .isEqualTo(mockRegionTwo); + + assertThat(repositoryFactory.resolveRegion(mockRepositoryMetadata, mockRegionOne.getFullPath())) + .isEqualTo(mockRegionOne); + + verifyZeroInteractions(mockRepositoryMetadata); + } + + @Test(expected = IllegalStateException.class) + public void resolveRegionWithRepositoryMetadataAndNonExistingRegionNamePath() { + + try { + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Long.class, PeopleRepository.class); + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.emptyList(), this.mappingContext); + + repositoryFactory.resolveRegion(mockRepositoryMetadata, "Test"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(GemfireRepositoryFactory.REGION_NOT_FOUND, + "Test", Person.class.getName()); + + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + @SuppressWarnings("unchecked") + public void newTemplateReturnsGemfireTemplateForPeopleRegion() { + + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Long.class, PeopleRepository.class); Region mockPeopleRegion = mockRegion("People", Long.class, Person.class); - Iterable> regions = Arrays.asList(mockRegion, mockPeopleRegion); + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Arrays.asList(this.mockRegion, mockPeopleRegion), mappingContext); - GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory( - regions, gemfireMappingContext); + GemfireTemplate gemfireTemplate = repositoryFactory.newTemplate(mockRepositoryMetadata); - GemfireTemplate gemfireTemplate = gemfireRepositoryFactory.getTemplate(mockRepositoryMetadata); - - assertThat(gemfireTemplate, is(notNullValue(GemfireTemplate.class))); - assertThat(gemfireTemplate.getRegion(), is(equalTo(mockPeopleRegion))); + assertThat(gemfireTemplate).isNotNull(); + assertThat(gemfireTemplate.getRegion()).isEqualTo(mockPeopleRegion); verify(mockPeopleRegion, times(1)).getAttributes(); - verify(mockPeopleRegion, times(1)).getFullPath(); - verify(mockPeopleRegion, times(1)).getName(); - verify(mockRegionAttributes, times(1)).getKeyConstraint(); + verify(this.mockRegionAttributes, times(1)).getKeyConstraint(); verify(mockRepositoryMetadata, times(1)).getDomainType(); verify(mockRepositoryMetadata, times(1)).getIdType(); + verify(mockRepositoryMetadata, times(1)).getRepositoryInterface(); + verifyNoMoreInteractions(mockRepositoryMetadata); } @Test - public void getTemplateReturnsGemfireTemplateForSimpleRegion() { - RepositoryMetadata mockRepositoryMetadata = mockRepositoryMetadata(Person.class, Long.class, - SampleCustomGemfireRepository.class); + @SuppressWarnings("unchecked") + public void newTemplateReturnsGemfireTemplateForSimpleRegion() { - Iterable> regions = Collections.>singleton(mockRegion); + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Long.class, TestGemfireRepository.class); - GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory( - regions, gemfireMappingContext); + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.singleton(this.mockRegion), this.mappingContext); - GemfireTemplate gemfireTemplate = gemfireRepositoryFactory.getTemplate(mockRepositoryMetadata); + GemfireTemplate gemfireTemplate = gemfireRepositoryFactory.newTemplate(mockRepositoryMetadata); - assertThat(gemfireTemplate, is(notNullValue(GemfireTemplate.class))); - assertThat(gemfireTemplate.getRegion(), is(equalTo(mockRegion))); + assertThat(gemfireTemplate).isNotNull(); + assertThat(gemfireTemplate.getRegion()).isEqualTo(this.mockRegion); + verify(this.mockRegion, times(1)).getAttributes(); + verify(this.mockRegionAttributes, times(1)).getKeyConstraint(); verify(mockRepositoryMetadata, times(1)).getDomainType(); verify(mockRepositoryMetadata, times(1)).getIdType(); + verify(mockRepositoryMetadata, times(1)).getRepositoryInterface(); + verifyNoMoreInteractions(mockRepositoryMetadata); } - @Test - public void getTemplateThrowsIllegalArgumentExceptionForIncompatibleRegionKeyTypeAndRepositoryIdType() { - RepositoryMetadata mockRepositoryMetadata = mockRepositoryMetadata(Person.class, Long.class, - PersonRepository.class); + @Test(expected = IllegalArgumentException.class) + public void newTemplateWithIncompatibleRegionKeyTypeAndRepositoryIdTypeThrowsIllegalArgumentException() { - Region mockPeopleRegion = mockRegion("People", String.class, Person.class); + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Long.class, PeopleRepository.class); - GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory( - Collections.>singleton(mockPeopleRegion), gemfireMappingContext); + Region mockPeopleRegion = mockRegion("People", Integer.class, Person.class); + + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.singleton(mockPeopleRegion), this.mappingContext); try { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage(String.format( - "The Region referenced only supports keys of type [%1$s], but the entity to be stored has an id of type [%2$s]", - String.class.getName(), Long.class.getName())); + gemfireRepositoryFactory.newTemplate(mockRepositoryMetadata); + } + catch (IllegalArgumentException expected) { - gemfireRepositoryFactory.getTemplate(mockRepositoryMetadata); + assertThat(expected).hasMessage(GemfireRepositoryFactory.REGION_REPOSITORY_ID_TYPE_MISMATCH, + "/People", Integer.class.getName(), PeopleRepository.class.getName(), Long.class.getName()); + + assertThat(expected).hasNoCause(); + + throw expected; } finally { + verify(mockPeopleRegion, times(1)).getAttributes(); + verify(mockPeopleRegion, atLeastOnce()).getFullPath(); + verify(this.mockRegionAttributes, times(1)).getKeyConstraint(); verify(mockRepositoryMetadata, times(1)).getDomainType(); verify(mockRepositoryMetadata, times(1)).getIdType(); - verify(mockPeopleRegion, times(1)).getAttributes(); - verify(mockPeopleRegion, times(1)).getFullPath(); - verify(mockPeopleRegion, times(1)).getName(); - verify(mockRegionAttributes, times(1)).getKeyConstraint(); + verify(mockRepositoryMetadata, times(2)).getRepositoryInterface(); + verifyNoMoreInteractions(mockRepositoryMetadata); } } - @Test - public void getTemplateThrowsIllegalStateExceptionForRegionNotFound() { - RepositoryMetadata mockRepositoryMetadata = mockRepositoryMetadata(Person.class, Long.class, - PersonRepository.class); + @Test(expected = IllegalArgumentException.class) + public void newTemplateWithIncompatibleRepositoryIdTypeAndEntityIdTypeThrowsIllegalArgumentException() { - GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory( - Collections.>singleton(mockRegion), gemfireMappingContext); + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Integer.class, PeopleIntegerRepository.class); + + Region mockPeopleRegion = mockRegion("People", null, null); + + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.singleton(mockPeopleRegion), this.mappingContext); try { - exception.expect(IllegalStateException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage(String.format( - "No Region [People] was found for domain class [%s]; Make sure you have configured a GemFire Region of that name in your application context", - Person.class.getName())); + gemfireRepositoryFactory.newTemplate(mockRepositoryMetadata); + } + catch (IllegalArgumentException expected) { - gemfireRepositoryFactory.getTemplate(mockRepositoryMetadata); + assertThat(expected).hasMessage(GemfireRepositoryFactory.REPOSITORY_ENTITY_ID_TYPE_MISMATCH, + PeopleIntegerRepository.class.getName(), Integer.class.getName(), Person.class.getName(), + Long.class.getName()); + + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPeopleRegion, times(1)).getAttributes(); + verify(this.mockRegionAttributes, times(1)).getKeyConstraint(); + verify(mockRepositoryMetadata, times(1)).getDomainType(); + verify(mockRepositoryMetadata, times(1)).getIdType(); + verify(mockRepositoryMetadata, times(2)).getRepositoryInterface(); + verifyNoMoreInteractions(mockRepositoryMetadata); + } + } + + @Test(expected = IllegalStateException.class) + @SuppressWarnings("unchecked") + public void newTemplateWithNonExistingRegionThrowsIllegalStateException() { + + RepositoryMetadata mockRepositoryMetadata = + mockRepositoryMetadata(Person.class, Long.class, PeopleRepository.class); + + GemfireRepositoryFactory gemfireRepositoryFactory = + new GemfireRepositoryFactory(Collections.singleton(this.mockRegion), this.mappingContext); + + try { + gemfireRepositoryFactory.newTemplate(mockRepositoryMetadata); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage(GemfireRepositoryFactory.REGION_NOT_FOUND, + "People", Person.class.getName(), PeopleRepository.class.getName()); + + assertThat(expected).hasNoCause(); + + throw expected; } finally { verify(mockRepositoryMetadata, times(2)).getDomainType(); verify(mockRepositoryMetadata, never()).getIdType(); - verify(mockRegion, times(1)).getFullPath(); - verify(mockRegion, times(1)).getName(); - verifyZeroInteractions(mockRegionAttributes); + verify(mockRepositoryMetadata, times(2)).getRepositoryInterface(); } } /** - * @link https://jira.spring.io/browse/SGF-112 + * @link Repositories should reject PagingAndSortingRepository and Pageable parameters */ - @Test + @Test(expected = IllegalStateException.class) + @SuppressWarnings("unchecked") public void rejectsInterfacesExtendingPagingAndSortingRepository() { - exception.expect(IllegalStateException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage(startsWith("Pagination is not supported by GemFire Repositories")); - GemfireRepositoryFactory repositoryFactory = new GemfireRepositoryFactory( - Collections.>singletonList(mockRegion), new GemfireMappingContext()); + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.singletonList(this.mockRegion), new GemfireMappingContext()); - repositoryFactory.getRepository(SamplePagingAndSortingRepository.class); + try { + repositoryFactory.getRepository(SamplePagingAndSortingRepository.class); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessageStartingWith("Pagination is not supported by GemFire Repositories"); + assertThat(expected).hasNoCause(); + + throw expected; + } } @Test + @SuppressWarnings("unchecked") public void usesConfiguredRepositoryBaseClass() { - GemfireRepositoryFactory repositoryFactory = new GemfireRepositoryFactory( - Collections.>singletonList(mockRegion), new GemfireMappingContext()); - repositoryFactory.setRepositoryBaseClass(CustomBaseRepository.class); + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(Collections.singletonList(this.mockRegion), this.mappingContext); - GemfireRepository gemfireRepository = repositoryFactory.getRepository(SampleCustomGemfireRepository.class, - new SampleCustomRepositoryImpl()); + repositoryFactory.setRepositoryBaseClass(TestCustomBaseRepository.class); - assertThat(((Advised) gemfireRepository).getTargetClass(), is(equalTo((Class) CustomBaseRepository.class))); + GemfireRepository gemfireRepository = + repositoryFactory.getRepository(TestGemfireRepository.class, + RepositoryComposition.RepositoryFragments.just(new TestCustomRepositoryImpl())); + + assertThat(((Advised) gemfireRepository).getTargetClass()).isEqualTo(TestCustomBaseRepository.class); } interface SamplePagingAndSortingRepository extends PagingAndSortingRepository { } - static class CustomBaseRepository extends SimpleGemfireRepository { + static class TestCustomBaseRepository extends SimpleGemfireRepository { - public CustomBaseRepository(GemfireTemplate template, EntityInformation entityInformation) { + public TestCustomBaseRepository(GemfireTemplate template, EntityInformation entityInformation) { super(template, entityInformation); } } - interface SampleCustomRepository { + interface TestCustomRepository { + + @SuppressWarnings("unused") void doCustomUpdate(T entity); + } - class SampleCustomRepositoryImpl implements SampleCustomRepository { + class TestCustomRepositoryImpl implements TestCustomRepository { + @Override - public void doCustomUpdate(final T entity) { - throw new UnsupportedOperationException("Not Implemented"); + public void doCustomUpdate(T entity) { + throw newUnsupportedOperationException("Not Implemented"); } } - interface SampleCustomGemfireRepository extends GemfireRepository, SampleCustomRepository { - } + interface TestGemfireRepository extends GemfireRepository, TestCustomRepository { } @org.springframework.data.gemfire.mapping.annotation.Region("People") - interface PersonRepository extends GemfireRepository { - } + interface PeopleRepository extends GemfireRepository { } + + @org.springframework.data.gemfire.mapping.annotation.Region("People") + interface PeopleIntegerRepository extends GemfireRepository { } + + @org.springframework.data.gemfire.mapping.annotation.Region + interface NonQualifiedRegionAnnotatedRepository extends GemfireRepository { } + } diff --git a/src/test/java/org/springframework/data/gemfire/util/CollectionUtilsUnitTests.java b/src/test/java/org/springframework/data/gemfire/util/CollectionUtilsUnitTests.java index 0fe29140..b740ad1b 100644 --- a/src/test/java/org/springframework/data/gemfire/util/CollectionUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/gemfire/util/CollectionUtilsUnitTests.java @@ -17,8 +17,6 @@ package org.springframework.data.gemfire.util; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -35,10 +33,11 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.springframework.data.gemfire.test.support.MapBuilder; /** * Unit tests for {@link CollectionUtils}. @@ -59,9 +58,6 @@ import org.junit.rules.ExpectedException; */ public class CollectionUtilsUnitTests { - @Rule - public ExpectedException exception = ExpectedException.none(); - @Test public void addAllIterableElementsToList() { @@ -88,14 +84,19 @@ public class CollectionUtilsUnitTests { assertThat(target).contains(1, 2, 3, 4, 5); } - @Test + @Test(expected = IllegalArgumentException.class) public void addIterableToNullCollection() { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage("Collection is required"); + try { + CollectionUtils.addAll(null, Collections.emptySet()); + } + catch (IllegalArgumentException expected) { - CollectionUtils.addAll(null, Collections.emptySet()); + assertThat(expected).hasMessage("Collection is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } } @Test @@ -185,6 +186,11 @@ public class CollectionUtilsUnitTests { assertThat(CollectionUtils.containsAny(null, 1)).isFalse(); } + @Test + public void containsAnyWithNullCollectionAndNullArrayIsFalse() { + assertThat(CollectionUtils.containsAny(null, (Object[]) null)).isFalse(); + } + @Test public void emptyIterableReturnsEmptyIterable() { @@ -197,25 +203,20 @@ public class CollectionUtilsUnitTests { @Test @SuppressWarnings("unchecked") - public void iterableEnumeration() { + public void iterableOfEnumeration() { - Enumeration mockEnumeration = mock(Enumeration.class, "MockEnumeration"); + Enumeration mockEnumeration = mock(Enumeration.class, "MockEnumeration"); when(mockEnumeration.hasMoreElements()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); - when(mockEnumeration.nextElement()).thenReturn("zero").thenReturn("one").thenReturn("two") + when(mockEnumeration.nextElement()).thenReturn(1).thenReturn(2).thenReturn(3) .thenThrow(new NoSuchElementException("Enumeration exhausted")); - Iterable iterable = CollectionUtils.iterable(mockEnumeration); + Iterable iterable = CollectionUtils.iterable(mockEnumeration); assertThat(iterable).isNotNull(); - - List actualList = new ArrayList(3); - - for (String element : iterable) { - actualList.add(element); - } - - assertThat(actualList).isEqualTo(Arrays.asList("zero", "one", "two")); + //assertThat(iterable).containsExactly(1, 2, 3); + assertThat(StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toSet())) + .containsExactly(1, 2, 3); verify(mockEnumeration, times(4)).hasMoreElements(); verify(mockEnumeration, times(3)).nextElement(); @@ -223,30 +224,93 @@ public class CollectionUtilsUnitTests { @Test @SuppressWarnings("unchecked") - public void iterableIterator() { + public void iterableOfSingleEnumeration() { - Iterator mockIterator = mock(Iterator.class, "MockIterator"); + Enumeration mockEnumeration = mock(Enumeration.class); + + when(mockEnumeration.hasMoreElements()).thenReturn(true).thenReturn(false); + when(mockEnumeration.nextElement()).thenReturn(1) + .thenThrow(new NoSuchElementException("Enumeration exhausted")); + + Iterable iterable = CollectionUtils.iterable(mockEnumeration); + + assertThat(iterable).isNotNull(); + //assertThat(iterable).containsExactly(1); + assertThat(StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toSet())) + .containsExactly(1); + + verify(mockEnumeration, times(2)).hasMoreElements(); + verify(mockEnumeration, times(1)).nextElement(); + } + + @Test + @SuppressWarnings("unchecked") + public void iterableOfNullEnumeration() { + + Iterable iterable = CollectionUtils.iterable((Enumeration) null); + + assertThat(iterable).isNotNull(); + assertThat(iterable).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void iterableOfIterator() { + + Iterator mockIterator = mock(Iterator.class, "MockIterator"); when(mockIterator.hasNext()).thenReturn(true).thenReturn(true).thenReturn(true).thenReturn(false); - when(mockIterator.next()).thenReturn("zero").thenReturn("one").thenReturn("two") + when(mockIterator.next()).thenReturn(1).thenReturn(2).thenReturn(3) .thenThrow(new NoSuchElementException("Iterator exhausted")); - Iterable iterable = CollectionUtils.iterable(mockIterator); + Iterable iterable = CollectionUtils.iterable(mockIterator); assertThat(iterable).isNotNull(); - List actualList = new ArrayList(3); + Set set = new HashSet<>(); - for (String element : iterable) { - actualList.add(element); - } + iterable.forEach(set::add); - assertThat(actualList).containsAll(Arrays.asList("zero", "one", "two")); + assertThat(set).hasSize(3); + assertThat(set).containsExactly(1, 2, 3); verify(mockIterator, times(4)).hasNext(); verify(mockIterator, times(3)).next(); } + @Test + @SuppressWarnings("unchecked") + public void iterableOfSingleIterator() { + + Iterator mockIterator = mock(Iterator.class, "MockIterator"); + + when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); + when(mockIterator.next()).thenReturn(1).thenThrow(new NoSuchElementException("Iterator exhausted")); + + Iterable iterable = CollectionUtils.iterable(mockIterator); + + assertThat(iterable).isNotNull(); + + Set set = new HashSet<>(); + + iterable.forEach(set::add); + + assertThat(set).hasSize(1); + assertThat(set).containsExactly(1); + + verify(mockIterator, times(2)).hasNext(); + verify(mockIterator, times(1)).next(); + } + + @Test + public void iterableOfNullIterator() { + + Iterable iterable = CollectionUtils.iterable((Iterator) null); + + assertThat(iterable).isNotNull(); + assertThat(iterable).isEmpty(); + } + @Test public void nullSafeCollectionWithNonNullCollection() { @@ -264,6 +328,23 @@ public class CollectionUtilsUnitTests { assertThat(collection.isEmpty()).isTrue(); } + @Test + public void nullSafeEnumerationWithNonNullEnumeration() { + + Enumeration mockEnumeration = mock(Enumeration.class); + + assertThat(CollectionUtils.nullSafeEnumeration(mockEnumeration)).isSameAs(mockEnumeration); + } + + @Test + public void nullSafeEnumerationWithNullEnumeration() { + + Enumeration enumeration = CollectionUtils.nullSafeEnumeration(null); + + assertThat(enumeration).isNotNull(); + assertThat(enumeration.hasMoreElements()).isFalse(); + } + @Test @SuppressWarnings("unchecked") public void nullSafeIterableWithNonNullIterable() { @@ -344,6 +425,23 @@ public class CollectionUtilsUnitTests { assertThat(CollectionUtils.nullSafeIterable((Iterable) null, null)).isNull(); } + @Test + public void nullSafeIteratorWithNonNullIterator() { + + Iterator mockIterator = mock(Iterator.class); + + assertThat(CollectionUtils.nullSafeIterator(mockIterator)).isSameAs(mockIterator); + } + + @Test + public void nullSafeIteratorWithNullIterator() { + + Iterator iterator = CollectionUtils.nullSafeIterator(null); + + assertThat(iterator).isNotNull(); + assertThat(iterator).isEmpty(); + } + @Test public void nullSafeListWithNonNullList() { @@ -395,18 +493,97 @@ public class CollectionUtilsUnitTests { assertThat(set.isEmpty()).isTrue(); } + @Test + public void nullSafeIsEmptyCollectionWithNonNulNonEmptyCollectionReturnsFalse() { + assertThat(CollectionUtils.nullSafeIsEmpty(Collections.singleton(1))).isFalse(); + assertThat(CollectionUtils.nullSafeIsEmpty(Collections.singletonList(1))).isFalse(); + } + + @Test + public void nullSafeIsEmptyCollectionWithEmptyCollectionReturnsTrue() { + assertThat(CollectionUtils.isEmpty(Collections.emptyList())).isTrue(); + assertThat(CollectionUtils.isEmpty(Collections.emptySet())).isTrue(); + } + + @Test + public void nullSafeIsEmptyCollectionWithNullCollectionReturnsTrue() { + assertThat(CollectionUtils.isEmpty((Collection) null)).isTrue(); + } + + @Test + public void nullSafeIsEmptyMapWithNonNullNonEmptyMapReturnsFalse() { + assertThat(CollectionUtils.isEmpty(Collections.singletonMap("key", "value"))).isFalse(); + } + + @Test + public void nullSafeIsEmptyMapWithEmptyMapReturnsTrue() { + assertThat(CollectionUtils.isEmpty(Collections.emptyMap())).isTrue(); + } + + @Test + public void nullSafeIsEmptyMapWithNullMapReturnsTrue() { + assertThat(CollectionUtils.isEmpty((Map) null)).isTrue(); + } + + @Test + public void nullSafeCollectionSizeWithNonNullNonEmptyCollectionReturnsSize() { + assertThat(CollectionUtils.nullSafeSize(Collections.singleton(1))).isEqualTo(1); + assertThat(CollectionUtils.nullSafeSize(Collections.singletonList(1))).isEqualTo(1); + } + + @Test + public void nullSafeCollectionSizeWithEmptyCollectionReturnsZero() { + assertThat(CollectionUtils.nullSafeSize(Collections.emptyList())).isZero(); + assertThat(CollectionUtils.nullSafeSize(Collections.emptySet())).isZero(); + } + + @Test + public void nullSafeCollectionSizeWithNullCollectionReturnsZero() { + assertThat(CollectionUtils.nullSafeSize((Collection) null)).isZero(); + } + + @Test + public void nullSafeMapSizeWithNonNullNonEmptyMapReturnsSize() { + assertThat(CollectionUtils.nullSafeSize(Collections.singletonMap("key", "value"))).isEqualTo(1); + } + + @Test + public void nullSafeMapSizeWithEmptyMapReturnsZero() { + assertThat(CollectionUtils.nullSafeSize(Collections.emptyMap())).isZero(); + } + + @Test + public void nullSafeMapSizeWithNullMapReturnsZero() { + assertThat(CollectionUtils.nullSafeSize((Map) null)).isZero(); + } + @Test public void sortIsSuccessful() { - List list = new ArrayList(Arrays.asList(2, 3, 1)); + List list = new ArrayList<>(Arrays.asList(2, 3, 1)); List sortedList = CollectionUtils.sort(list); assertThat(sortedList).isSameAs(list); assertThat(sortedList).isEqualTo(Arrays.asList(1, 2, 3)); } + @Test(expected = IllegalArgumentException.class) + public void sortWithNullListThrowsIllegalArgumentException() { + + try { + CollectionUtils.sort(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("List is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + @Test - public void subListFromListWithIndexesReturnsSubList() { + public void subListOfListWithIndexesReturnsSubList() { List list = Arrays.asList(0, 1, 2, 3); List subList = CollectionUtils.subList(list, 1, 3); @@ -417,8 +594,13 @@ public class CollectionUtilsUnitTests { assertThat(subList).containsAll(Arrays.asList(1, 3)); } + @Test(expected = IndexOutOfBoundsException.class) + public void subListOfListWithInvalidIndexThrowsIndexOutOfBoundsException() { + CollectionUtils.subList(Arrays.asList(0, 1, 2), 1, 3); + } + @Test - public void subListFromListWithNoIndexesReturnsEmptyList() { + public void subListOfListWithNoIndexesReturnsEmptyList() { List subList = CollectionUtils.subList(Arrays.asList(0, 1, 2)); @@ -426,18 +608,59 @@ public class CollectionUtilsUnitTests { assertThat(subList.isEmpty()).isTrue(); } - @Test(expected = IndexOutOfBoundsException.class) - public void subListFromListWithInvalidIndexThrowsIndexOutOfBoundsException() { - CollectionUtils.subList(Arrays.asList(0, 1, 2), 1, 3); + @Test(expected = IllegalArgumentException.class) + public void subListOfNullListThrowsIllegalArgumentException() { + + try { + CollectionUtils.subList(null, 1, 2, 3); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("List is required"); + assertThat(expected).hasNoCause(); + + throw expected; + } } @Test - public void subListWithNullSourceListThrowsIllegalArgumentException() { + public void toStringOfMultiEntryMap() { - exception.expect(IllegalArgumentException.class); - exception.expectCause(is(nullValue(Throwable.class))); - exception.expectMessage("List is required"); + Map map = MapBuilder.newMapBuilder() + .put("keyOne", "valueOne") + .put("keyTwo", "valueTwo") + .build(); - CollectionUtils.subList(null, 1, 2, 3); + String mapString = CollectionUtils.toString(map); + + assertThat(mapString).isNotNull(); + assertThat(mapString).isEqualTo("{\n\tkeyOne = valueOne,\n\tkeyTwo = valueTwo\n}"); + } + + @Test + public void toStringOfSingleEntryMap() { + + String mapString = CollectionUtils.toString(Collections.singletonMap("key", "value")); + + assertThat(mapString).isNotNull(); + assertThat(mapString).isEqualTo("{\n\tkey = value\n}"); + } + + @Test + public void toStringOfEmptyMap() { + + String mapString = CollectionUtils.toString(Collections.emptyMap()); + + assertThat(mapString).isNotNull(); + assertThat(mapString).isEqualTo("{\n\n}"); + } + + @Test + public void toStringOfNullMap() { + + String mapString = CollectionUtils.toString(null); + + assertThat(mapString).isNotNull(); + assertThat(mapString).isEqualTo("{\n\n}"); } } diff --git a/src/test/resources/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest-context.xml b/src/test/resources/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest-context.xml deleted file mode 100644 index 70002ee1..00000000 --- a/src/test/resources/org/springframework/data/gemfire/repository/sample/IncompatibleRegionKeyEntityIdAnimalRepositoryTest-context.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - springGemFireIncompatibleRegionKeyEntityIdAnimalRepositoryTest - 0 - warning - - - - - - - - - - - diff --git a/src/test/resources/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest-context.xml b/src/test/resources/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest-context.xml deleted file mode 100644 index d81a8ed7..00000000 --- a/src/test/resources/org/springframework/data/gemfire/repository/sample/PlantRepositoryTest-context.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - springGemFirePlantRepositoryTest - 0 - warning - - - - - - - - - - -