diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/GemfireTemplate.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/GemfireTemplate.java index 67d46759..fd9329fe 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/GemfireTemplate.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/GemfireTemplate.java @@ -291,13 +291,13 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation @Override @SuppressWarnings("unchecked") - public SelectResults find(String queryString, Object... params) throws InvalidDataAccessApiUsageException { + public SelectResults 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) 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) { diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java index 855447e3..905698a5 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/domain/ListablePage.java @@ -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 extends AbstractPageSupport { * @see #ListablePage(List) */ @SafeVarargs - public static ListablePage newListablePage(T... content) { + public static ListablePage newListablePage(@NonNull T... content) { return new ListablePage<>(Arrays.asList(content)); } @@ -67,7 +67,7 @@ public class ListablePage extends AbstractPageSupport { * @return a new {@link ListablePage} initialized with the given {@link List} for content. * @see #ListablePage(List) */ - public static ListablePage newListablePage(List content) { + public static ListablePage newListablePage(@Nullable List content) { return new ListablePage<>(content); } @@ -83,8 +83,8 @@ public class ListablePage extends AbstractPageSupport { * @see java.util.Collections#emptyList() * @see java.util.List */ - public ListablePage(List content) { - this.content = Optional.ofNullable(content).orElse(Collections.emptyList()); + public ListablePage(@Nullable List content) { + this.content = content != null ? content : Collections.emptyList(); } /** @@ -164,6 +164,9 @@ public class ListablePage extends AbstractPageSupport { */ @Override public Page map(Function converter) { - return newListablePage(getContent().stream().map(converter::apply).collect(Collectors.toList())); + + return newListablePage(getContent().stream() + .map(converter) + .collect(Collectors.toList())); } } diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java index 8d079899..3efa7900 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/GemfireRepository.java @@ -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 extends CrudRepository { - - /** - * 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 findAll(Sort sort); +public interface GemfireRepository extends PagingAndSortingRepository { /** * Save the entity wrapped by the given {@link Wrapper}. diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java index e0c1ac1e..bd722474 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryCreator.java @@ -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 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 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 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 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 } @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 diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java index 46de0525..5c2f8823 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethod.java @@ -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, GemfirePersistentProperty> mappingContext) { + public GemfireQueryMethod(@NonNull Method method, + @NonNull RepositoryMetadata metadata, + @NonNull ProjectionFactory projectionFactory, + @NonNull MappingContext, 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, 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); } } diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java index 795d9d4e..55d394a6 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java @@ -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. - *

+ * 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 queryPostProcessor = ProvidedQueryPostProcessor.IDENTITY; + private QueryPostProcessor 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 getQueryPostProcessor() { + protected @NonNull QueryPostProcessor 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 queryPostProcessor) { + public GemfireRepositoryQuery register(@Nullable QueryPostProcessor queryPostProcessor) { this.queryPostProcessor = this.queryPostProcessor.processBefore(queryPostProcessor); return this; } + @SuppressWarnings("rawtypes") enum ProvidedQueryPostProcessor implements QueryPostProcessor { IDENTITY { diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java index 0b9de90b..d018b74e 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java @@ -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 partsIterator = this.tree.getParts().iterator(); + Iterator partsIterator = getPartTree().getParts().iterator(); List stringParameters = new ArrayList<>(parameters.length); diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java index 56d8c3f8..d7d0e08d 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryBuilder.java @@ -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() { diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java index b28de2f9..4d700a65 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java @@ -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 {@link Class type} identifying the {@link Repository Repositories} to match on during registration. * @param {@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 extends Ordered { Object[] EMPTY_ARGUMENTS = new Object[0]; @@ -102,10 +105,11 @@ public interface QueryPostProcessor 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 extends Ordered * if the given {@link QueryPostProcessor} is {@literal null}. * @see #processAfter(QueryPostProcessor) */ - @NonNull - default QueryPostProcessor processBefore(@Nullable QueryPostProcessor queryPostProcessor) { + default @NonNull QueryPostProcessor processBefore(@Nullable QueryPostProcessor queryPostProcessor) { return queryPostProcessor == null ? this : (queryMethod, query, arguments) -> queryPostProcessor.postProcess(queryMethod, this.postProcess(queryMethod, query, arguments), arguments); } /** - * Builder method used to compose, 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 extends Ordered * if the given {@link QueryPostProcessor} is {@literal null}. * @see #processBefore(QueryPostProcessor) */ - @NonNull - default QueryPostProcessor processAfter(@Nullable QueryPostProcessor queryPostProcessor) { + default @NonNull QueryPostProcessor processAfter(@Nullable QueryPostProcessor queryPostProcessor) { return queryPostProcessor == null ? this : (queryMethod, query, arguments) -> this.postProcess(queryMethod, queryPostProcessor.postProcess(queryMethod, query, arguments), arguments); } diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java index 8053aadd..8f30427b 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java @@ -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; } /** diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java index 552c8e17..47593691 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java @@ -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 -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(); } /** diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java index ee34eea1..ae4258e8 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlKeyword.java @@ -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(); diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java index 7c2ee659..a7723693 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactory.java @@ -179,7 +179,7 @@ public class GemfireRepositoryFactory extends RepositoryFactorySupport { return (GemfirePersistentEntity) 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 optionalEntity = Optional.ofNullable(entity); + Optional> 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 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 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); } } diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java index 46aa4226..0d414385 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java @@ -331,9 +331,9 @@ public class GemfireRepositoryFactoryBean, S, ID> return this.declaredRepositoryType; } - @SuppressWarnings("unchecked") - @NonNull QueryPostProcessor getQueryPostProcessor() { - return (QueryPostProcessor) this.queryPostProcessor; + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull QueryPostProcessor getQueryPostProcessor() { + return (QueryPostProcessor) this.queryPostProcessor; } boolean isMatch(Class repositoryInterface) { diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java index ab3fe41e..d9581539 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java @@ -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 implements GemfireRepository { @@ -228,6 +234,16 @@ public class SimpleGemfireRepository implements GemfireRepository return toList(selectResults); } + @Override + public Page findAll(@NonNull Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null"); + + Iterable results = findAll(pageable.getSort()); + + return toPage(results, pageable); + } + @Override public @NonNull Iterable findAll(@NonNull Sort sort) { @@ -324,10 +340,41 @@ public class SimpleGemfireRepository implements GemfireRepository region.removeAll(region.keySet()); } + @NonNull List toList(@Nullable Iterable iterable) { + + return iterable instanceof List ? (List) iterable + : StreamSupport.stream(CollectionUtils.nullSafeIterable(iterable).spliterator(), false) + .collect(Collectors.toList()); + } + @NonNull List toList(@Nullable SelectResults selectResults) { return selectResults != null ? CollectionUtils.nullSafeList(selectResults.asList()) : Collections.emptyList(); } + + @NonNull Page toPage(@Nullable Iterable 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 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); + } } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethodUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethodUnitTests.java index e6a919bd..4b651387 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethodUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/GemfireQueryMethodUnitTests.java @@ -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); } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java index 21b0a59c..7ba300f2 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessorUnitTests.java @@ -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 mockQueryPostProcessorOne = mock(QueryPostProcessor.class); - QueryPostProcessor mockQueryPostProcessorTwo = mock(QueryPostProcessor.class); + QueryPostProcessor mockQueryPostProcessorOne = mock(QueryPostProcessor.class); + QueryPostProcessor mockQueryPostProcessorTwo = mock(QueryPostProcessor.class); when(mockQueryPostProcessorOne.processAfter(any())).thenCallRealMethod(); when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query); @@ -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 mockQueryPostProcessorOne = mock(QueryPostProcessor.class); - QueryPostProcessor mockQueryPostProcessorTwo = mock(QueryPostProcessor.class); + QueryPostProcessor mockQueryPostProcessorOne = mock(QueryPostProcessor.class); + QueryPostProcessor mockQueryPostProcessorTwo = mock(QueryPostProcessor.class); when(mockQueryPostProcessorOne.processBefore(any())).thenCallRealMethod(); when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), any())).thenReturn(query); diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Person.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Person.java index 49bb737b..1c020024 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Person.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/Person.java @@ -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()); } - } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java index 355cde63..787c04fc 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryUnitTests.java @@ -469,26 +469,14 @@ public class GemfireRepositoryFactoryUnitTests { } } - /** - * @link Repositories should reject PagingAndSortingRepository and Pageable parameters - */ - @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 diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java index 209268c8..7e629c79 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryIntegrationTests.java @@ -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 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 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 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() { diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java index 4ca08313..93fa0704 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTests.java @@ -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 asMap(Iterable animals) { + private @NonNull Map asMap(@Nullable Iterable animals) { Map 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 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 userSet = CollectionUtils.asSet(User.newUser("Jon Doe"), User.newUser("Jane Doe")); + + List 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 users = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation()) + .toList((Iterable) Collections::emptyIterator); + + assertThat(users).isNotNull(); + assertThat(users).isEmpty(); + } + + @Test + public void toListFromList() { + + List users = Collections.singletonList(User.newUser("Jon Doe")); + + List userList = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation()) + .toList((Iterable) users); + + assertThat(userList).isSameAs(users); + } + + @Test + public void toListFromNullIterableIsNullSafe() { + + List users = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation()) + .toList((Iterable) null); + + assertThat(users).isNotNull(); + assertThat(users).isEmpty(); + } + + @Test + public void toListFromSelectResults() { + + List users = Arrays.asList(User.newUser("Jon Doe"), User.newUser("Jane Doe")); + + SelectResults mockSelectResults = mock(SelectResults.class); + + doReturn(users).when(mockSelectResults).asList(); + + List 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 users = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mockEntityInformation()) + .toList((SelectResults) null); + + assertThat(users).isNotNull(); + assertThat(users).isEmpty(); + } + + @Test + public void toPageFromIterable() { + + List 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 pageOne = repository.toPage(users, mockPageable); + + assertPage(pageOne, 0, 1, 2, null, users.get(0)); + + doReturn(1).when(mockPageable).getPageNumber(); // page 2 + + Page pageTwo = repository.toPage(users, mockPageable); + + assertPage(pageTwo, 1, 1, 2, null, users.get(1)); + + doReturn(2).when(mockPageable).getPageNumber(); // page 3 + + Page 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 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 page = new SimpleGemfireRepository<>(newGemfireTemplate(mockRegion()), mock(EntityInformation.class)) + .toPage(null, mockPageable); + + assertThat(page).isNotNull(); + assertThat(page).isEmpty(); + } + + @Test + public void toPageWithOverflowFromIterable() { + + List 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 pageOne = repository.toPage(users, mockPageable); + + assertPage(pageOne, 0, 5, 2, null, users.toArray(new User[0])); + + doReturn(1).when(mockPageable).getPageNumber(); // page 2 + + Page 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; + + } }