SGF-713 - Override generated OQL from Repository query methods.

This commit is contained in:
John Blum
2018-02-16 16:08:40 -08:00
parent 5438e10904
commit 35bd627fd8
35 changed files with 2736 additions and 877 deletions

View File

@@ -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<T extends Repository, QUERY> 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<Repository, String> {
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, Long> {
Customer findByAccountNumber(String accountNumber);
List<Customer> findByLastNameLike(String lastName);
}
----
====
Then, I could have typed the `LoggingQueryPostProcessor` specifically to the `CustomerRepository`, like so...
.CustomerLoggingQueryPostProcessor
====
[source,java]
----
class LoggingQueryPostProcessor implements QueryPostProcessor<CustomerRepository, String> { .. }
----
====
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<CustomerRepository, String> {
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<Customer, Long> {
@Limit(5)
List<Customer> 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<Repository, String> {
@Override
int getOrder() {
return 1;
}
}
class CustomerQueryPostProcessor implements QueryPostProcessor<CustomerRepository, String> {
@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.

View File

@@ -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<T> extends BasicPersistentEntity<T, Gemfire
protected static String resolveRegionName(Class<?> persistentEntityType, Annotation regionAnnotation) {
Optional<String> 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<T> extends BasicPersistentEntity<T, Gemfire
}
/**
* Returns the name of the {@link org.apache.geode.cache.Region} in which this {@link PersistentEntity}
* will be stored.
* Returns the {@link String name} of the {@link org.apache.geode.cache.Region}
* in which this {@link PersistentEntity} will be stored.
*
* @return the name of the {@link org.apache.geode.cache.Region} in which this {@link PersistentEntity}
* will be stored.
* @return the {@link String name} of the {@link org.apache.geode.cache.Region}
* in which this {@link PersistentEntity} will be stored.
* @see org.apache.geode.cache.Region#getName()
*/
@NonNull
public String getRegionName() {
return this.regionName;
}
@@ -166,7 +169,7 @@ public class GemfirePersistentEntity<T> extends BasicPersistentEntity<T, Gemfire
return null;
}
return (property.isExplicitIdProperty() ? property : null);
return property.isExplicitIdProperty() ? property : null;
}
else {
return property;

View File

@@ -18,10 +18,13 @@ package org.springframework.data.gemfire.mapping;
import static org.springframework.data.gemfire.util.CollectionUtils.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Set;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
@@ -66,11 +69,13 @@ public class GemfirePersistentProperty extends AnnotationBasedPersistentProperty
}
/**
* Determines whether this {@link GemfirePersistentProperty} explicitly identifies an entity property identifier,
* one in which the user explicitly annotated a entity class member (field or getter/setter).
* Determines whether this {@link GemfirePersistentProperty} explicitly identifies
* an {@link GemfirePersistentEntity entity} identifier, one in which the user explicitly annotated
* the {@link GemfirePersistentEntity owning entity} class member ({@link Field} or property,
* i.e. {@link Method getter/setter}).
*
* @return a boolean value indicating whether this {@link GemfirePersistentProperty} explicitly identifies
* an entity property identifier.
* an {@link GemfirePersistentEntity entity} identifier.
* @see org.springframework.data.annotation.Id
* @see #isAnnotationPresent(Class)
*/
@@ -84,15 +89,37 @@ public class GemfirePersistentProperty extends AnnotationBasedPersistentProperty
*/
@Override
public boolean isIdProperty() {
return (super.isIdProperty() || SUPPORTED_IDENTIFIER_NAMES.contains(getName()));
return super.isIdProperty() || SUPPORTED_IDENTIFIER_NAMES.contains(getName());
}
/**
* Determines whether this {@link GemfirePersistentProperty persistent property} is {@literal transient}
* and thus impervious to persistent operations.
*
* A {@link GemfirePersistentProperty persistent property} is considered {@literal transient}
* if the {@link GemfirePersistentEntity owning entity's} field/property is annotated with
* {@link Transient} or the field/property is modified with {@link Modifier#TRANSIENT transient}.
*
* @return a boolean value indicating whether this {@link GemfirePersistentProperty persistent property}
* is {@literal transient} and thus impervious to persistent operations.
*/
@Override
public boolean isTransient() {
return super.isTransient()
|| getProperty().getField().filter(field -> 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
*/

View File

@@ -77,7 +77,8 @@ public class Regions implements Iterable<Region<?, ?>> {
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<?, T>) this.regions.get(regionName);
}

View File

@@ -44,8 +44,8 @@ import org.springframework.core.annotation.AliasFor;
public @interface Region {
@SuppressWarnings("unchecked")
List<Class<? extends Annotation>> REGION_ANNOTATION_TYPES = Arrays.asList(
ClientRegion.class, LocalRegion.class, PartitionRegion.class, ReplicateRegion.class, Region.class);
List<Class<? extends Annotation>> 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}

View File

@@ -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 "";

View File

@@ -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();
}
}

View File

@@ -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<QueryString, Predicates> {
@@ -47,6 +51,7 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
* @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<QueryString, Predicates>
*/
@Override
public QueryString createQuery(Sort dynamicSort) {
this.indexes = new IndexProvider();
return super.createQuery(dynamicSort);
}
@@ -96,7 +103,8 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
*/
@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()));

View File

@@ -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);
}
}

View File

@@ -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<?, String> 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<?, String> 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<?, String> queryPostProcessor) {
this.queryPostProcessor = this.queryPostProcessor.processBefore(queryPostProcessor);
return this;
}
enum ProvidedQueryPostProcessor implements QueryPostProcessor<Repository, String> {
IDENTITY {
@Override
public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {
return query;
}
}
}
}

View File

@@ -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<Part> partsIterator = tree.getParts().iterator();
List<Object> stringParameters = new ArrayList<Object>(parameters.length);
Iterator<Part> partsIterator = this.tree.getParts().iterator();
List<Object> 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();
}
}

View File

@@ -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;
}
/*

View File

@@ -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 <T> {@link Class type} identifying the {@link Repository Repositories} to match on for registration.
* @param <QUERY> {@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<T extends Repository, QUERY> 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<?, QUERY> processBefore(@Nullable QueryPostProcessor<?, QUERY> 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<?, QUERY> processAfter(@Nullable QueryPostProcessor<?, QUERY> queryPostProcessor) {
return queryPostProcessor == null ? this : (queryMethod, query, arguments) ->
this.postProcess(queryMethod, queryPostProcessor.postProcess(queryMethod, query, arguments), arguments);
}
}

View File

@@ -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("<HINT '\\w+'(, '\\w+')*>");
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 <T> Class<T> validateDomainType(Class<T> 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 <T> Class<T> validateDomainType(Class<T> 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<Integer> getInParameterIndexes() {
Pattern pattern = Pattern.compile(IN_PARAMETER_PATTERN);
Matcher matcher = pattern.matcher(query);
List<Integer> result = new ArrayList<>();
Matcher matcher = pattern.matcher(this.query);
List<Integer> 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;
}
}

View File

@@ -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<Repository, String> {
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;
}
}
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.util.StringUtils;
* @since 1.0.0
*/
public enum OqlKeyword {
AND,
AS,
COUNT,

View File

@@ -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<org.springframework.data.gemfire.mapping.annotation.Region> 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<? extends GemfirePersistentEntity<?>, 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<Region<?, ?>> regions,
MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext) {
public GemfireRepositoryFactory(@NonNull Iterable<Region<?, ?>> regions,
@NonNull MappingContext<? extends GemfirePersistentEntity<?>, 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<? extends GemfirePersistentEntity<?>, 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 <T, ID> GemfireEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return new DefaultGemfireEntityInformation<>(resolvePersistentEntity(domainClass));
}
GemfirePersistentEntity<T> entity =
(GemfirePersistentEntity<T>) 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<?, Serializable> 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 <T> GemfirePersistentEntity<T> resolvePersistentEntity(Class<?> domainType) {
return (GemfirePersistentEntity<T>) 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<GemfirePersistentEntity> 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<String> 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<QueryLookupStrategy> 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 extends QueryMethod> T newQueryMethod(Method method, RepositoryMetadata repositoryMetadata,
ProjectionFactory projectionFactory, EvaluationContextProvider evaluationContextProvider) {
return (T) new GemfireQueryMethod(method, repositoryMetadata, projectionFactory, getMappingContext());
}
}

View File

@@ -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<T extends Repository<S, ID>, S, ID extends Serializable>
public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
extends RepositoryFactoryBeanSupport<T, S, ID> implements ApplicationContextAware {
private ApplicationContext applicationContext;
private Iterable<Region<?, ?>> regions;
private MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext;
@@ -67,33 +87,49 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, 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<Region> 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()
.<Region<?, ?>>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<ApplicationContext> 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<? extends GemfirePersistentEntity<?>, 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<T extends Repository<S, ID>, 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<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> resolveGemfireMappingContext() {
MappingContext<? extends GemfirePersistentEntity<?>, 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<Region<?, ?>> getRegions() {
return this.regions;
@@ -123,7 +180,15 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, 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<T extends Repository<S, ID>, S, ID ext
*/
@Override
public void afterPropertiesSet() {
Assert.state(getGemfireMappingContext() != null, "GemfireMappingContext must not be null");
resolveGemfireMappingContext();
super.afterPropertiesSet();
}
protected class QueryPostProcessorRegistrationOnQueryCreationListener
implements QueryCreationListener<GemfireRepositoryQuery> {
private Iterable<QueryPostProcessorMetadata> queryPostProcessorsMetadata;
public QueryPostProcessorRegistrationOnQueryCreationListener(ApplicationContext applicationContext) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
List<QueryPostProcessor> queryPostProcessors =
new ArrayList<>(applicationContext.getBeansOfType(QueryPostProcessor.class).values());
queryPostProcessors.sort(OrderComparator.INSTANCE);
this.queryPostProcessorsMetadata = queryPostProcessors.stream()
.map(QueryPostProcessorMetadata::from)
.collect(Collectors.toList());
}
protected Iterable<QueryPostProcessorMetadata> 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<QueryPostProcessorKey, QueryPostProcessorMetadata> 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<TypeInformation<?>> 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<?, String> getQueryPostProcessor() {
return (QueryPostProcessor<?, String>) 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;
}
}
}

View File

@@ -66,6 +66,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @param entityInformation must not be {@literal null}.
*/
public SimpleGemfireRepository(GemfireTemplate template, EntityInformation<T, ID> entityInformation) {
Assert.notNull(template, "Template must not be null");
Assert.notNull(entityInformation, "EntityInformation must not be null");
@@ -79,9 +80,10 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public <U extends T> 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<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public <U extends T> Iterable<U> saveAll(Iterable<U> entities) {
Map<ID, U> 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<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public T save(Wrapper<T, ID> 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<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public long count() {
SelectResults<Integer> 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<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(template.get(id));
return Optional.ofNullable(this.template.get(id));
}
/*
@@ -150,8 +155,9 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Collection<T> findAll() {
SelectResults<T> 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<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Iterable<T> 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<T> selectResults = template.find(query.toString());
SelectResults<T> selectResults = this.template.find(query.toString());
return selectResults.asList();
}
@@ -177,9 +184,10 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public Collection<T> findAllById(Iterable<ID> ids) {
List<ID> keys = Streamable.of(ids).stream().collect(StreamUtils.toUnmodifiableList());
return CollectionUtils.<ID, T>nullSafeMap(template.getAll(keys)).values().stream()
return CollectionUtils.<ID, T>nullSafeMap(this.template.getAll(keys)).values().stream()
.filter(Objects::nonNull).collect(Collectors.toList());
}
@@ -189,7 +197,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public void deleteById(ID id) {
template.remove(id);
this.template.remove(id);
}
/*
@@ -198,7 +206,7 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public void delete(T entity) {
deleteById(entityInformation.getRequiredId(entity));
deleteById(this.entityInformation.getRequiredId(entity));
}
/*
@@ -216,8 +224,9 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
* @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<T, ID> implements GemfireRepository<T, ID>
* @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<T, ID> implements GemfireRepository<T, ID>
* @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<T, ID> implements GemfireRepository<T, ID>
* @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<T, ID> implements GemfireRepository<T, ID>
*/
@Override
public void deleteAll() {
template.execute((GemfireCallback<Void>) region -> {
this.template.execute((GemfireCallback<Void>) region -> {
if (isPartitioned(region) || isTransactionPresent(region)) {
doRegionClear(region);
}

View File

@@ -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 <E, T extends Collection<E>> T addAll(T collection, Iterable<E> iterable) {
public static <E, T extends Collection<E>> T addAll(@NonNull T collection, @Nullable Iterable<E> 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 <T> Set<T> asSet(T... elements) {
public static <T> Set<T> asSet(@NonNull T... elements) {
Set<T> 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 <T> Iterable<T> 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 <T> Iterable<T> iterable(Enumeration<T> enumeration) {
return () -> toIterator(enumeration);
@NonNull
public static <T> Iterable<T> iterable(@Nullable Enumeration<T> 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 <T> Iterable<T> iterable(Iterator<T> iterator) {
return () -> iterator;
@NonNull
public static <T> Iterable<T> iterable(@Nullable Iterator<T> 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 <T> Collection<T> nullSafeCollection(Collection<T> collection) {
@NonNull
public static <T> Collection<T> nullSafeCollection(@Nullable Collection<T> 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 <T> {@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 <T> Enumeration<T> nullSafeEnumeration(@Nullable Enumeration<T> 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 <T> 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 <T> Iterable<T> nullSafeIterable(Iterable<T> iterable) {
return iterable != null ? iterable : Collections::emptyIterator;
@NonNull
public static <T> Iterable<T> nullSafeIterable(@Nullable Iterable<T> 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 <E, T extends Iterable<E>> T nullSafeIterable(T iterable, T defaultIterable) {
@Nullable
public static <E, T extends Iterable<E>> 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 <T> {@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 <T> Iterator<T> nullSafeIterator(@Nullable Iterator<T> 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 <T> List<T> nullSafeList(List<T> list) {
@NonNull
public static <T> List<T> nullSafeList(@Nullable List<T> 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 <K, V> Map<K, V> nullSafeMap(Map<K, V> map) {
@NonNull
public static <K, V> Map<K, V> nullSafeMap(@Nullable Map<K, V> map) {
return map != null ? map : Collections.<K, V>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 <T> Set<T> nullSafeSet(Set<T> set) {
@NonNull
public static <T> Set<T> nullSafeSet(@Nullable Set<T> 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 <T extends Comparable<T>> List<T> sort(List<T> list) {
public static <T extends Comparable<T>> List<T> sort(@NonNull List<T> 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 <T> List<T> subList(List<T> source, int... indices) {
public static <T> List<T> subList(@NonNull List<T> 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));
}
}

View File

@@ -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<? extends Repository> repositoryType, Supplier<String> 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<String, Animal> clientRegion(GemFireCache gemfireCache) {
ClientRegionFactoryBean<String, Animal> 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<Object, Object> clientRegion(GemFireCache gemfireCache) {
ClientRegionFactoryBean<Object, Object> clientRegion = new ClientRegionFactoryBean<>();
clientRegion.setCache(gemfireCache);
clientRegion.setClose(false);
clientRegion.setShortcut(ClientRegionShortcut.PROXY);
return clientRegion;
}
}
}

View File

@@ -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);

View File

@@ -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<User> 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<Person> 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<Person> 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<Object, Object> peopleRegion(GemFireCache gemfireCache) {
ClientRegionFactoryBean<Object, Object> clientRegion = new ClientRegionFactoryBean<>();
clientRegion.setCache(gemfireCache);
clientRegion.setClose(false);
clientRegion.setShortcut(ClientRegionShortcut.LOCAL);
return clientRegion;
}
@Bean("Users")
public ClientRegionFactoryBean<Object, Object> usersRegion(GemFireCache gemfireCache) {
ClientRegionFactoryBean<Object, Object> clientRegion = new ClientRegionFactoryBean<>();
clientRegion.setCache(gemfireCache);
clientRegion.setClose(false);
clientRegion.setShortcut(ClientRegionShortcut.LOCAL);
return clientRegion;
}
@Bean
GemfireMappingContext mappingContext() {
return new GemfireMappingContext();
}
@Bean
GemfireRepositoryFactoryBean<PersonRepository, Person, Long> personRepository() {
return new GemfireRepositoryFactoryBean<>(PersonRepository.class);
}
@Bean
GemfireRepositoryFactoryBean<UserRepository, User, String> userRepository() {
return new GemfireRepositoryFactoryBean<>(UserRepository.class);
}
@Bean
FindOrderedLimitedPeopleByFirstNameQueryPostProcessor personQueryPostProcess() {
return new FindOrderedLimitedPeopleByFirstNameQueryPostProcessor(1);
}
@Bean
RecordingQueryPostProcessor recordingQueryPostProcessor() {
return new RecordingQueryPostProcessor();
}
}
static class FindOrderedLimitedPeopleByFirstNameQueryPostProcessor implements QueryPostProcessor<PersonRepository, String> {
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<Repository, String> {
List<String> 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;
}
}
}

View File

@@ -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<?, String> mockQueryPostProcessorOne = mock(QueryPostProcessor.class);
QueryPostProcessor<?, String> 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<?, String> 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<?, String> mockQueryPostProcessorOne = mock(QueryPostProcessor.class);
QueryPostProcessor<?, String> 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<?, String> 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);
}
}

View File

@@ -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, "<HINT 'ExampleIndex'>")).isTrue();
assertThat(matches(HINT_PATTERN, "<HINT 'IdIdx'> SELECT * FROM /Example WHERE id = $1")).isTrue();
assertThat(matches(HINT_PATTERN, "<HINT 'LastNameIdx', 'BirthDateIdx'> 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, "<HINT>")).isFalse();
assertThat(matches(HINT_PATTERN, "<HINT ''>")).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, "<TRACE>")).isTrue();
assertThat(matches(TRACE_PATTERN, "<TRACE> SELECT * FROM /Example")).isTrue();
assertThat(matches(TRACE_PATTERN, "<TRACE>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, "<TRACE SELECT * FROM /Example>")).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("<HINT 'IdIdx'> 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("<HINT 'IdIdx', 'SpatialIdx', 'TxDateIdx'> 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("<TRACE> 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("<TRACE> <HINT 'IdIdx'> 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("<TRACE> <HINT 'IdIdx', 'NameIdx'> IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 20");
}
}

View File

@@ -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<String> 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<String> expectedList = Arrays.asList("a", "b", "c");
when(mockResultsBag.asList()).thenReturn(expectedList);
@@ -83,7 +85,9 @@ public class StringBasedGemfireRepositoryQueryTest {
@Test
public void testToCollectionWithCollection() {
List<String> 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(
"<TRACE> <HINT 'IdIdx', 'NameIdx'> IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 10")));
assertThat(postProcessedQueryString, is(notNullValue()));
assertThat(postProcessedQueryString,
is(equalTo("<TRACE> <HINT 'IdIdx', 'NameIdx'> 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(
"<TRACE> <HINT 'LastNameIdx'> SELECT * FROM /Example LIMIT 25")));
assertThat(postProcessedQueryString, is(notNullValue()));
assertThat(postProcessedQueryString,
is(equalTo("<TRACE> <HINT 'LastNameIdx'> 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; <TRACE> SELECT * FROM /Example")));
assertThat(postProcessedQueryString, is(notNullValue()));
assertThat(postProcessedQueryString,
is(equalTo("IMPORT org.example.domain.Type; <TRACE> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}
}

View File

@@ -93,7 +93,8 @@ public class User implements Comparable<User> {
@Override
public boolean equals(final Object obj) {
if (obj == this) {
if (this == obj) {
return true;
}
@@ -113,9 +114,12 @@ public class User implements Comparable<User> {
@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<User> {
public String toString() {
return getUsername();
}
}

View File

@@ -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<Object, Object> mockRegion;
private Region mockRegion;
@Mock
@SuppressWarnings("rawtypes")
private RegionAttributes mockRegionAttributes;
@SuppressWarnings("unchecked")
protected <K, V> Region<K, V> configureMockRegion(Region<K, V> mockRegion, String name,
Class<K> keyType, Class<V> 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 <K, V> Region<K, V> mockRegion(String name, Class<K> keyType, Class<V> 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<Class<?>>() {
@Override public Class<?> answer(InvocationOnMock invocation) throws Throwable {
return domainType;
}
});
when(mockRepositoryMetadata.getIdType()).then(new Answer<Class<?>>() {
@Override public Class<?> answer(InvocationOnMock invocation) throws Throwable {
return idType;
}
});
when(mockRepositoryMetadata.getRepositoryInterface()).then(new Answer<Class<?>>() {
@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.<Region<?, ?>>emptyList(), null);
@SuppressWarnings("unchecked")
private <K, V> Region<K, V> mockRegion(String name, Class<K> keyType, Class<V> 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 <K, V> Region<K, V> configureMockRegion(Region<K, V> mockRegion, String name,
Class<K> keyType, Class<V> 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.<Region<?, ?>>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.<Region<?, ?>>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<Long, Person> mockPeopleRegion = mockRegion("People", Long.class, Person.class);
Iterable<Region<?, ?>> 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.<Long, Person>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<Region<?, ?>> regions = Collections.<Region<?, ?>>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<String, Person> mockPeopleRegion = mockRegion("People", String.class, Person.class);
RepositoryMetadata mockRepositoryMetadata =
mockRepositoryMetadata(Person.class, Long.class, PeopleRepository.class);
GemfireRepositoryFactory gemfireRepositoryFactory = new GemfireRepositoryFactory(
Collections.<Region<?, ?>>singleton(mockPeopleRegion), gemfireMappingContext);
Region<Integer, Person> 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.<Region<?, ?>>singleton(mockRegion), gemfireMappingContext);
RepositoryMetadata mockRepositoryMetadata =
mockRepositoryMetadata(Person.class, Integer.class, PeopleIntegerRepository.class);
Region<String, Person> 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 <a href="https://jira.spring.io/browse/SGF-112">Repositories should reject PagingAndSortingRepository and Pageable parameters</a>
*/
@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.<Region<?, ?>>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.<Region<?, ?>>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<Person, Long> {
}
static class CustomBaseRepository<T, ID extends Serializable> extends SimpleGemfireRepository<T, ID> {
static class TestCustomBaseRepository<T, ID extends Serializable> extends SimpleGemfireRepository<T, ID> {
public CustomBaseRepository(GemfireTemplate template, EntityInformation<T, ID> entityInformation) {
public TestCustomBaseRepository(GemfireTemplate template, EntityInformation<T, ID> entityInformation) {
super(template, entityInformation);
}
}
interface SampleCustomRepository<T> {
interface TestCustomRepository<T> {
@SuppressWarnings("unused")
void doCustomUpdate(T entity);
}
class SampleCustomRepositoryImpl<T> implements SampleCustomRepository<T> {
class TestCustomRepositoryImpl<T> implements TestCustomRepository<T> {
@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<Person, Long>, SampleCustomRepository<Person> {
}
interface TestGemfireRepository extends GemfireRepository<Person, Long>, TestCustomRepository<Person> { }
@org.springframework.data.gemfire.mapping.annotation.Region("People")
interface PersonRepository extends GemfireRepository<Person, Long> {
}
interface PeopleRepository extends GemfireRepository<Person, Long> { }
@org.springframework.data.gemfire.mapping.annotation.Region("People")
interface PeopleIntegerRepository extends GemfireRepository<Person, Integer> { }
@org.springframework.data.gemfire.mapping.annotation.Region
interface NonQualifiedRegionAnnotatedRepository extends GemfireRepository<Person, Long> { }
}

View File

@@ -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<String> mockEnumeration = mock(Enumeration.class, "MockEnumeration");
Enumeration<Object> 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<String> iterable = CollectionUtils.iterable(mockEnumeration);
Iterable<Object> iterable = CollectionUtils.iterable(mockEnumeration);
assertThat(iterable).isNotNull();
List<String> actualList = new ArrayList<String>(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<String> mockIterator = mock(Iterator.class, "MockIterator");
Enumeration<Object> mockEnumeration = mock(Enumeration.class);
when(mockEnumeration.hasMoreElements()).thenReturn(true).thenReturn(false);
when(mockEnumeration.nextElement()).thenReturn(1)
.thenThrow(new NoSuchElementException("Enumeration exhausted"));
Iterable<Object> 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<Object> 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<String> iterable = CollectionUtils.iterable(mockIterator);
Iterable<Object> iterable = CollectionUtils.iterable(mockIterator);
assertThat(iterable).isNotNull();
List<String> actualList = new ArrayList<String>(3);
Set<Object> 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<Object> mockIterator = mock(Iterator.class, "MockIterator");
when(mockIterator.hasNext()).thenReturn(true).thenReturn(false);
when(mockIterator.next()).thenReturn(1).thenThrow(new NoSuchElementException("Iterator exhausted"));
Iterable<Object> iterable = CollectionUtils.iterable(mockIterator);
assertThat(iterable).isNotNull();
Set<Object> 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<Integer> list = new ArrayList<Integer>(Arrays.asList(2, 3, 1));
List<Integer> list = new ArrayList<>(Arrays.asList(2, 3, 1));
List<Integer> 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<Integer> list = Arrays.asList(0, 1, 2, 3);
List<Integer> 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<Integer> 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<String, String> map = MapBuilder.<String, String>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}");
}
}

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:gfe="http://www.springframework.org/schema/gemfire"
xmlns:gfe-data="http://www.springframework.org/schema/data/gemfire"
xmlns:repo="http://www.springframework.org/schema/data/repository"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/gemfire http://www.springframework.org/schema/gemfire/spring-gemfire.xsd
http://www.springframework.org/schema/data/gemfire http://www.springframework.org/schema/data/gemfire/spring-data-gemfire.xsd
http://www.springframework.org/schema/data/repository http://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
">
<util:properties id="gemfireProperties">
<prop key="name">springGemFireIncompatibleRegionKeyEntityIdAnimalRepositoryTest</prop>
<prop key="mcast-port">0</prop>
<prop key="log-level">warning</prop>
</util:properties>
<gfe:cache properties-ref="gemfireProperties"/>
<gfe:replicated-region
id="Rabbits"
persistent="false"
key-constraint="java.lang.String"
value-constraint="org.springframework.data.gemfire.repository.sample.Animal"/>
<gfe-data:repositories base-package="org.springframework.data.gemfire.repository.sample">
<repo:include-filter type="assignable" expression="org.springframework.data.gemfire.repository.sample.RabbitRepository"/>
</gfe-data:repositories>
</beans>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:gfe="http://www.springframework.org/schema/gemfire"
xmlns:gfe-data="http://www.springframework.org/schema/data/gemfire"
xmlns:repo="http://www.springframework.org/schema/data/repository"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/gemfire http://www.springframework.org/schema/gemfire/spring-gemfire.xsd
http://www.springframework.org/schema/data/gemfire http://www.springframework.org/schema/data/gemfire/spring-data-gemfire.xsd
http://www.springframework.org/schema/data/repository http://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
">
<util:properties id="gemfireProperties">
<prop key="name">springGemFirePlantRepositoryTest</prop>
<prop key="mcast-port">0</prop>
<prop key="log-level">warning</prop>
</util:properties>
<gfe:cache properties-ref="gemfireProperties"/>
<gfe:replicated-region
id="Plants"
persistent="false"
key-constraint="java.lang.Long"
value-constraint="org.springframework.data.gemfire.repository.sample.Plant"/>
<gfe-data:repositories base-package="org.springframework.data.gemfire.repository.sample">
<repo:include-filter type="assignable" expression="org.springframework.data.gemfire.repository.sample.PlantRepository"/>
</gfe-data:repositories>
</beans>