DATAGEODE-263 - Add support for PagingAndSortingRepositories.

This commit is contained in:
John Blum
2020-10-01 14:44:39 -07:00
parent 23633141fc
commit 9c4a2b18ec
21 changed files with 885 additions and 372 deletions

View File

@@ -291,13 +291,13 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation
@Override
@SuppressWarnings("unchecked")
public <E> SelectResults<E> find(String queryString, Object... params) throws InvalidDataAccessApiUsageException {
public <E> SelectResults<E> find(String queryString, Object... arguments) throws InvalidDataAccessApiUsageException {
try {
QueryService queryService = resolveQueryService(getRegion());
Query query = queryService.newQuery(queryString);
Object result = query.execute(params);
Object result = query.execute(arguments);
if (result instanceof SelectResults) {
return (SelectResults<E>) result;
@@ -314,8 +314,8 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation
catch (GemFireCheckedException cause) {
throw convertGemFireAccessException(cause);
}
catch (GemFireException caue) {
throw convertGemFireAccessException(caue);
catch (GemFireException cause) {
throw convertGemFireAccessException(cause);
}
catch (RuntimeException cause) {

View File

@@ -14,20 +14,20 @@
* limitations under the License.
*
*/
package org.springframework.data.gemfire.domain;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.domain.support.AbstractPageSupport;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* The {@link ListablePage} class is a Spring Data {@link Page} implementation wrapping a {@link List} as the content
@@ -54,7 +54,7 @@ public class ListablePage<T> extends AbstractPageSupport<T> {
* @see #ListablePage(List)
*/
@SafeVarargs
public static <T> ListablePage<T> newListablePage(T... content) {
public static <T> ListablePage<T> newListablePage(@NonNull T... content) {
return new ListablePage<>(Arrays.asList(content));
}
@@ -67,7 +67,7 @@ public class ListablePage<T> extends AbstractPageSupport<T> {
* @return a new {@link ListablePage} initialized with the given {@link List} for content.
* @see #ListablePage(List)
*/
public static <T> ListablePage<T> newListablePage(List<T> content) {
public static <T> ListablePage<T> newListablePage(@Nullable List<T> content) {
return new ListablePage<>(content);
}
@@ -83,8 +83,8 @@ public class ListablePage<T> extends AbstractPageSupport<T> {
* @see java.util.Collections#emptyList()
* @see java.util.List
*/
public ListablePage(List<T> content) {
this.content = Optional.ofNullable(content).orElse(Collections.emptyList());
public ListablePage(@Nullable List<T> content) {
this.content = content != null ? content : Collections.emptyList();
}
/**
@@ -164,6 +164,9 @@ public class ListablePage<T> extends AbstractPageSupport<T> {
*/
@Override
public <S> Page<S> map(Function<? super T, ? extends S> converter) {
return newListablePage(getContent().stream().map(converter::apply).collect(Collectors.toList()));
return newListablePage(getContent().stream()
.map(converter)
.collect(Collectors.toList()));
}
}

View File

@@ -15,28 +15,16 @@
*/
package org.springframework.data.gemfire.repository;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
/**
* Apache Geode extension of the Spring Data {@link CrudRepository} interface.
* Apache Geode extension of the Spring Data {@link PagingAndSortingRepository} interface.
*
* @author Oliver Gierke
* @author John Blum
* @see org.springframework.data.repository.CrudRepository
* @see org.springframework.data.repository.PagingAndSortingRepository
*/
public interface GemfireRepository<T, ID> extends CrudRepository<T, ID> {
/**
* Returns all entities ordered by the given {@link Sort}.
*
* @param sort {@link Sort} defining the ordering criteria.
* @return all entities ordered by the given {@link Sort}.
* @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
* @see org.springframework.data.domain.Sort
* @see java.lang.Iterable
*/
Iterable<T> findAll(Sort sort);
public interface GemfireRepository<T, ID> extends PagingAndSortingRepository<T, ID> {
/**
* Save the entity wrapped by the given {@link Wrapper}.

View File

@@ -13,20 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.repository.query;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.mapping.GemfirePersistentEntity;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link AbstractQueryCreator} to create {@link QueryString} instances.
*
@@ -59,10 +58,6 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
this.indexes = new IndexProvider();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#createQuery(org.springframework.data.domain.Sort)
*/
@Override
public QueryString createQuery(Sort dynamicSort) {
@@ -71,37 +66,21 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
return super.createQuery(dynamicSort);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator)
*/
@Override
protected Predicates create(Part part, Iterator<Object> iterator) {
return Predicates.create(part, this.indexes);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator)
*/
@Override
protected Predicates and(Part part, Predicates base, Iterator<Object> iterator) {
return base.and(Predicates.create(part, this.indexes));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object)
*/
@Override
protected Predicates or(Predicates base, Predicates criteria) {
return base.or(criteria);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort)
*/
@Override
protected QueryString complete(Predicates criteria, Sort sort) {
@@ -130,14 +109,13 @@ class GemfireQueryCreator extends AbstractQueryCreator<QueryString, Predicates>
}
@Override
@SuppressWarnings("all")
public boolean hasNext() {
return (index <= Integer.MAX_VALUE);
return this.index <= Integer.MAX_VALUE;
}
@Override
public Integer next() {
return index++;
return this.index++;
}
@Override

View File

@@ -28,8 +28,12 @@ import org.springframework.data.gemfire.repository.query.annotation.Limit;
import org.springframework.data.gemfire.repository.query.annotation.Trace;
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.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -49,60 +53,108 @@ public class GemfireQueryMethod extends QueryMethod {
private final Method method;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
/**
* Constructs a new instance of {@link GemfireQueryMethod} from the given {@link Method}
* and {@link RepositoryMetadata}.
*
* @param method must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @param factory must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
* @param method {@link Method} object backing the actual {@literal query} for this {@link QueryMethod};
* must not be {@literal null}.
* @param metadata {@link RepositoryMetadata} containing metadata about the {@link Repository}
* to which this {@link QueryMethod} belongs; must not be {@literal null}.
* @param projectionFactory {@link ProjectionFactory} used to handle the {@literal query} {@literal projection};
* must not be {@literal null}.
* @param mappingContext {@link MappingContext} used to map {@link Object entities} to Apache Geode and back to
* {@link Object entities}; must not be {@literal null}.
* @see #GemfireQueryMethod(Method, RepositoryMetadata, ProjectionFactory, MappingContext, QueryMethodEvaluationContextProvider)
* @see org.springframework.data.repository.core.RepositoryMetadata
* @see org.springframework.data.projection.ProjectionFactory
* @see org.springframework.data.mapping.context.MappingContext
* @see java.lang.reflect.Method
*/
public GemfireQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext) {
public GemfireQueryMethod(@NonNull Method method,
@NonNull RepositoryMetadata metadata,
@NonNull ProjectionFactory projectionFactory,
@NonNull MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext) {
super(method, metadata, factory);
this(method, metadata, projectionFactory, mappingContext, null);
}
/**
* Constructs a new instance of {@link GemfireQueryMethod} from the given {@link Method}
* and {@link RepositoryMetadata}.
*
* @param method {@link Method} object backing the actual {@literal query} for this {@link QueryMethod};
* must not be {@literal null}.
* @param metadata {@link RepositoryMetadata} containing metadata about the {@link Repository}
* to which this {@link QueryMethod} belongs; must not be {@literal null}.
* @param projectionFactory {@link ProjectionFactory} used to handle the {@literal query} {@literal projection};
* must not be {@literal null}.
* @param mappingContext {@link MappingContext} used to map {@link Object entities} to Apache Geode and back to
* {@link Object entities}; must not be {@literal null}.
* @param evaluationContextProvider {@link QueryMethodEvaluationContextProvider} used to process {@literal SpEL}
* expressions.
* @see org.springframework.data.repository.query.QueryMethodEvaluationContextProvider
* @see org.springframework.data.repository.core.RepositoryMetadata
* @see org.springframework.data.projection.ProjectionFactory
* @see org.springframework.data.mapping.context.MappingContext
* @see java.lang.reflect.Method
*/
public GemfireQueryMethod(@NonNull Method method,
@NonNull RepositoryMetadata metadata,
@NonNull ProjectionFactory projectionFactory,
@NonNull MappingContext<? extends GemfirePersistentEntity<?>, GemfirePersistentProperty> mappingContext,
@Nullable QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, metadata, projectionFactory);
Assert.notNull(mappingContext, "MappingContext must not be null");
assertNonPagingQueryMethod(method);
this.method = method;
this.entity = mappingContext.getPersistentEntity(getDomainClass());
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* Asserts that the query method is a non-Paging query method since GemFire does not support pagination
* as it has no concept of a Cursor.
* Determines whether the {@link Method} backing this {@link QueryMethod} is a {@link Pageable} {@link Method},
* which requires special logic given Apache Geode does not support pagination since it has no concept of a
* {@literal Database Cursor}.
*
* @param method the query method to be evaluated
* @throws java.lang.IllegalStateException if the query method contains a parameter of type Pageable.
* @see org.springframework.data.domain.Pageable
* @param method {@literal query} {@link Method} to be evaluate.
* @return a boolean value indicating whether the {@link Method} has a parameter of type {@link Pageable}.
* @see java.lang.reflect.Method#getParameterTypes()
* @see org.springframework.data.domain.Pageable
*/
private void assertNonPagingQueryMethod(Method method) {
private boolean isPageableQueryMethod(@NonNull Method method) {
for (Class<?> type : method.getParameterTypes()) {
if (Pageable.class.isAssignableFrom(type)) {
String message =
String.format("Pagination is not supported by GemFire Repositories; Offending method: %s",
method.getName());
throw new IllegalStateException(message);
return true;
}
}
return false;
}
/**
* Returns the {@link GemfirePersistentEntity} the method deals with.
* Returns the {@link Method} reference on which this {@link QueryMethod} is based.
*
* @return the {@link GemfirePersistentEntity} the method deals with.
* @return the {@link Method} reference on which this {@link QueryMethod} is based.
* @see java.lang.reflect.Method
*/
public GemfirePersistentEntity<?> getPersistentEntity() {
protected @NonNull Method getMethod() {
return this.method;
}
/**
* Returns the {@link GemfirePersistentEntity} handled by this {@link QueryMethod}.
*
* @return the {@link GemfirePersistentEntity} handled by this {@link QueryMethod}.
* @see org.springframework.data.gemfire.mapping.GemfirePersistentEntity
*/
public @NonNull GemfirePersistentEntity<?> getPersistentEntity() {
return this.entity;
}
@@ -118,15 +170,16 @@ public class GemfireQueryMethod extends QueryMethod {
}
/**
* Returns the annotated query for the query method if present.
* Returns the {@link Query} annotated OQL query value for this {@link QueryMethod} if present.
*
* @return the annotated query or {@literal null} in case it's empty or not present.
* @return the {@link Query} annotated OQL query value or {@literal null} in case it's {@literal null}, empty
* or not present.
* @see org.springframework.data.gemfire.repository.Query
* @see java.lang.reflect.Method#getAnnotation(Class)
*/
String getAnnotatedQuery() {
public @Nullable String getAnnotatedQuery() {
Query query = this.method.getAnnotation(Query.class);
Query query = getMethod().getAnnotation(Query.class);
String queryString = query != null ? (String) AnnotationUtils.getValue(query) : null;
@@ -142,7 +195,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasHint() {
return this.method.isAnnotationPresent(Hint.class);
return getMethod().isAnnotationPresent(Hint.class);
}
/**
@@ -154,7 +207,7 @@ public class GemfireQueryMethod extends QueryMethod {
*/
public String[] getHints() {
Hint hint = method.getAnnotation(Hint.class);
Hint hint = getMethod().getAnnotation(Hint.class);
return hint != null ? hint.value() : EMPTY_STRING_ARRAY;
}
@@ -168,7 +221,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasImport() {
return this.method.isAnnotationPresent(Import.class);
return getMethod().isAnnotationPresent(Import.class);
}
/**
@@ -180,7 +233,7 @@ public class GemfireQueryMethod extends QueryMethod {
*/
public String getImport() {
Import importStatement = method.getAnnotation(Import.class);
Import importStatement = getMethod().getAnnotation(Import.class);
return importStatement != null ? importStatement.value() : null;
}
@@ -194,7 +247,7 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasLimit() {
return this.method.isAnnotationPresent(Limit.class);
return getMethod().isAnnotationPresent(Limit.class);
}
/**
@@ -206,7 +259,7 @@ public class GemfireQueryMethod extends QueryMethod {
*/
public int getLimit() {
Limit limit = method.getAnnotation(Limit.class);
Limit limit = getMethod().getAnnotation(Limit.class);
return limit != null ? limit.value() : Integer.MAX_VALUE;
}
@@ -219,6 +272,6 @@ public class GemfireQueryMethod extends QueryMethod {
* @see java.lang.reflect.Method#isAnnotationPresent(Class)
*/
public boolean hasTrace() {
return this.method.isAnnotationPresent(Trace.class);
return getMethod().isAnnotationPresent(Trace.class);
}
}

View File

@@ -18,25 +18,28 @@ 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.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Base class for GemFire specific {@link RepositoryQuery} implementations.
* <p>
* Abstract base class for Apache Geode specific {@link RepositoryQuery} implementations.
*
* @author Oliver Gierke
* @author David Turanski
* @author John Blum
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
* @see org.springframework.data.repository.Repository
* @see org.springframework.data.repository.query.RepositoryQuery
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
*/
@SuppressWarnings("rawtypes")
public abstract class GemfireRepositoryQuery implements RepositoryQuery {
private final GemfireQueryMethod queryMethod;
private QueryPostProcessor<?, String> queryPostProcessor = ProvidedQueryPostProcessor.IDENTITY;
private QueryPostProcessor<Repository, String> queryPostProcessor = ProvidedQueryPostProcessor.IDENTITY;
/*
* (non-Javadoc)
/**
* Constructor used for testing purposes only!
*/
GemfireRepositoryQuery() {
@@ -45,11 +48,11 @@ public abstract class GemfireRepositoryQuery implements RepositoryQuery {
/**
* Constructs a new instance of {@link GemfireRepositoryQuery} initialized with
* the given {@link GemfireQueryMethod}.
* the given {@link GemfireQueryMethod} implementing the {@link Repository} {@link QueryMethod}.
*
* @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}.
* @param queryMethod {@link GemfireQueryMethod} capturing the metadata and implementation of the {@link Repository}
* {@link QueryMethod}; must not be {@literal null}.
* @throws IllegalArgumentException if {@link GemfireQueryMethod} is {@literal null}.
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
*/
public GemfireRepositoryQuery(GemfireQueryMethod queryMethod) {
@@ -59,23 +62,36 @@ public abstract class GemfireRepositoryQuery implements RepositoryQuery {
this.queryMethod = queryMethod;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
/**
* Returns a reference to the {@link Repository} {@link GemfireQueryMethod} modeling the Apache Geode OQL query.
*
* @return a reference to the {@link Repository} {@link GemfireQueryMethod} modeling the Apache Geode OQL query.
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
* @see #getQueryMethod()
*/
protected @NonNull GemfireQueryMethod getGemfireQueryMethod() {
return (GemfireQueryMethod) getQueryMethod();
}
/**
* Returns a reference to the {@link Repository} {@link QueryMethod} modeling the data store query.
*
* @return a reference to the {@link Repository} {@link QueryMethod} modeling the data store query.
* @see org.springframework.data.repository.query.QueryMethod
*/
@Override
public QueryMethod getQueryMethod() {
public @NonNull QueryMethod getQueryMethod() {
return this.queryMethod;
}
/**
* Returns a reference to the composed {@link QueryPostProcessor QueryPostProcessors}, which are applied
* to {@literal OQL queries} prior to execution.
* Returns a reference to the composed {@link QueryPostProcessor QueryPostProcessors} that are applied to
* {@literal OQL queries} prior to execution.
*
* @return a reference to the composed {@link QueryPostProcessor QueryPostProcessors}.
* @see org.springframework.data.gemfire.repository.query.QueryPostProcessor
*/
protected QueryPostProcessor<?, String> getQueryPostProcessor() {
protected @NonNull QueryPostProcessor<Repository, String> getQueryPostProcessor() {
return this.queryPostProcessor;
}
@@ -92,11 +108,12 @@ public abstract class GemfireRepositoryQuery implements RepositoryQuery {
* @return this {@link GemfireRepositoryQuery}.
* @see org.springframework.data.gemfire.repository.query.QueryPostProcessor#processBefore(QueryPostProcessor)
*/
public GemfireRepositoryQuery register(QueryPostProcessor<?, String> queryPostProcessor) {
public GemfireRepositoryQuery register(@Nullable QueryPostProcessor<Repository, String> queryPostProcessor) {
this.queryPostProcessor = this.queryPostProcessor.processBefore(queryPostProcessor);
return this;
}
@SuppressWarnings("rawtypes")
enum ProvidedQueryPostProcessor implements QueryPostProcessor<Repository, String> {
IDENTITY {

View File

@@ -22,58 +22,90 @@ 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.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
/**
* {@link GemfireRepositoryQuery} backed by a {@link PartTree}, deriving an OQL query
* from the backing query method's name/signature.
* from the backing {@link QueryMethod QueryMethod's} name/signature.
*
* @author Oliver Gierke
* @author John Blum
* @see org.springframework.data.gemfire.GemfireTemplate
* @see org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery
* @see org.springframework.data.repository.query.QueryMethod
* @see org.springframework.data.repository.query.RepositoryQuery
* @see org.springframework.data.repository.query.parser.Part
* @see org.springframework.data.repository.query.parser.PartTree
*/
public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery {
private final GemfireQueryMethod method;
private final GemfireTemplate template;
private final PartTree tree;
/**
* Creates a new {@link PartTreeGemfireRepositoryQuery} using the given {@link GemfireQueryMethod} and
* {@link GemfireTemplate}.
* Constructs a new instance of {@link PartTreeGemfireRepositoryQuery} initialized with
* the given {@link GemfireQueryMethod} and {@link GemfireTemplate}.
*
* @param method must not be {@literal null}.
* @param template must not be {@literal null}.
* @param queryMethod {@link GemfireQueryMethod} implementing the {@link RepositoryQuery};
* must not be {@literal null}.
* @param template {@link GemfireTemplate} used to execute {@literal QOL queries};
* must not be {@literal null}.
* @throws IllegalArgumentException if {@link GemfireQueryMethod} or {@link GemfireTemplate} are {@literal null}.
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
* @see org.springframework.data.gemfire.GemfireTemplate
*/
public PartTreeGemfireRepositoryQuery(GemfireQueryMethod method, GemfireTemplate template) {
public PartTreeGemfireRepositoryQuery(GemfireQueryMethod queryMethod, GemfireTemplate template) {
super(method);
super(queryMethod);
Class<?> domainClass = method.getEntityInformation().getJavaType();
Assert.notNull(template, "GemfireTemplate must not be null");
this.method = method;
this.template = template;
this.tree = new PartTree(method.getName(), domainClass);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
/**
* Returns a {@link PartTree} object consisting of the parts of the (OQL) query.
*
* @return a {@link PartTree} object consisting of the parts of the (OQL) query.
* @see org.springframework.data.repository.query.parser.PartTree
*/
protected @NonNull PartTree getPartTree() {
return this.tree;
}
/**
* 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 @NonNull GemfireTemplate getTemplate() {
return this.template;
}
/**
* @inheritDoc
*/
@Override
public Object execute(Object[] arguments) {
QueryString query = createQuery(this.method, this.tree, arguments);
GemfireQueryMethod queryMethod = getGemfireQueryMethod();
GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(query, this.method, this.template);
QueryString query = newQueryString(queryMethod, getPartTree(), arguments);
GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(query, queryMethod, getTemplate());
return repositoryQuery.execute(prepareStringParameters(arguments));
}
private QueryString createQuery(GemfireQueryMethod queryMethod, PartTree tree, Object[] arguments) {
private QueryString newQueryString(GemfireQueryMethod queryMethod, PartTree tree, Object[] arguments) {
ParametersParameterAccessor parameterAccessor =
new ParametersParameterAccessor(queryMethod.getParameters(), arguments);
@@ -86,17 +118,18 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery {
private GemfireRepositoryQuery newRepositoryQuery(QueryString query,
GemfireQueryMethod queryMethod, GemfireTemplate template) {
GemfireRepositoryQuery repositoryQuery =
StringBasedGemfireRepositoryQuery repositoryQuery =
new StringBasedGemfireRepositoryQuery(query.toString(), queryMethod, template);
repositoryQuery.register(getQueryPostProcessor());
repositoryQuery.asDerivedQuery();
return repositoryQuery;
}
private Object[] prepareStringParameters(Object[] parameters) {
Iterator<Part> partsIterator = this.tree.getParts().iterator();
Iterator<Part> partsIterator = getPartTree().getParts().iterator();
List<Object> stringParameters = new ArrayList<>(parameters.length);

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.repository.query;
import org.apache.geode.cache.Region;
@@ -21,6 +20,7 @@ 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;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -39,14 +39,16 @@ class QueryBuilder {
private final String query;
/* (non-Javadoc) */
static String asQuery(GemfirePersistentEntity<?> entity, PartTree tree) {
return String.format(SELECT_OQL_TEMPLATE, tree.isDistinct() ? OqlKeyword.DISTINCT : "",
entity.getRegionName(), DEFAULT_ALIAS).replaceAll("\\s{2,}", " ");
String distinctKeyword = tree.isDistinct() ? OqlKeyword.DISTINCT.toString() : "";
String regionName = entity.getRegionName();
String query = String.format(SELECT_OQL_TEMPLATE, distinctKeyword, regionName, DEFAULT_ALIAS)
.replaceAll("\\s{2,}", " "); // single space tokens
return query;
}
/* (non-Javadoc) */
static String validateQuery(String query) {
Assert.hasText(query, "Query is required");
return query;
@@ -85,7 +87,7 @@ class QueryBuilder {
* @see org.springframework.data.gemfire.repository.query.Predicate
* @see #withPredicate(String, Predicate)
*/
public QueryString create(Predicate predicate) {
public QueryString create(@Nullable Predicate predicate) {
return new QueryString(withPredicate(this.query, predicate));
}
@@ -98,16 +100,15 @@ class QueryBuilder {
* or just a {@link String} containing the query if the {@link Predicate} is {@literal null}.
* @see org.springframework.data.gemfire.repository.query.Predicate
*/
protected String withPredicate(String query, Predicate predicate) {
protected String withPredicate(String query, @Nullable Predicate predicate) {
return predicate != null
? String.format(WHERE_CLAUSE_TEMPLATE, query, predicate.toString(DEFAULT_ALIAS))
: query;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
/**
* @inheritDoc
*/
@Override
public String toString() {

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.repository.query;
import java.util.Properties;
@@ -31,24 +30,28 @@ import org.springframework.lang.Nullable;
* a given {@link QUERY query} and possibly return a new or modified version of the same {@link QUERY query}.
*
* {@link QueryPostProcessor QueryPostProcessors} are useful for handling and processing {@link QUERY queries}
* generated from {@link Repository} {@link QueryMethod query methods}, and give a developer an opportunity,
* derived from {@link Repository} {@link QueryMethod QueryMethods}, and give a developer the opportunity,
* via the callback, to further process the generated {@link QUERY query}.
*
* {@link QueryPostProcessor QueryPostProcessors} can be used on both {@literal generated} {@link QUERY queries}
* {@link QueryPostProcessor QueryPostProcessors} can be used on both {@literal derived} {@link QUERY queries}
* and {@literal manual} {@link QUERY queries}. {@literal Manual} {@link QUERY queries} are defined as
* {@link QUERY queries} specified using SDG's {@link Query @Query} annotation or by defining a {@literal named}
* {@link QUERY query} in a module-specific {@link Properties} files.
* {@link QUERY query} in a module-specific {@link Properties} files
* (e.g. {@literal META-INF/gemfire-named-queries.properties}).
*
* @author John Blum
* @param <T> {@link Class type} identifying the {@link Repository Repositories} to match on during registration.
* @param <QUERY> {@link Class type} of the query to process.
* @see java.lang.FunctionalInterface
* @see org.springframework.core.Ordered
* @see org.springframework.data.gemfire.repository.Query
* @see org.springframework.data.repository.Repository
* @see org.springframework.data.repository.core.NamedQueries
* @see org.springframework.data.repository.query.QueryMethod
* @since 2.1.0
*/
@FunctionalInterface
@SuppressWarnings("rawtypes")
public interface QueryPostProcessor<T extends Repository, QUERY> extends Ordered {
Object[] EMPTY_ARGUMENTS = new Object[0];
@@ -102,10 +105,11 @@ public interface QueryPostProcessor<T extends Repository, QUERY> extends Ordered
QUERY postProcess(@NonNull QueryMethod queryMethod, QUERY query, Object... arguments);
/**
* Builder method used to compose, or combine this {@link QueryPostProcessor QueryPostProcessors}
* Builder method used to compose this {@link QueryPostProcessor QueryPostProcessor}
* with the given {@link QueryPostProcessor}.
*
* This {@link QueryPostProcessor} will come before the given {@link QueryPostProcessor} in the processing chain.
* This {@link QueryPostProcessor} will process the query before the given {@link QueryPostProcessor}
* in the processing chain.
*
* @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}.
* @return a composed {@link QueryPostProcessor} consisting of this {@link QueryPostProcessor}
@@ -113,17 +117,17 @@ public interface QueryPostProcessor<T extends Repository, QUERY> extends Ordered
* if the given {@link QueryPostProcessor} is {@literal null}.
* @see #processAfter(QueryPostProcessor)
*/
@NonNull
default QueryPostProcessor<?, QUERY> processBefore(@Nullable QueryPostProcessor<?, QUERY> queryPostProcessor) {
default @NonNull QueryPostProcessor<T, QUERY> processBefore(@Nullable QueryPostProcessor<T, QUERY> queryPostProcessor) {
return queryPostProcessor == null ? this : (queryMethod, query, arguments) ->
queryPostProcessor.postProcess(queryMethod, this.postProcess(queryMethod, query, arguments), arguments);
}
/**
* Builder method used to compose, or combine this {@link QueryPostProcessor QueryPostProcessors}
* Builder method used to compose this {@link QueryPostProcessor QueryPostProcessors}
* with the given {@link QueryPostProcessor}.
*
* This {@link QueryPostProcessor} will come after the given {@link QueryPostProcessor} in the processing chain.
* This {@link QueryPostProcessor} will process the query after the given {@link QueryPostProcessor}
* in the processing chain.
*
* @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}.
* @return a composed {@link QueryPostProcessor} consisting of the given {@link QueryPostProcessor}
@@ -131,8 +135,7 @@ public interface QueryPostProcessor<T extends Repository, QUERY> extends Ordered
* if the given {@link QueryPostProcessor} is {@literal null}.
* @see #processBefore(QueryPostProcessor)
*/
@NonNull
default QueryPostProcessor<?, QUERY> processAfter(@Nullable QueryPostProcessor<?, QUERY> queryPostProcessor) {
default @NonNull QueryPostProcessor<T, QUERY> processAfter(@Nullable QueryPostProcessor<T, QUERY> queryPostProcessor) {
return queryPostProcessor == null ? this : (queryMethod, query, arguments) ->
this.postProcess(queryMethod, queryPostProcessor.postProcess(queryMethod, query, arguments), arguments);
}

View File

@@ -13,11 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
@@ -28,20 +25,26 @@ import org.apache.geode.cache.Region;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.repository.query.support.OqlKeyword;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.repository.Repository;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link QueryString} is a utility class used to construct GemFire OQL query statements.
* {@link QueryString} is a utility class used to construct Apache Geode OQL query statements.
*
* This is an internal class used by the SDG {@link Repository} infrastructure extension
*
* @author Oliver Gierke
* @author David Turanski
* @author John Blum
* @see java.util.regex.Matcher
* @see java.util.regex.Pattern
* @see org.apache.geode.cache.Region
* @see org.springframework.data.domain.Sort
* @see org.springframework.data.gemfire.repository.query.support.OqlKeyword
* @see org.apache.geode.cache.Region
*/
public class QueryString {
@@ -67,37 +70,39 @@ public class QueryString {
private static final String STAR_QUERY = "*";
/**
* Factory method used to construct an instance of {@link QueryString} initialized with the given {@link String query}.
* Factory method used to construct a new instance of {@link QueryString} initialized with
* the given {@link String OQL 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) {
public static QueryString of(@NonNull String query) {
return new QueryString(query);
}
/**
* 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.
* Factory method used to construct a new instance of {@link QueryString} initialized with
* the given {@link Class domain type} for which the {@link String OQL 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}.
* @param domainType {@link Class application domain model type} for which the {@link String OQL query}
* will be created.
* @return a new {@link QueryString} for the given {@link Class application domain model type}.
* @throws IllegalArgumentException if {@link Class application domain model type} is {@literal null}.
* @see #QueryString(Class)
*/
public static QueryString from(Class<?> domainType) {
public static QueryString from(@NonNull 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.
* Factory method used to construct a new instance of {@link QueryString} that creates an {@link String OQL query}
* counting objects of the specified {@link Class application domain model type}.
*
* @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}.
* @param domainType {@link Class application domain model type} for which the OQL query will be created.
* @return a new count {@link QueryString}.
* @throws IllegalArgumentException if {@link Class application domain model type} is {@literal null}.
* @see #QueryString(Class)
*/
public static QueryString count(Class<?> domainType) {
@@ -125,37 +130,37 @@ public class QueryString {
* 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.
* @throws IllegalArgumentException if {@link String query} is {@literal null} or {@literal empty}.
* @see #validateQuery(String)
* @see java.lang.String
*/
public QueryString(String query) {
public QueryString(@NonNull String query) {
this.query = validateQuery(query);
}
/**
* 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.
* Constructs a new instance of {@link QueryString} initialized with the given
* {@link Class application domain model type}, which is used to construct an OQL {@literal SELECT} query statement.
*
* @param domainType {@link Class application domain type} to query; must not be {@literal null}.
* @throws IllegalArgumentException if {@link Class domain type} is {@literal null}.
* @param domainType {@link Class application domain model type} to query; must not be {@literal null}.
* @throws IllegalArgumentException if the {@link Class application domain model type} is {@literal null}.
* @see #QueryString(Class, boolean)
*/
@SuppressWarnings("unused")
public QueryString(Class<?> domainType) {
public QueryString(@NonNull Class<?> domainType) {
this(domainType, false);
}
/**
* 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.
* Constructs a new instance of {@link QueryString} initialized with the given
* {@link Class application domain model type}, which is used to construct an OQL {@literal SELECT} query statement.
*
* {@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}.
* of the objects for the given {@link Class applicatlion domain model 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}.
* @param domainType {@link Class application domain model type} to query; must not be {@literal null}.
* @param asCountQuery boolean value to indicate if this is a select count query.
* @throws IllegalArgumentException if the {@link Class application domain model type} is {@literal null}.
* @see #asQuery(Class, boolean)
* @see #QueryString(String)
*/
@@ -201,7 +206,7 @@ public class QueryString {
*/
public QueryString bindIn(Collection<?> values) {
if (!nullSafeIsEmpty(values)) {
if (!CollectionUtils.nullSafeIsEmpty(values)) {
return QueryString.of(this.query.replaceFirst(IN_PATTERN, String.format("(%s)",
StringUtils.collectionToDelimitedString(values, ", ", "'", "'"))));
}
@@ -213,7 +218,7 @@ public class QueryString {
* Replaces the {@link Class domain classes} referenced inside the current {@link String query}
* with the given {@link Region}.
*
* @param domainClass {@link Class type} of the persistent entity to query; must not be {@literal null}.
* @param domainType {@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}.
@@ -221,7 +226,7 @@ public class QueryString {
* @see java.lang.Class
*/
@SuppressWarnings("unused")
public QueryString fromRegion(Class<?> domainClass, Region<?, ?> region) {
public QueryString fromRegion(Class<?> domainType, Region<?, ?> region) {
return QueryString.of(this.query.replaceAll(REGION_PATTERN, region.getFullPath()));
}
@@ -284,7 +289,7 @@ public class QueryString {
* @param hints array of {@link String Strings} containing query hints.
* @return a new {@link QueryString} if hints are not null or empty, or return this {@link QueryString}.
*/
public QueryString withHints(String... hints) {
public QueryString withHints(@NonNull String... hints) {
if (!ObjectUtils.isEmpty(hints)) {
@@ -307,9 +312,11 @@ public class QueryString {
* @param importExpression {@link String} containing the import clause.
* @return a new {@link QueryString} if an import was declared, or return this {@link QueryString}.
*/
public QueryString withImport(String importExpression) {
return StringUtils.hasText(importExpression) ?
QueryString.of(String.format(IMPORT_OQL_TEMPLATE, importExpression, this.query)) : this;
public QueryString withImport(@NonNull String importExpression) {
return StringUtils.hasText(importExpression)
? QueryString.of(String.format(IMPORT_OQL_TEMPLATE, importExpression, this.query))
: this;
}
/**
@@ -318,8 +325,11 @@ public class QueryString {
* @param limit {@link Integer} indicating the number of results to return from the query.
* @return a new {@link QueryString} if a limit was specified, or return this {@link QueryString}.
*/
public QueryString withLimit(Integer limit) {
return limit != null ? QueryString.of(String.format(LIMIT_OQL_TEMPLATE, this.query, limit)) : this;
public QueryString withLimit(@NonNull Integer limit) {
return limit != null
? QueryString.of(String.format(LIMIT_OQL_TEMPLATE, this.query, limit))
: this;
}
/**

View File

@@ -15,7 +15,6 @@
*/
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;
@@ -25,13 +24,14 @@ import org.apache.geode.cache.query.SelectResults;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.repository.Query;
import org.springframework.data.gemfire.util.CollectionUtils;
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.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link GemfireRepositoryQuery} using plain {@link String} based OQL queries.
@@ -40,18 +40,18 @@ import org.springframework.util.StringUtils;
* @author David Turanski
* @author John Blum
*/
@SuppressWarnings("unused")
public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
private static final String INVALID_QUERY = "Paging and modifying queries are not supported";
private static final String INVALID_QUERY = "Modifying queries are not supported";
private boolean userDefinedQuery = false;
private volatile boolean userDefinedQuery = false;
private final GemfireTemplate template;
private final QueryString query;
/*
* (non-Javadoc)
/**
* Constructor used for testing purposes only!
*/
StringBasedGemfireRepositoryQuery() {
@@ -66,33 +66,29 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
}
/**
* Creates a new {@link StringBasedGemfireRepositoryQuery} using the given {@link GemfireQueryMethod} and
* {@link GemfireTemplate}. The actual query {@link String} will be looked up from the query method.
* Constructs a new instance of {@link StringBasedGemfireRepositoryQuery} initialized with
* the given {@link String query}, {@link GemfireQueryMethod} and {@link GemfireTemplate}.
*
* @param queryMethod must not be {@literal null}.
* @param template must not be {@literal null}.
*/
public StringBasedGemfireRepositoryQuery(GemfireQueryMethod queryMethod, GemfireTemplate template) {
this(queryMethod.getAnnotatedQuery(), queryMethod, template);
}
/**
* Creates a new {@link StringBasedGemfireRepositoryQuery} using the given query {@link String},
* {@link GemfireQueryMethod} and {@link GemfireTemplate}.
*
* @param query will fall back to the query annotated to the given {@link GemfireQueryMethod} if {@literal null}.
* @param queryMethod must not be {@literal null}.
* @param template must not be {@literal null}.
* @param query {@link String} containing the {@literal OQL query} to execute;
* must not be {@literal null} or empty.
* @param queryMethod {@link GemfireQueryMethod} implementing the {@link RepositoryQuery};
* must not be {@literal null}.
* @param template {@link GemfireTemplate} used to execute {@literal QOL queries};
* must not be {@literal null}.
* @throws IllegalArgumentException if {@link GemfireQueryMethod} or {@link GemfireTemplate} are {@literal null}.
* @throws IllegalStateException if the {@link GemfireQueryMethod} represents a modifying query.
* @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod
* @see org.springframework.data.gemfire.GemfireTemplate
*/
public StringBasedGemfireRepositoryQuery(String query, GemfireQueryMethod queryMethod, GemfireTemplate template) {
super(queryMethod);
Assert.hasText(query, "Query must not be null or empty");
Assert.notNull(template, "GemfireTemplate must not be null");
Assert.state(!(queryMethod.isModifyingQuery() || queryMethod.isPageQuery()), INVALID_QUERY);
Assert.state(!queryMethod.isModifyingQuery(), INVALID_QUERY);
this.userDefinedQuery |= !StringUtils.hasText(query);
this.query = QueryString.of(StringUtils.hasText(query) ? query : queryMethod.getAnnotatedQuery());
this.query = QueryString.of(query);
this.template = template;
register(ProvidedQueryPostProcessors.LIMIT
@@ -102,21 +98,53 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
}
/**
* Sets this {@link RepositoryQuery} to be user-defined.
* Builder method used to set this {@link RepositoryQuery} as derived.
*
* @return a boolean value indicating whether the OQL query was derived from the {@link Repository} infrastructure
* {@link QueryMethod} name/signature.
* @see #asUserDefinedQuery()
*/
public @NonNull StringBasedGemfireRepositoryQuery asDerivedQuery() {
this.userDefinedQuery = false;
return this;
}
/**
* Builder method used to set this {@link RepositoryQuery} as user-defined.
*
* @return this {@link RepositoryQuery}.
* @see #isUserDefinedQuery()
*/
public StringBasedGemfireRepositoryQuery asUserDefinedQuery() {
public @NonNull StringBasedGemfireRepositoryQuery asUserDefinedQuery() {
this.userDefinedQuery = true;
return this;
}
/**
* Determines whether the query represented by this {@link RepositoryQuery} is user-defined or was generated by
* the Spring Data {@link Repository} infrastructure.
* Determines whether the OQL query represented by this {@link RepositoryQuery} is derived from
* the {@link Repository} infrastructure {@link QueryMethod} name/signature conventions.
*
* @return a boolean value indicating whether this {@link RepositoryQuery} is user-defined or was generated by
* the Spring Data {@link Repository} infrastructure.
* @return a boolean value indicating if the OQL query represented by this {@link RepositoryQuery} is derived.
* @see #asDerivedQuery()
* @see #isUserDefinedQuery()
*/
public boolean isDerivedQuery() {
return !isUserDefinedQuery();
}
/**
* Determines whether the OQL query represented by this {@link RepositoryQuery} is user-defined
* or was generated by the Spring Data {@link Repository} infrastructure.
*
* An {@literal OQL query} is user-defined if the query was specified using the {@link Query} annotation on
* the {@link Repository} {@link QueryMethod} or was specified in the {@literal <module>-named-queries.properties}
* file.
*
* Derived queries are not user-defined.
*
* @return a boolean value indicating whether the OQL query represented this {@link RepositoryQuery} is user-defined.
* @see #asUserDefinedQuery()
* @see #isDerivedQuery()
*/
public boolean isUserDefinedQuery() {
return this.userDefinedQuery;
@@ -128,7 +156,7 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
* @return a reference to the {@link QueryString managed query}.
* @see org.springframework.data.gemfire.repository.query.QueryString
*/
protected QueryString getQuery() {
protected @NonNull QueryString getQuery() {
return this.query;
}
@@ -138,13 +166,12 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
* @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() {
protected @NonNull GemfireTemplate getTemplate() {
return this.template;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
/**
* @inheritDoc
*/
@Override
public Object execute(Object[] arguments) {
@@ -209,7 +236,9 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery {
methodReturnType = methodReturnType != null ? methodReturnType : Void.class;
return nullSafeSize(result) == 1 && !Void.TYPE.equals(methodReturnType) && !method.isCollectionQuery();
return CollectionUtils.nullSafeSize(result) == 1
&& !Void.TYPE.equals(methodReturnType)
&& !method.isCollectionQuery();
}
/**

View File

@@ -101,10 +101,9 @@ public enum OqlKeyword {
* @see #name()
*/
public String getKeyword() {
return (StringUtils.hasText(this.keyword) ? this.keyword : name());
return StringUtils.hasText(this.keyword) ? this.keyword : name();
}
/* (non-Javadoc) */
@Override
public String toString() {
return getKeyword();

View File

@@ -179,7 +179,7 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
return (GemfirePersistentEntity<T>) getMappingContext().getPersistentEntity(domainType);
}
private Region<?, ?> resolveRegion(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity entity) {
private Region<?, ?> resolveRegion(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity<?> entity) {
String resolvedRegionName = getRepositoryRegionName(repositoryMetadata)
.orElseGet(() -> getEntityRegionName(repositoryMetadata, entity));
@@ -187,31 +187,10 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
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) {
@Nullable GemfirePersistentEntity<?> entity) {
Optional<GemfirePersistentEntity> optionalEntity = Optional.ofNullable(entity);
Optional<GemfirePersistentEntity<?>> optionalEntity = Optional.ofNullable(entity);
return optionalEntity
.map(GemfirePersistentEntity::getRegionName)
@@ -237,15 +216,30 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
return Optional.ofNullable(getRegions().getRegion(regionNamePath))
.orElseThrow(() -> newIllegalStateException(REGION_NOT_FOUND,
regionNamePath, repositoryMetadata.getDomainType().getName(),
repositoryMetadata.getRepositoryInterface().getName()));
repositoryMetadata.getRepositoryInterface().getName()));
}
private Region<?, ?> validate(RepositoryMetadata repositoryMetadata, GemfirePersistentEntity<?> entity,
Region<?, ?> region) {
Assert.notNull(region, "Region must not be null");
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;
}
/*
* (non-Javadoc)
*
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport
* #getQueryLookupStrategy(Key, EvaluationContextProvider)
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
@@ -258,18 +252,17 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
GemfireTemplate template = newTemplate(repositoryMetadata);
if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedGemfireRepositoryQuery(queryMethod, template).asUserDefinedQuery();
}
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
return new StringBasedGemfireRepositoryQuery(namedQueries.getQuery(namedQueryName),
queryMethod, template).asUserDefinedQuery();
}
// NOTE: Named OQL queries from gemfire-named-queries.properties take precedence over
// OQL queries declared in @Query annotated on Repository query methods.
String query = namedQueries.hasQuery(namedQueryName) ? namedQueries.getQuery(namedQueryName)
: queryMethod.hasAnnotatedQuery() ? queryMethod.getAnnotatedQuery()
: null;
return new PartTreeGemfireRepositoryQuery(queryMethod, template);
return StringUtils.hasText(query)
? new StringBasedGemfireRepositoryQuery(query, queryMethod, template).asUserDefinedQuery()
: new PartTreeGemfireRepositoryQuery(queryMethod, template); // derived query
});
}
@@ -277,6 +270,7 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport {
protected <T extends QueryMethod> T newQueryMethod(Method method, RepositoryMetadata repositoryMetadata,
ProjectionFactory projectionFactory, QueryMethodEvaluationContextProvider evaluationContextProvider) {
return (T) new GemfireQueryMethod(method, repositoryMetadata, projectionFactory, getMappingContext());
return (T) new GemfireQueryMethod(method, repositoryMetadata, projectionFactory, getMappingContext(),
evaluationContextProvider);
}
}

View File

@@ -331,9 +331,9 @@ public class GemfireRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
return this.declaredRepositoryType;
}
@SuppressWarnings("unchecked")
@NonNull QueryPostProcessor<?, String> getQueryPostProcessor() {
return (QueryPostProcessor<?, String>) this.queryPostProcessor;
@SuppressWarnings({ "rawtypes", "unchecked" })
@NonNull QueryPostProcessor<Repository, String> getQueryPostProcessor() {
return (QueryPostProcessor<Repository, String>) this.queryPostProcessor;
}
boolean isMatch(Class<?> repositoryInterface) {

View File

@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheTransactionManager;
@@ -30,6 +31,9 @@ import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.SelectResults;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.GemfireCallback;
import org.springframework.data.gemfire.GemfireTemplate;
@@ -39,6 +43,7 @@ import org.springframework.data.gemfire.repository.query.QueryString;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.gemfire.util.SpringUtils;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.util.StreamUtils;
import org.springframework.data.util.Streamable;
@@ -50,7 +55,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple, basic {@link CrudRepository} implementation for Apache Geode.
* Simple, basic {@link PagingAndSortingRepository} / {@link CrudRepository} implementation for Apache Geode.
*
* @author Oliver Gierke
* @author David Turanski
@@ -61,6 +66,7 @@ import org.slf4j.LoggerFactory;
* @see org.springframework.data.gemfire.GemfireTemplate
* @see org.springframework.data.gemfire.repository.GemfireRepository
* @see org.springframework.data.repository.CrudRepository
* @see org.springframework.data.repository.PagingAndSortingRepository
* @see org.springframework.data.repository.core.EntityInformation
*/
public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID> {
@@ -228,6 +234,16 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
return toList(selectResults);
}
@Override
public Page<T> findAll(@NonNull Pageable pageable) {
Assert.notNull(pageable, "Pageable must not be null");
Iterable<T> results = findAll(pageable.getSort());
return toPage(results, pageable);
}
@Override
public @NonNull Iterable<T> findAll(@NonNull Sort sort) {
@@ -324,10 +340,41 @@ public class SimpleGemfireRepository<T, ID> implements GemfireRepository<T, ID>
region.removeAll(region.keySet());
}
@NonNull List<T> toList(@Nullable Iterable<T> iterable) {
return iterable instanceof List ? (List<T>) iterable
: StreamSupport.stream(CollectionUtils.nullSafeIterable(iterable).spliterator(), false)
.collect(Collectors.toList());
}
@NonNull List<T> toList(@Nullable SelectResults<T> selectResults) {
return selectResults != null
? CollectionUtils.nullSafeList(selectResults.asList())
: Collections.emptyList();
}
@NonNull Page<T> toPage(@Nullable Iterable<T> iterable, @NonNull Pageable pageable) {
Assert.notNull(pageable, "Pageable must not be null");
int pageNumber = pageable.getPageNumber();
int pageSize = pageable.getPageSize();
Assert.isTrue(pageNumber >= 0, () -> String.format("Page Number [%d] must be greater than equal to 0",
pageNumber));
Assert.isTrue(pageSize > 0, () -> String.format("Page Size [%d] must be greater than equal to 1",
pageSize));
int startIndex = pageNumber * pageSize;
int endIndex = startIndex + pageSize;
List<T> results = toList(iterable);
int total = results.size();
return results.isEmpty() || total <= startIndex ? Page.empty()
: new PageImpl<>(results.subList(startIndex, Math.min(total, endIndex)), pageable, total);
}
}

View File

@@ -51,12 +51,12 @@ import org.springframework.util.ObjectUtils;
@RunWith(MockitoJUnitRunner.class)
public class GemfireQueryMethodUnitTests {
private GemfireMappingContext context = new GemfireMappingContext();
private GemfireMappingContext mappingContext = new GemfireMappingContext();
private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
private ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
@Mock
private RepositoryMetadata metadata;
private RepositoryMetadata repositoryMetadata;
protected void assertQueryHints(GemfireQueryMethod queryMethod, String... expectedHints) {
@@ -114,149 +114,138 @@ public class GemfireQueryMethodUnitTests {
@Before
public void setup() {
doReturn(Person.class).when(this.metadata).getDomainType();
doReturn(Person.class).when(this.metadata).getReturnedDomainClass(any(Method.class));
doReturn(ClassTypeInformation.from(Object.class)).when(this.metadata).getReturnType(any(Method.class));
doReturn(Person.class).when(this.repositoryMetadata).getDomainType();
doReturn(Person.class).when(this.repositoryMetadata).getReturnedDomainClass(any(Method.class));
doReturn(ClassTypeInformation.from(Object.class)).when(this.repositoryMetadata).getReturnType(any(Method.class));
}
@Test
public void detectsAnnotatedQueryCorrectly() throws Exception {
GemfireQueryMethod method =
new GemfireQueryMethod(Sample.class.getMethod("annotated"), this.metadata, this.factory, this.context);
new GemfireQueryMethod(Sample.class.getMethod("annotated"),
this.repositoryMetadata, this.projectionFactory, this.mappingContext);
assertThat(method.hasAnnotatedQuery()).isTrue();
assertThat(method.getAnnotatedQuery()).isEqualTo("foo");
method = new GemfireQueryMethod(Sample.class.getMethod("annotatedButEmpty"), this.metadata, this.factory, this.context);
method = new GemfireQueryMethod(Sample.class.getMethod("annotatedButEmpty"),
this.repositoryMetadata, this.projectionFactory, this.mappingContext);
assertThat(method.hasAnnotatedQuery()).isFalse();
assertThat(method.getAnnotatedQuery()).isNull();
method = new GemfireQueryMethod(Sample.class.getMethod("notAnnotated"), this.metadata, this.factory, this.context);
method = new GemfireQueryMethod(Sample.class.getMethod("notAnnotated"),
this.repositoryMetadata, this.projectionFactory, this.mappingContext);
assertThat(method.hasAnnotatedQuery()).isFalse();
assertThat(method.getAnnotatedQuery()).isNull();
}
/**
* @link https://jira.spring.io/browse/SGF-112
*/
@Test(expected = IllegalStateException.class)
public void rejectsQueryMethodWithPageableParameter() throws Exception {
try {
new GemfireQueryMethod(Invalid.class.getMethod("someMethod", Pageable.class), this.metadata, this.factory, this.context);
}
catch (IllegalStateException expected) {
assertThat(expected)
.hasMessageStartingWith("Pagination is not supported by GemFire Repositories; Offending method: someMethod");
assertThat(expected).hasNoCause();
throw expected;
}
@Test
public void acceptsQueryMethodWithPageableParameter() throws Exception {
new GemfireQueryMethod(PageablePojo.class.getMethod("pageableMethod", Pageable.class),
this.repositoryMetadata, this.projectionFactory, this.mappingContext);
}
@Test
public void detectsQueryHintsCorrectly() throws Exception {
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithHint"),
this.metadata, this.factory, this.context).hasHint()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasHint()).isTrue();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithImport"),
this.metadata, this.factory, this.context).hasHint()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasHint()).isFalse();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context).hasHint()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasHint()).isTrue();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("unlimitedQuery"),
this.metadata, this.factory, this.context).hasHint()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasHint()).isFalse();
}
@Test
public void detectsQueryImportsCorrectly() throws Exception {
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithHint"),
this.metadata, this.factory, this.context).hasImport()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasImport()).isFalse();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithImport"),
this.metadata, this.factory, this.context).hasImport()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasImport()).isTrue();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context).hasImport()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasImport()).isTrue();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("unlimitedQuery"),
this.metadata, this.factory, this.context).hasImport()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasImport()).isFalse();
}
@Test
public void detectsQueryLimitsCorrectly() throws Exception {
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithHint"),
this.metadata, this.factory, this.context).hasLimit()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasLimit()).isFalse();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithImport"),
this.metadata, this.factory, this.context).hasLimit()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasLimit()).isFalse();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context).hasLimit()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasLimit()).isTrue();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("unlimitedQuery"),
this.metadata, this.factory, this.context).hasLimit()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasLimit()).isFalse();
}
@Test
public void detectsQueryTracingCorrectly() throws Exception {
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithHint"),
this.metadata, this.factory, this.context).hasTrace()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasTrace()).isTrue();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithImport"),
this.metadata, this.factory, this.context).hasTrace()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasTrace()).isFalse();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context).hasTrace()).isFalse();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasTrace()).isFalse();
assertThat(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("unlimitedQuery"),
this.metadata, this.factory, this.context).hasTrace()).isTrue();
this.repositoryMetadata, this.projectionFactory, this.mappingContext).hasTrace()).isTrue();
}
@Test
public void hintOnQueryWithHint() throws Exception {
assertQueryHints(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithHint"),
this.metadata, this.factory, this.context), "IdIdx", "LastNameIdx");
this.repositoryMetadata, this.projectionFactory, this.mappingContext), "IdIdx", "LastNameIdx");
assertQueryHints(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context), "BirthDateIdx");
this.repositoryMetadata, this.projectionFactory, this.mappingContext), "BirthDateIdx");
}
@Test
public void hintOnQueryWithNoHints() throws Exception {
assertNoQueryHints(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithImport"),
this.metadata, this.factory, this.context));
this.repositoryMetadata, this.projectionFactory, this.mappingContext));
}
@Test
public void importOnQueryWithImport() throws Exception {
assertImportStatement(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithImport"),
this.metadata, this.factory, this.context), "org.example.app.domain.ExampleType");
this.repositoryMetadata, this.projectionFactory, this.mappingContext), "org.example.app.domain.ExampleType");
assertImportStatement(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context), "org.example.app.domain.Person");
this.repositoryMetadata, this.projectionFactory, this.mappingContext), "org.example.app.domain.Person");
}
@Test
public void importOnQueryWithNoImports() throws Exception {
assertNoImportStatement(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("queryWithHint"),
this.metadata, this.factory, this.context));
this.repositoryMetadata, this.projectionFactory, this.mappingContext));
}
@Test
public void limitOnQueryWithLimit() throws Exception {
assertLimitedQuery(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("limitedQuery"),
this.metadata, this.factory, this.context), 1024);
this.repositoryMetadata, this.projectionFactory, this.mappingContext), 1024);
}
@Test
public void limitOnQueryWithNoLimits() throws Exception {
assertUnlimitedQuery(new GemfireQueryMethod(AnnotatedQueryMethods.class.getMethod("unlimitedQuery"),
this.metadata, this.factory, this.context));
this.repositoryMetadata, this.projectionFactory, this.mappingContext));
}
@SuppressWarnings("unused")
@@ -273,9 +262,9 @@ public class GemfireQueryMethodUnitTests {
}
@SuppressWarnings("unused")
interface Invalid {
interface PageablePojo {
Page<?> someMethod(Pageable pageable);
Page<?> pageableMethod(Pageable pageable);
}

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.repository.query;
import static org.assertj.core.api.Assertions.assertThat;
@@ -28,10 +27,11 @@ import static org.mockito.Mockito.when;
import org.junit.Test;
import org.mockito.InOrder;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryMethod;
/**
* Unit tests for {@link QueryPostProcessor}.
* Unit Tests for {@link QueryPostProcessor}.
*
* @author John Blum
* @see org.junit.Test
@@ -42,15 +42,15 @@ import org.springframework.data.repository.query.QueryMethod;
public class QueryPostProcessorUnitTests {
@Test
@SuppressWarnings("unchecked")
@SuppressWarnings({ "rawtypes", "unchecked" })
public void processAfterReturnsCompositeQueryPostProcessorAndPostProcessesInOrder() {
QueryMethod mockQueryMethod = mock(QueryMethod.class);
String query = "SELECT * FROM /Test";
QueryPostProcessor<?, String> mockQueryPostProcessorOne = mock(QueryPostProcessor.class);
QueryPostProcessor<?, String> mockQueryPostProcessorTwo = mock(QueryPostProcessor.class);
QueryPostProcessor<Repository, String> mockQueryPostProcessorOne = mock(QueryPostProcessor.class);
QueryPostProcessor<Repository, String> mockQueryPostProcessorTwo = mock(QueryPostProcessor.class);
when(mockQueryPostProcessorOne.processAfter(any())).thenCallRealMethod();
when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query);
@@ -82,15 +82,15 @@ public class QueryPostProcessorUnitTests {
assertThat(mockQueryPostProcessor.processAfter(null)).isSameAs(mockQueryPostProcessor);
}
@Test
@SuppressWarnings("unchecked")
@SuppressWarnings({ "rawtypes", "unchecked" })
public void processBeforeReturnsCompositeQueryPostProcessorAndPostProcessesInOrder() {
QueryMethod mockQueryMethod = mock(QueryMethod.class);
String query = "SELECT * FROM /Test";
QueryPostProcessor<?, String> mockQueryPostProcessorOne = mock(QueryPostProcessor.class);
QueryPostProcessor<?, String> mockQueryPostProcessorTwo = mock(QueryPostProcessor.class);
QueryPostProcessor<Repository, String> mockQueryPostProcessorOne = mock(QueryPostProcessor.class);
QueryPostProcessor<Repository, String> mockQueryPostProcessorTwo = mock(QueryPostProcessor.class);
when(mockQueryPostProcessorOne.processBefore(any())).thenCallRealMethod();
when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query);

View File

@@ -26,11 +26,13 @@ import org.springframework.data.gemfire.mapping.annotation.Region;
import org.springframework.util.ObjectUtils;
/**
* The Person class models a person.
* Abstract Data Type (ADT) modeling a Person.
*
* @author Oliver Gierke
* @author John Blum
* @see java.io.Serializable
* @see org.springframework.data.annotation.Id
* @see org.springframework.data.gemfire.mapping.annotation.Region
*/
@Region("simple")
@JsonIgnoreProperties("name")
@@ -47,8 +49,7 @@ public class Person implements Serializable {
public String lastname;
@PersistenceConstructor
public Person() {
}
public Person() { }
public Person(Long id) {
this.id = id;
@@ -113,10 +114,6 @@ public class Person implements Serializable {
return String.format("%1$s %2$s", getFirstname(), getLastname());
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
@@ -133,10 +130,6 @@ public class Person implements Serializable {
return (this.id != null && this.id.equals(that.id));
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.id);
@@ -146,5 +139,4 @@ public class Person implements Serializable {
public String toString() {
return String.format("{ @type = %1$s, id = %2$d, name = %3$s }", getClass().getName(), id, getName());
}
}

View File

@@ -469,26 +469,14 @@ public class GemfireRepositoryFactoryUnitTests {
}
}
/**
* @link <a href="https://jira.spring.io/browse/SGF-112">Repositories should reject PagingAndSortingRepository and Pageable parameters</a>
*/
@Test(expected = IllegalStateException.class)
@Test
@SuppressWarnings("unchecked")
public void rejectsInterfacesExtendingPagingAndSortingRepository() {
public void acceptsInterfacesExtendingPagingAndSortingRepository() {
GemfireRepositoryFactory repositoryFactory =
new GemfireRepositoryFactory(Collections.singletonList(this.mockRegion), new GemfireMappingContext());
try {
repositoryFactory.getRepository(SamplePagingAndSortingRepository.class);
}
catch (IllegalStateException expected) {
assertThat(expected).hasMessageStartingWith("Pagination is not supported by GemFire Repositories");
assertThat(expected).hasNoCause();
throw expected;
}
repositoryFactory.getRepository(SamplePagingAndSortingRepository.class);
}
@Test

View File

@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Resource;
@@ -34,6 +35,9 @@ import org.apache.geode.cache.util.CacheListenerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.LocalRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.PeerCacheApplication;
@@ -105,6 +109,52 @@ public class SimpleGemfireRepositoryIntegrationTests {
assertThat(this.regionClearListener.eventFired).isTrue();
}
@Test
public void findAllPaged() {
assertThat(this.repository.count()).isEqualTo(0);
List<Person> people = Arrays.asList(
new Person(1L, "Jon", "Doe"),
new Person(2L, "Jane", "Doe"),
new Person(3L, "Cookie", "Doe"),
new Person(4L, "Pie", "Doe"),
new Person(5L, "Sour", "Doe")
);
people.forEach(person -> this.template.put(person.getId(), person));
assertThat(this.repository.count()).isEqualTo(5);
Sort orderByFirstNameAscending = Sort.by("firstname").ascending();
Page<Person> pageOne =
this.repository.findAll(PageRequest.of(0, 3, orderByFirstNameAscending));
assertThat(pageOne).isNotNull();
assertThat(pageOne).isNotEmpty();
assertThat(pageOne.getNumber()).isEqualTo(0);
assertThat(pageOne.getNumberOfElements()).isEqualTo(3);
assertThat(pageOne.getSize()).isEqualTo(3);
assertThat(pageOne.getSort()).isEqualTo(orderByFirstNameAscending);
assertThat(pageOne.getTotalElements()).isEqualTo(people.size());
assertThat(pageOne.getTotalPages()).isEqualTo(2);
assertThat(pageOne.getContent()).containsExactly(people.get(2), people.get(1), people.get(0));
Page<Person> pageTwo =
this.repository.findAll(PageRequest.of(1, 3, Sort.by("firstname").ascending()));
assertThat(pageTwo).isNotNull();
assertThat(pageTwo).isNotEmpty();
assertThat(pageTwo.getNumber()).isEqualTo(1);
assertThat(pageTwo.getNumberOfElements()).isEqualTo(2);
assertThat(pageTwo.getSize()).isEqualTo(3);
assertThat(pageTwo.getSort()).isEqualTo(orderByFirstNameAscending);
assertThat(pageTwo.getTotalElements()).isEqualTo(people.size());
assertThat(pageTwo.getTotalPages()).isEqualTo(2);
assertThat(pageTwo.getContent()).containsExactly(people.get(3), people.get(4));
}
@Test
public void findAllWithIds() {

View File

@@ -57,15 +57,27 @@ import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.query.SelectResults;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.repository.Wrapper;
import org.springframework.data.gemfire.repository.sample.Animal;
import org.springframework.data.gemfire.repository.sample.Identifiable;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.gemfire.util.RegionUtils;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.slf4j.Logger;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* Unit Tests for {@link SimpleGemfireRepository}.
*
@@ -76,6 +88,9 @@ import org.slf4j.Logger;
* @see org.mockito.Mockito
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.Region
* @see org.springframework.data.domain.Page
* @see org.springframework.data.domain.Pageable
* @see org.springframework.data.domain.Sort
* @see org.springframework.data.gemfire.GemfireTemplate
* @see org.springframework.data.gemfire.repository.Wrapper
* @see org.springframework.data.gemfire.repository.support.SimpleGemfireRepository
@@ -87,11 +102,11 @@ public class SimpleGemfireRepositoryUnitTests {
private final AtomicLong idSequence = new AtomicLong(0L);
private Map<Long, Animal> asMap(Iterable<Animal> animals) {
private @NonNull Map<Long, Animal> asMap(@Nullable Iterable<Animal> animals) {
Map<Long, Animal> animalMap = new HashMap<>();
for (Animal animal : animals) {
for (Animal animal : CollectionUtils.nullSafeIterable(animals)) {
animalMap.put(animal.getId(), animal);
}
@@ -132,8 +147,8 @@ public class SimpleGemfireRepositoryUnitTests {
CacheTransactionManager mockCacheTransactionManager = mock(CacheTransactionManager.class,
String.format("%s.MockCacheTransactionManager", name));
when(mockCache.getCacheTransactionManager()).thenReturn(mockCacheTransactionManager);
when(mockCacheTransactionManager.exists()).thenReturn(transactionExists);
doReturn(mockCacheTransactionManager).when(mockCache).getCacheTransactionManager();
doReturn(transactionExists).when(mockCacheTransactionManager).exists();
return mockCache;
}
@@ -155,7 +170,7 @@ public class SimpleGemfireRepositoryUnitTests {
}
private Long resolveId(Long id) {
return (id != null ? id : idSequence.incrementAndGet());
return id != null ? id : idSequence.incrementAndGet();
}
}).when(mockEntityInformation).getRequiredId(any(Animal.class));
@@ -171,8 +186,8 @@ public class SimpleGemfireRepositoryUnitTests {
Region mockRegion = mock(Region.class, String.format("%s.MockRegion", name));
when(mockRegion.getName()).thenReturn(name);
when(mockRegion.getFullPath()).thenReturn(String.format("%1$s%2$s", Region.SEPARATOR, name));
doReturn(name).when(mockRegion).getName();
doReturn(RegionUtils.toRegionPath(name)).when(mockRegion).getFullPath();
return mockRegion;
}
@@ -181,13 +196,13 @@ public class SimpleGemfireRepositoryUnitTests {
Region mockRegion = mockRegion(name);
when(mockRegion.getRegionService()).thenReturn(mockCache);
doReturn(mockCache).when(mockRegion).getRegionService();
RegionAttributes mockRegionAttributes = mock(RegionAttributes.class,
String.format("%s.MockRegionAttributes", name));
when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes);
when(mockRegionAttributes.getDataPolicy()).thenReturn(dataPolicy);
doReturn(mockRegionAttributes).when(mockRegion).getAttributes();
doReturn(dataPolicy).when(mockRegionAttributes).getDataPolicy();
return mockRegion;
}
@@ -598,6 +613,96 @@ public class SimpleGemfireRepositoryUnitTests {
verifyNoMoreInteractions(mockRegion, mockSelectResults, mockTemplate);
}
// Page Numbers 0 based indexed
private void assertPage(Page page, int pageNumber, int pageSize, int total, Sort orderBy, User... content) {
int totalPages = (total / pageSize) + (total % pageSize > 0 ? 1 : 0);
assertThat(page).isNotNull();
assertThat(page).isNotEmpty();
assertThat(page.getNumber()).isEqualTo(pageNumber);
assertThat(page.getNumberOfElements()).isEqualTo(content.length);
assertThat(page.getSize()).isEqualTo(pageSize);
assertThat(page.getSort()).isEqualTo(orderBy);
assertThat(page.getTotalElements()).isEqualTo(total);
assertThat(page.getTotalPages()).isEqualTo(totalPages);
assertThat(page.getContent()).containsExactly(content);
}
@Test
public void findAllPagedSuccessfully() {
Sort orderBy = Sort.by("name");
Region mockRegion = mockRegion();
Pageable mockPageable = mock(Pageable.class);
doReturn(true).when(mockPageable).isPaged();
doReturn(0).when(mockPageable).getPageNumber(); // page 1
doReturn(5).when(mockPageable).getPageSize();
doReturn(orderBy).when(mockPageable).getSort();
EntityInformation mockEntityInformation = mockEntityInformation();
SimpleGemfireRepository repository =
spy(new SimpleGemfireRepository(newGemfireTemplate(mockRegion), mockEntityInformation));
List<User> users = Arrays.asList(
User.newUser("Jon Doe"),
User.newUser("Jane Doe"),
User.newUser("Cookie Doe"),
User.newUser("Fro Doe"),
User.newUser("Lan Doe"),
User.newUser("Pie Doe"),
User.newUser("Sour Doe")
);
doReturn(users).when(repository).findAll(eq(orderBy));
Page pageOne = repository.findAll(mockPageable);
assertPage(pageOne, 0, 5, users.size(), orderBy,
User.newUser("Jon Doe"),
User.newUser("Jane Doe"),
User.newUser("Cookie Doe"),
User.newUser("Fro Doe"),
User.newUser("Lan Doe")
);
doReturn(1).when(mockPageable).getPageNumber(); // page 2
Page pageTwo = repository.findAll(mockPageable);
assertPage(pageTwo, 1, 5, users.size(), orderBy,
User.newUser("Pie Doe"),
User.newUser("Sour Doe")
);
doReturn(2).when(mockPageable).getPageNumber(); // page 2
Page pageThree = repository.findAll(mockPageable);
assertThat(pageThree).isNotNull();
assertThat(pageThree).isEmpty();
}
@Test(expected = IllegalArgumentException.class)
public void findAllPagedWithNullPageableThrowsIllegalArgumentException() {
try {
new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.findAll((Pageable) null);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Pageable must not be null");
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test
public void findAllByIdSuccessfully() {
@@ -873,4 +978,238 @@ public class SimpleGemfireRepositoryUnitTests {
verify(mockRegion, times(0)).clear();
verify(mockRegion, times(1)).removeAll(eq(keys));
}
@Test
public void toListFromIterable() {
Set<User> userSet = CollectionUtils.asSet(User.newUser("Jon Doe"), User.newUser("Jane Doe"));
List<User> userList = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.toList((Iterable) userSet);
assertThat(userList).isNotNull();
assertThat(userList).isNotSameAs(userSet);
assertThat(userList).hasSize(userSet.size());
assertThat(userList).containsExactly(userSet.toArray(new User[0]));
}
@Test
public void toListFromEmptyIterable() {
List<User> users = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.toList((Iterable) Collections::emptyIterator);
assertThat(users).isNotNull();
assertThat(users).isEmpty();
}
@Test
public void toListFromList() {
List<User> users = Collections.singletonList(User.newUser("Jon Doe"));
List<User> userList = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.toList((Iterable) users);
assertThat(userList).isSameAs(users);
}
@Test
public void toListFromNullIterableIsNullSafe() {
List<User> users = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.toList((Iterable) null);
assertThat(users).isNotNull();
assertThat(users).isEmpty();
}
@Test
public void toListFromSelectResults() {
List<User> users = Arrays.asList(User.newUser("Jon Doe"), User.newUser("Jane Doe"));
SelectResults<User> mockSelectResults = mock(SelectResults.class);
doReturn(users).when(mockSelectResults).asList();
List<User> userList = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.toList((SelectResults) mockSelectResults);
assertThat(userList).isSameAs(users);
verify(mockSelectResults, times(1)).asList();
verifyNoMoreInteractions(mockSelectResults);
}
@Test
public void toListFromNullSelectResultsIsNullSafe() {
List<User> users = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation())
.toList((SelectResults) null);
assertThat(users).isNotNull();
assertThat(users).isEmpty();
}
@Test
public void toPageFromIterable() {
List<User> users = Arrays.asList(User.newUser("Jon Doe"), User.newUser("Jane Doe"));
Pageable mockPageable = mock(Pageable.class);
doReturn(true).when(mockPageable).isPaged();
doReturn(0).when(mockPageable).getPageNumber(); // page 1
doReturn(1).when(mockPageable).getPageSize();
SimpleGemfireRepository repository =
new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class));
Page<User> pageOne = repository.toPage(users, mockPageable);
assertPage(pageOne, 0, 1, 2, null, users.get(0));
doReturn(1).when(mockPageable).getPageNumber(); // page 2
Page<User> pageTwo = repository.toPage(users, mockPageable);
assertPage(pageTwo, 1, 1, 2, null, users.get(1));
doReturn(2).when(mockPageable).getPageNumber(); // page 3
Page<User> pageThree = repository.toPage(users, mockPageable);
assertThat(pageThree).isNotNull();
assertThat(pageThree).isEmpty();
}
@Test
public void toPageFromEmptyIterable() {
Pageable mockPageable = mock(Pageable.class);
doReturn(0).when(mockPageable).getPageNumber();
doReturn(5).when(mockPageable).getPageSize();
Page<User> page = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class))
.toPage(Collections.emptyList(), mockPageable);
assertThat(page).isNotNull();
assertThat(page).isEmpty();
}
@Test
public void toPageFromNullIterableIsNullSafe() {
Pageable mockPageable = mock(Pageable.class);
doReturn(0).when(mockPageable).getPageNumber();
doReturn(5).when(mockPageable).getPageSize();
Page<User> page = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class))
.toPage(null, mockPageable);
assertThat(page).isNotNull();
assertThat(page).isEmpty();
}
@Test
public void toPageWithOverflowFromIterable() {
List<User> users = Arrays.asList(User.newUser("Jon Doe"), User.newUser("Jane Doe"));
Pageable mockPageable = mock(Pageable.class);
doReturn(true).when(mockPageable).isPaged();
doReturn(0).when(mockPageable).getPageNumber(); // page 1
doReturn(5).when(mockPageable).getPageSize();
SimpleGemfireRepository repository =
new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class));
Page<User> pageOne = repository.toPage(users, mockPageable);
assertPage(pageOne, 0, 5, 2, null, users.toArray(new User[0]));
doReturn(1).when(mockPageable).getPageNumber(); // page 2
Page<User> pageTwo = repository.toPage(users, mockPageable);
assertThat(pageTwo).isNotNull();
assertThat(pageTwo).isEmpty();
}
@Test(expected = IllegalArgumentException.class)
public void toPageWithNullPageableThrowsIllegalArgumentException() {
try {
new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class))
.toPage(Collections.emptyList(), null);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Pageable must not be null");
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test(expected = IllegalArgumentException.class)
public void toPageWithInvalidPageNumberThrowsIllegalArgumentException() {
Pageable mockPageable = mock(Pageable.class);
doReturn(-1).when(mockPageable).getPageNumber();
doReturn(5).when(mockPageable).getPageSize();
try {
new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class))
.toPage(Collections.singletonList(User.newUser("Jon Doe")), mockPageable);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Page Number [-1] must be greater than equal to 0");
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test(expected = IllegalArgumentException.class)
public void toPageWithInvalidPageSizeThrowsIllegalArgumentException() {
Pageable mockPageable = mock(Pageable.class);
doReturn(1).when(mockPageable).getPageNumber();
doReturn(-5).when(mockPageable).getPageSize();
try {
new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class))
.toPage(Collections.singletonList(User.newUser("Jon Doe")), mockPageable);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Page Size [-5] must be greater than equal to 1");
assertThat(expected).hasNoCause();
throw expected;
}
}
@Getter
@ToString(of = "name")
@EqualsAndHashCode(of = "name")
@RequiredArgsConstructor(staticName = "newUser")
@org.springframework.data.gemfire.mapping.annotation.Region("Users")
static class User {
@Id
Long id;
@NonNull
final String name;
}
}