From 35e841722e16b465b530a4a82f074caa40de392a Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 9 Oct 2020 15:17:09 -0700 Subject: [PATCH] DATAGEODE-263 - Add Paging framework and infrastructure supporting classes. --- .../data/gemfire/GemfireTemplate.java | 34 +- .../query/AbstractSelectResults.java | 223 ++++++ .../repository/query/GemfireQueryMethod.java | 27 +- .../query/GemfireRepositoryQuery.java | 15 + .../repository/query/PagedQueryString.java | 83 +++ .../repository/query/PagedSelectResults.java | 139 ++++ .../query/PartTreeGemfireRepositoryQuery.java | 6 +- .../gemfire/repository/query/QueryString.java | 277 +++++-- .../StringBasedGemfireRepositoryQuery.java | 438 ++++++++++- .../repository/query/support/OqlKeyword.java | 7 +- .../query/support/OqlQueryExecutor.java | 96 +++ .../repository/query/support/PagingUtils.java | 295 ++++++++ .../TemplateBasedOqlQueryExecutor.java | 74 ++ .../UnsupportedQueryExecutionException.java | 69 ++ .../support/SimpleGemfireRepository.java | 23 +- .../data/gemfire/util/ArrayUtils.java | 53 ++ .../GemfireTemplateIntegrationTests.java | 227 ++++-- .../query/AbstractSelectResultsUnitTests.java | 322 +++++++++ .../query/PagedSelectResultsUnitTests.java | 282 ++++++++ .../query/QueryStringUnitTests.java | 184 ++++- .../support/OqlQueryExecutorUnitTests.java | 158 ++++ .../query/support/PagingUtilsUnitTests.java | 677 ++++++++++++++++++ ...emplateBasedOqlQueryExecutorUnitTests.java | 93 +++ .../data/gemfire/repository/sample/User.java | 16 +- .../gemfire/util/ArrayUtilsUnitTests.java | 36 + 25 files changed, 3584 insertions(+), 270 deletions(-) create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/AbstractSelectResults.java create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedQueryString.java create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedSelectResults.java create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutor.java create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/PagingUtils.java create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutor.java create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/UnsupportedQueryExecutionException.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/AbstractSelectResultsUnitTests.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/PagedSelectResultsUnitTests.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutorUnitTests.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/PagingUtilsUnitTests.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutorUnitTests.java 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 fd9329fe..d3d68d2c 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 @@ -268,7 +268,7 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation public SelectResults query(String query) { try { - return this.getRegion().query(query); + return getRegion().query(query); } catch (IndexInvalidException | QueryInvalidException cause) { throw convertGemFireQueryException(cause); @@ -291,21 +291,26 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation @Override @SuppressWarnings("unchecked") - public SelectResults find(String queryString, Object... arguments) throws InvalidDataAccessApiUsageException { + public SelectResults find(String query, Object... arguments) throws InvalidDataAccessApiUsageException { try { QueryService queryService = resolveQueryService(getRegion()); - Query query = queryService.newQuery(queryString); - Object result = query.execute(arguments); + + Query compiledQuery = queryService.newQuery(query); + + Object result = compiledQuery.execute(arguments); if (result instanceof SelectResults) { return (SelectResults) result; } else { - throw new InvalidDataAccessApiUsageException(String.format( - "The result from executing query [%1$s] was not an instance of SelectResults [%2$s]", - queryString, result)); + + String message = + String.format("The result from executing query [%1$s] was not an instance of SelectResults [%2$s]", + query, result); + + throw new InvalidDataAccessApiUsageException(message); } } catch (IndexInvalidException | QueryInvalidException cause) { @@ -329,13 +334,15 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation @Override @SuppressWarnings("unchecked") - public T findUnique(String queryString, Object... params) throws InvalidDataAccessApiUsageException { + public T findUnique(String query, Object... arguments) throws InvalidDataAccessApiUsageException { try { QueryService queryService = resolveQueryService(getRegion()); - Query query = queryService.newQuery(queryString); - Object result = query.execute(params); + + Query compiledQuery = queryService.newQuery(query); + + Object result = compiledQuery.execute(arguments); if (result instanceof SelectResults) { @@ -347,8 +354,11 @@ public class GemfireTemplate extends GemfireAccessor implements GemfireOperation result = results.get(0); } else { - throw new InvalidDataAccessApiUsageException(String.format( - "The result returned from query [%1$s]) was not unique [%2$s]", queryString, result)); + + String message = String.format("The result returned from query [%1$s]) was not unique [%2$s]", + query, result); + + throw new InvalidDataAccessApiUsageException(message); } } diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/AbstractSelectResults.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/AbstractSelectResults.java new file mode 100644 index 00000000..678135ba --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/AbstractSelectResults.java @@ -0,0 +1,223 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.geode.cache.query.SelectResults; +import org.apache.geode.cache.query.types.CollectionType; +import org.apache.geode.cache.query.types.ObjectType; + +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * An abstract base class implementation of Apache Geode's {@link SelectResults} interface and Java {@link Collection} + * interface, which delegates to, and is backed by a given, required {@link SelectResults} instance. + * + * @author John Blum + * @see java.util.Collection + * @see org.apache.geode.cache.query.SelectResults + * @since 2.4.0 + */ +public class AbstractSelectResults implements SelectResults { + + private final SelectResults selectResults; + + /** + * Constructs a new instance of {@link SelectResults} initialized with the given, required {@link SelectResults} + * instance backing this base class. + * + * @param selectResults {@link SelectResults} delegate backing this implementation; must not be {@literal null}. + * @throws IllegalArgumentException if {@link SelectResults} is {@literal null}. + * @see org.apache.geode.cache.query.SelectResults + */ + public AbstractSelectResults(@NonNull SelectResults selectResults) { + + Assert.notNull(selectResults, "SelectResults must not be null"); + + this.selectResults = selectResults; + } + + /** + * Return the configured, underlying {@link SelectResults} used as the delegate + * backing this {@link SelectResults} implementation. + * + * @return the configured, underlying {@link SelectResults}. + * @see org.apache.geode.cache.query.SelectResults + */ + protected @NonNull SelectResults getSelectResults() { + return this.selectResults; + } + + /** + * @inheritDoc + */ + @Override + public List asList() { + return getSelectResults().asList(); + } + + /** + * @inheritDoc + */ + @Override + public Set asSet() { + return getSelectResults().asSet(); + } + + /** + * @inheritDoc + */ + @Override + public CollectionType getCollectionType() { + return getSelectResults().getCollectionType(); + } + + /** + * @inheritDoc + */ + @Override + public boolean isModifiable() { + return getSelectResults().isModifiable(); + } + + /** + * @inheritDoc + */ + @Override + public int occurrences(T result) { + return getSelectResults().occurrences(result); + } + + /** + * @inheritDoc + */ + @Override + public void setElementType(ObjectType objectType) { + getSelectResults().setElementType(objectType); + } + + // java.util.Collection interface methods + + /** + * @inheritDoc + */ + @Override + public boolean add(T result) { + return getSelectResults().add(result); + } + + /** + * @inheritDoc + */ + @Override + public boolean addAll(Collection results) { + return getSelectResults().addAll(results); + } + + /** + * @inheritDoc + */ + @Override + public void clear() { + getSelectResults().clear(); + } + + /** + * @inheritDoc + */ + @Override + public boolean contains(Object result) { + return getSelectResults().contains(result); + } + + /** + * @inheritDoc + */ + @Override + public boolean containsAll(Collection results) { + return getSelectResults().containsAll(results); + } + + /** + * @inheritDoc + */ + @Override + public boolean isEmpty() { + return getSelectResults().isEmpty(); + } + + /** + * @inheritDoc + */ + @Override + public Iterator iterator() { + return getSelectResults().iterator(); + } + + /** + * @inheritDoc + */ + @Override + public boolean remove(Object result) { + return getSelectResults().remove(result); + } + + /** + * @inheritDoc + */ + @Override + public boolean removeAll(Collection results) { + return getSelectResults().removeAll(results); + } + + /** + * @inheritDoc + */ + @Override + public boolean retainAll(Collection results) { + return getSelectResults().retainAll(results); + } + + /** + * @inheritDoc + */ + @Override + public int size() { + return getSelectResults().size(); + } + + /** + * @inheritDoc + */ + @Override + public Object[] toArray() { + return getSelectResults().toArray(); + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("all") + public E[] toArray(E[] array) { + return getSelectResults().toArray(array); + } +} 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 5c2f8823..025d188c 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 @@ -18,7 +18,6 @@ package org.springframework.data.gemfire.repository.query; import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.data.domain.Pageable; import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; import org.springframework.data.gemfire.mapping.GemfirePersistentProperty; import org.springframework.data.gemfire.repository.Query; @@ -38,11 +37,13 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Apache Geode specific {@link QueryMethod} implementation. + * {@link QueryMethod} implementation for Apache Geode. * * @author Oliver Gierke * @author John Blum * @see java.lang.reflect.Method + * @see org.springframework.data.gemfire.repository.Query + * @see org.springframework.data.repository.Repository * @see org.springframework.data.repository.query.QueryMethod */ public class GemfireQueryMethod extends QueryMethod { @@ -53,6 +54,7 @@ public class GemfireQueryMethod extends QueryMethod { private final Method method; + @SuppressWarnings("unused") private final QueryMethodEvaluationContextProvider evaluationContextProvider; /** @@ -117,27 +119,6 @@ public class GemfireQueryMethod extends QueryMethod { this.evaluationContextProvider = evaluationContextProvider; } - /** - * 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 {@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 boolean isPageableQueryMethod(@NonNull Method method) { - - for (Class type : method.getParameterTypes()) { - if (Pageable.class.isAssignableFrom(type)) { - return true; - } - } - - return false; - } - /** * Returns the {@link Method} reference on which this {@link QueryMethod} is based. * 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 55d394a6..f14b869c 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 @@ -22,6 +22,9 @@ import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Abstract base class for Apache Geode specific {@link RepositoryQuery} implementations. * @@ -35,6 +38,8 @@ import org.springframework.util.Assert; @SuppressWarnings("rawtypes") public abstract class GemfireRepositoryQuery implements RepositoryQuery { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final GemfireQueryMethod queryMethod; private QueryPostProcessor queryPostProcessor = ProvidedQueryPostProcessor.IDENTITY; @@ -73,6 +78,16 @@ public abstract class GemfireRepositoryQuery implements RepositoryQuery { return (GemfireQueryMethod) getQueryMethod(); } + /** + * Returns the configured SLF4J {@link Logger} used log statements. + * + * @return the configured SLF4J {@link Logger}. + * @see org.slf4j.Logger + */ + protected @NonNull Logger getLogger() { + return this.logger; + } + /** * Returns a reference to the {@link Repository} {@link QueryMethod} modeling the data store query. * diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedQueryString.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedQueryString.java new file mode 100644 index 00000000..952da35c --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedQueryString.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query; + +import java.util.Optional; + +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * {@link QueryString} implementation handling {@literal paging} functionality and behavior. + * + * @author John Blum + * @see org.springframework.data.gemfire.repository.query.QueryString + * @since 2.4.0 + */ +public class PagedQueryString extends QueryString { + + /** + * Factory method used to construct a new instance of {@link PagedQueryString} from an existing, {@literal non-null} + * {@link QueryString}. + * + * @param queryString {@link QueryString} on which the {@link PagedQueryString} will be based. + * @return a new instance of {@link PagedQueryString} initialized with the {@literal OQL query} + * from the given {@link QueryString}. + * @throws IllegalArgumentException if {@link QueryString} is {@literal null}. + * @see org.springframework.data.gemfire.repository.query.QueryString + * @see #of(String) + */ + public static PagedQueryString of(@NonNull QueryString queryString) { + + Assert.notNull(queryString, "QueryString must not be null"); + + return of(queryString.getQuery()); + } + + /** + * Factory method used to construct a new instance of {@link PagedQueryString} initialized with + * the given {@literal OQL-based query}. + * + * @param query {@link String} containing the OQL query statement. + * @return a new instance of {@link PagedQueryString} initialized with the given {@literal OQL-based query}. + * @throws IllegalArgumentException if the {@link String OQL query} is {@literal null} or {@literal empty}. + * @see #PagedQueryString(String) + */ + public static PagedQueryString of(@NonNull String query) { + return new PagedQueryString(query); + } + + private GemfireQueryMethod queryMethod; + + /** + * Constructs a new instance of {@link PagedQueryString} initialized with the given {@literal OQL-based query}. + * + * @param query {@link String} containing the OQL query statement. + * @throws IllegalArgumentException if the {@link String OQL query} is {@literal null} or {@literal empty}. + */ + public PagedQueryString(@NonNull String query) { + super(query); + } + + protected Optional getQueryMethod() { + return Optional.ofNullable(this.queryMethod); + } + + public PagedQueryString withQueryMethod(GemfireQueryMethod queryMethod) { + this.queryMethod = queryMethod; + return this; + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedSelectResults.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedSelectResults.java new file mode 100644 index 00000000..9d4be6b9 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/PagedSelectResults.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.geode.cache.query.SelectResults; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.gemfire.repository.query.support.PagingUtils; +import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * An Apache Geode {@link SelectResults} implementation with support for {@literal Paging}. + * + * @author John Blum + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.gemfire.repository.query.AbstractSelectResults + * @see org.springframework.data.gemfire.repository.query.support.PagingUtils + * @see org.springframework.data.util.Lazy + * @since 2.4.0 + */ +public class PagedSelectResults extends AbstractSelectResults { + + protected static final String NON_NULL_PAGEABLE_MESSAGE = "Pageable must not be null"; + + private Lazy> pagedList; + + private Pageable pageRequest; + + /** + * Constructs a new instance of {@link PagedSelectResults} initialized with the given, required + * {@link SelectResults} and {@link Pageable} object encapsulating the details of the requested page. + * + * @param selectResults {@link SelectResults} to wrap; must not be {@literal null}. + * @param pageable {@link Pageable} object encapsulating the details of the requested page; + * must not be {@literal null}. + * @throws IllegalArgumentException if the {@link SelectResults} or the {@link Pageable} object is {@literal null}. + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.domain.Pageable + */ + public PagedSelectResults(@NonNull SelectResults selectResults, @NonNull Pageable pageable) { + + super(selectResults); + + Assert.notNull(pageable, NON_NULL_PAGEABLE_MESSAGE); + + this.pageRequest = pageable; + this.pagedList = newLazyPagedList(); + } + + // WARNING newLazyPagedList() allows the `this` reference to escape when called inside the constructor, + // but this class makes no Thread-safety guarantees either. + private Lazy> newLazyPagedList() { + return Lazy.of(() -> PagingUtils.getPagedList(getSelectResults().asList(), getPageRequest())); + } + + /** + * Returns the {@link Pageable} object encapsulating the details of the requested page. + * + * @return the {@link Pageable} object encapsulating the details of the requested page. + * @see org.springframework.data.domain.Pageable + */ + protected @NonNull Pageable getPageRequest() { + return this.pageRequest; + } + + /** + * @inheritDoc + */ + @Override + public Set asSet() { + return new HashSet<>(asList()); + } + + /** + * @inheritDoc + */ + @Override + public List asList() { + return this.pagedList.get(); + } + + /** + * @inheritDoc + */ + @Override + public Iterator iterator() { + return Collections.unmodifiableList(asList()).iterator(); + } + + /** + * @inheritDoc + */ + @Override + public int size() { + return asList().size(); + } + + /** + * Builder method used to allow a new {@link Pageable page request} in order to get a different page of results + * from the underlying {@link SelectResults}. + * + * @param pageRequest {@link Pageable} object encapsulating the details of the requested page; + * must not be {@literal null}. + * @return this {@link PagedSelectResults}. + * @throws IllegalArgumentException if {@link Pageable} is {@literal null}. + * @see org.springframework.data.domain.Pageable + */ + public PagedSelectResults with(@NonNull Pageable pageRequest) { + + Assert.notNull(pageRequest, NON_NULL_PAGEABLE_MESSAGE); + + this.pageRequest = pageRequest; + this.pagedList = newLazyPagedList(); + + return this; + } +} 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 d018b74e..de28b92e 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 @@ -100,7 +100,7 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { QueryString query = newQueryString(queryMethod, getPartTree(), arguments); - GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(query, queryMethod, getTemplate()); + GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(queryMethod, query, getTemplate()); return repositoryQuery.execute(prepareStringParameters(arguments)); } @@ -115,8 +115,8 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { return queryCreator.createQuery(parameterAccessor.getSort()); } - private GemfireRepositoryQuery newRepositoryQuery(QueryString query, - GemfireQueryMethod queryMethod, GemfireTemplate template) { + private GemfireRepositoryQuery newRepositoryQuery(GemfireQueryMethod queryMethod, + QueryString query, GemfireTemplate template) { StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(query.toString(), queryMethod, template); 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 8f30427b..ecb4d7a7 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 @@ -18,6 +18,7 @@ package org.springframework.data.gemfire.repository.query; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -28,14 +29,19 @@ 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.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * {@link QueryString} is a utility class used to construct Apache Geode OQL query statements. + * {@link QueryString} is a base class used to construct and model syntactically valid Apache Geode + * OQL query statements. * - * This is an internal class used by the SDG {@link Repository} infrastructure extension + * {@link QueryString} uses {@link Pattern} based recognition and {@link Matcher matching} to parse and modify + * the OQL query statement. + * + * This is an internal class used by the SDG {@link Repository} infrastructure extension. * * @author Oliver Gierke * @author David Turanski @@ -45,6 +51,7 @@ import org.springframework.util.StringUtils; * @see org.apache.geode.cache.Region * @see org.springframework.data.domain.Sort * @see org.springframework.data.gemfire.repository.query.support.OqlKeyword + * @see org.springframework.data.repository.Repository */ public class QueryString { @@ -55,27 +62,38 @@ public class QueryString { protected static final Pattern TRACE_PATTERN = Pattern.compile(""); // OQL Query Templates - private static final String HINTS_OQL_TEMPLATE = " %2$s"; - private static final String IMPORT_OQL_TEMPLATE = "IMPORT %1$s; %2$s"; - private static final String LIMIT_OQL_TEMPLATE = "%1$s LIMIT %2$d"; - private static final String SELECT_OQL_TEMPLATE = "SELECT %1$s FROM /%2$s"; - private static final String TRACE_OQL_TEMPLATE = " %1$s"; + protected static final String HINTS_OQL_TEMPLATE = " %2$s"; + protected static final String IMPORT_OQL_TEMPLATE = "IMPORT %1$s; %2$s"; + protected static final String LIMIT_OQL_TEMPLATE = "%1$s LIMIT %2$d"; + protected static final String SELECT_OQL_TEMPLATE = "SELECT %1$s FROM /%2$s"; + protected static final String TRACE_OQL_TEMPLATE = " %1$s"; // OQL Query Regular Expression Patterns - private static final String IN_PATTERN = "(?<=IN (SET|LIST) )\\$\\d"; - private static final String IN_PARAMETER_PATTERN = "(?<=IN (SET|LIST) \\$)\\d"; - private static final String REGION_PATTERN = "\\/(\\/?\\w)+"; + protected static final String COUNT_PROJECTION = "count(*)"; + protected static final String IN_PATTERN = "(?<=IN (SET|LIST) )\\$\\d"; + protected static final String IN_PARAMETER_PATTERN = "(?<=IN (SET|LIST) \\$)\\d"; + protected static final String REGION_PATTERN = "\\/(\\/?\\w)+"; + protected static final String STAR_PROJECTION = "*"; - private static final String COUNT_QUERY = "count(*)"; - private static final String STAR_QUERY = "*"; + /** + * @deprecated use {@link #COUNT_PROJECTION}. + */ + @Deprecated + protected static final String COUNT_QUERY = COUNT_PROJECTION; + + /** + * @deprecated use {@link #STAR_PROJECTION}. + */ + @Deprecated + protected static final String STAR_QUERY = STAR_PROJECTION; /** * Factory method used to construct a new instance of {@link QueryString} initialized with - * the given {@link String OQL query}. + * the given {@link String OQL query statement}. * * @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. + * @throws IllegalArgumentException if {@link String query} is {@literal null} or {@literal empty}. * @see #QueryString(String) */ public static QueryString of(@NonNull String query) { @@ -83,8 +101,8 @@ public class QueryString { } /** - * 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. + * Factory method used to construct a new instance of {@link QueryString} initialized with the given + * {@link Class application domain model type} from which the {@link String OQL query} will be created. * * @param domainType {@link Class application domain model type} for which the {@link String OQL query} * will be created. @@ -98,7 +116,7 @@ public class QueryString { /** * 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}. + * to count the number of objects of the specified {@link Class application domain model type}. * * @param domainType {@link Class application domain model type} for which the OQL query will be created. * @return a new count {@link QueryString}. @@ -109,17 +127,54 @@ public class QueryString { return new QueryString(domainType, true); } - static String asQuery(Class domainType, boolean isCountQuery) { - return String.format(SELECT_OQL_TEMPLATE, isCountQuery ? COUNT_QUERY : STAR_QUERY, - validateDomainType(domainType).getSimpleName()); + /** + * Null-safe method used to extract {@literal digits} from the given {@link String} value as a whole number. + * + * @param value {@link String} to evaluate. + * @return the {@literal digits} extracted from the give {@link String} value as a whole number + * or an {@link String#isEmpty() empty String} if the given {@link String} is {@literal null}, {@literal empty} + * or contains no {@literal digits}. + * @see java.lang.String + */ + protected static String getDigitsOnly(@Nullable String value) { + + StringBuilder builder = new StringBuilder(); + + if (StringUtils.hasText(value)) { + for (char c : value.toCharArray()) { + if (Character.isDigit(c)) { + builder.append(c); + } + } + } + + return builder.toString(); } - static Class validateDomainType(Class domainType) { + static String asQuery(Class domainType, boolean isCountQuery) { + return String.format(SELECT_OQL_TEMPLATE, resolveProjection(isCountQuery), resolveFrom(domainType)); + } + + static String resolveFrom(@NonNull Class domainType) { + + return Optional.of(validateDomainType(domainType)) + .filter(it -> it.isAnnotationPresent(org.springframework.data.gemfire.mapping.annotation.Region.class)) + .map(it -> it.getAnnotation(org.springframework.data.gemfire.mapping.annotation.Region.class)) + .map(it -> it.value()) + .filter(StringUtils::hasText) + .orElseGet(() -> domainType.getSimpleName()); + } + + static @NonNull String resolveProjection(boolean isCountQuery) { + return isCountQuery ? COUNT_PROJECTION : STAR_PROJECTION; + } + + static @NonNull Class validateDomainType(@NonNull Class domainType) { Assert.notNull(domainType, "Domain type is required"); return domainType; } - static String validateQuery(String query) { + static @NonNull String validateQuery(@NonNull String query) { Assert.hasText(query, String.format("Query [%s] is required", query)); return query; } @@ -140,7 +195,7 @@ public class QueryString { /** * 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. + * {@link Class application domain model type} used to construct an OQL {@literal SELECT} query statement. * * @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}. @@ -164,37 +219,119 @@ public class QueryString { * @see #asQuery(Class, boolean) * @see #QueryString(String) */ - public QueryString(Class domainType, boolean asCountQuery) { + public QueryString(@NonNull Class domainType, boolean asCountQuery) { this(asQuery(domainType, asCountQuery)); } /** - * Replaces the {@literal SELECT query} with a {@literal SELECT DISTINCT query} if the {@link String query} - * is not already distinct; i.e. does not contain the {@literal DISTINCT} keyword. + * Determines whether a {@literal LIMIT} is present in the OQL query. * - * @return a {@literal SELECT DISTINCT query} if {@link String query} does not contain - * the {@literal DISTINCT} keyword. - * @see java.lang.String#replaceFirst(String, String) - * @see #asDistinct(String) + * @return a boolean value determining whether a {@literal LIMIT} is present in the OQL query. + * @see #getLimit() */ - public QueryString asDistinct() { - return QueryString.of(asDistinct(this.query)); + public boolean isLimited() { + return LIMIT_PATTERN.matcher(getQuery()).find(); } /** - * Replaces the {@literal SELECT query} with a {@literal SELECT DISTINCT query} if the {@link String query} - * is not already distinct; i.e. does not contain the {@literal DISTINCT} keyword. + * Returns the parameter indexes used in this query. * - * @param query {@link String} containing the {@link String query} to evaluate. - * @return a {@literal SELECT DISTINCT query} if {@link String query} does not contain - * the {@literal DISTINCT} keyword. + * @return the parameter indexes used in this query or an empty {@link Iterable} if no parameter indexes are used. + * @see java.lang.Iterable + */ + public Iterable getInParameterIndexes() { + + Pattern pattern = Pattern.compile(IN_PARAMETER_PATTERN); + + Matcher matcher = pattern.matcher(getQuery()); + + List indexes = new ArrayList<>(); + + while (matcher.find()) { + indexes.add(Integer.parseInt(matcher.group())); + } + + return indexes; + } + + /** + * Gets the {@literal LIMIT} number. + * + * Use {@link #isLimited()} to determine whether the {@link String OQL query statement} has a {@literal LIMIT}. + * + * @return an {@link Integer} value containing the {@literal LIMIT} number or {@link Integer#MAX_VALUE} + * if the {@link String OQL query statement} is not {@link #isLimited() limited}. + * @see #isLimited() + */ + public int getLimit() { + + String query = getQuery(); + + Matcher matcher = LIMIT_PATTERN.matcher(query); + + if (matcher.find()) { + + int startIndex = matcher.start(); + int endIndex = matcher.end(); + + String limit = query.substring(startIndex, endIndex); + + return Integer.parseInt(getDigitsOnly(limit)); + } + + return Integer.MAX_VALUE; + } + + /** + * Returns the {@link String OQL query statement} from which this {@link QueryString} was constructed. + * + * @return the {@link String OQL query}; never {@literal null} or {@literal empty}. + */ + protected @NonNull String getQuery() { + return this.query; + } + + /** + * Null-safe method to adjust the {@literal LIMIT} of the {@link String OQL query} to use the new, + * given {@link Integer LIMIT}. + * + * @param limit {@link Integer} value specifying the new query {@literal LIMIT}. + * @return a new {@link QueryString} with the adjusted query {@literal LIMIT}. + * @see #withLimit(Integer) + */ + public QueryString adjustLimit(@Nullable Integer limit) { + + return limit != null + ? QueryString.of(LIMIT_PATTERN.matcher(getQuery()).replaceAll("").trim()).withLimit(limit) + : this; + } + + /** + * Replaces an OQL {@literal SELECT} query with an OQL {@literal SELECT DISTINCT} query if the {@link String query} + * is not already {@literal distinct}; i.e. does not contain the {@literal DISTINCT} OQL keyword. + * + * @return a {@literal SELECT DISTINCT} {@link QueryString query} if the {@link String query} does not contain + * the {@literal DISTINCT} OQL keyword. + * @see #asDistinct(String) + */ + public QueryString asDistinct() { + return QueryString.of(asDistinct(getQuery())); + } + + /** + * Replaces an OQL {@literal SELECT} query with an OQL {@literal SELECT DISTINCT} query if the {@link String query} + * is not already {@literal distinct}; i.e. does not contain the {@literal DISTINCT} OQL keyword. + * + * @param query {@link String} containing the query to evaluate. + * @return a {@literal SELECT DISTINCT} {@link String query} if the {@link String query} does not contain + * the {@literal DISTINCT} OQL keyword. * @see java.lang.String#replaceFirst(String, String) */ String asDistinct(String query) { return query.contains(OqlKeyword.DISTINCT.getKeyword()) ? query : query.replaceFirst(OqlKeyword.SELECT.getKeyword(), - String.format("%1$s %2$s", OqlKeyword.SELECT.getKeyword(), OqlKeyword.DISTINCT.getKeyword())); + String.format("%1$s %2$s", OqlKeyword.SELECT.getKeyword(), OqlKeyword.DISTINCT.getKeyword())); } /** @@ -207,7 +344,7 @@ public class QueryString { public QueryString bindIn(Collection values) { if (!CollectionUtils.nullSafeIsEmpty(values)) { - return QueryString.of(this.query.replaceFirst(IN_PATTERN, String.format("(%s)", + return QueryString.of(getQuery().replaceFirst(IN_PATTERN, String.format("(%s)", StringUtils.collectionToDelimitedString(values, ", ", "'", "'")))); } @@ -218,37 +355,24 @@ public class QueryString { * Replaces the {@link Class domain classes} referenced inside the current {@link String query} * with the given {@link Region}. * - * @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}. + * @param domainType {@link Class type} of the persistent entity to query; must not be {@literal null}. * @return a new {@link QueryString} with an OQL {@literal SELECT statement} having a {@literal FROM clause} * based on the selected {@link Region}. * @see org.apache.geode.cache.Region * @see java.lang.Class */ @SuppressWarnings("unused") - public QueryString fromRegion(Class domainType, Region region) { - return QueryString.of(this.query.replaceAll(REGION_PATTERN, region.getFullPath())); + public QueryString fromRegion(Region region, Class domainType) { + return QueryString.of(getQuery().replaceAll(REGION_PATTERN, region.getFullPath())); } - /** - * Returns the parameter indexes used in this query. - * - * @return the parameter indexes used in this query or an empty {@link Iterable} if none are used. - * @see java.lang.Iterable + /** + * @deprecated use {@link #fromRegion(Region, Class)}. */ - public Iterable getInParameterIndexes() { - - Pattern pattern = Pattern.compile(IN_PARAMETER_PATTERN); - - Matcher matcher = pattern.matcher(this.query); - - List indexes = new ArrayList<>(); - - while (matcher.find()) { - indexes.add(Integer.parseInt(matcher.group())); - } - - return indexes; + @Deprecated + public QueryString fromRegion(Class domainType, Region region) { + return fromRegion(region, domainType); } /** @@ -260,7 +384,7 @@ public class QueryString { * @see org.springframework.data.domain.Sort * @see org.springframework.data.gemfire.repository.query.QueryString */ - public QueryString orderBy(Sort sort) { + public @NonNull QueryString orderBy(@Nullable Sort sort) { if (hasSort(sort)) { @@ -273,13 +397,20 @@ public class QueryString { orderByClause.append(String.format("%1$s %2$s", order.getProperty(), order.getDirection())); } - return new QueryString(String.format("%1$s %2$s", asDistinct(this.query), orderByClause.toString())); + return new QueryString(String.format("%1$s %2$s", asDistinct(getQuery()), orderByClause.toString())); } return this; } - private boolean hasSort(Sort sort) { + /** + * Null-safe method to determine whether the {@link Sort} is valid (i.e. has been specified by the caller). + * + * @param sort {@link Sort} to evaluate. + * @return a boolean value indicating whether the {@link Sort} is valid. + * @see org.springframework.data.domain.Sort + */ + private boolean hasSort(@Nullable Sort sort) { return sort != null && sort.iterator().hasNext(); } @@ -289,7 +420,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(@NonNull String... hints) { + public @NonNull QueryString withHints(@NonNull String... hints) { if (!ObjectUtils.isEmpty(hints)) { @@ -300,7 +431,7 @@ public class QueryString { builder.append(String.format("'%s'", hint)); } - return QueryString.of(String.format(HINTS_OQL_TEMPLATE, builder.toString(), this.query)); + return QueryString.of(String.format(HINTS_OQL_TEMPLATE, builder.toString(), getQuery())); } return this; @@ -312,10 +443,10 @@ 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(@NonNull String importExpression) { + public @NonNull QueryString withImport(@NonNull String importExpression) { return StringUtils.hasText(importExpression) - ? QueryString.of(String.format(IMPORT_OQL_TEMPLATE, importExpression, this.query)) + ? QueryString.of(String.format(IMPORT_OQL_TEMPLATE, importExpression, getQuery())) : this; } @@ -325,10 +456,10 @@ 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(@NonNull Integer limit) { + public @NonNull QueryString withLimit(@NonNull Integer limit) { return limit != null - ? QueryString.of(String.format(LIMIT_OQL_TEMPLATE, this.query, limit)) + ? QueryString.of(String.format(LIMIT_OQL_TEMPLATE, getQuery(), limit)) : this; } @@ -337,19 +468,21 @@ public class QueryString { * * @return a new {@link QueryString} with tracing enabled. */ - public QueryString withTrace() { - return QueryString.of(String.format(TRACE_OQL_TEMPLATE, this.query)); + public @NonNull QueryString withTrace() { + return QueryString.of(String.format(TRACE_OQL_TEMPLATE, getQuery())); } /** * Returns a {@link String} representation of this {@link QueryString}. * + * Returns the complete {@link String OQL query statement}. + * * @return a {@link String} representation of this {@link QueryString}. * @see java.lang.Object#toString() * @see java.lang.String */ @Override public String toString() { - return this.query; + return getQuery(); } } 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 47593691..b41a6d15 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 @@ -17,20 +17,29 @@ package org.springframework.data.gemfire.repository.query; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import org.apache.geode.cache.query.SelectResults; import org.springframework.dao.IncorrectResultSizeDataAccessException; +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.GemfireTemplate; import org.springframework.data.gemfire.repository.Query; +import org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor; +import org.springframework.data.gemfire.repository.query.support.PagingUtils; +import org.springframework.data.gemfire.repository.query.support.TemplateBasedOqlQueryExecutor; 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.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,6 +48,17 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author David Turanski * @author John Blum + * @see org.apache.geode.cache.query.SelectResults + * @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.Query + * @see org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery + * @see org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor + * @see org.springframework.data.repository.Repository + * @see org.springframework.data.repository.query.QueryMethod + * @see org.springframework.data.repository.query.RepositoryQuery */ @SuppressWarnings("unused") public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { @@ -49,6 +69,9 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { private final GemfireTemplate template; + private final OqlQueryExecutor nonPagedQueryExecutor; + private final OqlQueryExecutor pagedQueryExecutor; + private final QueryString query; /** @@ -57,6 +80,8 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { StringBasedGemfireRepositoryQuery() { this.query = null; + this.nonPagedQueryExecutor = (queryMethod, query, arguments) -> null; + this.pagedQueryExecutor = (queryMethod, query, arguments) -> null; this.template = null; register(ProvidedQueryPostProcessors.LIMIT @@ -91,6 +116,12 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { this.query = QueryString.of(query); this.template = template; + this.nonPagedQueryExecutor = new TemplateBasedOqlQueryExecutor(template); + + this.pagedQueryExecutor = new SmartPagedOqlQueryExecutor(template) + .thenExecuteWith(new TwoPhasePagedOqlQueryExecutor(template) + .thenExecuteWith(new TemplateBasedOqlQueryExecutor(template))); + register(ProvidedQueryPostProcessors.LIMIT .processBefore(ProvidedQueryPostProcessors.IMPORT) .processBefore(ProvidedQueryPostProcessors.HINT) @@ -150,6 +181,30 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { return this.userDefinedQuery; } + /** + * Returns the configured {@link OqlQueryExecutor} (strategy) used to execute Apache Geode + * {@link Page non-paged} {@link String OQL queries}. + * + * @return the configured {@link OqlQueryExecutor} (strategy) used to execute Apache Geode + * {@link Page non-paged} {@link String OQL queries}. + * @see org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor + */ + protected @NonNull OqlQueryExecutor getNonPagedQueryExecutor() { + return this.nonPagedQueryExecutor; + } + + /** + * Returns the configured {@link OqlQueryExecutor} (strategy) used to execute Apache Geode + * {@link Page paged} {@link String OQL queries}. + * + * @return the configured {@link OqlQueryExecutor} (strategy) used to execute Apache Geode + * {@link Page paged} {@link String OQL queries}. + * @see org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor + */ + protected @NonNull OqlQueryExecutor getPagedQueryExecutor() { + return this.pagedQueryExecutor; + } + /** * Returns a reference to the {@link QueryString managed query}. * @@ -178,20 +233,42 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { QueryMethod queryMethod = getQueryMethod(); - QueryString query = preProcess(queryMethod, getQuery(), arguments); + QueryString query = getQuery(); + + String preparedQuery = prepareQuery(queryMethod, query, arguments); + + SelectResults selectResults = + resolveOqlQueryExecutor(queryMethod).execute(queryMethod, preparedQuery, arguments); + + return processQueryResults(queryMethod, selectResults, arguments); + } + + /** + * Prepares the OQL query statement to execute. + * + * @param queryMethod {@link QueryMethod} modeling the OQL query. + * @param query {@link QueryString} containing the OQL query statement. + * @param arguments array of {@link Object} values containing the arguments for the OQL query bind in parameters. + * @return the {@literal prepared} OQL query to execute. + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor + * @see org.springframework.data.gemfire.repository.query.QueryString + * @see org.springframework.data.repository.query.QueryMethod + * @see #bindInParameters(QueryMethod, QueryString, Object[]) + * @see #resolveFromClause(QueryMethod, QueryString) + * @see #getQueryPostProcessor() + */ + protected @NonNull String prepareQuery(@NonNull QueryMethod queryMethod, @NonNull QueryString query, + @NonNull Object[] arguments) { + + query = bindInParameters(queryMethod, resolveFromClause(queryMethod, query), arguments); String queryString = query.toString(); String processedQueryString = getQueryPostProcessor().postProcess(queryMethod, queryString, arguments); - SelectResults selectResults = getTemplate().find(processedQueryString, arguments); - - return postProcess(queryMethod, selectResults); + return processedQueryString; } - QueryString preProcess(QueryMethod queryMethod, QueryString query, Object[] arguments) { - - query = isUserDefinedQuery() ? query - : query.fromRegion(queryMethod.getEntityInformation().getJavaType(), getTemplate().getRegion()); + private QueryString bindInParameters(QueryMethod queryMethod, QueryString query, Object[] arguments) { ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor(queryMethod.getParameters(), arguments); @@ -203,13 +280,53 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { return query; } - Object postProcess(QueryMethod queryMethod, SelectResults selectResults) { + private QueryString resolveFromClause(QueryMethod queryMethod, QueryString query) { - Collection collection = toCollection(selectResults); + return isUserDefinedQuery() ? query + : query.fromRegion(getTemplate().getRegion(), queryMethod.getEntityInformation().getJavaType()); + } + + /** + * Resolves the {@link OqlQueryExecutor} used to execute the {@link String OQL query statement} modeled by + * the given {@link QueryMethod}. + * + * @param queryMethod {@link QueryMethod} used to resolve the {@link OqlQueryExecutor}; must not be {@literal null}. + * @return the resolve {@link OqlQueryExecutor} appropriate for executing the {@link String OQL query statement} + * modeled by the give {@link QueryMethod}. + * @see org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor + * @see org.springframework.data.repository.query.QueryMethod + */ + protected @NonNull OqlQueryExecutor resolveOqlQueryExecutor(@NonNull QueryMethod queryMethod) { + + return PagingUtils.isPagingPresent(queryMethod) + ? getPagedQueryExecutor() + : getNonPagedQueryExecutor(); + } + + /** + * Processes the OQL query {@link SelectResults result set}. + * + * @param queryMethod {@link QueryMethod} modeling the OQL query. + * @param selectResults {@link SelectResults} from the execution of the OQL query. + * @return the OQL query results. + * @throws IncorrectResultSizeDataAccessException if the query result does not match + * the {@link QueryMethod} {@link Class return type}. + * @throws IllegalStateException if the OQL query is not supported based on the return value. + * @see org.springframework.data.repository.query.QueryMethod + * @see org.apache.geode.cache.query.SelectResults + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected @Nullable Object processQueryResults(@NonNull QueryMethod queryMethod, + @NonNull SelectResults selectResults, @NonNull Object... arguments) { + + Collection collection = toCollection(selectResults); if (queryMethod.isCollectionQuery()) { return collection; } + else if (queryMethod.isPageQuery()) { + return new PageImpl(new ArrayList<>(collection), PagingUtils.getPageRequest(queryMethod, arguments), Integer.MAX_VALUE); + } else if (queryMethod.isQueryForEntity()) { if (collection.isEmpty()) { return null; @@ -229,8 +346,7 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { } } - @SuppressWarnings("all") - boolean isSingleNonEntityResult(QueryMethod method, Collection result) { + private boolean isSingleNonEntityResult(QueryMethod method, Collection result) { Class methodReturnType = method.getReturnedObjectType(); @@ -252,21 +368,14 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @see org.springframework.util.CollectionUtils#arrayToList(Object) * @see org.apache.geode.cache.query.SelectResults */ - Collection toCollection(Object source) { + @SuppressWarnings("rawtypes") + @NonNull Collection toCollection(@Nullable Object source) { - if (source instanceof SelectResults) { - return ((SelectResults) source).asList(); - } - - if (source instanceof Collection) { - return (Collection) source; - } - - if (source == null) { - return Collections.emptyList(); - } - - return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singletonList(source); + return source == null ? Collections.emptyList() + : source instanceof SelectResults ? ((SelectResults) source).asList() + : source instanceof Collection ? (Collection) source + : source.getClass().isArray() ? CollectionUtils.arrayToList(source) + : Collections.singletonList(source); } @SuppressWarnings("rawtypes") @@ -344,4 +453,281 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { } } } + + /** + * A {@link SimplePagedOqlQueryExecutor} implementation that implements a paged OQL query statement + * using a 2-phase approach. + * + * The first phase executes a {@literal keys query} (or OQL query for keys) satisfying the user's defined + * OQL query predicate(s) specified in the {@literal WHERE} clause, {@link Sort sorted} according to + * the user-defined {@literal ORDER BY} clause. + * + * The returned keys are then filtered by the {@link Pageable requested page}. + * + * Then, in the second (and final) phase, the keys returned by the {@literal keys query} are used as a predicate + * in the user's original OQL query statement to limit the results returned to exactly those keys satisfying + * the {@link Pageable requested page}. + * + * @see SimplePagedOqlQueryExecutor + */ + static class TwoPhasePagedOqlQueryExecutor extends SimplePagedOqlQueryExecutor { + + /** + * Constructs a new instance of {@link TwoPhasePagedOqlQueryExecutor} initialized with the given, required + * {@link GemfireTemplate}. + * + * @param template {@link GemfireTemplate} used to execute Apache Geode OQL queries; must not be {@literal null}. + * @throws IllegalArgumentException if {@link GemfireTemplate} is {@literal null}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + TwoPhasePagedOqlQueryExecutor(@NonNull GemfireTemplate template) { + super(template); + } + + /** + * Simply return the {@link SelectResults} as is. + * + * The {@link SelectResults} were already limited to the {@link Pageable requested page} in this 2-phased + * paged query implementation. + * + * @param selectResults {@link SelectResults} to process; must not be {@literal null}. + * @param pageRequest {@link Pageable} object encapsulating the details of the {@link Page requested page}; + * must not be {@literal null}. + * @return the {@link SelectResults} as is. + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.domain.Pageable + */ + @Override + @SuppressWarnings("rawtypes") + protected SelectResults processPagedQueryResults(SelectResults selectResults, Pageable pageRequest) { + //return selectResults; + return super.processPagedQueryResults(selectResults, pageRequest); + } + } + + /** + * A {@literal smart} {@link PageLimitingOqlQueryExecutor} implementation that looks ahead at + * the {@link Pageable requested page}, and if the user requested page on or the number of results needed + * to satisfy the contents of the page are withing a pre-defined, configurable/tunable threshold, then the limited + * {@link SelectResults OQL query result set} is returned. + * + * Alternatively, a 2-phased paged query can be used when the page number or page size is relatively large. Another + * consideration is the size of the objects returned in the {@link SelectResults OQL query result set}. + * + * @see PageLimitingOqlQueryExecutor + */ + class SmartPagedOqlQueryExecutor extends PageLimitingOqlQueryExecutor { + + // TODO enable this parameter to be configurable/tunable using the Spring Environment + final int PAGED_QUERY_RESULT_SET_LIMIT_THRESHOLD = + Integer.getInteger("spring.data.gemfire.query.limit.threshold", 101); + + /** + * Constructs a new instance of {@link SmartPagedOqlQueryExecutor} initialized with the given, required + * {@link GemfireTemplate}. + * + * @param template {@link GemfireTemplate} used to execute Apache Geode OQL queries; must not be {@literal null}. + * @throws IllegalArgumentException if {@link GemfireTemplate} is {@literal null}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + SmartPagedOqlQueryExecutor(@NonNull GemfireTemplate template) { + super(template); + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("rawtypes") + protected SelectResults doExecute(Pageable pageRequest, QueryMethod queryMethod, String query, + Object... arguments) { + + if (isExecutable(pageRequest)) { + return super.doExecute(pageRequest, queryMethod, query, arguments); + } + + throw newUnsupportedQueryExecutionException(query); + } + + /** + * Determines whether this {@link OqlQueryExecutor} can execute the {@link String OQL query} directly (as is). + * + * Just execute the OQL query as is, bypassing the 2-phase implementation, if the requested page + * is the first page or the query result set size would be less than the threshold (e.g. 101 results). + * Dynamically supports 10 pages if page size were 10, 5 pages if page size were 20, 4 pages if page size + * were 25, 2 pages if page size were 50 and so on. + * + * @param pageRequest {@link Pageable} object encapsulating the details of the requested page. + * @return a boolean value indicating whether the original {@link String OQL query} should be executed directly, + * bypassing the 2-phase implementation. + * @see org.springframework.data.domain.Pageable + */ + protected boolean isExecutable(@NonNull Pageable pageRequest) { + + return PagingUtils.isPageOne(pageRequest) + || PagingUtils.getQueryResultSetLimitForPage(pageRequest) < PAGED_QUERY_RESULT_SET_LIMIT_THRESHOLD; + } + } + + /** + * A {@link SimplePagedOqlQueryExecutor} implementation that applies a {@literal LIMIT} to + * the {@link String OQL query statement} based on the {@link Pageable requested page} in order to limit + * the {@link SelectResults query result set}, or number of {@link Object results}, returned by + * the {@link String OQL query}. + * + * @see SimplePagedOqlQueryExecutor + */ + class PageLimitingOqlQueryExecutor extends SimplePagedOqlQueryExecutor { + + /** + * Constructs a new instance of {@link PageLimitingOqlQueryExecutor} initialized with the given, required + * {@link GemfireTemplate}. + * + * @param template {@link GemfireTemplate} used to execute Apache Geode OQL queries; must not be {@literal null}. + * @throws IllegalArgumentException if {@link GemfireTemplate} is {@literal null}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + PageLimitingOqlQueryExecutor(@NonNull GemfireTemplate template) { + super(template); + } + + /** + * @inheritDoc + */ + @Override + protected String preparePagedQuery(String query, Pageable pageRequest) { + + String pagedQuery = super.preparePagedQuery(query, pageRequest); + + QueryString pagedQueryString = QueryString.of(pagedQuery); + + int pagedQueryResultSetLimit = PagingUtils.getQueryResultSetLimitForPage(pageRequest); + + if (pagedQueryString.isLimited()) { + + int queryLimit = pagedQueryString.getLimit(); + + if (pagedQueryResultSetLimit < queryLimit) { + pagedQueryString = pagedQueryString.adjustLimit(pagedQueryResultSetLimit); + } + else { + + int startIndex = PagingUtils.getQueryResultSetStartIndexForPage(pageRequest); + + Assert.state(queryLimit > startIndex, + () -> String.format("The user-defined OQL query result set LIMIT [%d] must be greater than the requested page offset [%d]", + queryLimit, startIndex)); + + int endIndex = PagingUtils.getQueryResultSetEndIndexForPage(pageRequest); + + if (queryLimit < endIndex) { + getLogger().warn(String.format("The requested page ending at index [%d] may be truncated by the user-defined OQL query result set LIMIT [%d]", + endIndex, queryLimit)); + } + } + } + else { + pagedQueryString = pagedQueryString.withLimit(pagedQueryResultSetLimit); + } + + return pagedQueryString.toString(); + } + } + + /** + * Abstract base class for {@link Page paged} OQL queries and {@link OqlQueryExecutor} implementations. + * + * This base class implementation simply returns the entire/full (i.e. non-limited) {@link SelectResults OQL query result set} + * and then performs the paging logic to extract subsets of the results based on the {@link Page requested page}. + * + * @see org.springframework.data.gemfire.repository.query.support.TemplateBasedOqlQueryExecutor + */ + static abstract class SimplePagedOqlQueryExecutor extends TemplateBasedOqlQueryExecutor { + + /** + * Constructs a new instance of {@link SimplePagedOqlQueryExecutor} initialized with the given, required + * {@link GemfireTemplate}. + * + * @param template {@link GemfireTemplate} used to execute Apache Geode OQL queries; must not be {@literal null}. + * @throws IllegalArgumentException if {@link GemfireTemplate} is {@literal null}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + SimplePagedOqlQueryExecutor(@NonNull GemfireTemplate template) { + super(template); + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("rawtypes") + public @NonNull SelectResults execute(@NonNull QueryMethod queryMethod, @NonNull String query, + @NonNull Object... arguments) { + + if (PagingUtils.isPagingPresent(queryMethod)) { + + Pageable pageRequest = PagingUtils.getPageRequest(queryMethod, arguments); + + return doExecute(pageRequest, queryMethod, query, arguments); + } + + throw newUnsupportedQueryExecutionException(query); + } + + /** + * Executes the {@link String OQL query statement}. + * + * @param pageRequest {@link Pageable} object encapsulating the details of the {@link Page requested paged}; + * must not be {@literal null}. + * @param queryMethod {@link QueryMethod} modeling the {@link String OQL query statement} to be executed; + * must not be {@literal null}. + * @param query {@link String} containing the OQL query statement; + * must not be {@literal null} or {@literal empty}. + * @param arguments array of {@link Object arguments} passed to the placeholders in + * the {@link String OQL query statement}. + * @return the {@link SelectResults} from executing the {@link String OQL query statement}. + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.repository.query.QueryMethod + */ + @SuppressWarnings("rawtypes") + protected SelectResults doExecute(@NonNull Pageable pageRequest, @NonNull QueryMethod queryMethod, + @NonNull String query, @NonNull Object... arguments) { + + String preparedQuery = preparePagedQuery(query, pageRequest); + + SelectResults selectResults = super.execute(queryMethod, preparedQuery, arguments); + + return processPagedQueryResults(selectResults, pageRequest); + } + + /** + * Prepares the required {@link String OQL query statement} as a paged query. + * + * @param query {@link String} containing the OQL query statement to prepare. + * @param pageRequest {@link Pageable} object containing the details of the {@link Page requested page}. + * @return the prepared {@link String OQL query}. + * @see org.springframework.data.domain.Pageable + */ + protected @NonNull String preparePagedQuery(String query, Pageable pageRequest) { + return query; + } + + /** + * Processes the {@link SelectResults} as a {@link Page paged} query result set. + * + * @param selectResults {@link SelectResults} to process; must not be {@literal null}. + * @param pageRequest {@link Pageable} object encapsulating the details of the {@link Page requested page}; + * must not be {@literal null}. + * @return the processed {@link SelectResults}. + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.domain.Pageable + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected @NonNull SelectResults processPagedQueryResults(@NonNull SelectResults selectResults, + @NonNull Pageable pageRequest) { + + return new PagedSelectResults(selectResults, pageRequest); + } + } } 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 ae4258e8..694637ce 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 @@ -14,17 +14,16 @@ * limitations under the License. * */ - package org.springframework.data.gemfire.repository.query.support; import org.springframework.util.StringUtils; /** - * The OqlKeyword enum represents the range of keywords (Reserved Words) - * in GemFire's Object Query Language (OQL). + * The {@link OqlKeyword} enum represents the complete set of keywords (Reserved Words) + * in Apache Geode's Object Query Language (OQL). * * @author John Blum - * @see Supported Keywords + * @see Supported Keywords * @since 1.0.0 */ public enum OqlKeyword { diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutor.java new file mode 100644 index 00000000..12224af1 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutor.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +import org.apache.geode.cache.query.SelectResults; + +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.NonNull; + +/** + * A Strategy interface for executing Apache Geode OQL queries (e.g. {@literal SELECT} statements). + * + * @author John Blum + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.repository.query.QueryMethod + * @since 2.4.0 + */ +@FunctionalInterface +public interface OqlQueryExecutor { + + String NON_EXECUTABLE_QUERY_MESSAGE = "OQL query [%1$s] is not executable by this executor [%2$s]"; + + /** + * Executes the given {@link String OQL query}. + * + * @param queryMethod {@link QueryMethod} modeling the OQl query. + * @param query {@link String} containing the Apache Geode OQL query. + * @param arguments array of {@link Object arguments} used for the bind in OQL query parameters. + * @return the {@link SelectResults OQL query result set}. + * @throws UnsupportedQueryExecutionException if this {@link OqlQueryExecutor} cannot execute (i.e. handle) + * the OQL query. + * @see org.springframework.data.repository.query.QueryMethod + * @see org.apache.geode.cache.query.SelectResults + */ + @SuppressWarnings("rawtypes") + SelectResults execute(QueryMethod queryMethod, String query, Object... arguments); + + /** + * Constructs a new instance of {@link UnsupportedQueryExecutionException} initialized with a canned message + * containing the given OQL query that could not be executed by the {@link OqlQueryExecutor} implementation. + * + * @param query {@link String OQL query} that could not be executed. + * @return a new {@link UnsupportedQueryExecutionException}. + * @see org.springframework.data.gemfire.repository.query.support.UnsupportedQueryExecutionException + */ + default UnsupportedQueryExecutionException newUnsupportedQueryExecutionException(String query) { + return new UnsupportedQueryExecutionException(String.format(NON_EXECUTABLE_QUERY_MESSAGE, query, + this.getClass().getName())); + } + + /** + * Null-safe composition method to {@literal compose} {@literal this} {@link OqlQueryExecutor} with + * the given {@link OqlQueryExecutor}. + * + * {@link OqlQueryExecutor} implementations should be {@literal composed} in an order that is most suitable to + * the execution of the OQL query first. Meaning, the outer most {@link OqlQueryExecutor} should be the most + * suitable {@link OqlQueryExecutor} to execute the given OQL query followed by the next most suitable + * {@link OqlQueryExecutor} in the composition (i.e. chain) and so on until the OQL query is either successfully + * executed (handled) or the composition is exhausted, in which case, an {@link Exception} could be thrown. + * + * If an {@link OqlQueryExecutor is unable to execute, or handle, the given OQL query, then it must throw + * an {@link UnsupportedQueryExecutionException } to triggger the next {@link OqlQueryExecutor} in the composition. + * + * @param queryExecutor {@link OqlQueryExecutor} to compose with this {@link OqlQueryExecutor}; + * must not be {@literal null}. + * @return a composed {@link OqlQueryExecutor} consisting of this {@link OqlQueryExecutor} composed with + * the given {@link OqlQueryExecutor}. If the {@link OqlQueryExecutor} is {@literal null}, then this method + * returns this {@link OqlQueryExecutor}. + * @see Composite Software Design Pattern + */ + default OqlQueryExecutor thenExecuteWith(@NonNull OqlQueryExecutor queryExecutor) { + + return queryExecutor == null ? this + : (queryMethod, query, arguments) -> { + try { + return this.execute(queryMethod, query, arguments); + } + catch (UnsupportedQueryExecutionException cause) { + return queryExecutor.execute(queryMethod, query, arguments); + } + }; + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/PagingUtils.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/PagingUtils.java new file mode 100644 index 00000000..73543aa0 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/PagingUtils.java @@ -0,0 +1,295 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Utility class used to work with {@link Collection}, {@link Page} and {@link Pageable} objects. + * + * @author John Blum + * @see java.util.Collection + * @see org.springframework.data.domain.Page + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.repository.support.PageableExecutionUtils + * @since 2.4.0 + */ +public abstract class PagingUtils { + + public static final String INVALID_PAGE_NUMBER_MESSAGE = "Page Number [%d] must be greater than equal to 0"; + public static final String INVALID_PAGE_SIZE_MESSAGE = "Page Size [%d] must be greater than equal to 1"; + public static final String NON_NULL_PAGEABLE_MESSAGE = "Pageable must not be null"; + + protected static final Function DEFAULT_IS_PAGE_QUERY_FUNCTION = QueryMethod::isPageQuery; + + static Function isPageQueryFunction = DEFAULT_IS_PAGE_QUERY_FUNCTION; + + static Function hasPageableParameterFunction = queryMethod -> + Optional.ofNullable(queryMethod) + .map(QueryMethod::getParameters) + .filter(Parameters::hasPageableParameter) + .isPresent(); + + /** + * Asserts that the {@link Pageable} object is valid. + * + * @param pageable {@link Pageable} object to evaluate. + * @throws IllegalArgumentException if {@link Pageable} is {@literal null} or page number is less than {@literal 0} + * or the page size is less than {@literal 1}. + * @see org.springframework.data.domain.Pageable + */ + public static void assertPageable(@NonNull Pageable pageable) { + + Assert.notNull(pageable, NON_NULL_PAGEABLE_MESSAGE); + + int pageNumber = pageable.getPageNumber(); + + Assert.isTrue(pageNumber >= 0, () -> String.format(INVALID_PAGE_NUMBER_MESSAGE, pageNumber)); + + int pageSize = pageable.getPageSize(); + + Assert.isTrue(pageSize > 0, () -> String.format(INVALID_PAGE_SIZE_MESSAGE, pageSize)); + } + + /** + * Null-safe method to determine whether the given {@link Pageable page request} is for page one. + * + * @param pageable {@link Pageable page request} to evaluate. + * @return a boolean value indicating whether the given {@link Pageable page request} is for page one. + * @see org.springframework.data.domain.Pageable + */ + public static boolean isPageOne(@NonNull Pageable pageable) { + return pageable != null && pageable.getPageNumber() == 0; + } + + /** + * Null-safe method used to determine whether the given {@link QueryMethod} represents (models) a paged query. + * + * @param queryMethod {@link QueryMethod} to evaluate for paging. + * @return a boolean value indicating whether the given {@link QueryMethod} represents (models) a paged query. + * @see org.springframework.data.repository.query.QueryMethod + */ + public static boolean isPagingPresent(@Nullable QueryMethod queryMethod) { + + return queryMethod != null + && (isPageQueryFunction.apply(queryMethod) + || hasPageableParameterFunction.apply(queryMethod)); + } + + /** + * Gets a {@literal page} from the given {@link List}. + * + * This method is {@literal null-safe}, and guards against a {@literal null} {@link List} and {@link Pageable}. + * + * @param {@link Class type} of the {@link List} elements; must not be {@literal null}. + * @param list {@link List} from which to extract a page of elements; must not be {@literal null}. + * @param pageable {@link Pageable} object encapsulating the details for the page requested. + * @return a {@link List sub-List} containing the contents for the requested page. + * @see #getQueryResultSetStartIndexForPage(Pageable) + * @see #getQueryResultSetEndIndexForPage(Pageable) + * @see org.springframework.data.domain.Pageable + * @see java.util.List + */ + public static @NonNull List getPagedList(@NonNull List list, @NonNull Pageable pageable) { + + list = CollectionUtils.nullSafeList(list); + + int total = list.size(); + int startIndex = getQueryResultSetStartIndexForPage(pageable); + int endIndex = getQueryResultSetEndIndexForPage(pageable); + + return list.isEmpty() || total <= startIndex + ? Collections.emptyList() + : list.subList(startIndex, Math.min(total, endIndex)); + } + + /** + * Finds the {@link Pageable page request} argument from an array of arguments passed to + * the given {@link QueryMethod}. + * + * @param queryMethod invoked {@link QueryMethod}; must not be {@literal null}. + * @param arguments array of {@link Object arguments} passed to the {@link QueryMethod}; + * must not be {@literal null}. + * @return the {@link Pageable page request} argument in the array of {@link Object arguments} + * passed to the {@link QueryMethod}. + * @throws IllegalArgumentException if {@link QueryMethod} is {@literal null}, or the {@link QueryMethod} parameter + * count is not equal to the argument count, or the indexed {@link QueryMethod} argument is not an instance of + * {@link Pageable}. + * @throws IllegalStateException if the {@link QueryMethod} does not have a {@link Pageable} parameter. + * @see org.springframework.data.repository.query.QueryMethod + * @see org.springframework.data.domain.Pageable + */ + public static @NonNull Pageable getPageRequest(@NonNull QueryMethod queryMethod, @NonNull Object... arguments) { + + Assert.notNull(queryMethod, "QueryMethod must not be null"); + + Parameters queryMethodParameters = queryMethod.getParameters(); + + Assert.state(queryMethodParameters.hasPageableParameter(), + () -> String.format("QueryMethod [%s] does not have a Pageable parameter", queryMethod)); + + arguments = ArrayUtils.nullSafeArray(arguments, Object.class); + + long queryMethodArgumentCount = arguments.length; + long queryMethodParameterCount = queryMethodParameters.stream().count(); + + Assert.isTrue(queryMethodArgumentCount == queryMethodParameterCount, + () -> String.format("The number of arguments [%d] must match the number of QueryMethod [%s] parameters [%d]", + queryMethodArgumentCount, queryMethod, queryMethodParameterCount)); + + int pageableIndex = queryMethodParameters.getPageableIndex(); + + Object pageableArgument = arguments[pageableIndex]; + + Assert.isInstanceOf(Pageable.class, pageableArgument, + () -> String.format("Argument [%1$s] must be of type [%2$s]", pageableArgument, Pageable.class.getName())); + + return (Pageable) pageableArgument; + } + + /** + * Null-safe method used to determine the starting index in the query result set for populating the content + * of the {@link Page}. + * + * @param pageable {@link Pageable} object encapsulating the details of the requested {@link Page}. + * @return the start index in the query result set to populate the content of the {@link Page}. + * @see org.springframework.data.domain.Pageable + * @see #getQueryResultSetEndIndexForPage(Pageable) + */ + public static int getQueryResultSetStartIndexForPage(@Nullable Pageable pageable) { + return pageable != null ? pageable.getPageNumber() * pageable.getPageSize() : 0; + } + + /** + * Null-safe method used to determine the end index in the query result set for populating the content + * of the {@link Page}. + * + * @param pageable {@link Pageable} object encapsulating the details of the requested {@link Page}. + * @return the end index in the query result set to populate the content of the {@link Page}. + * @see org.springframework.data.domain.Pageable + * @see #getQueryResultSetStartIndexForPage(Pageable) + */ + public static int getQueryResultSetEndIndexForPage(@Nullable Pageable pageable) { + return pageable != null ? getQueryResultSetStartIndexForPage(pageable) + pageable.getPageSize() : 0; + } + + /** + * Null-safe method used to determine the maximum results that would be returned by a query + * given the {@link Pageable} object specifying the requested {@link Page}. + * + * @param pageable {@link Pageable} object encapsulating the details of the requested {@link Page}. + * @return the maximum results that would be returned by a query given the {@link Pageable} object + * specifying the requested {@link Page}. + * @see org.springframework.data.domain.Pageable + * @see #normalizePageNumber(Pageable) + */ + public static int getQueryResultSetLimitForPage(@Nullable Pageable pageable) { + return pageable != null ? normalizePageNumber(pageable) * pageable.getPageSize() : 0; + } + + /** + * Null-safe method used to normalize 0 index based page numbers (i.e. 0, 1, 2, ...) to natural page numbers + * (i.e. 1, 2, 3, ...) using the given {@link Page}. + * + * @param page {@link Page} used to determine the page number to normalize. + * @return the normalized page number from the 0 index based page number. + * @see org.springframework.data.domain.Page + * @see #normalize(int) + */ + public static int normalizePageNumber(@Nullable Page page) { + return page != null ? normalize(page.getNumber()) : 0; + } + + /** + * Null-safe method used to normalize 0 index based page numbers (i.e. 0, 1, 2, ...) to natural page numbers + * (i.e. 1, 2, 3, ...) using the given {@link Pageable}. + * + * @param pageable {@link Pageable} used to determine the page number to normalize. + * @return the normalized page number from the 0 index based page number. + * @see org.springframework.data.domain.Pageable + * @see #normalize(int) + */ + public static int normalizePageNumber(@Nullable Pageable pageable) { + return pageable != null ? normalize(pageable.getPageNumber()) : 0; + } + + /** + * Normalizes 0 index based page numbers (i.e. 0, 1, 2, ...) to natural page numbers (i.e. 1, 2, 3, ...). + * + * @param pageNumber The {@link Integer#TYPE page number} to normalize. + * @return the normalized page number from the 0 index based page number. + */ + protected static int normalize(int pageNumber) { + return Math.max(pageNumber, -1) + 1; + } + + /** + * Null-safe method to determine the size (number of elements) of the {@link Iterable}. + * + * The {@link Iterable} object may be an array, a {@link Collection} or simply a stream backing, + * pure {@link Iterable} object. + * + * @param iterable {@link Iterable} object to evaluate. + * @return the size (number of elements) contained by the {@link Iterable} object. If the {@link Iterable} object + * is {@literal null}, then this method will return {@literal 0}. + * @see java.lang.Iterable + */ + protected static long nullSafeSize(@Nullable Iterable iterable) { + + return iterable == null ? 0 + : iterable instanceof Collection ? ((Collection) iterable).size() + : StreamSupport.stream(iterable.spliterator(), false).count(); + } + + /** + * Gets a {@link Page} view from the given {@link List} based on the {@link Pageable} object (page request). + * + * @param {@link Class type} of the {@link List} elements. + * @param list {@link List} of content from which to extract a {@link Page}; must not be {@literal null}. + * @param pageable {@link Pageable} object encapsulating the details of the {@link Page} requested; + * must not be {@literal null}. + * @return a {@literal non-null} {@link Page} view from the given {@link List} based on the {@link Pageable} object + * (page request). + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.domain.Page + * @see #getPagedList(List, Pageable) + * @see java.util.List + */ + public static @NonNull Page toPage(@NonNull List list, @NonNull Pageable pageable) { + + List pagedList = getPagedList(list, pageable); + + return pagedList.isEmpty() + ? Page.empty() + : new PageImpl<>(pagedList, pageable, nullSafeSize(list)); + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutor.java new file mode 100644 index 00000000..e58122e6 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutor.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +import org.apache.geode.cache.query.SelectResults; + +import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * {@link OqlQueryExecutor} implementation using SDG's {@link GemfireTemplate} to execute Apache Geode + * {@link String OQL queries}. + * + * @author John Blum + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor + * @see org.springframework.data.repository.query.QueryMethod + * @since 2.4.0 + */ +public class TemplateBasedOqlQueryExecutor implements OqlQueryExecutor { + + private final GemfireTemplate template; + + /** + * Constructs a new instance of {@link TemplateBasedOqlQueryExecutor} initialized with the given, + * required {@link GemfireTemplate} used to execute Apache Geode {@link String OQL queries}. + * + * @param template {@link GemfireTemplate} used to execute Apache Geode {@link String OQL queries}; + * must not be {@literal null}. + * @throws IllegalArgumentException if {@link GemfireTemplate} is {@literal null}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + public TemplateBasedOqlQueryExecutor(@NonNull GemfireTemplate template) { + + Assert.notNull(template, "GemfireTemplate must not be null"); + + this.template = template; + } + + /** + * Gets the configured {@link GemfireTemplate} used to execute Apache Geode {@link String OQL queries}. + * + * @return the configured {@link GemfireTemplate} used to execute Apache Geode {@link String OQL queries}. + * @see org.springframework.data.gemfire.GemfireTemplate + */ + protected @NonNull GemfireTemplate getTemplate() { + return this.template; + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("rawtypes") + public SelectResults execute(QueryMethod queryMethod, @NonNull String query, @NonNull Object... arguments) { + return getTemplate().find(query, arguments); + } +} diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/UnsupportedQueryExecutionException.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/UnsupportedQueryExecutionException.java new file mode 100644 index 00000000..7e54ea89 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/repository/query/support/UnsupportedQueryExecutionException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +/** + * A Java {@link RuntimeException} indicating that the Apache Geode OQL query could not be executed (i.e. handled) + * by the {@link OqlQueryExecutor}. + * + * @author John Blum + * @see java.lang.RuntimeException + * @since 2.4.0 + */ +@SuppressWarnings("unused") +public class UnsupportedQueryExecutionException extends RuntimeException { + + /** + * Constructs a new, uninitialized instance of {@link UnsupportedQueryExecutionException}. + */ + public UnsupportedQueryExecutionException() { } + + /** + * Constructs a new instance of {@link UnsupportedQueryExecutionException} initialized with + * the given {@link String message} describing the exception. + * + * @param message {@link String} containing a description of the exception. + * @see java.lang.String + */ + public UnsupportedQueryExecutionException(String message) { + super(message); + } + + /** + * Constructs a new instance of {@link UnsupportedQueryExecutionException} initialized with + * the given {@link Throwable} as the underlying {@literal cause} of this exception. + * + * @param cause {@link Throwable} used as the cause of this exception. + * @see java.lang.Throwable + */ + public UnsupportedQueryExecutionException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new instance of {@link UnsupportedQueryExecutionException} initialized with + * the given {@link String message} describing the exception and given {@link Throwable} + * as the underlying {@literal cause} of this exception. + * + * @param message {@link String} containing a description of the exception. + * @param cause {@link Throwable} used as the cause of this exception. + * @see java.lang.String + * @see java.lang.Throwable + */ + public UnsupportedQueryExecutionException(String message, Throwable cause) { + super(message, cause); + } +} 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 d9581539..37d034b7 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 @@ -32,7 +32,6 @@ 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; @@ -40,6 +39,7 @@ import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.gemfire.repository.GemfireRepository; import org.springframework.data.gemfire.repository.Wrapper; import org.springframework.data.gemfire.repository.query.QueryString; +import org.springframework.data.gemfire.repository.query.support.PagingUtils; import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.data.gemfire.util.SpringUtils; import org.springframework.data.repository.CrudRepository; @@ -248,7 +248,7 @@ public class SimpleGemfireRepository implements GemfireRepository public @NonNull Iterable findAll(@NonNull Sort sort) { QueryString query = QueryString.of("SELECT * FROM /RegionPlaceholder") - .fromRegion(getEntityInformation().getJavaType(), getRegion()) + .fromRegion(getRegion(), getEntityInformation().getJavaType()) .orderBy(sort); SelectResults selectResults = getTemplate().find(query.toString()); @@ -356,25 +356,10 @@ public class SimpleGemfireRepository implements GemfireRepository @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; + PagingUtils.assertPageable(pageable); 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); + return PagingUtils.toPage(results, pageable); } } diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/ArrayUtils.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/ArrayUtils.java index 8460572d..b9631d8b 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/ArrayUtils.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/ArrayUtils.java @@ -14,7 +14,10 @@ package org.springframework.data.gemfire.util; import java.lang.reflect.Array; import java.util.Arrays; +import java.util.Iterator; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** @@ -203,4 +206,54 @@ public abstract class ArrayUtils { return array; } + + /** + * Converts the given array into an {@link Iterable} object. + * + * @param {@link Class type} of the array elements; mut not be {@literal null}. + * @param array array to convert to an {@link Iterable}. + * @return an {@link Iterable} object from the given array. + * @throws IllegalArgumentException if the array is {@literal null}. + * @see java.lang.Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable toIterable(@NonNull T... array) { + return IterableArray.of(array); + } + + protected static class IterableArray implements Iterable { + + @SuppressWarnings("unchecked") + protected static IterableArray of(@NonNull T... array) { + return new IterableArray<>(array); + } + + private final T[] array; + + protected IterableArray(@NonNull T[] array) { + + Assert.notNull(array, "Array must not be null"); + + this.array = array; + } + + @Override + public Iterator iterator() { + + return new Iterator() { + + int index = 0; + + @Override + public boolean hasNext() { + return this.index < IterableArray.this.array.length; + } + + @Override + public T next() { + return IterableArray.this.array[this.index++]; + } + }; + } + } } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/GemfireTemplateIntegrationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/GemfireTemplateIntegrationTests.java index 3992ecbc..b70642cf 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/GemfireTemplateIntegrationTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/GemfireTemplateIntegrationTests.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire; import static org.assertj.core.api.Assertions.assertThat; @@ -26,18 +25,19 @@ import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import javax.annotation.Resource; -import org.apache.geode.cache.GemFireCache; -import org.apache.geode.cache.Region; -import org.apache.geode.cache.query.SelectResults; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.query.SelectResults; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -45,26 +45,31 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.gemfire.repository.sample.User; import org.springframework.data.gemfire.util.CacheUtils; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringRunner; /** - * Integration tests for {@link GemfireTemplate}. + * Integration Tests for {@link GemfireTemplate}. * * @author John Blum + * @see java.util.Properties * @see org.junit.Test + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.context.annotation.Configuration * @see org.springframework.data.gemfire.GemfireTemplate * @see org.springframework.test.context.ContextConfiguration - * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner + * @see org.springframework.test.context.junit4.SpringRunner * @since 1.4.0 */ -@RunWith(SpringJUnit4ClassRunner.class) +@RunWith(SpringRunner.class) @ContextConfiguration @SuppressWarnings("unused") public class GemfireTemplateIntegrationTests { - protected static final String DEFAULT_GEMFIRE_LOG_LEVEL = "warning"; + protected static final String DEFAULT_GEMFIRE_LOG_LEVEL = "error"; - protected static final List TEST_USERS = new ArrayList(9); + protected static final List TEST_USERS = new ArrayList<>(9); static { TEST_USERS.add(newUser("jonDoe")); @@ -96,18 +101,22 @@ public class GemfireTemplateIntegrationTests { } protected static User newUser(String username, String email, Calendar since, Boolean active) { + User user = new User(username); + user.setActive(Boolean.TRUE.equals(active)); user.setEmail(email); user.setSince(since); + return user; } protected String getKey(User user) { - return (user != null ? user.getUsername() : null); + return user != null ? user.getUsername() : null; } protected User getUser(String username) { + for (User user : TEST_USERS) { if (user.getUsername().equals(username)) { return user; @@ -118,8 +127,9 @@ public class GemfireTemplateIntegrationTests { } protected List getUsers(String... usernames) { + List usernameList = Arrays.asList(usernames); - List users = new ArrayList(usernames.length); + List users = new ArrayList<>(usernames.length); for (User user : TEST_USERS) { if (usernameList.contains(user.getUsername())) { @@ -139,7 +149,8 @@ public class GemfireTemplateIntegrationTests { } protected Map getUsersAsMap(Iterable users) { - Map userMap = new HashMap(); + + Map userMap = new HashMap<>(); for (User user : users) { userMap.put(getKey(user), user); @@ -149,133 +160,147 @@ public class GemfireTemplateIntegrationTests { } protected void assertNullEquals(Object value1, Object value2) { - assertThat(value1 == null ? value2 == null : value1.equals(value2)).isTrue(); + assertThat(Objects.equals(value1, value2)).isTrue(); } @Before public void setup() { - assertThat(users).isNotNull(); - if (users.isEmpty()) { + assertThat(this.users).isNotNull(); + + if (this.users.isEmpty()) { for (User user : TEST_USERS) { - users.put(getKey(user), user); + this.users.put(getKey(user), user); } - assertThat(users.isEmpty()).isFalse(); - assertThat(users.size()).isEqualTo(TEST_USERS.size()); + assertThat(this.users.size()).isEqualTo(TEST_USERS.size()); } } @Test public void containsKey() { - assertThat(usersTemplate.containsKey(getKey(getUser("jonDoe")))).isTrue(); - assertThat(usersTemplate.containsKey("dukeNukem")).isFalse(); + + assertThat(this.usersTemplate.containsKey(getKey(getUser("jonDoe")))).isTrue(); + assertThat(this.usersTemplate.containsKey("dukeNukem")).isFalse(); } @Test public void containsKeyOnServer() { - assumeThat(CacheUtils.isClient(gemfireCache), is(true)); - assertThat(usersTemplate.containsKeyOnServer(getKey(getUser("jackHandy")))).isTrue(); - assertThat(usersTemplate.containsKeyOnServer("maxPayne")).isFalse(); + + assumeThat(CacheUtils.isClient(this.gemfireCache), is(true)); + + assertThat(this.usersTemplate.containsKeyOnServer(getKey(getUser("jackHandy")))).isTrue(); + assertThat(this.usersTemplate.containsKeyOnServer("maxPayne")).isFalse(); } @Test public void containsValue() { - assertThat(usersTemplate.containsValue(getUser("pieDoe"))).isTrue(); - assertThat(usersTemplate.containsValue(newUser("pieDough"))).isFalse(); + + assertThat(this.usersTemplate.containsValue(getUser("pieDoe"))).isTrue(); + assertThat(this.usersTemplate.containsValue(newUser("pieDough"))).isFalse(); } @Test public void containsValueForKey() { - assertThat(usersTemplate.containsValueForKey(getKey(getUser("cookieDoe")))).isTrue(); - assertThat(usersTemplate.containsValueForKey("chocolateChipCookieDoe")).isFalse(); + + assertThat(this.usersTemplate.containsValueForKey(getKey(getUser("cookieDoe")))).isTrue(); + assertThat(this.usersTemplate.containsValueForKey("chocolateChipCookieDoe")).isFalse(); } @Test public void create() { + User bartSimpson = newUser("bartSimpson"); - usersTemplate.create(getKey(bartSimpson), bartSimpson); + this.usersTemplate.create(getKey(bartSimpson), bartSimpson); - assertThat(users.containsKey(getKey(bartSimpson))).isTrue(); - assertThat(users.containsValueForKey(getKey(bartSimpson))).isTrue(); - assertThat(users.containsValue(bartSimpson)).isTrue(); - assertThat(users.get(getKey(bartSimpson))).isEqualTo(bartSimpson); + assertThat(this.users.containsKey(getKey(bartSimpson))).isTrue(); + assertThat(this.users.containsValueForKey(getKey(bartSimpson))).isTrue(); + assertThat(this.users.containsValue(bartSimpson)).isTrue(); + assertThat(this.users.get(getKey(bartSimpson))).isEqualTo(bartSimpson); } @Test public void get() { + String key = getKey(getUser("imaPigg")); - assertThat(usersTemplate.get(key)).isEqualTo(users.get(key)); - assertNullEquals(users.get("mrT"), usersTemplate.get("mrT")); + assertThat(this.usersTemplate.get(key)).isEqualTo(this.users.get(key)); + assertNullEquals(this.users.get("mrT"), this.usersTemplate.get("mrT")); } @Test public void put() { + User peterGriffon = newUser("peterGriffon"); - assertThat(usersTemplate.put(getKey(peterGriffon), peterGriffon)).isNull(); - assertThat(users.get(getKey(peterGriffon))).isEqualTo(peterGriffon); + assertThat(this.usersTemplate.put(getKey(peterGriffon), peterGriffon)).isNull(); + assertThat(this.users.get(getKey(peterGriffon))).isEqualTo(peterGriffon); } @Test public void putIfAbsent() { + User stewieGriffon = newUser("stewieGriffon"); - assertThat(users.containsValue(stewieGriffon)).isFalse(); - assertThat(usersTemplate.putIfAbsent(getKey(stewieGriffon), stewieGriffon)).isNull(); - assertThat(users.containsValue(stewieGriffon)).isTrue(); - assertThat(usersTemplate.putIfAbsent(getKey(stewieGriffon), newUser("megGriffon"))).isEqualTo(stewieGriffon); - assertThat(users.get(getKey(stewieGriffon))).isEqualTo(stewieGriffon); + assertThat(this.users.containsValue(stewieGriffon)).isFalse(); + assertThat(this.usersTemplate.putIfAbsent(getKey(stewieGriffon), stewieGriffon)).isNull(); + assertThat(this.users.containsValue(stewieGriffon)).isTrue(); + assertThat(this.usersTemplate.putIfAbsent(getKey(stewieGriffon), newUser("megGriffon"))).isEqualTo(stewieGriffon); + assertThat(this.users.get(getKey(stewieGriffon))).isEqualTo(stewieGriffon); } @Test public void remove() { - User mandyHandy = users.get(getKey(getUser("mandyHandy"))); + + User mandyHandy = this.users.get(getKey(getUser("mandyHandy"))); assertThat(mandyHandy).isNotNull(); - assertThat(usersTemplate.remove(getKey(mandyHandy))).isEqualTo(mandyHandy); - assertThat(users.containsKey(getKey(mandyHandy))).isFalse(); - assertThat(users.containsValue(mandyHandy)).isFalse(); - assertThat(users.containsKey("loisGriffon")).isFalse(); - assertThat(usersTemplate.remove("loisGriffon")).isNull(); - assertThat(users.containsKey("loisGriffon")).isFalse(); + assertThat(this.usersTemplate.remove(getKey(mandyHandy))).isEqualTo(mandyHandy); + assertThat(this.users.containsKey(getKey(mandyHandy))).isFalse(); + assertThat(this.users.containsValue(mandyHandy)).isFalse(); + assertThat(this.users.containsKey("loisGriffon")).isFalse(); + assertThat(this.usersTemplate.remove("loisGriffon")).isNull(); + assertThat(this.users.containsKey("loisGriffon")).isFalse(); } @Test public void replace() { - User randyHandy = users.get(getKey(getUser("randyHandy"))); + + User randyHandy = this.users.get(getKey(getUser("randyHandy"))); User lukeFluke = newUser("lukeFluke"); User chrisGriffon = newUser("chrisGriffon"); assertThat(randyHandy).isNotNull(); - assertThat(usersTemplate.replace(getKey(randyHandy), lukeFluke)).isEqualTo(randyHandy); - assertThat(users.get(getKey(randyHandy))).isEqualTo(lukeFluke); - assertThat(users.containsValue(randyHandy)).isFalse(); - assertThat(users.containsValue(chrisGriffon)).isFalse(); - assertThat(usersTemplate.replace(getKey(chrisGriffon), chrisGriffon)).isNull(); - assertThat(users.containsValue(chrisGriffon)).isFalse(); + assertThat(this.usersTemplate.replace(getKey(randyHandy), lukeFluke)).isEqualTo(randyHandy); + assertThat(this.users.get(getKey(randyHandy))).isEqualTo(lukeFluke); + assertThat(this.users.containsValue(randyHandy)).isFalse(); + assertThat(this.users.containsValue(chrisGriffon)).isFalse(); + assertThat(this.usersTemplate.replace(getKey(chrisGriffon), chrisGriffon)).isNull(); + assertThat(this.users.containsValue(chrisGriffon)).isFalse(); } @Test public void replaceOldValueWithNewValue() { + User jackHandy = getUser("jackHandy"); User imaPigg = getUser("imaPigg"); - assertThat(users.containsValue(jackHandy)).isTrue(); - assertThat(usersTemplate.replace(getKey(jackHandy), null, imaPigg)).isFalse(); - assertThat(users.containsValue(jackHandy)).isTrue(); - assertThat(users.get(getKey(jackHandy))).isEqualTo(jackHandy); - assertThat(usersTemplate.replace(getKey(jackHandy), jackHandy, imaPigg)).isTrue(); - assertThat(users.containsValue(jackHandy)).isFalse(); - assertThat(users.get(getKey(jackHandy))).isEqualTo(imaPigg); + assertThat(this.users.containsValue(jackHandy)).isTrue(); + assertThat(this.usersTemplate.replace(getKey(jackHandy), null, imaPigg)).isFalse(); + assertThat(this.users.containsValue(jackHandy)).isTrue(); + assertThat(this.users.get(getKey(jackHandy))).isEqualTo(jackHandy); + assertThat(this.usersTemplate.replace(getKey(jackHandy), jackHandy, imaPigg)).isTrue(); + assertThat(this.users.containsValue(jackHandy)).isFalse(); + assertThat(this.users.get(getKey(jackHandy))).isEqualTo(imaPigg); } @Test public void getAllReturnsNoResults() { + List keys = Arrays.asList("keyOne", "keyTwo", "keyThree"); - Map users = usersTemplate.getAll(keys); + + Map users = this.usersTemplate.getAll(keys); assertThat(users).isNotNull(); assertThat(users).isEqualTo(this.users.getAll(keys)); @@ -283,7 +308,8 @@ public class GemfireTemplateIntegrationTests { @Test public void getAllReturnsResults() { - Map users = usersTemplate.getAll(Arrays.asList( + + Map users = this.usersTemplate.getAll(Arrays.asList( getKey(getUser("jonDoe")), getKey(getUser("pieDoe")))); assertThat(users).isNotNull(); @@ -292,24 +318,26 @@ public class GemfireTemplateIntegrationTests { @Test public void putAll() { + User batMan = newUser("batMan"); User spiderMan = newUser("spiderMan"); User superMan = newUser("superMan"); Map userMap = getUsersAsMap(batMan, spiderMan, superMan); - assertThat(users.keySet().containsAll(userMap.keySet())).isFalse(); - assertThat(users.values().containsAll(userMap.values())).isFalse(); + assertThat(this.users.keySet().containsAll(userMap.keySet())).isFalse(); + assertThat(this.users.values().containsAll(userMap.values())).isFalse(); - usersTemplate.putAll(userMap); + this.usersTemplate.putAll(userMap); - assertThat(users.keySet().containsAll(userMap.keySet())).isTrue(); - assertThat(users.values().containsAll(userMap.values())).isTrue(); + assertThat(this.users.keySet().containsAll(userMap.keySet())).isTrue(); + assertThat(this.users.values().containsAll(userMap.values())).isTrue(); } @Test public void query() { - SelectResults queryResults = usersTemplate.query("username LIKE '%Doe'"); + + SelectResults queryResults = this.usersTemplate.query("username LIKE '%Doe'"); assertThat(queryResults).isNotNull(); @@ -322,7 +350,9 @@ public class GemfireTemplateIntegrationTests { @Test public void find() { - SelectResults findResults = usersTemplate.find("SELECT u FROM /Users u WHERE u.username LIKE $1 AND u.active = $2", "%Doe", true); + + SelectResults findResults = + this.usersTemplate.find("SELECT u FROM /Users u WHERE u.username LIKE $1 AND u.active = $2", "%Doe", true); assertThat(findResults).isNotNull(); @@ -333,9 +363,46 @@ public class GemfireTemplateIntegrationTests { assertThat(usersFound.containsAll(getUsers("jonDoe", "cookieDoe"))).isTrue(); } + // The following query is syntactically correct but does NOT work!!! + // "SELECT keys FROM /Users u, u.keySet keys WHERE u.active = false ORDER BY u.username ASC" + @Test + public void findKeys() { + + User mandyHandy = getUser("mandyHandy"); + + this.users.put(mandyHandy.getUsername(), mandyHandy); + + String query = "SELECT u.key FROM /Users.entrySet u WHERE u.value.active = false ORDER BY u.value.username ASC"; + + SelectResults results = this.usersTemplate.find(query); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(4); + assertThat(results.asList()).containsExactly("janeDoe", "mandyHandy", "pieDoe", "randyHandy"); + } + + @Test + public void findLimitedKeys() { + + String query = "SELECT u.key" + + " FROM /Users.entrySet u" + + " WHERE u.value.active = false" + + " AND u.value.username LIKE '%Doe'" + + " ORDER BY u.value.username ASC" + + " LIMIT 1"; + + SelectResults results = this.usersTemplate.find(query); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(1); + assertThat(results.asList()).containsExactly("janeDoe"); + } + @Test public void findUniqueReturnsResult() { - User jonDoe = usersTemplate.findUnique("SELECT u FROM /Users u WHERE u.username = $1", "jonDoe"); + + User jonDoe = + this.usersTemplate.findUnique("SELECT u FROM /Users u WHERE u.username = $1", "jonDoe"); assertThat(jonDoe).isNotNull(); assertThat(jonDoe).isEqualTo(getUser("jonDoe")); @@ -343,12 +410,12 @@ public class GemfireTemplateIntegrationTests { @Test(expected = InvalidDataAccessApiUsageException.class) public void findUniqueReturnsNoResult() { - usersTemplate.findUnique("SELECT u FROM /Users u WHERE u.username = $1", "benDover"); + this.usersTemplate.findUnique("SELECT u FROM /Users u WHERE u.username = $1", "benDover"); } @Test(expected = InvalidDataAccessApiUsageException.class) - public void findUnqiueReturnsTooManyResults() { - usersTemplate.findUnique("SELECT u FROM /Users u WHERE u.username LIKE $1", "%Doe"); + public void findUniqueReturnsTooManyResults() { + this.usersTemplate.findUnique("SELECT u FROM /Users u WHERE u.username LIKE $1", "%Doe"); } @Configuration @@ -374,6 +441,7 @@ public class GemfireTemplateIntegrationTests { @Bean CacheFactoryBean gemfireCache() { + CacheFactoryBean gemfireCache = new CacheFactoryBean(); gemfireCache.setClose(false); @@ -384,7 +452,8 @@ public class GemfireTemplateIntegrationTests { @Bean(name = "Users") LocalRegionFactoryBean usersRegion(GemFireCache gemfireCache) { - LocalRegionFactoryBean usersRegion = new LocalRegionFactoryBean(); + + LocalRegionFactoryBean usersRegion = new LocalRegionFactoryBean<>(); usersRegion.setCache(gemfireCache); usersRegion.setClose(false); @@ -394,7 +463,7 @@ public class GemfireTemplateIntegrationTests { } @Bean - GemfireTemplate usersRegionTemplate(Region simple) { + GemfireTemplate usersTemplate(Region simple) { return new GemfireTemplate(simple); } } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/AbstractSelectResultsUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/AbstractSelectResultsUnitTests.java new file mode 100644 index 00000000..68380b0a --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/AbstractSelectResultsUnitTests.java @@ -0,0 +1,322 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.query.SelectResults; +import org.apache.geode.cache.query.types.CollectionType; +import org.apache.geode.cache.query.types.ObjectType; + +import org.springframework.lang.NonNull; + +/** + * Unit Tests for {@link AbstractSelectResults}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.gemfire.repository.query.AbstractSelectResults + * @since 2.4.0 + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class AbstractSelectResultsUnitTests { + + @Mock + private SelectResults mockSelectResults; + + @Test + public void constructsAbstractSelectResultsSuccessfully() { + + AbstractSelectResults selectResults = new TestSelectResults(this.mockSelectResults); + + assertThat(selectResults).isNotNull(); + assertThat(selectResults.getSelectResults()).isSameAs(this.mockSelectResults); + + verifyNoInteractions(this.mockSelectResults); + } + + @Test(expected = IllegalArgumentException.class) + public void constructAbstractSelectResultsWithNullThrowsIllegalArgumentException() { + + try { + new TestSelectResults(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("SelectResults must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void asListCallsSelectResultsAsList() { + + List mockList = mock(List.class); + + doReturn(mockList).when(this.mockSelectResults).asList(); + + assertThat(new TestSelectResults(this.mockSelectResults).asList()).isEqualTo(mockList); + + verify(this.mockSelectResults, times(1)).asList(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void asSetCallsSelectResultsAsSet() { + + Set mockSet = mock(Set.class); + + doReturn(mockSet).when(this.mockSelectResults).asSet(); + + assertThat(new TestSelectResults(this.mockSelectResults).asSet()).isEqualTo(mockSet); + + verify(this.mockSelectResults, times(1)).asSet(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void getCollectionTypeCallsSelectResultsGetCollectionType() { + + CollectionType mockCollectionType = mock(CollectionType.class); + + doReturn(mockCollectionType).when(this.mockSelectResults).getCollectionType(); + + assertThat(new TestSelectResults(this.mockSelectResults).getCollectionType()).isEqualTo(mockCollectionType); + + verify(this.mockSelectResults, times(1)).getCollectionType(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void isModifiableCallsSelectResultsIsModifiable() { + + doReturn(true).when(this.mockSelectResults).isModifiable(); + + assertThat(new TestSelectResults(this.mockSelectResults).isModifiable()).isTrue(); + + verify(this.mockSelectResults, times(1)).isModifiable(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void occurrencesCallsSelectResultsOccurrences() { + + doReturn(5).when(this.mockSelectResults).occurrences(eq("test")); + + assertThat(new TestSelectResults(this.mockSelectResults).occurrences("test")).isEqualTo(5); + + verify(this.mockSelectResults, times(1)).occurrences(eq("test")); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void setObjectTypeCallsSelectResultsSetObjectType() { + + ObjectType mockObjectType = mock(ObjectType.class); + + new TestSelectResults(this.mockSelectResults).setElementType(mockObjectType); + + verify(this.mockSelectResults, times(1)).setElementType(eq(mockObjectType)); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void addCallsSelectResultsAdd() { + + doReturn(true).when(this.mockSelectResults).add(any()); + + assertThat(new TestSelectResults(this.mockSelectResults).add("test")).isTrue(); + + verify(this.mockSelectResults, times(1)).add(eq("test")); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void addAllCallsSelectResultsAddAll() { + + Collection mockCollection = mock(Collection.class); + + doReturn(true).when(this.mockSelectResults).addAll(anyCollection()); + + assertThat(new TestSelectResults(this.mockSelectResults).addAll(mockCollection)).isTrue(); + + verify(this.mockSelectResults, times(1)).addAll(eq(mockCollection)); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void clearCallsSelectResultsClear() { + + new TestSelectResults(this.mockSelectResults).clear(); + + verify(this.mockSelectResults, times(1)).clear(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void containsCallsSelectResultsContains() { + + doReturn(false).when(this.mockSelectResults).contains(any()); + + assertThat(new TestSelectResults(this.mockSelectResults).contains("test")).isFalse(); + + verify(this.mockSelectResults, times(1)).contains(eq("test")); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void containsAllCallsSelectResultsContainsAll() { + + Collection mockCollection = mock(Collection.class); + + doReturn(true).when(this.mockSelectResults).containsAll(anyCollection()); + + assertThat(new TestSelectResults(this.mockSelectResults).containsAll(mockCollection)).isTrue(); + + verify(this.mockSelectResults, times(1)).containsAll(eq(mockCollection)); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void isEmptyCallsSelectResultsIsEmpty() { + + doReturn(false).when(this.mockSelectResults).isEmpty(); + + assertThat(new TestSelectResults(this.mockSelectResults).isEmpty()).isFalse(); + + verify(this.mockSelectResults, times(1)).isEmpty(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void iteratorCallsSelectResultsIterator() { + + Iterator mockIterator = mock(Iterator.class); + + doReturn(mockIterator).when(this.mockSelectResults).iterator(); + + assertThat(new TestSelectResults(this.mockSelectResults).iterator()).isEqualTo(mockIterator); + + verify(this.mockSelectResults, times(1)).iterator(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void removeCallsSelectResultsRemove() { + + doReturn(true).when(this.mockSelectResults).remove(any()); + + assertThat(new TestSelectResults(this.mockSelectResults).remove("mock")).isTrue(); + + verify(this.mockSelectResults, times(1)).remove(eq("mock")); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void removeAllCallsSelectResultsRemoveAll() { + + Collection mockCollection = mock(Collection.class); + + doReturn(false).when(this.mockSelectResults).removeAll(anyCollection()); + + assertThat(new TestSelectResults(this.mockSelectResults).removeAll(mockCollection)).isFalse(); + + verify(this.mockSelectResults, times(1)).removeAll(eq(mockCollection)); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void retainAllCallsSelectResultsRetainAll() { + + Collection mockCollection = mock(Collection.class); + + doReturn(true).when(this.mockSelectResults).retainAll(anyCollection()); + + assertThat(new TestSelectResults(this.mockSelectResults).retainAll(mockCollection)).isTrue(); + + verify(this.mockSelectResults, times(1)).retainAll(eq(mockCollection)); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void sizeCallsSelectResultsSize() { + + doReturn(50).when(this.mockSelectResults).size(); + + assertThat(new TestSelectResults(this.mockSelectResults).size()).isEqualTo(50); + + verify(this.mockSelectResults, times(1)).size(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void toArrayCallsSelectResultsToArray() { + + Object[] array = { "test", "mock" }; + + doReturn(array).when(this.mockSelectResults).toArray(); + + assertThat(new TestSelectResults(this.mockSelectResults).toArray()).isEqualTo(array); + + verify(this.mockSelectResults, times(1)).toArray(); + verifyNoMoreInteractions(this.mockSelectResults); + } + + @Test + public void toArrayWithArrayArgumentCallsSelectResultsToArray() { + + Object[] array = { "test", "mock" }; + + doReturn(array).when(this.mockSelectResults).toArray(eq(new Object[0])); + + assertThat(new TestSelectResults(this.mockSelectResults).toArray(new Object[0])).isEqualTo(array); + + verify(this.mockSelectResults, times(1)).toArray(eq(new Object[0])); + verifyNoMoreInteractions(this.mockSelectResults); + } + + static class TestSelectResults extends AbstractSelectResults { + + TestSelectResults(@NonNull SelectResults selectResults) { + super(selectResults); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/PagedSelectResultsUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/PagedSelectResultsUnitTests.java new file mode 100644 index 00000000..047812f6 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/PagedSelectResultsUnitTests.java @@ -0,0 +1,282 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.query.SelectResults; + +import org.springframework.data.domain.Pageable; + +/** + * Unit Tests for {@link PagedSelectResults}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.cache.query.SelectResults + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.gemfire.repository.query.PagedSelectResults + * @since 2.4.0 + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class PagedSelectResultsUnitTests { + + @Mock + private Pageable mockPageable; + + @Mock + private SelectResults mockSelectResults; + + @Test + public void constructPagedSelectResultsIsCorrect() { + + PagedSelectResults selectResults = new PagedSelectResults<>(this.mockSelectResults, this.mockPageable); + + assertThat(selectResults).isNotNull(); + assertThat(selectResults.getSelectResults()).isSameAs(this.mockSelectResults); + assertThat(selectResults.getPageRequest()).isSameAs(this.mockPageable); + + verifyNoInteractions(this.mockPageable, mockSelectResults); + } + + @Test(expected = IllegalArgumentException.class) + public void constructPagedSelectResultsWithNullPageableThrowsIllegalArgumentException() { + + try { + new PagedSelectResults<>(this.mockSelectResults, null); + } + catch(IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Pageable must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyNoInteractions(this.mockSelectResults); + } + } + + @Test(expected = IllegalArgumentException.class) + public void constructPagedSelectResultsWithNullSelectResultsThrowsIllegalArgumentException() { + + try { + new PagedSelectResults<>(null, this.mockPageable); + } + catch(IllegalArgumentException expected) { + + assertThat(expected).hasMessage("SelectResults must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyNoInteractions(this.mockPageable); + } + } + + @Test + public void asListIsCorrect() { + + List names = Arrays.asList("Jon Doe", "Jane Doe", "Cookie Doe", "Pie Doe", "Sour Doe"); + + doReturn(names).when(this.mockSelectResults).asList(); + doReturn(1).when(this.mockPageable).getPageNumber(); + doReturn(2).when(this.mockPageable).getPageSize(); + + PagedSelectResults selectResults = new PagedSelectResults<>(this.mockSelectResults, this.mockPageable); + + assertThat(selectResults).isNotNull(); + assertThat(selectResults.getSelectResults()).isEqualTo(this.mockSelectResults); + assertThat(selectResults.getPageRequest()).isEqualTo(this.mockPageable); + + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + + List pagedNames = selectResults.asList(); + + assertThat(pagedNames).isNotNull(); + assertThat(pagedNames).hasSize(2); + assertThat(pagedNames).containsExactly("Cookie Doe", "Pie Doe"); + + verify(this.mockSelectResults, times(1)).asList(); + verify(this.mockPageable, times(2)).getPageNumber(); + verify(this.mockPageable, times(3)).getPageSize(); + verifyNoMoreInteractions(this.mockSelectResults, this.mockPageable); + } + + @Test + public void asPageListIsCorrect() { + + List names = Arrays.asList("Jon Doe", "Jane Doe", "Cookie Doe", "Pie Doe", "Sour Doe"); + + doReturn(names).when(this.mockSelectResults).asList(); + doReturn(0).when(this.mockPageable).getPageNumber(); + doReturn(3).when(this.mockPageable).getPageSize(); + + PagedSelectResults selectResults = new PagedSelectResults<>(this.mockSelectResults, this.mockPageable); + + assertThat(selectResults).isNotNull(); + assertThat(selectResults.getSelectResults()).isEqualTo(this.mockSelectResults); + assertThat(selectResults.getPageRequest()).isEqualTo(this.mockPageable); + + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + + List pagedNames = selectResults.asList(); + + assertThat(pagedNames).isNotNull(); + assertThat(pagedNames).hasSize(3); + assertThat(pagedNames).containsExactly("Jon Doe", "Jane Doe", "Cookie Doe"); + + verify(this.mockSelectResults, times(1)).asList(); + verify(this.mockPageable, times(2)).getPageNumber(); + verify(this.mockPageable, times(3)).getPageSize(); + verifyNoMoreInteractions(this.mockSelectResults, this.mockPageable); + reset(this.mockSelectResults, this.mockPageable); + + // Page 2 + doReturn(names).when(this.mockSelectResults).asList(); + doReturn(1).when(this.mockPageable).getPageNumber(); + doReturn(3).when(this.mockPageable).getPageSize(); + + selectResults = selectResults.with(this.mockPageable); + + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + + assertThat(selectResults).isNotNull(); + assertThat(selectResults.getSelectResults()).isEqualTo(this.mockSelectResults); + assertThat(selectResults.getPageRequest()).isEqualTo(this.mockPageable); + + pagedNames = selectResults.asList(); + + assertThat(pagedNames).isNotNull(); + assertThat(pagedNames).hasSize(2); + assertThat(pagedNames).containsExactly("Pie Doe", "Sour Doe"); + + verify(this.mockSelectResults, times(1)).asList(); + verify(this.mockPageable, times(2)).getPageNumber(); + verify(this.mockPageable, times(3)).getPageSize(); + verifyNoMoreInteractions(this.mockSelectResults, this.mockPageable); + reset(this.mockSelectResults, this.mockPageable); + + // Page 3 + doReturn(names).when(this.mockSelectResults).asList(); + doReturn(2).when(this.mockPageable).getPageNumber(); + doReturn(3).when(this.mockPageable).getPageSize(); + + selectResults = selectResults.with(this.mockPageable); + + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + + assertThat(selectResults).isNotNull(); + assertThat(selectResults.getSelectResults()).isEqualTo(this.mockSelectResults); + assertThat(selectResults.getPageRequest()).isEqualTo(this.mockPageable); + + pagedNames = selectResults.asList(); + + assertThat(pagedNames).isNotNull(); + assertThat(pagedNames).isEmpty(); + + verify(this.mockSelectResults, times(1)).asList(); + verify(this.mockPageable, times(2)).getPageNumber(); + verify(this.mockPageable, times(3)).getPageSize(); + verifyNoMoreInteractions(this.mockSelectResults, this.mockPageable); + reset(this.mockSelectResults, this.mockPageable); + } + + @Test + public void asSetCallsAsList() { + + List list = Collections.singletonList("mock"); + + PagedSelectResults selectResults = + spy(new PagedSelectResults<>(this.mockSelectResults, this.mockPageable)); + + doReturn(list).when(selectResults).asList(); + + assertThat(selectResults.asSet()).containsExactly("mock"); + + verify(selectResults, times(1)).asList(); + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + } + + @Test(expected = UnsupportedOperationException.class) + public void iteratorCallsAsList() { + + List list = Collections.singletonList("test"); + + PagedSelectResults selectResults = + spy(new PagedSelectResults<>(this.mockSelectResults, this.mockPageable)); + + doReturn(list).when(selectResults).asList(); + + Iterator iterator = selectResults.iterator(); + + assertThat(iterator).isNotNull(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo("test"); + assertThat(iterator.hasNext()).isFalse(); + + try { + iterator.remove(); + } + finally { + verify(selectResults, times(1)).asList(); + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + } + } + + @Test + public void sizeCallsAsList() { + + List mockList = mock(List.class); + + doReturn(50).when(mockList).size(); + + PagedSelectResults selectResults = + spy(new PagedSelectResults<>(this.mockSelectResults, this.mockPageable)); + + doReturn(mockList).when(selectResults).asList(); + + assertThat(selectResults.size()).isEqualTo(50); + + verify(selectResults, times(1)).asList(); + verify(mockList, times(1)).size(); + verifyNoMoreInteractions(mockList); + verifyNoInteractions(this.mockSelectResults, this.mockPageable); + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java index 3a7beb1a..9dfa0d23 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/QueryStringUnitTests.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.repository.query; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.springframework.data.gemfire.repository.query.QueryString.HINT_PATTERN; import static org.springframework.data.gemfire.repository.query.QueryString.IMPORT_PATTERN; @@ -28,37 +28,44 @@ import static org.springframework.data.gemfire.repository.query.QueryString.TRAC import java.util.Arrays; import java.util.regex.Pattern; -import org.apache.geode.cache.Region; - -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.apache.geode.cache.Region; + +import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort; -import org.springframework.data.gemfire.repository.sample.Person; import org.springframework.data.gemfire.repository.sample.RootUser; +import org.springframework.data.gemfire.test.model.Person; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; /** - * Test suite of test cases testing the contract and functionality of the {@link QueryString} class. + * Unit Tests for {@link QueryString}. * * @author Oliver Gierke * @author John Blum + * @see java.util.regex.Pattern * @see org.junit.Test * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.apache.geode.cache.Region + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.data.domain.Sort * @see org.springframework.data.gemfire.repository.query.QueryString */ @RunWith(MockitoJUnitRunner.class) public class QueryStringUnitTests { - @Rule - public ExpectedException exception = ExpectedException.none(); - @Mock @SuppressWarnings("rawtypes") - Region region; + private Region region; private Sort.Order newSortOrder(String property) { return newSortOrder(property, Sort.Direction.ASC); @@ -72,14 +79,19 @@ public class QueryStringUnitTests { return Sort.by(orders); } + @Test + public void constructQueryStringWithAtRegionAnnotatedDomainType() { + assertThat(new QueryString(Person.class).toString()).isEqualTo("SELECT * FROM /People"); + } + @Test public void constructQueryStringWithDomainType() { - assertThat(new QueryString(Person.class).toString()).isEqualTo("SELECT * FROM /Person"); + assertThat(new QueryString(User.class).toString()).isEqualTo("SELECT * FROM /User"); } @Test public void constructQueryStringWithDomainTypeAsCount() { - assertThat(new QueryString(Person.class, true).toString()).isEqualTo("SELECT count(*) FROM /Person"); + assertThat(new QueryString(User.class, true).toString()).isEqualTo("SELECT count(*) FROM /User"); } @Test(expected = IllegalArgumentException.class) @@ -107,20 +119,20 @@ public class QueryStringUnitTests { @Test(expected = IllegalArgumentException.class) public void constructQueryStringWithBlankQueryThrowsIllegalArgumentException() { - assertUnspecifiedQueryThrowsIllegalArgumentException(" "); + assertInvalidQueryThrowsIllegalArgumentException(" "); } @Test(expected = IllegalArgumentException.class) public void constructQueryStringWithEmptyQueryThrowsIllegalArgumentException() { - assertUnspecifiedQueryThrowsIllegalArgumentException(""); + assertInvalidQueryThrowsIllegalArgumentException(""); } @Test(expected = IllegalArgumentException.class) public void constructQueryStringWithNullQueryThrowsIllegalArgumentException() { - assertUnspecifiedQueryThrowsIllegalArgumentException(null); + assertInvalidQueryThrowsIllegalArgumentException(null); } - private void assertUnspecifiedQueryThrowsIllegalArgumentException(String query) { + private void assertInvalidQueryThrowsIllegalArgumentException(String query) { try { new QueryString(query); @@ -144,21 +156,94 @@ public class QueryStringUnitTests { } @Test - public void queryStringFromDomainType() { + public void queryStringFromAtRegionAnnotatedDomainType() { QueryString query = QueryString.from(Person.class); assertThat(query).isNotNull(); - assertThat(query.toString()).isEqualTo("SELECT * FROM /Person"); + assertThat(query.toString()).isEqualTo("SELECT * FROM /People"); + } + + @Test + public void queryStringFromDomainType() { + + QueryString query = QueryString.from(User.class); + + assertThat(query).isNotNull(); + assertThat(query.toString()).isEqualTo("SELECT * FROM /User"); + } + + @Test + public void queryStringCountingObjectsOfAtAnnotatedDomainType() { + + QueryString query = QueryString.count(Person.class); + + assertThat(query).isNotNull(); + assertThat(query.toString()).isEqualTo("SELECT count(*) FROM /People"); } @Test public void queryStringCountingObjectsOfDomainType() { - QueryString query = QueryString.count(Person.class); + QueryString query = QueryString.count(User.class); assertThat(query).isNotNull(); - assertThat(query.toString()).isEqualTo("SELECT count(*) FROM /Person"); + assertThat(query.toString()).isEqualTo("SELECT count(*) FROM /User"); + } + + @Test + public void getDigitsOnlyIsCorrect() { + + assertThat(QueryString.getDigitsOnly("1")).isEqualTo("1"); + assertThat(QueryString.getDigitsOnly(" 2")).isEqualTo("2"); + assertThat(QueryString.getDigitsOnly(" 2 34 ")).isEqualTo("234"); + assertThat(QueryString.getDigitsOnly("abc123")).isEqualTo("123"); + assertThat(QueryString.getDigitsOnly("O1E2l4")).isEqualTo("124"); + assertThat(QueryString.getDigitsOnly("lOlO")).isEqualTo(""); + assertThat(QueryString.getDigitsOnly(" ")).isEqualTo(""); + assertThat(QueryString.getDigitsOnly("")).isEqualTo(""); + assertThat(QueryString.getDigitsOnly(null)).isEqualTo(""); + } + + @Test + public void isLimitedWithLimitBasedQueryReturnsTrue() { + assertThat(QueryString.of("SELECT * FROM /Test LIMIT 50").isLimited()).isTrue(); + } + + @Test + public void isLimitedWithUnlimitedQueryReturnsFalse() { + assertThat(QueryString.of("SELECT * FROM /Test").isLimited()).isFalse(); + } + + @Test + public void isLimitedWithQueryHavingInvalidLimitSyntaxReturnsFalse() { + + assertThat(QueryString.of("SELECT * FROM /Test LIMIT").isLimited()).isFalse(); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT abc").isLimited()).isFalse(); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT abc123").isLimited()).isFalse(); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT lO").isLimited()).isFalse(); + assertThat(QueryString.of("SELECT * FROM /Test LMT 10").isLimited()).isFalse(); + assertThat(QueryString.of("SELECT * FROM /Test 10").isLimited()).isFalse(); + } + + @Test + public void getLimitReturnsIntegerValue() { + + assertThat(QueryString.of("SELECT * FROM /Test LIMIT 1").getLimit()).isEqualTo(1); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT 10").getLimit()).isEqualTo(10); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT 21").getLimit()).isEqualTo(21); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT 421").getLimit()).isEqualTo(421); + } + + @Test + public void getLimitReturnsIntegerMaxValue() { + + assertThat(QueryString.of("SELECT * FROM /Test LIMIT").getLimit()).isEqualTo(Integer.MAX_VALUE); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT abc").getLimit()).isEqualTo(Integer.MAX_VALUE); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT abc123").getLimit()).isEqualTo(Integer.MAX_VALUE); + assertThat(QueryString.of("SELECT * FROM /Test LIMIT lO").getLimit()).isEqualTo(Integer.MAX_VALUE); + assertThat(QueryString.of("SELECT * FROM /Test LMT 10").getLimit()).isEqualTo(Integer.MAX_VALUE); + assertThat(QueryString.of("SELECT * FROM /Test 10").getLimit()).isEqualTo(Integer.MAX_VALUE); } @Test @@ -244,6 +329,38 @@ public class QueryStringUnitTests { return pattern.matcher(value).find(); } + @Test + public void adjustLimitWithQueryHavingLimit() { + + QueryString original = QueryString.of("SELECT * FROM /Test LIMIT 10"); + QueryString adjusted = original.adjustLimit(20); + + assertThat(adjusted).isNotNull(); + assertThat(adjusted).isNotSameAs(original); + assertThat(adjusted.toString()).isEqualTo("SELECT * FROM /Test LIMIT 20"); + } + + @Test + public void adjustLimitWithQueryHavingNoLimit() { + + QueryString original = QueryString.of("SELECT * FROM /Test"); + QueryString adjusted = original.adjustLimit(25); + + assertThat(adjusted).isNotNull(); + assertThat(adjusted).isNotSameAs(original); + assertThat(adjusted.toString()).isEqualTo("SELECT * FROM /Test LIMIT 25"); + } + + @Test + public void adjustLimitWithNullLimit() { + + QueryString original = QueryString.of("SELECT * FROM /Test LIMIT 10"); + QueryString adjusted = original.adjustLimit(null); + + assertThat(adjusted).isSameAs(original); + assertThat(adjusted.toString()).isEqualTo("SELECT * FROM /Test LIMIT 10"); + } + @Test public void asDistinctQuery() { @@ -268,10 +385,11 @@ public class QueryStringUnitTests { when(this.region.getFullPath()).thenReturn("/foo/bar"); - assertThat(query.toString()).isEqualTo("SELECT * FROM /Person"); - assertThat(query.fromRegion(Person.class, this.region).toString()).isEqualTo("SELECT * FROM /foo/bar"); + assertThat(query.toString()).isEqualTo("SELECT * FROM /People"); + assertThat(query.fromRegion(this.region, Person.class).toString()).isEqualTo("SELECT * FROM /foo/bar"); verify(this.region, times(1)).getFullPath(); + verifyNoMoreInteractions(this.region); } // SGF-156, SGF-251 @@ -282,10 +400,11 @@ public class QueryStringUnitTests { when(this.region.getFullPath()).thenReturn("/People"); - assertThat(query.fromRegion(Person.class, this.region).toString()) + assertThat(query.fromRegion(this.region, Person.class).toString()) .isEqualTo("SELECT * FROM /People p WHERE p.lastname = $1"); verify(this.region, times(1)).getFullPath(); + verifyNoMoreInteractions(this.region); } // SGF-252 @@ -296,10 +415,11 @@ public class QueryStringUnitTests { when(this.region.getFullPath()).thenReturn("/Remote/Root/Users"); - assertThat(query.fromRegion(RootUser.class, this.region).toString()) + assertThat(query.fromRegion(this.region, RootUser.class).toString()) .isEqualTo("SELECT * FROM /Remote/Root/Users u WHERE u.username = $1"); verify(this.region, times(1)).getFullPath(); + verifyNoMoreInteractions(this.region); } @Test @@ -428,4 +548,18 @@ public class QueryStringUnitTests { assertThat(query.toString()) .isEqualTo(" IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 20"); } + + @Getter + @ToString(of = "name") + @EqualsAndHashCode(of = "name") + @RequiredArgsConstructor(staticName = "newUser") + @SuppressWarnings("unused") + static class User { + + @Id + private Long id; + + @NonNull + private final String name; + } } diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutorUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutorUnitTests.java new file mode 100644 index 00000000..ec713b71 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/OqlQueryExecutorUnitTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException; + +import org.junit.Test; + +import org.springframework.data.repository.query.QueryMethod; + +/** + * Unit Tests for {@link OqlQueryExecutor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.repository.query.support.OqlQueryExecutor + * @since 2.4.0 + */ +public class OqlQueryExecutorUnitTests { + + @Test + @SuppressWarnings("all") + public void newUnsupportedQueryExecutionExceptionIsCorrect() { + + OqlQueryExecutor mockQueryExecutor = mock(OqlQueryExecutor.class); + + doCallRealMethod().when(mockQueryExecutor).newUnsupportedQueryExecutionException(anyString()); + + UnsupportedQueryExecutionException exception = + mockQueryExecutor.newUnsupportedQueryExecutionException("SELECT * FROM /TestRegion"); + + assertThat(exception).isNotNull(); + assertThat(exception).hasMessage(OqlQueryExecutor.NON_EXECUTABLE_QUERY_MESSAGE, + "SELECT * FROM /TestRegion", mockQueryExecutor.getClass().getName()); + assertThat(exception).hasNoCause(); + } + + @Test + public void thenExecuteWithComposesOqlQueryExecutorsCorrectly() { + + OqlQueryExecutor one = mock(OqlQueryExecutor.class); + OqlQueryExecutor two = mock(OqlQueryExecutor.class); + + doCallRealMethod().when(one).thenExecuteWith(any()); + + assertThat(one.thenExecuteWith(null)).isSameAs(one); + + OqlQueryExecutor composed = one.thenExecuteWith(two); + + assertThat(composed).isNotNull(); + assertThat(composed).isNotSameAs(one); + assertThat(composed).isNotSameAs(two); + } + + @Test + public void composedOqlQueryExecutorExecutesOne() { + + String query = "SELECT * FROM /TestRegion"; + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + OqlQueryExecutor one = mock(OqlQueryExecutor.class); + OqlQueryExecutor two = mock(OqlQueryExecutor.class); + + doCallRealMethod().when(one).thenExecuteWith(any()); + + OqlQueryExecutor composed = one.thenExecuteWith(two); + + assertThat(composed).isNotNull(); + + composed.execute(mockQueryMethod, query, "test"); + + verify(one, times(1)).execute(eq(mockQueryMethod), eq(query), eq("test")); + verifyNoInteractions(two); + } + + @Test(expected = IllegalArgumentException.class) + public void composedOqlQueryExecutorExecutesOneThenShortCircuitsWhenExceptionIsThrown() { + + String query = "SELECT * FROM /TestRegion"; + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + OqlQueryExecutor one = mock(OqlQueryExecutor.class); + OqlQueryExecutor two = mock(OqlQueryExecutor.class); + + doCallRealMethod().when(one).thenExecuteWith(any()); + doThrow(newIllegalArgumentException("test")) + .when(one).execute(any(QueryMethod.class), anyString(), any()); + + OqlQueryExecutor composed = one.thenExecuteWith(two); + + assertThat(composed).isNotNull(); + + try { + composed.execute(mockQueryMethod, query, "junk"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("test"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(one, times(1)).execute(eq(mockQueryMethod), eq(query), eq("junk")); + verifyNoInteractions(two); + } + } + + @Test + public void composedOqlQueryExecutorExecutesTwoWhenOneThrowsUnsupportedQueryExecutionException() { + + String query = "SELECT * FROM /TestRegion"; + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + OqlQueryExecutor one = mock(OqlQueryExecutor.class); + OqlQueryExecutor two = mock(OqlQueryExecutor.class); + + doCallRealMethod().when(one).thenExecuteWith(any()); + doThrow(new UnsupportedQueryExecutionException("test")) + .when(one).execute(any(QueryMethod.class), anyString(), any()); + + OqlQueryExecutor composed = one.thenExecuteWith(two); + + assertThat(composed).isNotNull(); + + composed.execute(mockQueryMethod, query, "mock"); + + verify(one, times(1)).execute(eq(mockQueryMethod), eq(query), eq("mock")); + verify(two, times(1)).execute(eq(mockQueryMethod), eq(query), eq("mock")); + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/PagingUtilsUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/PagingUtilsUnitTests.java new file mode 100644 index 00000000..181f1718 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/PagingUtilsUnitTests.java @@ -0,0 +1,677 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.Test; +import org.mockito.InOrder; + +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.mapping.annotation.Region; +import org.springframework.data.gemfire.repository.query.GemfireQueryMethod; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.test.context.event.annotation.AfterTestClass; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Unit Tests for {@link PagingUtils}. + * + * @author John Blum + * @see java.lang.Iterable + * @see java.util.List + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.data.domain.Page + * @see org.springframework.data.domain.Pageable + * @see org.springframework.data.gemfire.repository.query.support.PagingUtils + * @since 2.4.0 + */ +public class PagingUtilsUnitTests { + + @AfterTestClass + public static void tearDown() { + PagingUtils.isPageQueryFunction = PagingUtils.DEFAULT_IS_PAGE_QUERY_FUNCTION; + } + + @Test + public void assertPageableIsCorrect() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(0).when(mockPageable).getPageNumber(); + doReturn(10).when(mockPageable).getPageSize(); + + PagingUtils.assertPageable(mockPageable); + + verify(mockPageable, times(1)).getPageNumber(); + verify(mockPageable, times(1)).getPageSize(); + } + + @Test(expected = IllegalArgumentException.class) + public void assertPageableWithInvalidPageNumberThrowsIllegalArgumentException() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(-1).when(mockPageable).getPageNumber(); + + try { + PagingUtils.assertPageable(mockPageable); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(PagingUtils.INVALID_PAGE_NUMBER_MESSAGE, -1); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPageable, times(1)).getPageNumber(); + verify(mockPageable, never()).getPageSize(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void assertPageableWithInvalidPageSizeThrowsIllegalArgumentException() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(1).when(mockPageable).getPageNumber(); + doReturn(0).when(mockPageable).getPageSize(); + + try { + PagingUtils.assertPageable(mockPageable); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(PagingUtils.INVALID_PAGE_SIZE_MESSAGE, 0); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPageable, times(1)).getPageNumber(); + verify(mockPageable, times(1)).getPageSize(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void assertPageableWithNull() { + + try { + PagingUtils.assertPageable(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(PagingUtils.NON_NULL_PAGEABLE_MESSAGE); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void isPageOneForPageOneReturnsTrue() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(0).when(mockPageable).getPageNumber(); + + assertThat(PagingUtils.isPageOne(mockPageable)).isTrue(); + + verify(mockPageable, times(1)).getPageNumber(); + verifyNoMoreInteractions(mockPageable); + } + + @Test + public void isPageOneForPageTwoReturnsFalse() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(1).when(mockPageable).getPageNumber(); + + assertThat(PagingUtils.isPageOne(mockPageable)).isFalse(); + + verify(mockPageable, times(1)).getPageNumber(); + verifyNoMoreInteractions(mockPageable); + } + + @Test + public void isPageOneWithNullReturnsFalse() { + assertThat(PagingUtils.isPageOne(null)).isFalse(); + } + + @Test + public void isPagingPresentForQueryMethodReturningPageIsTrue() { + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + PagingUtils.isPageQueryFunction = queryMethod -> true; + + assertThat(PagingUtils.isPagingPresent(mockQueryMethod)).isTrue(); + } + + @Test + public void isPagingPresentForQueryMethodWithPageableParameterIsTrue() { + + Parameters mockParameters = mock(Parameters.class); + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + doReturn(mockParameters).when(mockQueryMethod).getParameters(); + doReturn(true).when(mockParameters).hasPageableParameter(); + + PagingUtils.isPageQueryFunction = queryMethod -> false; + + assertThat(PagingUtils.isPagingPresent(mockQueryMethod)).isTrue(); + + verify(mockQueryMethod, times(1)).getParameters(); + verify(mockParameters, times(1)).hasPageableParameter(); + verifyNoMoreInteractions(mockQueryMethod, mockParameters); + } + + @Test + public void isPagingPresentForNonQueryMethodIsFalse() { + + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class); + + PagingUtils.isPageQueryFunction = queryMethod -> false; + + doReturn(null).when(mockQueryMethod).getParameters(); + + assertThat(PagingUtils.isPagingPresent(mockQueryMethod)).isFalse(); + + verify(mockQueryMethod, times(1)).getParameters(); + verifyNoMoreInteractions(mockQueryMethod); + } + + @Test + public void isPagingPresentForNullQueryMethodIsFalse() { + assertThat(PagingUtils.isPagingPresent(null)).isFalse(); + } + + @Test + public void toPageIsCorrect() { + + List users = Arrays.asList( + User.newUser("Jon Doe"), + User.newUser("Jane Doe"), + User.newUser("Cookie Doe"), + User.newUser("Fro Doe"), + User.newUser("Joe Doe"), + User.newUser("Lan Doe"), + User.newUser("Pie Doe"), + User.newUser("Play Doe"), + User.newUser("Sour Doe") + ); + + Pageable mockPageable = mock(Pageable.class); + + Sort orderBy = Sort.by("name").ascending(); + + doReturn(true).when(mockPageable).isPaged(); + doReturn(0).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + doReturn(orderBy).when(mockPageable).getSort(); + + Page pageOne = PagingUtils.toPage(users, mockPageable); + + assertThat(pageOne).isNotNull(); + assertThat(pageOne).isNotEmpty(); + assertThat(pageOne.getNumber()).isEqualTo(0); + assertThat(pageOne.getNumberOfElements()).isEqualTo(5); + assertThat(pageOne.getSize()).isEqualTo(5); + assertThat(pageOne.getSort()).isEqualTo(orderBy); + assertThat(pageOne.getTotalElements()).isEqualTo(users.size()); + assertThat(pageOne.getTotalPages()).isEqualTo(2); + assertThat(pageOne.getContent()).containsExactly(users.subList(0, 5).toArray(new User[0])); + + doReturn(1).when(mockPageable).getPageNumber(); + + Page pageTwo = PagingUtils.toPage(users, mockPageable); + + assertThat(pageTwo).isNotNull(); + assertThat(pageTwo).isNotEmpty(); + assertThat(pageTwo.getNumber()).isEqualTo(1); + assertThat(pageTwo.getNumberOfElements()).isEqualTo(4); + assertThat(pageTwo.getSize()).isEqualTo(5); + assertThat(pageTwo.getSort()).isEqualTo(orderBy); + assertThat(pageTwo.getTotalElements()).isEqualTo(users.size()); + assertThat(pageTwo.getTotalPages()).isEqualTo(2); + assertThat(pageTwo.getContent()).containsExactly(users.subList(5, users.size()).toArray(new User[0])); + + doReturn(2).when(mockPageable).getPageNumber(); + + Page pageThree = PagingUtils.toPage(users, mockPageable); + + assertThat(pageThree).isNotNull(); + assertThat(pageThree).isEmpty(); + } + + @Test + public void getPagedListFromListWithSizeLessThanPageSize() { + + List users = Arrays.asList( + User.newUser("Jon Doe"), + User.newUser("Jane Doe"), + User.newUser("Pie Doe") + ); + + Pageable mockPageable = mock(Pageable.class); + + doReturn(true).when(mockPageable).isPaged(); + doReturn(0).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + List pageOne = PagingUtils.getPagedList(users, mockPageable); + + assertThat(pageOne).isNotNull(); + assertThat(pageOne).isNotEmpty(); + assertThat(pageOne).containsExactly(users.toArray(new User[0])); + } + + @Test + public void getPagedListFromListResultingInEmptyPage() { + + List users = Arrays.asList( + User.newUser("Jon Doe"), + User.newUser("Jane Doe"), + User.newUser("Pie Doe") + ); + + Pageable mockPageable = mock(Pageable.class); + + doReturn(true).when(mockPageable).isPaged(); + doReturn(1).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + List pageOne = PagingUtils.getPagedList(users, mockPageable); + + assertThat(pageOne).isNotNull(); + assertThat(pageOne).isEmpty(); + } + + @Test + public void getPagedListFromEmptyList() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(true).when(mockPageable).isPaged(); + doReturn(0).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + List pageOne = PagingUtils.getPagedList(Collections.emptyList(), mockPageable); + + assertThat(pageOne).isNotNull(); + assertThat(pageOne).isEmpty(); + } + + @Test + public void getPagedListFromNullList() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(true).when(mockPageable).isPaged(); + doReturn(0).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + List pageOne = PagingUtils.getPagedList(null, mockPageable); + + assertThat(pageOne).isNotNull(); + assertThat(pageOne).isEmpty(); + } + + @Test + public void getPagedListFromNullPageable() { + + List pageOne = PagingUtils.getPagedList(Collections.singletonList(User.newUser("Jon Doe")), null); + + assertThat(pageOne).isNotNull(); + assertThat(pageOne).isEmpty(); + } + + @Test + public void getPageRequestIsCorrect() { + + Pageable mockPageable = mock(Pageable.class); + + Object[] arguments = { "test", mockPageable, "mock" }; + + Parameters mockParameters = mock(Parameters.class); + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + Stream mockStream = mock(Stream.class); + + doReturn(mockParameters).when(mockQueryMethod).getParameters(); + doReturn(1).when(mockParameters).getPageableIndex(); + doReturn(true).when(mockParameters).hasPageableParameter(); + doReturn(mockStream).when(mockParameters).stream(); + doReturn(3L).when(mockStream).count(); + + Pageable pageable = PagingUtils.getPageRequest(mockQueryMethod, arguments); + + assertThat(pageable).isSameAs(mockPageable); + + InOrder order = inOrder(mockQueryMethod, mockParameters, mockStream); + + order.verify(mockQueryMethod, times(1)).getParameters(); + order.verify(mockParameters, times(1)).hasPageableParameter(); + order.verify(mockParameters, times(1)).stream(); + order.verify(mockStream, times(1)).count(); + order.verify(mockParameters, times(1)).getPageableIndex(); + verifyNoMoreInteractions(mockQueryMethod, mockParameters, mockStream); + verifyNoInteractions(mockPageable); + } + + @Test(expected = IllegalArgumentException.class) + public void getPageRequestWithNullQueryMethodThrowsIllegalArgumentException() { + + try { + PagingUtils.getPageRequest(null, "test", "mock"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("QueryMethod must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalStateException.class) + public void getPageRequestFromQueryMethodHavingNoPageableParameter() { + + Parameters mockParameters = mock(Parameters.class); + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + doReturn(mockParameters).when(mockQueryMethod).getParameters(); + doReturn(false).when(mockParameters).hasPageableParameter(); + + try { + PagingUtils.getPageRequest(mockQueryMethod, "test", "mock"); + } + catch (IllegalStateException expected) { + + assertThat(expected) + .hasMessage("QueryMethod [%s] does not have a Pageable parameter", mockQueryMethod); + + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryMethod, times(1)).getParameters(); + verify(mockParameters, times(1)).hasPageableParameter(); + verifyNoMoreInteractions(mockQueryMethod, mockParameters); + } + } + + @Test(expected = IllegalArgumentException.class) + public void getPageRequestFromQueryMethodHavingLessParametersThanArgumentsThrowsIllegalArgumentException() { + + Object[] arguments = { 1, 2, 3 }; + + Parameters mockParameters = mock(Parameters.class); + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + Stream mockStream = mock(Stream.class); + + doReturn(mockParameters).when(mockQueryMethod).getParameters(); + doReturn(true).when(mockParameters).hasPageableParameter(); + doReturn(mockStream).when(mockParameters).stream(); + doReturn(2L).when(mockStream).count(); + + try { + PagingUtils.getPageRequest(mockQueryMethod, arguments); + } + catch (IllegalArgumentException expected) { + + assertThat(expected) + .hasMessage("The number of arguments [%d] must match the number of QueryMethod [%s] parameters [2]", + arguments.length, mockQueryMethod); + + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryMethod, times(1)).getParameters(); + verify(mockParameters, times(1)).hasPageableParameter(); + verify(mockParameters, times(1)).stream(); + verify(mockStream, times(1)).count(); + verifyNoMoreInteractions(mockQueryMethod, mockParameters, mockStream); + } + } + + @Test(expected = IllegalArgumentException.class) + public void getPageRequestFromQueryMethodWherePageableParameterIndexDoesMatchPageableArgument() { + + Pageable mockPageable = mock(Pageable.class); + + Object[] arguments = { mockPageable, "test", -1 }; + + Parameters mockParameters = mock(Parameters.class); + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + Stream mockStream = mock(Stream.class); + + doReturn(mockParameters).when(mockQueryMethod).getParameters(); + doReturn(1).when(mockParameters).getPageableIndex(); + doReturn(true).when(mockParameters).hasPageableParameter(); + doReturn(mockStream).when(mockParameters).stream(); + doReturn(3L).when(mockStream).count(); + + try { + PagingUtils.getPageRequest(mockQueryMethod, arguments); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Argument [test] must be of type [%2$s]", Pageable.class.getName()); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockQueryMethod, times(1)).getParameters(); + verify(mockParameters, times(1)).hasPageableParameter(); + verify(mockParameters, times(1)).stream(); + verify(mockStream, times(1)).count(); + verify(mockParameters, times(1)).getPageableIndex(); + verifyNoMoreInteractions(mockQueryMethod, mockParameters, mockStream); + } + } + + @Test + public void getQueryResultSetStartIndexForPageIsCorrect() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(0).doReturn(1).doReturn(2).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + assertThat(PagingUtils.getQueryResultSetStartIndexForPage(mockPageable)).isZero(); + assertThat(PagingUtils.getQueryResultSetStartIndexForPage(mockPageable)).isEqualTo(5); + assertThat(PagingUtils.getQueryResultSetStartIndexForPage(mockPageable)).isEqualTo(10); + + verify(mockPageable, times(3)).getPageNumber(); + verify(mockPageable, times(3)).getPageSize(); + verifyNoMoreInteractions(mockPageable); + } + + @Test + public void getQueryResultSetStartIndexForPageWithNullPageableIsCorrect() { + assertThat(PagingUtils.getQueryResultSetStartIndexForPage(null)).isZero(); + } + + @Test + public void getQueryResultSetEndIndexForPageIsCorrect() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(0).doReturn(1).doReturn(2).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + assertThat(PagingUtils.getQueryResultSetEndIndexForPage(mockPageable)).isEqualTo(5); + assertThat(PagingUtils.getQueryResultSetEndIndexForPage(mockPageable)).isEqualTo(10); + assertThat(PagingUtils.getQueryResultSetEndIndexForPage(mockPageable)).isEqualTo(15); + + verify(mockPageable, times(3)).getPageNumber(); + verify(mockPageable, times(6)).getPageSize(); + verifyNoMoreInteractions(mockPageable); + } + + @Test + public void getQueryResultSetEndIndexForPageWithNullPageableIsCorrect() { + assertThat(PagingUtils.getQueryResultSetEndIndexForPage(null)).isZero(); + } + + @Test + public void getQueryResultSetLimitForPageIsCorrect() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(0).doReturn(1).doReturn(2).when(mockPageable).getPageNumber(); + doReturn(5).when(mockPageable).getPageSize(); + + assertThat(PagingUtils.getQueryResultSetLimitForPage(mockPageable)).isEqualTo(5); + assertThat(PagingUtils.getQueryResultSetLimitForPage(mockPageable)).isEqualTo(10); + assertThat(PagingUtils.getQueryResultSetLimitForPage(mockPageable)).isEqualTo(15); + + verify(mockPageable, times(3)).getPageNumber(); + verify(mockPageable, times(3)).getPageSize(); + verifyNoMoreInteractions(mockPageable); + } + + @Test + public void getQueryResultSetLimitForPageWithNullPageableIsCorrect() { + assertThat(PagingUtils.getQueryResultSetLimitForPage(null)).isZero(); + } + + @Test + public void normalizePageNumberFromPage() { + + Page mockPage = mock(Page.class); + + doReturn(0).when(mockPage).getNumber(); + + assertThat(PagingUtils.normalizePageNumber(mockPage)).isOne(); + + verify(mockPage, times(1)).getNumber(); + verifyNoMoreInteractions(mockPage); + } + + @Test + public void normalizePageNumberFromNullPage() { + assertThat(PagingUtils.normalizePageNumber((Page) null)).isZero(); + } + + @Test + public void normalizePageNumberFromPageable() { + + Pageable mockPageable = mock(Pageable.class); + + doReturn(0).when(mockPageable).getPageNumber(); + + assertThat(PagingUtils.normalizePageNumber(mockPageable)).isOne(); + + verify(mockPageable, times(1)).getPageNumber(); + verifyNoMoreInteractions(mockPageable); + } + + @Test + public void normalizePageNumberFromNullPageable() { + assertThat(PagingUtils.normalizePageNumber((Pageable) null)).isZero(); + } + + @Test + public void normalizesZeroIndexBasedPageNumbers() { + + assertThat(PagingUtils.normalize(-2)).isEqualTo(0); + assertThat(PagingUtils.normalize(-1)).isEqualTo(0); + assertThat(PagingUtils.normalize(0)).isEqualTo(1); + assertThat(PagingUtils.normalize(1)).isEqualTo(2); + assertThat(PagingUtils.normalize(2)).isEqualTo(3); + } + + @Test + public void nullSafeSizeFromCollection() { + + assertThat(PagingUtils.nullSafeSize(Collections.singleton(User.newUser("Jon Doe")))).isOne(); + assertThat(PagingUtils.nullSafeSize(Arrays.asList(User.newUser("Jon Doe"), User.newUser("Jane Doe"), User.newUser("Pie Doe")))).isEqualTo(3); + } + + @Test + public void nullSafeSizeFromEmptyCollection() { + assertThat(PagingUtils.nullSafeSize(Collections.emptyList())).isZero(); + } + + @Test + public void nullSafeSizeFromIterable() { + assertThat(PagingUtils.nullSafeSize(ArrayUtils.toIterable(User.newUser("Jon Doe"), User.newUser("Jane Doe")))).isEqualTo(2); + } + + @Test + public void nullSafeSizeFromEmptyIterable() { + assertThat(PagingUtils.nullSafeSize(CollectionUtils.emptyIterable())).isZero(); + } + + @Test + public void nullSafeSizeFromNull() { + assertThat(PagingUtils.nullSafeSize(null)).isZero(); + } + + @Getter + @Region("Users") + @ToString(of = "name") + @EqualsAndHashCode(of = "name") + @RequiredArgsConstructor(staticName = "newUser") + static class User { + + @Id + Long id; + + @NonNull + final String name; + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutorUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutorUnitTests.java new file mode 100644 index 00000000..119d2704 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/query/support/TemplateBasedOqlQueryExecutorUnitTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.gemfire.repository.query.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import org.junit.Test; + +import org.apache.geode.cache.query.SelectResults; + +import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.repository.query.QueryMethod; + +/** + * Unit Tests for {@link TemplateBasedOqlQueryExecutor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.data.gemfire.repository.query.support.TemplateBasedOqlQueryExecutor + * @since 2.4.0 + */ +public class TemplateBasedOqlQueryExecutorUnitTests { + + @Test + public void constructTemplateBasedOqlQueryExecutorSuccessfully() { + + GemfireTemplate mockTemplate = mock(GemfireTemplate.class); + + TemplateBasedOqlQueryExecutor queryExecutor = new TemplateBasedOqlQueryExecutor(mockTemplate); + + assertThat(queryExecutor).isNotNull(); + assertThat(queryExecutor.getTemplate()).isEqualTo(mockTemplate); + + verifyNoInteractions(mockTemplate); + } + + @Test(expected = IllegalArgumentException.class) + public void constructTemplateBasedOqlQueryExecutorWithNullTemplateThrowsIllegalArgumentException() { + + try { + new TemplateBasedOqlQueryExecutor(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("GemfireTemplate must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void executeCallsTemplateFind() { + + GemfireTemplate mockTemplate = mock(GemfireTemplate.class); + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + SelectResults mockSelectResults = mock(SelectResults.class); + + String query = "SELECT * FROM /TestRegion WHERE id = $1"; + + doReturn(mockSelectResults).when(mockTemplate).find(eq(query), eq(1)); + + TemplateBasedOqlQueryExecutor queryExecutor = new TemplateBasedOqlQueryExecutor(mockTemplate); + + assertThat(queryExecutor.execute(mockQueryMethod, query, 1)).isEqualTo(mockSelectResults); + + verify(mockTemplate, times(1)).find(eq(query), eq(1)); + verifyNoInteractions(mockQueryMethod, mockSelectResults); + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/User.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/User.java index 825811c2..221111d4 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/User.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/repository/sample/User.java @@ -16,6 +16,7 @@ package org.springframework.data.gemfire.repository.sample; import java.util.Calendar; +import java.util.Objects; import org.springframework.data.annotation.Id; import org.springframework.data.gemfire.mapping.annotation.Region; @@ -23,11 +24,12 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * The User class represents an authorized user of a service or computer system, etc. + * Abstract Data Type (ADT) modeling an authorized user of an application, software service or computer system. * * @author John Blum * @see java.lang.Comparable * @see org.springframework.data.annotation.Id + * @see org.springframework.data.gemfire.mapping.annotation.Region * @see Region * @since 1.4.0 */ @@ -45,11 +47,11 @@ public class User implements Comparable { private final String username; public User(String username) { - Assert.hasText(username, "The username is required!"); + Assert.hasText(username, "Username is required"); this.username = username; } - public void setActive(final Boolean active) { + public void setActive(Boolean active) { this.active = Boolean.TRUE.equals(active); } @@ -66,7 +68,7 @@ public class User implements Comparable { } public String getEmail() { - return email; + return this.email; } public void setSince(final Calendar since) { @@ -74,11 +76,11 @@ public class User implements Comparable { } public Calendar getSince() { - return since; + return this.since; } public String getUsername() { - return username; + return this.username; } @Override @@ -87,7 +89,7 @@ public class User implements Comparable { } protected static boolean equalsIgnoreNull(Object obj1, Object obj2) { - return obj1 == null ? obj2 == null : obj1.equals(obj2); + return Objects.equals(obj1, obj2); } @Override diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/util/ArrayUtilsUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/util/ArrayUtilsUnitTests.java index 32a5b463..709ca8c4 100644 --- a/spring-data-geode/src/test/java/org/springframework/data/gemfire/util/ArrayUtilsUnitTests.java +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/util/ArrayUtilsUnitTests.java @@ -242,4 +242,40 @@ public class ArrayUtilsUnitTests { assertThat(sortedArray).isSameAs(array); assertThat(sortedArray).isEqualTo(new Comparable[] { 1, 2, 3 }); } + + @Test + public void toIterableFromArray() { + + Integer[] array = { 1, 2, 3 }; + + Iterable iterable = ArrayUtils.toIterable(array); + + assertThat(iterable).isNotNull(); + assertThat(iterable).hasSize(array.length); + assertThat(iterable).containsExactly(array); + } + + @Test + public void toIterableFromEmptyArray() { + + Iterable iterable = ArrayUtils.toIterable(); + + assertThat(iterable).isNotNull(); + assertThat(iterable).isEmpty(); + } + + @Test(expected = IllegalArgumentException.class) + public void toIterableFromNullArrayThrowsIllegalArgumentException() { + + try { + ArrayUtils.toIterable((Object[]) null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Array must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } }