From 0e3ec2a1242467604391dbc2b4c996d1537c035b Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 9 Mar 2018 14:40:56 -0800 Subject: [PATCH] SGF-713 - Override generated OQL from Repository query methods. --- .../query/AbstractQueryPostProcessor.java | 157 ++++++++++++ .../query/GemfireRepositoryQuery.java | 49 +++- .../query/PartTreeGemfireRepositoryQuery.java | 29 ++- .../repository/query/QueryPostProcessor.java | 111 +++++++++ .../gemfire/repository/query/QueryString.java | 5 + .../StringBasedGemfireRepositoryQuery.java | 156 ++++++++---- .../support/GemfireRepositoryFactoryBean.java | 227 +++++++++++++++-- .../AbstractQueryPostProcessorUnitTests.java | 126 ++++++++++ .../QueryPostProcessingIntegrationTests.java | 232 ++++++++++++++++++ ...StringBasedGemfireRepositoryQueryTest.java | 48 ++-- 10 files changed, 1031 insertions(+), 109 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessor.java create mode 100644 src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java create mode 100644 src/test/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessorUnitTests.java create mode 100644 src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessingIntegrationTests.java diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessor.java b/src/main/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessor.java new file mode 100644 index 00000000..91f1d266 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessor.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.repository.query; + +import java.util.Properties; + +import org.springframework.core.Ordered; +import org.springframework.data.gemfire.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.util.Assert; + +/** + * The {@link AbstractQueryPostProcessor} class is an abstract base class for simplifying the implementation + * of {@link QueryPostProcessor QueryPostProcessors}. + * + * {@link QueryPostProcessor QueryPostProcessors} are useful for handling and processing {@link QUERY queries} + * generated from {@link Repository} {@link QueryMethod query methods}, and give a developer an opportunity, + * via the callback, to further process the generated {@link QUERY query}. + * + * {@link QueryPostProcessor QueryPostProcessors} can be used on both {@literal generated} {@link QUERY queries} + * and {@literal manual} {@link QUERY queries}. {@literal Manual} {@link QUERY queries} are defined as + * {@link QUERY queries} specified using SDG's {@link Query @Query} annotation or by defining a {@literal named} + * {@link QUERY query} in a module-specific {@link Properties} files. + * + * @author John Blum + * @param {@link Class type} identifying the {@link Repository Repositories} to match on during registration. + * @param {@link Class type} of the query to process. + * @see org.springframework.core.Ordered + * @see org.springframework.data.repository.Repository + * @see org.springframework.data.repository.query.QueryMethod + * @since 2.1.0 + */ +public abstract class AbstractQueryPostProcessor implements QueryPostProcessor { + + protected static final Object[] EMPTY_ARRAY = {}; + + /** + * Defines the {@link Integer order} of this {@link QueryPostProcessor} relative to + * other {@link QueryPostProcessor QueryPostProcessors} in a sort. + * + * Defaults to the {@link Ordered#LOWEST_PRECEDENCE}. + * + * @return an {@link Integer} value specifying the order of this {@link QueryPostProcessor} relative to + * other {@link QueryPostProcessor QueryPostProcessors} in a sort. + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + /** + * Callback method invoked by the Spring Data (SD) {@link Repository} framework to allow the user to process + * the given {@link QUERY query} and (possibly) return a new or modified version of the {@link QUERY query}. + * + * This callback is invoked for {@literal queries} generated from a SD {@link Repository} {@link QueryMethod} + * signature as well as {@literal queries} specified and defined in {@link NamedQueries}, + * or even using SDG's {@link Query @Query} annotation. + * + * @param query {@link QUERY query} to process. + * @return a new or modified version of the same {@link QUERY query}. + * @see org.springframework.data.repository.query.QueryMethod + * @see #postProcess(QueryMethod, Object, Object...) + */ + @Override + public QUERY postProcess(QueryMethod queryMethod, QUERY query) { + return postProcess(queryMethod, query, EMPTY_ARRAY); + } + + /** + * Builder method used to compose, or combine this {@link QueryPostProcessor QueryPostProcessors} + * with the given {@link QueryPostProcessor}. + * + * This {@link QueryPostProcessor} will come before the given {@link QueryPostProcessor} in the processing chain. + * + * @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}. + * @return a composed {@link QueryPostProcessor} consisting of this {@link QueryPostProcessor} + * followed by the given {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor} + * if the given {@link QueryPostProcessor} is {@literal null}. + * @see #processAfter(QueryPostProcessor) + */ + public QueryPostProcessor processBefore(QueryPostProcessor queryPostProcessor) { + return ComposableQueryPostProcessor.compose(this, queryPostProcessor); + } + + /** + * Builder method used to compose, or combine this {@link QueryPostProcessor} with + * the given {@link QueryPostProcessor}. + * + * This {@link QueryPostProcessor} will come after the given {@link QueryPostProcessor} in the processing chain. + * + * @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}. + * @return a composed {@link QueryPostProcessor} consisting of the given {@link QueryPostProcessor} + * followed by this {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor} + * if the given {@link QueryPostProcessor} is {@literal null}. + * @see #processBefore(QueryPostProcessor) + */ + public QueryPostProcessor processAfter(QueryPostProcessor queryPostProcessor) { + return ComposableQueryPostProcessor.compose(queryPostProcessor, this); + } + + protected static class ComposableQueryPostProcessor + extends AbstractQueryPostProcessor { + + @SuppressWarnings("unchecked") + protected static QueryPostProcessor compose( + QueryPostProcessor first, QueryPostProcessor second) { + + return first == null ? second : second == null ? first + : new ComposableQueryPostProcessor(first, second); + } + + private final QueryPostProcessor first; + private final QueryPostProcessor second; + + protected ComposableQueryPostProcessor(QueryPostProcessor left, QueryPostProcessor second) { + + Assert.notNull(left, "The first QueryPostProcessor operand must not be null"); + Assert.notNull(second, "The second QueryPostProcessor operand must not be null"); + + this.first = left; + this.second = second; + } + + protected QueryPostProcessor getFirst() { + return this.first; + } + + protected QueryPostProcessor getSecond() { + return this.second; + } + + @Override + public QUERY postProcess(QueryMethod queryMethod, QUERY query, Object... arguments) { + + return getSecond().postProcess(queryMethod, + getFirst().postProcess(queryMethod, query, arguments), arguments); + } + } +} diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java index e7e065ed..dddf8dbc 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/GemfireRepositoryQuery.java @@ -15,6 +15,7 @@ */ package org.springframework.data.gemfire.repository.query; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; @@ -28,10 +29,12 @@ import org.springframework.util.Assert; * @see org.springframework.data.gemfire.repository.query.GemfireQueryMethod * @see org.springframework.data.repository.query.RepositoryQuery */ -abstract class GemfireRepositoryQuery implements RepositoryQuery { +public abstract class GemfireRepositoryQuery implements RepositoryQuery { private final GemfireQueryMethod queryMethod; + private QueryPostProcessor queryPostProcessor = ProvidedQueryPostProcessor.INSTANCE; + /* * (non-Javadoc) * Constructor used for testing purposes only! @@ -42,15 +45,15 @@ abstract class GemfireRepositoryQuery implements RepositoryQuery { /** * Creates a new {@link GemfireRepositoryQuery} using the given {@link GemfireQueryMethod}. - * + * * @param queryMethod must not be {@literal null}. */ public GemfireRepositoryQuery(GemfireQueryMethod queryMethod) { - Assert.notNull(queryMethod); + Assert.notNull(queryMethod, "QueryMethod must not be null"); this.queryMethod = queryMethod; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() */ @@ -59,4 +62,42 @@ abstract class GemfireRepositoryQuery implements RepositoryQuery { return this.queryMethod; } + /** + * Returns a reference to the composed {@link QueryPostProcessor QueryPostProcessors}, which are applied + * to {@literal OQL queries} prior to execution. + * + * @return a reference to the composed {@link QueryPostProcessor QueryPostProcessors}. + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor + */ + protected QueryPostProcessor getQueryPostProcessor() { + return this.queryPostProcessor; + } + + /** + * Registers the given {@link QueryPostProcessor} to use for processing {@literal OQL queries} + * generated from {@link Repository} {@link QueryMethod query methods}. + * + * Registration always links the given {@link QueryPostProcessor} to the end of the processing chain + * of previously registered {@link QueryPostProcessor QueryPostProcessors}. In other words, the given + * {@link QueryPostProcessor} argument will process {@literal OQL queries} only after all + * {@link QueryPostProcessor QueryPostProcessor} registered before it. + * + * @param queryPostProcessor {@link QueryPostProcessor} to register. + * @return this {@link GemfireRepositoryQuery}. + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor#processBefore(QueryPostProcessor) + */ + public GemfireRepositoryQuery register(QueryPostProcessor queryPostProcessor) { + this.queryPostProcessor = this.queryPostProcessor.processBefore(queryPostProcessor); + return this; + } + + static class ProvidedQueryPostProcessor extends AbstractQueryPostProcessor { + + static final ProvidedQueryPostProcessor INSTANCE = new ProvidedQueryPostProcessor(); + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + return query; + } + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java index 9051838f..9bacb0d2 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/PartTreeGemfireRepositoryQuery.java @@ -22,14 +22,13 @@ import java.util.List; import org.springframework.data.domain.Sort; import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; /** * {@link GemfireRepositoryQuery} backed by a {@link PartTree} and thus, deriving an OQL query from the backing query * method's name. - * + * * @author Oliver Gierke */ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { @@ -41,7 +40,7 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { /** * Creates a new {@link PartTreeGemfireRepositoryQuery} using the given {@link GemfireQueryMethod} and * {@link GemfireTemplate}. - * + * * @param method must not be {@literal null}. * @param template must not be {@literal null}. */ @@ -56,24 +55,39 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { this.template = template; } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) */ @Override public Object execute(Object[] parameters) { - ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor(method.getParameters(), parameters); - QueryString query = new GemfireQueryCreator(tree, method.getPersistentEntity()) + ParametersParameterAccessor parameterAccessor = + new ParametersParameterAccessor(this.method.getParameters(), parameters); + + QueryString query = new GemfireQueryCreator(this.tree, this.method.getPersistentEntity()) .createQuery(parameterAccessor.getSort()); - RepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(query.toString(), method, template); + GemfireRepositoryQuery repositoryQuery = newRepositoryQuery(query, this.method, this.template); return repositoryQuery.execute(prepareStringParameters(parameters)); } + private GemfireRepositoryQuery newRepositoryQuery(QueryString query, + GemfireQueryMethod queryMethod, GemfireTemplate template) { + + GemfireRepositoryQuery repositoryQuery = + new StringBasedGemfireRepositoryQuery(query.toString(), queryMethod, template); + + repositoryQuery.register(getQueryPostProcessor()); + + return repositoryQuery; + } + private Object[] prepareStringParameters(Object[] parameters) { + Iterator partsIterator = tree.getParts().iterator(); + List stringParameters = new ArrayList(parameters.length); for (Object parameter : parameters) { @@ -99,5 +113,4 @@ public class PartTreeGemfireRepositoryQuery extends GemfireRepositoryQuery { return stringParameters.toArray(); } - } diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java new file mode 100644 index 00000000..f82d9440 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryPostProcessor.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.repository.query; + +import java.util.Properties; + +import org.springframework.core.Ordered; +import org.springframework.data.gemfire.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.QueryMethod; + +/** + * The {@link QueryPostProcessor} interface defines a contract for implementations to post process + * a given {@link QUERY query} and possibly return a new or modified version of the same {@link QUERY query}. + * + * {@link QueryPostProcessor QueryPostProcessors} are useful for handling and processing {@link QUERY queries} + * generated from {@link Repository} {@link QueryMethod query methods}, and give a developer an opportunity, + * via the callback, to further process the generated {@link QUERY query}. + * + * {@link QueryPostProcessor QueryPostProcessors} can be used on both generated {@link QUERY queries} + * and {@literal manual} {@link QUERY queries}. {@literal Manual} {@link QUERY queries} are defined as + * {@link QUERY queries} specified using SDG's {@link Query @Query} annotation or by defining a {@literal named} + * {@link QUERY query} in a module-specific {@link Properties} files. + * + * @author John Blum + * @param {@link Class type} identifying the {@link Repository Repositories} to match on during registration. + * @param {@link Class type} of the query to process. + * @see org.springframework.core.Ordered + * @see org.springframework.data.gemfire.repository.Query + * @see org.springframework.data.repository.Repository + * @see org.springframework.data.repository.query.QueryMethod + * @since 2.1.0 + */ +public interface QueryPostProcessor extends Ordered { + + /** + * Callback method invoked by the Spring Data (SD) {@link Repository} framework to allow the user to process + * the given {@link QUERY query} and (possibly) return a new or modified version of the {@link QUERY query}. + * + * This callback is invoked for {@literal queries} generated from a SD {@link Repository} {@link QueryMethod} + * signature as well as {@literal queries} specified and defined in {@link NamedQueries}, + * or even using SDG's {@link Query @Query} annotation. + * + * @param query {@link QUERY query} to process. + * @return a new or modified version of the same {@link QUERY query}. + * @see org.springframework.data.repository.query.QueryMethod + * @see #postProcess(QueryMethod, Object, Object...) + */ + QUERY postProcess(QueryMethod queryMethod, QUERY query); + + /** + * Callback method invoked by the Spring Data (SD) {@link Repository} framework to allow the user to process + * the given {@link QUERY query} and (possibly) return a new or modified version of the {@link QUERY query}. + * + * This callback is invoked for {@literal queries} generated from a SD {@link Repository} {@link QueryMethod} + * signature as well as {@literal queries} specified and defined in {@link NamedQueries}, + * or even using SDG's {@link Query @Query} annotation. + * + * @param query {@link QUERY query} to process. + * @param arguments array of {@link Object Objects} containing the arguments to the query parameters. + * @return a new or modified version of the same {@link QUERY query}. + * @see org.springframework.data.repository.query.QueryMethod + * @see #postProcess(QueryMethod, Object) + */ + QUERY postProcess(QueryMethod queryMethod, QUERY query, Object... arguments); + + /** + * Builder method used to compose, or combine this {@link QueryPostProcessor QueryPostProcessors} + * with the given {@link QueryPostProcessor}. + * + * This {@link QueryPostProcessor} will come before the given {@link QueryPostProcessor} in the processing chain. + * + * @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}. + * @return a composed {@link QueryPostProcessor} consisting of this {@link QueryPostProcessor} + * followed by the given {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor} + * if the given {@link QueryPostProcessor} is {@literal null}. + * @see #processAfter(QueryPostProcessor) + */ + QueryPostProcessor processBefore(QueryPostProcessor queryPostProcessor); + + /** + * Builder method used to compose, or combine this {@link QueryPostProcessor} with + * the given {@link QueryPostProcessor}. + * + * This {@link QueryPostProcessor} will come after the given {@link QueryPostProcessor} in the processing chain. + * + * @param queryPostProcessor {@link QueryPostProcessor} to compose with this {@link QueryPostProcessor}. + * @return a composed {@link QueryPostProcessor} consisting of the given {@link QueryPostProcessor} + * followed by this {@link QueryPostProcessor}. Returns this {@link QueryPostProcessor} + * if the given {@link QueryPostProcessor} is {@literal null}. + * @see #processBefore(QueryPostProcessor) + */ + QueryPostProcessor processAfter(QueryPostProcessor queryPostProcessor); + +} diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java b/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java index 628bfdf2..8aa93ca2 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/QueryString.java @@ -63,6 +63,11 @@ public class QueryString { validateDomainType(domainType).getSimpleName()); } + /* (non-Javadoc) */ + static QueryString of(String query) { + return new QueryString(query); + } + /* (non-Javadoc) */ static Class validateDomainType(Class domainType) { Assert.notNull(domainType, "domainType must not be null"); diff --git a/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java b/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java index 523e205e..12bc30e9 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java +++ b/src/main/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQuery.java @@ -18,16 +18,17 @@ package org.springframework.data.gemfire.repository.query; import java.util.Collection; import java.util.Collections; +import com.gemstone.gemfire.cache.query.SelectResults; + import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import com.gemstone.gemfire.cache.query.SelectResults; - /** * {@link GemfireRepositoryQuery} using plain {@link String} based OQL queries. *

@@ -49,8 +50,14 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * Constructor used for testing purposes only! */ StringBasedGemfireRepositoryQuery() { - query = null; - template = null; + + this.query = null; + this.template = null; + + register(LimitQueryPostProcessor.INSTANCE + .processBefore(ImportQueryPostProcessor.INSTANCE) + .processBefore(HintQueryPostProcessor.INSTANCE) + .processBefore(TraceQueryPostProcessor.INSTANCE)); } /** @@ -73,17 +80,20 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @param template must not be {@literal null}. */ public StringBasedGemfireRepositoryQuery(String query, GemfireQueryMethod queryMethod, GemfireTemplate template) { + super(queryMethod); - Assert.notNull(template); + Assert.notNull(template, "GemfireTemplate must not be null"); + Assert.state(!(queryMethod.isModifyingQuery() || queryMethod.isPageQuery()), INVALID_QUERY); this.userDefinedQuery |= !StringUtils.hasText(query); this.query = new QueryString(StringUtils.hasText(query) ? query : queryMethod.getAnnotatedQuery()); this.template = template; - if (queryMethod.isModifyingQuery() || queryMethod.isPageQuery()) { - throw new IllegalStateException(INVALID_QUERY); - } + register(LimitQueryPostProcessor.INSTANCE + .processBefore(ImportQueryPostProcessor.INSTANCE) + .processBefore(HintQueryPostProcessor.INSTANCE) + .processBefore(TraceQueryPostProcessor.INSTANCE)); } /* @@ -106,27 +116,30 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) */ @Override - public Object execute(Object[] parameters) { - QueryMethod localQueryMethod = getQueryMethod(); + public Object execute(Object[] arguments) { - QueryString query = (isUserDefinedQuery() ? this.query : this.query.forRegion( - localQueryMethod.getEntityInformation().getJavaType(), template.getRegion())); + QueryMethod queryMethod = getQueryMethod(); - ParametersParameterAccessor parameterAccessor = new ParametersParameterAccessor( - localQueryMethod.getParameters(), parameters); + QueryString query = isUserDefinedQuery() ? this.query + : this.query.forRegion(queryMethod.getEntityInformation().getJavaType(), this.template.getRegion()); + + ParametersParameterAccessor parameterAccessor = + new ParametersParameterAccessor(queryMethod.getParameters(), arguments); for (Integer index : query.getInParameterIndexes()) { query = query.bindIn(toCollection(parameterAccessor.getBindableValue(index - 1))); } - query = applyQueryAnnotationExtensions(localQueryMethod, query); + String queryString = getQueryPostProcessor().postProcess(queryMethod, query.toString(), arguments); - Collection result = toCollection(template.find(query.toString(), parameters)); + SelectResults selectResults = this.template.find(queryString, arguments); - if (localQueryMethod.isCollectionQuery()) { + Collection result = toCollection(selectResults); + + if (queryMethod.isCollectionQuery()) { return result; } - else if (localQueryMethod.isQueryForEntity()) { + else if (queryMethod.isQueryForEntity()) { if (result.isEmpty()) { return null; } @@ -137,7 +150,7 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { throw new IncorrectResultSizeDataAccessException(1, result.size()); } } - else if (isSingleResultNonEntityQuery(localQueryMethod, result)) { + else if (isSingleResultNonEntityQuery(queryMethod, result)) { return result.iterator().next(); } else { @@ -145,31 +158,8 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { } } - QueryString applyQueryAnnotationExtensions(final QueryMethod queryMethod, final QueryString queryString) { - QueryString resolvedQueryString = queryString; - - if (queryMethod instanceof GemfireQueryMethod) { - GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; - String query = queryString.toString().toUpperCase(); - - if (gemfireQueryMethod.hasImport() && !QueryString.IMPORT_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withImport(gemfireQueryMethod.getImport()); - } - if (gemfireQueryMethod.hasHint() && !QueryString.HINT_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withHints(gemfireQueryMethod.getHints()); - } - if (gemfireQueryMethod.hasLimit() && !QueryString.LIMIT_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withLimit(gemfireQueryMethod.getLimit()); - } - if (gemfireQueryMethod.hasTrace() && !QueryString.TRACE_PATTERN.matcher(query).find()) { - resolvedQueryString = resolvedQueryString.withTrace(); - } - } - - return resolvedQueryString; - } - boolean isSingleResultNonEntityQuery(QueryMethod method, Collection result) { + return (!method.isCollectionQuery() && method.getReturnedObjectType() != null && !Void.TYPE.equals(method.getReturnedObjectType()) && result != null && result.size() == 1); } @@ -186,6 +176,7 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { * @see com.gemstone.gemfire.cache.query.SelectResults */ Collection toCollection(final Object source) { + if (source instanceof SelectResults) { return ((SelectResults) source).asList(); } @@ -201,4 +192,83 @@ public class StringBasedGemfireRepositoryQuery extends GemfireRepositoryQuery { return (source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singletonList(source)); } + protected static class HintQueryPostProcessor extends AbstractQueryPostProcessor { + + protected static final HintQueryPostProcessor INSTANCE = new HintQueryPostProcessor(); + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasHint() && !QueryString.HINT_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withHints(gemfireQueryMethod.getHints()).toString(); + } + } + + return query; + } + } + + protected static class ImportQueryPostProcessor extends AbstractQueryPostProcessor { + + protected static final ImportQueryPostProcessor INSTANCE = new ImportQueryPostProcessor(); + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasImport() && !QueryString.IMPORT_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withImport(gemfireQueryMethod.getImport()).toString(); + } + } + + return query; + } + } + + protected static class LimitQueryPostProcessor extends AbstractQueryPostProcessor { + + protected static final LimitQueryPostProcessor INSTANCE = new LimitQueryPostProcessor(); + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasLimit() && !QueryString.LIMIT_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withLimit(gemfireQueryMethod.getLimit()).toString(); + } + } + + return query; + } + } + + protected static class TraceQueryPostProcessor extends AbstractQueryPostProcessor { + + protected static final TraceQueryPostProcessor INSTANCE = new TraceQueryPostProcessor(); + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + if (queryMethod instanceof GemfireQueryMethod) { + + GemfireQueryMethod gemfireQueryMethod = (GemfireQueryMethod) queryMethod; + + if (gemfireQueryMethod.hasTrace() && !QueryString.TRACE_PATTERN.matcher(query).find()) { + query = QueryString.of(query).withTrace().toString(); + } + } + + return query; + } + } } diff --git a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java index c554596a..4ab36013 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/repository/support/GemfireRepositoryFactoryBean.java @@ -16,27 +16,41 @@ package org.springframework.data.gemfire.repository.support; +import static org.springframework.data.gemfire.util.CollectionUtils.nullSafeList; + import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; -import org.springframework.data.gemfire.mapping.GemfirePersistentProperty; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; -import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.util.Assert; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import com.gemstone.gemfire.cache.Region; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.OrderComparator; +import org.springframework.data.gemfire.mapping.GemfirePersistentEntity; +import org.springframework.data.gemfire.mapping.GemfirePersistentProperty; +import org.springframework.data.gemfire.repository.query.GemfireRepositoryQuery; +import org.springframework.data.gemfire.repository.query.QueryPostProcessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.RepositoryDefinition; +import org.springframework.data.repository.core.support.QueryCreationListener; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + /** * {@link FactoryBean} adapter for {@link GemfireRepositoryFactory}. - * + * * @author Oliver Gierke * @author John Blum * @see org.springframework.beans.factory.FactoryBean @@ -54,19 +68,21 @@ import com.gemstone.gemfire.cache.Region; public class GemfireRepositoryFactoryBean, S, ID extends Serializable> extends RepositoryFactoryBeanSupport implements ApplicationContextAware { + private ApplicationContext applicationContext; + private Iterable> regions; private MappingContext, GemfirePersistentProperty> mappingContext; - + /** * Creates a new {@link GemfireRepositoryFactoryBean} for the given repository interface. - * + * * @param repositoryInterface must not be {@literal null}. */ public GemfireRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } - + /** * Sets a reference to the Spring {@link ApplicationContext} in which this object runs. * @@ -77,17 +93,22 @@ public class GemfireRepositoryFactoryBean, S, ID ext @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + this.applicationContext = applicationContext; + Collection regions = applicationContext.getBeansOfType(Region.class).values(); + this.regions = (Iterable) Collections.unmodifiableCollection(regions); } - + /** * Configures the {@link MappingContext} used to perform domain object type to store mappings. - * + * * @param mappingContext the {@link MappingContext} to set. * @see org.springframework.data.gemfire.mapping.GemfireMappingContext * @see org.springframework.data.mapping.context.MappingContext */ + @Autowired(required = false) public void setGemfireMappingContext(MappingContext, GemfirePersistentProperty> mappingContext) { setMappingContext(mappingContext); this.mappingContext = mappingContext; @@ -124,10 +145,22 @@ public class GemfireRepositoryFactoryBean, S, ID ext */ @Override protected RepositoryFactorySupport createRepositoryFactory() { - return new GemfireRepositoryFactory(getRegions(), getGemfireMappingContext()); + + GemfireRepositoryFactory repositoryFactory = + new GemfireRepositoryFactory(getRegions(), getGemfireMappingContext()); + + if (this.applicationContext != null) { + + QueryCreationListener listener = + new QueryPostProcessorRegistrationOnQueryCreationListener(this.applicationContext); + + repositoryFactory.addQueryCreationListener(listener); + } + + return repositoryFactory; } - - /* + + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#afterPropertiesSet() */ @@ -136,4 +169,156 @@ public class GemfireRepositoryFactoryBean, S, ID ext Assert.state(getGemfireMappingContext() != null, "GemfireMappingContext must not be null"); super.afterPropertiesSet(); } + + protected class QueryPostProcessorRegistrationOnQueryCreationListener + implements QueryCreationListener { + + private Iterable queryPostProcessorsMetadata; + + protected QueryPostProcessorRegistrationOnQueryCreationListener(ApplicationContext applicationContext) { + + Assert.notNull(applicationContext, "ApplicationContext must not be null"); + + List queryPostProcessors = + new ArrayList(applicationContext.getBeansOfType(QueryPostProcessor.class).values()); + + Collections.sort(queryPostProcessors, OrderComparator.INSTANCE); + + List queryPostProcessorsMetadata = + new ArrayList(); + + for (QueryPostProcessor queryPostProcessor : queryPostProcessors) { + queryPostProcessorsMetadata.add(QueryPostProcessorMetadata.from(queryPostProcessor)); + } + + this.queryPostProcessorsMetadata = queryPostProcessorsMetadata; + } + + protected Iterable getQueryPostProcessorsMetadata() { + return this.queryPostProcessorsMetadata; + } + + @Override + public void onCreation(GemfireRepositoryQuery repositoryQuery) { + + Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); + + for (QueryPostProcessorMetadata metadata : getQueryPostProcessorsMetadata()) { + if (metadata.isMatch(repositoryInterface)) { + metadata.register(repositoryQuery); + } + } + } + } + + static class QueryPostProcessorMetadata { + + private static final Map cache = + new WeakHashMap(); + + private final Class declaredRepositoryType; + + private final QueryPostProcessor queryPostProcessor; + + static synchronized QueryPostProcessorMetadata from(QueryPostProcessor queryPostProcessor) { + + QueryPostProcessorKey key = QueryPostProcessorKey.of(queryPostProcessor); + + QueryPostProcessorMetadata metadata = cache.get(key); + + if (metadata == null) { + metadata = new QueryPostProcessorMetadata(key.getQueryPostProcessor()); + cache.put(key, metadata); + } + + return metadata; + } + + @SuppressWarnings("unchecked") + QueryPostProcessorMetadata(QueryPostProcessor queryPostProcessor) { + + Assert.notNull(queryPostProcessor, "QueryPostProcessor must not be null"); + + this.queryPostProcessor = queryPostProcessor; + + List> typeArguments = ClassTypeInformation.from(queryPostProcessor.getClass()) + .getSuperTypeInformation(QueryPostProcessor.class) + .getTypeArguments(); + + this.declaredRepositoryType = resolveDeclaredRepositoryType(typeArguments); + } + + Class resolveDeclaredRepositoryType(List> typeArguments) { + return !nullSafeList(typeArguments).isEmpty() ? typeArguments.get(0).getType() : Repository.class; + } + + Class getDeclaredRepositoryType() { + return this.declaredRepositoryType; + } + + @SuppressWarnings("unchecked") + QueryPostProcessor getQueryPostProcessor() { + return (QueryPostProcessor) this.queryPostProcessor; + } + + boolean isMatch(Class repositoryInterface) { + + return repositoryInterface != null + && (getDeclaredRepositoryType().isAssignableFrom(repositoryInterface) + || repositoryInterface.isAnnotationPresent(RepositoryDefinition.class)); + } + + GemfireRepositoryQuery register(GemfireRepositoryQuery repositoryQuery) { + + repositoryQuery.register(getQueryPostProcessor()); + + return repositoryQuery; + } + + protected static class QueryPostProcessorKey { + + private QueryPostProcessor queryPostProcessor; + + public static QueryPostProcessorKey of(QueryPostProcessor queryPostProcessor) { + + Assert.notNull(queryPostProcessor, "QueryPostProcessor must not be null"); + + QueryPostProcessorKey key = new QueryPostProcessorKey(); + + key.queryPostProcessor = queryPostProcessor; + + return key; + } + + protected QueryPostProcessor getQueryPostProcessor() { + return queryPostProcessor; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof QueryPostProcessorKey)) { + return false; + } + + QueryPostProcessorKey that = (QueryPostProcessorKey) obj; + + return this.getQueryPostProcessor().equals(that.getQueryPostProcessor()); + } + + @Override + public int hashCode() { + + int hashValue = 17; + + hashValue = 37 * hashValue + getQueryPostProcessor().hashCode(); + + return hashValue; + } + } + } } diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessorUnitTests.java b/src/test/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessorUnitTests.java new file mode 100644 index 00000000..f9e23904 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/query/AbstractQueryPostProcessorUnitTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.repository.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyVararg; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.InOrder; +import org.springframework.data.repository.query.QueryMethod; + +/** + * Unit tests for {@link AbstractQueryPostProcessor}. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.data.gemfire.repository.query.AbstractQueryPostProcessor + * @since 1.0.0 + */ +public class AbstractQueryPostProcessorUnitTests { + + @Test + @SuppressWarnings("unchecked") + public void processAfterReturnsCompositeQueryPostProcessorAndPostProcessesInOrder() { + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + String query = "SELECT * FROM /Test"; + + AbstractQueryPostProcessor mockQueryPostProcessorOne = mock(AbstractQueryPostProcessor.class); + AbstractQueryPostProcessor mockQueryPostProcessorTwo = mock(AbstractQueryPostProcessor.class); + + when(mockQueryPostProcessorOne.processAfter(any(QueryPostProcessor.class))).thenCallRealMethod(); + when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), anyVararg())).thenReturn(query); + when(mockQueryPostProcessorTwo.postProcess(any(QueryMethod.class), anyString(), anyVararg())).thenReturn(query); + + QueryPostProcessor composite = mockQueryPostProcessorOne.processAfter(mockQueryPostProcessorTwo); + + assertThat(composite).isNotNull(); + assertThat(composite).isNotSameAs(mockQueryPostProcessorOne); + assertThat(composite).isNotSameAs(mockQueryPostProcessorTwo); + assertThat(composite.postProcess(mockQueryMethod, query)).isEqualTo(query); + + InOrder inOrder = inOrder(mockQueryPostProcessorOne, mockQueryPostProcessorTwo); + + inOrder.verify(mockQueryPostProcessorTwo, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), anyVararg()); + + inOrder.verify(mockQueryPostProcessorOne, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), anyVararg()); + } + + @Test + @SuppressWarnings("unchecked") + public void processAfterReturnsThis() { + + AbstractQueryPostProcessor mockQueryPostProcessor = mock(AbstractQueryPostProcessor.class); + + when(mockQueryPostProcessor.processAfter(any(AbstractQueryPostProcessor.class))).thenCallRealMethod(); + + assertThat(mockQueryPostProcessor.processAfter(null)).isSameAs(mockQueryPostProcessor); + } + @Test + @SuppressWarnings("unchecked") + public void processBeforeReturnsCompositeQueryPostProcessorAndPostProcessesInOrder() { + + QueryMethod mockQueryMethod = mock(QueryMethod.class); + + String query = "SELECT * FROM /Test"; + + AbstractQueryPostProcessor mockQueryPostProcessorOne = mock(AbstractQueryPostProcessor.class); + AbstractQueryPostProcessor mockQueryPostProcessorTwo = mock(AbstractQueryPostProcessor.class); + + when(mockQueryPostProcessorOne.processBefore(any(QueryPostProcessor.class))).thenCallRealMethod(); + when(mockQueryPostProcessorOne.postProcess(any(QueryMethod.class), anyString(), anyVararg())).thenReturn(query); + when(mockQueryPostProcessorTwo.postProcess(any(QueryMethod.class), anyString(), anyVararg())).thenReturn(query); + + QueryPostProcessor composite = mockQueryPostProcessorOne.processBefore(mockQueryPostProcessorTwo); + + assertThat(composite).isNotNull(); + assertThat(composite).isNotSameAs(mockQueryPostProcessorOne); + assertThat(composite).isNotSameAs(mockQueryPostProcessorTwo); + assertThat(composite.postProcess(mockQueryMethod, query)).isEqualTo(query); + + InOrder inOrder = inOrder(mockQueryPostProcessorOne, mockQueryPostProcessorTwo); + + inOrder.verify(mockQueryPostProcessorOne, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), anyVararg()); + + inOrder.verify(mockQueryPostProcessorTwo, times(1)) + .postProcess(eq(mockQueryMethod), eq(query), anyVararg()); + } + + @Test + @SuppressWarnings("unchecked") + public void processBeforeReturnsThis() { + + AbstractQueryPostProcessor mockQueryPostProcessor = mock(AbstractQueryPostProcessor.class); + + when(mockQueryPostProcessor.processBefore(any(QueryPostProcessor.class))).thenCallRealMethod(); + + assertThat(mockQueryPostProcessor.processBefore(null)).isSameAs(mockQueryPostProcessor); + } +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessingIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessingIntegrationTests.java new file mode 100644 index 00000000..aad1ec6d --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/query/QueryPostProcessingIntegrationTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.data.gemfire.repository.query; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +import com.gemstone.gemfire.cache.GemFireCache; +import com.gemstone.gemfire.cache.client.ClientRegionShortcut; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.mapping.GemfireMappingContext; +import org.springframework.data.gemfire.repository.sample.Person; +import org.springframework.data.gemfire.repository.sample.PersonRepository; +import org.springframework.data.gemfire.repository.sample.User; +import org.springframework.data.gemfire.repository.sample.UserRepository; +import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link QueryPostProcessor} framework infrastructure. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.data.gemfire.repository.query.QueryPostProcessor + * @since 1.0.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class QueryPostProcessingIntegrationTests { + + private static final AtomicLong idSequence = new AtomicLong(0L); + + @Autowired + private FindOrderedLimitedPeopleByFirstNameQueryPostProcessor peopleQueryPostProcessor; + + @Autowired + private PersonRepository personRepository; + + @Autowired + private RecordingQueryPostProcessor recordingQueryPostProcessor; + + @Autowired + private UserRepository userRepository; + + private static Person newPerson(String firstName, String lastName) { + + Person person = new Person(firstName, lastName); + + person.id = idSequence.incrementAndGet(); + + return person; + } + + @Before + public void setup() { + + this.personRepository.save(newPerson("Jon", "Doe")); + this.personRepository.save(newPerson("Cookie", "Doe")); + this.personRepository.save(newPerson("Pie", "Doe")); + this.personRepository.save(newPerson("Sour", "Doe")); + this.personRepository.save(newPerson("Jack", "BeNimble")); + this.personRepository.save(newPerson("Jack", "BeQuick")); + this.personRepository.save(newPerson("Jack", "Black")); + this.personRepository.save(newPerson("Jack", "JumpedOverTheCandleStick")); + this.personRepository.save(newPerson("Jack", "Handy")); + this.personRepository.save(newPerson("Jack", "Sparrow")); + this.personRepository.save(newPerson("Agent", "Smith")); + + this.userRepository.save(new User("abuser")); + this.userRepository.save(new User("jdoe")); + this.userRepository.save(new User("root")); + } + + @Test + public void queryPostProcessingProcessesGeneratedOqlQueries() { + + assertThat(this.personRepository.count()).isEqualTo(11); + assertThat(this.userRepository.count()).isEqualTo(3); + assertThat(this.recordingQueryPostProcessor.queries).isEmpty(); + + List users = this.userRepository.findDistinctByUsernameLike("%doe"); + + assertThat(users).hasSize(1); + assertThat(this.recordingQueryPostProcessor.queries).hasSize(1); + assertThat(this.recordingQueryPostProcessor.queries) + .containsExactly("SELECT DISTINCT * FROM /Users x WHERE x.username LIKE $1"); + + Collection bakingDoes = this.personRepository.findByFirstnameIn("Cookie", "Pie", "Sour"); + + assertThat(bakingDoes).hasSize(3); + assertThat(this.recordingQueryPostProcessor.queries).hasSize(2); + assertThat(this.recordingQueryPostProcessor.queries).containsExactly( + "SELECT DISTINCT * FROM /Users x WHERE x.username LIKE $1", + "SELECT * FROM /simple x WHERE x.firstname IN SET ('Cookie', 'Pie', 'Sour')" + ); + + Collection jacks = this.personRepository.findByFirstname("Jack"); + + assertThat(jacks).hasSize(1); + assertThat(jacks.iterator().next().getName()).isEqualTo("Jack Sparrow"); + + assertThat(this.recordingQueryPostProcessor.queries).hasSize(3); + assertThat(this.recordingQueryPostProcessor.queries).containsExactly( + "SELECT DISTINCT * FROM /Users x WHERE x.username LIKE $1", + "SELECT * FROM /simple x WHERE x.firstname IN SET ('Cookie', 'Pie', 'Sour')", + "SELECT DISTINCT * FROM /simple x WHERE x.firstname = $1 ORDER BY lastname DESC LIMIT 1" + ); + } + + @ClientCacheApplication + @SuppressWarnings("unused") + static class TestConfiguration { + + @Bean("simple") + public ClientRegionFactoryBean peopleRegion(GemFireCache gemfireCache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean(); + + clientRegion.setCache(gemfireCache); + clientRegion.setClose(false); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + + return clientRegion; + } + + @Bean("Users") + public ClientRegionFactoryBean usersRegion(GemFireCache gemfireCache) { + + ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean(); + + clientRegion.setCache(gemfireCache); + clientRegion.setClose(false); + clientRegion.setShortcut(ClientRegionShortcut.LOCAL); + + return clientRegion; + } + + @Bean + GemfireMappingContext mappingContext() { + return new GemfireMappingContext(); + } + + @Bean + GemfireRepositoryFactoryBean personRepository() { + return new GemfireRepositoryFactoryBean(PersonRepository.class); + } + + @Bean + GemfireRepositoryFactoryBean userRepository() { + return new GemfireRepositoryFactoryBean(UserRepository.class); + } + + @Bean + FindOrderedLimitedPeopleByFirstNameQueryPostProcessor personQueryPostProcess() { + return new FindOrderedLimitedPeopleByFirstNameQueryPostProcessor(1); + } + + @Bean + RecordingQueryPostProcessor recordingQueryPostProcessor() { + return new RecordingQueryPostProcessor(); + } + } + + static class FindOrderedLimitedPeopleByFirstNameQueryPostProcessor extends AbstractQueryPostProcessor { + + private final int limit; + + FindOrderedLimitedPeopleByFirstNameQueryPostProcessor(int limit) { + this.limit = limit; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + + return "findByFirstname".equals(queryMethod.getName()) + ? query.trim().replace("SELECT", "SELECT DISTINCT") + .concat(" ORDER BY lastname DESC").concat(String.format(" LIMIT %d", this.limit)) + : query; + } + } + + static class RecordingQueryPostProcessor extends AbstractQueryPostProcessor { + + List queries = new CopyOnWriteArrayList(); + + @Override + public int getOrder() { + return 1; + } + + @Override + public String postProcess(QueryMethod queryMethod, String query, Object... arguments) { + this.queries.add(query); + return query; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java b/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java index ec1055dd..fb479456 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java +++ b/src/test/java/org/springframework/data/gemfire/repository/query/StringBasedGemfireRepositoryQueryTest.java @@ -18,9 +18,6 @@ package org.springframework.data.gemfire.repository.query; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.sameInstance; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -39,11 +36,11 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import org.junit.Test; - import com.gemstone.gemfire.cache.query.SelectResults; import com.gemstone.gemfire.cache.query.internal.ResultsBag; +import org.junit.Test; + /** * The SpringBasedGemfireRepositoryQueryTest class is a test suite of test cases testing the contract and functionality * of the StringBasedGemfireRepositoryQuery class. @@ -122,6 +119,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void applyAllQueryAnnotationExtensions() { + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class, "MockGemfireQueryMethod"); when(mockQueryMethod.hasHint()).thenReturn(true); @@ -132,18 +130,13 @@ public class StringBasedGemfireRepositoryQueryTest { when(mockQueryMethod.getLimit()).thenReturn(10); when(mockQueryMethod.hasTrace()).thenReturn(true); - QueryString queryString = new QueryString("SELECT * FROM /Example"); - - assertThat(queryString.toString(), is(equalTo("SELECT * FROM /Example"))); - StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(); - QueryString actualQueryString = repositoryQuery.applyQueryAnnotationExtensions(mockQueryMethod, queryString); + String query = repositoryQuery.getQueryPostProcessor() + .postProcess(mockQueryMethod, "SELECT * FROM /Example"); - assertThat(actualQueryString, is(notNullValue())); - assertThat(actualQueryString, is(not(sameInstance(queryString)))); - assertThat(actualQueryString.toString(), is(equalTo( - " IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 10"))); + assertThat(query, + is(equalTo(" IMPORT org.example.domain.Type; SELECT * FROM /Example LIMIT 10"))); verify(mockQueryMethod, times(1)).hasHint(); verify(mockQueryMethod, times(1)).getHints(); @@ -156,6 +149,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void applyHintLimitAndTraceQueryAnnotationExtensionsWithExistingHintAndLimit() { + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class, "MockGemfireQueryMethod"); when(mockQueryMethod.hasHint()).thenReturn(true); @@ -165,18 +159,12 @@ public class StringBasedGemfireRepositoryQueryTest { when(mockQueryMethod.getLimit()).thenReturn(50); when(mockQueryMethod.hasTrace()).thenReturn(true); - QueryString queryString = new QueryString(" SELECT * FROM /Example LIMIT 25"); - - assertThat(queryString.toString(), is(equalTo(" SELECT * FROM /Example LIMIT 25"))); - StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(); - QueryString actualQueryString = repositoryQuery.applyQueryAnnotationExtensions(mockQueryMethod, queryString); + String query = repositoryQuery.getQueryPostProcessor() + .postProcess(mockQueryMethod, " SELECT * FROM /Example LIMIT 25"); - assertThat(actualQueryString, is(notNullValue())); - assertThat(actualQueryString, is(not(sameInstance(queryString)))); - assertThat(actualQueryString.toString(), is(equalTo( - " SELECT * FROM /Example LIMIT 25"))); + assertThat(query, is(equalTo(" SELECT * FROM /Example LIMIT 25"))); verify(mockQueryMethod, times(1)).hasHint(); verify(mockQueryMethod, never()).getHints(); @@ -189,6 +177,7 @@ public class StringBasedGemfireRepositoryQueryTest { @Test public void applyImportAndTraceQueryAnnotationExtensionsWithExistingTrace() { + GemfireQueryMethod mockQueryMethod = mock(GemfireQueryMethod.class, "MockGemfireQueryMethod"); when(mockQueryMethod.hasHint()).thenReturn(false); @@ -197,18 +186,12 @@ public class StringBasedGemfireRepositoryQueryTest { when(mockQueryMethod.hasLimit()).thenReturn(false); when(mockQueryMethod.hasTrace()).thenReturn(true); - QueryString queryString = new QueryString(" SELECT * FROM /Example"); - - assertThat(queryString.toString(), is(equalTo(" SELECT * FROM /Example"))); - StringBasedGemfireRepositoryQuery repositoryQuery = new StringBasedGemfireRepositoryQuery(); - QueryString actualQueryString = repositoryQuery.applyQueryAnnotationExtensions(mockQueryMethod, queryString); + String query = repositoryQuery.getQueryPostProcessor() + .postProcess(mockQueryMethod, " SELECT * FROM /Example"); - assertThat(actualQueryString, is(notNullValue())); - assertThat(actualQueryString, is(not(sameInstance(queryString)))); - assertThat(actualQueryString.toString(), is(equalTo( - "IMPORT org.example.domain.Type; SELECT * FROM /Example"))); + assertThat(query, is(equalTo("IMPORT org.example.domain.Type; SELECT * FROM /Example"))); verify(mockQueryMethod, times(1)).hasHint(); verify(mockQueryMethod, never()).getHints(); @@ -218,5 +201,4 @@ public class StringBasedGemfireRepositoryQueryTest { verify(mockQueryMethod, never()).getLimit(); verify(mockQueryMethod, times(1)).hasTrace(); } - }