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