From 67f638d9539c86e234f45050526df959e8e38418 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 5 Aug 2014 17:09:42 +0200 Subject: [PATCH] DATAMONGO-990 - Add support for SpEL expressions in @Query. Ported and adapted support for SpEL expressions @Query annotations from Spring Data JPA. StringBasedMongoQuery can now evaluate SpEL fragments in queries with the help of the given EvaluationContextProvider. Introduced EvaluationContextProvider to AbstractMongoQuery. Exposed access to actual parameter values in MongoParameterAccessor. Original pull request: #285. --- .../repository/query/AbstractMongoQuery.java | 12 +- .../query/ConvertingParameterAccessor.java | 9 ++ .../query/MongoParameterAccessor.java | 9 ++ .../MongoParametersParameterAccessor.java | 13 +- .../repository/query/PartTreeMongoQuery.java | 10 +- .../query/StringBasedMongoQuery.java | 117 ++++++++++++++--- .../support/MongoRepositoryFactory.java | 30 +++-- .../mongodb/repository/PersonRepository.java | 19 +++ .../PersonRepositoryIntegrationTests.java | 49 ++++++- .../SampleEvaluationContextExtension.java | 124 ++++++++++++++++++ .../query/AbstractMongoQueryUnitTests.java | 18 ++- .../query/PartTreeMongoQueryUnitTests.java | 13 +- .../query/StringBasedMongoQueryUnitTests.java | 33 ++++- .../query/StubParameterAccessor.java | 9 ++ ...rsonRepositoryIntegrationTests-context.xml | 6 + 15 files changed, 423 insertions(+), 48 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SampleEvaluationContextExtension.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index b5fb1b234..2d17a538d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -31,6 +31,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.util.CloseableIterator; @@ -51,20 +52,25 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final MongoQueryMethod method; private final MongoOperations operations; + private final EvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. * * @param method must not be {@literal null}. * @param operations must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations) { + public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, + EvaluationContextProvider evaluationContextProvider) { Assert.notNull(operations); Assert.notNull(method); + Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!"); this.method = method; this.operations = operations; + this.evaluationContextProvider = evaluationContextProvider; } /* @@ -158,6 +164,10 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { */ protected abstract boolean isDeleteQuery(); + public EvaluationContextProvider getEvaluationContextProvider() { + return evaluationContextProvider; + } + private abstract class Execution { abstract Object execute(Query query); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index bbed7e3ed..6fd9c7e63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -41,6 +41,7 @@ import com.mongodb.DBRef; * * @author Oliver Gierke * @author Christoph Strobl + * @author Thomas Darimont */ public class ConvertingParameterAccessor implements MongoParameterAccessor { @@ -239,6 +240,14 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + return delegate.getValues(); + } /** * Custom {@link Iterator} that adds a method to access elements in a converted manner. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 9b39b16d7..6c67778e3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -26,6 +26,7 @@ import org.springframework.data.repository.query.ParameterAccessor; * * @author Oliver Gierke * @author Christoph Strobl + * @author Thomas Darimont */ public interface MongoParameterAccessor extends ParameterAccessor { @@ -51,4 +52,12 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @since 1.6 */ TextCriteria getFullText(); + + /** + * Returns the raw parameter values of the underlying query method. + * + * @return + * @since 1.8 + */ + Object[] getValues(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index aea9f371e..7a8af59f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -29,20 +29,23 @@ import org.springframework.util.ClassUtils; * * @author Oliver Gierke * @author Christoph Strobl + * @author Thomas Darimont */ public class MongoParametersParameterAccessor extends ParametersParameterAccessor implements MongoParameterAccessor { private final MongoQueryMethod method; + private final Object[] values; /** * Creates a new {@link MongoParametersParameterAccessor}. * * @param method must not be {@literal null}. - * @param values must not be {@@iteral null}. + * @param values must not be {@literal null}. */ public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values) { super(method.getParameters(), values); this.method = method; + this.values = values; } public Range getDistanceRange() { @@ -121,4 +124,12 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso "Expected full text parameter to be one of String, Term or TextCriteria but found %s.", ClassUtils.getShortName(fullText.getClass()))); } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + return values; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index df5cabbe5..db8749754 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -22,6 +22,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.PartTree; @@ -34,6 +35,7 @@ import com.mongodb.util.JSONParseException; * * @author Oliver Gierke * @author Christoph Strobl + * @author Thomas Darimont */ public class PartTreeMongoQuery extends AbstractMongoQuery { @@ -45,11 +47,13 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. * * @param method must not be {@literal null}. - * @param template must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) { + public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, + EvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations); + super(method, mongoOperations, evaluationContextProvider); this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); this.isGeoNearQuery = method.isGeoNearQuery(); this.context = mongoOperations.getConverter().getMappingContext(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index b088d22a1..ca3ad2b2d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -26,6 +26,11 @@ import org.slf4j.LoggerFactory; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.mongodb.DBObject; @@ -43,7 +48,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private static final String COUND_AND_DELETE = "Manually defined query for %s cannot be both a count and delete query at the same time!"; private static final Logger LOG = LoggerFactory.getLogger(StringBasedMongoQuery.class); - private static final ParameterBindingParser PARSER = ParameterBindingParser.INSTANCE; + private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE; private final String query; private final String fieldSpec; @@ -51,15 +56,19 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private final boolean isDeleteQuery; private final List queryParameterBindings; private final List fieldSpecParameterBindings; + private final SpelExpressionParser expressionParser; /** * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod} and {@link MongoOperations}. * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + * @param expressionParser must not be {@literal null}. */ - public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) { - this(method.getAnnotatedQuery(), method, mongoOperations); + public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, + EvaluationContextProvider evaluationContextProvider, SpelExpressionParser expressionParser) { + this(method.getAnnotatedQuery(), method, mongoOperations, evaluationContextProvider, expressionParser); } /** @@ -67,17 +76,25 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { * {@link MongoOperations}. * * @param method must not be {@literal null}. + * @param expressionParser must not be {@literal null}. * @param template must not be {@literal null}. */ - public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations) { + public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations, + EvaluationContextProvider evaluationContextProvider, SpelExpressionParser expressionParser) { - super(method, mongoOperations); + super(method, mongoOperations, evaluationContextProvider); - this.query = query; - this.queryParameterBindings = PARSER.parseParameterBindingsFrom(query); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); - this.fieldSpec = method.getFieldSpecification(); - this.fieldSpecParameterBindings = PARSER.parseParameterBindingsFrom(method.getFieldSpecification()); + this.expressionParser = expressionParser; + + this.queryParameterBindings = new ArrayList(); + this.query = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(query, + this.queryParameterBindings); + + this.fieldSpecParameterBindings = new ArrayList(); + this.fieldSpec = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings( + method.getFieldSpecification(), this.fieldSpecParameterBindings); this.isCountQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().count() : false; this.isDeleteQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().delete() : false; @@ -170,7 +187,12 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { */ private String getParameterValueForBinding(ConvertingParameterAccessor accessor, ParameterBinding binding) { - Object value = accessor.getBindableValue(binding.getParameterIndex()); + Object value = null; + if (binding.isExpression()) { + value = evaluateExpression(binding.getExpression(), accessor.getValues()); + } else { + value = accessor.getBindableValue(binding.getParameterIndex()); + } if (value instanceof String && binding.isQuoted()) { return (String) value; @@ -179,6 +201,21 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { return JSON.serialize(value); } + /** + * Evaluates the given {@code expressionString}. + * + * @param expressionString + * @param parameterValues + * @return + */ + private Object evaluateExpression(String expressionString, Object[] parameterValues) { + + EvaluationContext evaluationContext = getEvaluationContextProvider().getEvaluationContext( + getQueryMethod().getParameters(), parameterValues); + Expression expression = expressionParser.parseExpression(expressionString); + return expression.getValue(evaluationContext, Object.class); + } + /** * A parser that extracts the parameter bindings from a given query string. * @@ -192,6 +229,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private static final String PARSEABLE_PARAMETER = "\"" + PARAMETER_PREFIX + "$1\""; private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); private static final Pattern PARSEABLE_BINDING_PATTERN = Pattern.compile("\"?" + PARAMETER_PREFIX + "(\\d+)\"?"); + private static final Pattern PARAMETER_EXPRESSION_PATTERN = Pattern.compile("((:|\\?)#\\{([^}]+)\\})"); private final static int PARAMETER_INDEX_GROUP = 1; @@ -200,22 +238,53 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { * {@link Collections#emptyList()}. * * @param input - * @param conversionService must not be {@literal null}. + * @param bindings * @return */ - public List parseParameterBindingsFrom(String input) { + public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List bindings) { if (!StringUtils.hasText(input)) { - return Collections.emptyList(); + return input; } - List bindings = new ArrayList(); + String transformedInput = transformQueryAndCollectExpressionParametersIntoBindings(bindings, input); - String parseableInput = makeParameterReferencesParseable(input); + String parseableInput = makeParameterReferencesParseable(transformedInput); collectParameterReferencesIntoBindings(bindings, JSON.parse(parseableInput)); - return bindings; + return transformedInput; + } + + private String transformQueryAndCollectExpressionParametersIntoBindings(List bindings, + String input) { + + Matcher matcher = PARAMETER_EXPRESSION_PATTERN.matcher(input); + + StringBuilder result = new StringBuilder(); + + int lastPos = 0; + + int exprIndex = 0; + while (matcher.find()) { + + int startOffSet = matcher.start(); + result.append(input.subSequence(lastPos, startOffSet)); + + String expression = matcher.group(3); + + result.append("'?expr").append(exprIndex).append("'"); + + lastPos = matcher.end(); + + bindings.add(new ParameterBinding(exprIndex, true, expression)); + + exprIndex++; + } + + result.append(input.subSequence(lastPos, input.length())); + + return result.toString(); } private String makeParameterReferencesParseable(String input) { @@ -292,6 +361,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private final int parameterIndex; private final boolean quoted; + private final String expression; /** * Creates a new {@link ParameterBinding} with the given {@code parameterIndex} and {@code quoted} information. @@ -300,9 +370,14 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { * @param quoted whether or not the parameter is already quoted. */ public ParameterBinding(int parameterIndex, boolean quoted) { + this(parameterIndex, quoted, null); + } + + public ParameterBinding(int parameterIndex, boolean quoted, String expression) { this.parameterIndex = parameterIndex; this.quoted = quoted; + this.expression = expression; } public boolean isQuoted() { @@ -314,7 +389,15 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { } public String getParameter() { - return "?" + parameterIndex; + return "?" + (isExpression() ? "expr" : "") + parameterIndex; + } + + public String getExpression() { + return expression; + } + + public boolean isExpression() { + return this.expression != null; } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 644fed6f5..07f3a8ef8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.springframework.data.querydsl.QueryDslUtils.*; +import static org.springframework.data.querydsl.QueryDslUtils.QUERY_DSL_PRESENT; import java.io.Serializable; import java.lang.reflect.Method; @@ -35,18 +35,23 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** * Factory to create {@link MongoRepository} instances. * * @author Oliver Gierke + * @author Thomas Darimont */ public class MongoRepositoryFactory extends RepositoryFactorySupport { + private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + private final MongoOperations mongoOperations; private final MappingContext, MongoPersistentProperty> mappingContext; @@ -87,13 +92,13 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { return getTargetRepositoryViaReflection(information, entityInformation, mongoOperations); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key) + + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) */ @Override - protected QueryLookupStrategy getQueryLookupStrategy(Key key) { - return new MongoQueryLookupStrategy(); + protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) { + return new MongoQueryLookupStrategy(evaluationContextProvider); } /* @@ -118,9 +123,16 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { * {@link QueryLookupStrategy} to create {@link PartTreeMongoQuery} instances. * * @author Oliver Gierke + * @author Thomas Darimont */ private class MongoQueryLookupStrategy implements QueryLookupStrategy { + private final EvaluationContextProvider evaluationContextProvider; + + public MongoQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = evaluationContextProvider; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.repository.core.NamedQueries) @@ -132,11 +144,11 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringBasedMongoQuery(namedQuery, queryMethod, mongoOperations); + return new StringBasedMongoQuery(namedQuery, queryMethod, mongoOperations, evaluationContextProvider, EXPRESSION_PARSER); } else if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedMongoQuery(queryMethod, mongoOperations); + return new StringBasedMongoQuery(queryMethod, mongoOperations, evaluationContextProvider, EXPRESSION_PARSER); } else { - return new PartTreeMongoQuery(queryMethod, mongoOperations); + return new PartTreeMongoQuery(queryMethod, mongoOperations, evaluationContextProvider); } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index a45e55c72..eae2c02e1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -34,6 +34,7 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.repository.query.Param; /** * Sample repository managing {@link Person} entities. @@ -333,4 +334,22 @@ public interface PersonRepository extends MongoRepository, Query */ @Query("{ firstname : { $in : ?0 }}") Stream findByCustomQueryWithStreamingCursorByFirstnames(List firstnames); + + /** + * @see DATAMONGO-990 + */ + @Query("{ firstname : ?#{[0]}}") + List findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname); + + /** + * @see DATAMONGO-990 + */ + @Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }") + List findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname); + + /** + * @see DATAMONGO-990 + */ + @Query("{ firstname : :#{#firstname}}") + List findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java index 2c5a72a03..6fb76c0eb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java @@ -15,6 +15,14 @@ */ package org.springframework.data.mongodb.repository; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import org.junit.Test; +import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension.SampleSecurityContextHolder; import org.springframework.test.context.ContextConfiguration; /** @@ -24,4 +32,43 @@ import org.springframework.test.context.ContextConfiguration; * @author Thomas Darimont */ @ContextConfiguration -public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests {} +public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { + + /** + * @see DATAMONGO-990 + */ + @Test + public void shouldFindByFirstnameForSpELExpressionWithParameterIndexOnly() { + + List users = repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Dave"); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(dave)); + } + + /** + * @see DATAMONGO-990 + */ + @Test + public void shouldFindByFirstnameAndCurrentUserWithCustomQuery() { + + SampleSecurityContextHolder.getCurrent().setPrincipal(dave); + List users = repository.findWithSpelByFirstnameAndCurrentUserWithCustomQuery("Dave"); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(dave)); + } + + /** + * @see DATAMONGO-990 + */ + @Test + public void shouldFindByFirstnameForSpELExpressionWithParameterVariableOnly() { + + List users = repository.findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly("Dave"); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(dave)); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SampleEvaluationContextExtension.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SampleEvaluationContextExtension.java new file mode 100644 index 000000000..70becf6ab --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SampleEvaluationContextExtension.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015 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.mongodb.repository; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.data.repository.query.spi.EvaluationContextExtension; +import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport; + +/** + * A sample implementation of a custom {@link EvaluationContextExtension}. + * + * @author Thomas Darimont + */ +public class SampleEvaluationContextExtension extends EvaluationContextExtensionSupport { + + @Override + public String getExtensionId() { + return "security"; + } + + @Override + public Map getProperties() { + return Collections.singletonMap("principal", SampleSecurityContextHolder.getCurrent().getPrincipal()); + } + + /** + * @author Thomas Darimont + */ + public static class SampleSecurityContextHolder { + + private static ThreadLocal auth = new ThreadLocal() { + + protected SampleAuthentication initialValue() { + return new SampleAuthentication(new SampleUser(-1, "anonymous")); + } + + }; + + public static SampleAuthentication getCurrent() { + return auth.get(); + } + + public static void clear() { + auth.remove(); + } + } + + /** + * @author Thomas Darimont + */ + public static class SampleAuthentication { + + private Object principal; + + public SampleAuthentication(Object principal) { + this.principal = principal; + } + + public Object getPrincipal() { + return principal; + } + + public void setPrincipal(Object principal) { + this.principal = principal; + } + } + + /** + * @author Thomas Darimont + */ + public static class SampleUser { + + private Object id; + private String name; + + public SampleUser(Object id, String name) { + this.id = id; + this.name = name; + } + + public Object getId() { + return id; + } + + public void setId(Object id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public SampleUser withName(String name) { + + this.name = name; + return this; + } + + public SampleUser withId(Object id) { + + this.id = id; + return this; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index f7399ae7d..d6bdb0771 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -15,10 +15,14 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.util.Arrays; @@ -54,6 +58,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.DefaultEvaluationContextProvider; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBObject; @@ -64,6 +69,7 @@ import com.mongodb.WriteResult; * * @author Christoph Strobl * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(MockitoJUnitRunner.class) public class AbstractMongoQueryUnitTests { @@ -314,7 +320,7 @@ public class AbstractMongoQueryUnitTests { private boolean isDeleteQuery; public MongoQueryFake(MongoQueryMethod method, MongoOperations operations) { - super(method, operations); + super(method, operations, DefaultEvaluationContextProvider.INSTANCE); } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java index 493a1e685..fff955e5c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java @@ -15,10 +15,11 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; -import static org.springframework.data.mongodb.core.query.IsTextQuery.*; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.data.mongodb.core.query.IsTextQuery.isTextQuery; import java.lang.reflect.Method; @@ -43,6 +44,7 @@ import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.DefaultEvaluationContextProvider; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.util.JSONParseException; @@ -52,6 +54,7 @@ import com.mongodb.util.JSONParseException; * * @author Christoph Strobl * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(MockitoJUnitRunner.class) public class PartTreeMongoQueryUnitTests { @@ -169,7 +172,7 @@ public class PartTreeMongoQueryUnitTests { Method method = Repo.class.getMethod(methodName, paramTypes); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadataMock, mappingContext); - return new PartTreeMongoQuery(queryMethod, mongoOperationsMock); + return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, DefaultEvaluationContextProvider.INSTANCE); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(e.getMessage(), e); } catch (SecurityException e) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java index b25cabd4b..18cd497fe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java @@ -15,9 +15,10 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.util.Collections; @@ -41,6 +42,8 @@ import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.DefaultEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; @@ -57,6 +60,8 @@ import com.mongodb.DBRef; @RunWith(MockitoJUnitRunner.class) public class StringBasedMongoQueryUnitTests { + SpelExpressionParser PARSER = new SpelExpressionParser(); + @Mock MongoOperations operations; @Mock RepositoryMetadata metadata; @Mock DbRefResolver factory; @@ -76,7 +81,8 @@ public class StringBasedMongoQueryUnitTests { Method method = SampleRepository.class.getMethod("findByLastname", String.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, converter.getMappingContext()); - StringBasedMongoQuery mongoQuery = new StringBasedMongoQuery(queryMethod, operations); + StringBasedMongoQuery mongoQuery = new StringBasedMongoQuery(queryMethod, operations, + DefaultEvaluationContextProvider.INSTANCE, PARSER); ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews"); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); @@ -258,6 +264,21 @@ public class StringBasedMongoQueryUnitTests { assertThat(query.getQueryObject(), is(reference.getQueryObject())); } + /** + * @see DATAMONGO-990 + */ + @Test + public void shouldSupportExpressionsInCustomQueries() throws Exception { + + ConvertingParameterAccessor accesor = StubParameterAccessor.getAccessor(converter, "Matthews"); + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithExpression", String.class); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); + org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : 'Matthews'}"); + + assertThat(query.getQueryObject(), is(reference.getQueryObject())); + } + /** * @see DATAMONGO-1070 */ @@ -293,7 +314,7 @@ public class StringBasedMongoQueryUnitTests { Method method = SampleRepository.class.getMethod(name, parameters); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, converter.getMappingContext()); - return new StringBasedMongoQuery(queryMethod, operations); + return new StringBasedMongoQuery(queryMethod, operations, DefaultEvaluationContextProvider.INSTANCE, PARSER); } private interface SampleRepository { @@ -334,5 +355,7 @@ public class StringBasedMongoQueryUnitTests { @Query("{ ?0 : ?1}") Object methodWithPlaceholderInKeyOfJsonStructure(String keyReplacement, String valueReplacement); + @Query(value = "{'lastname': ?#{[0]} }") + List findByQueryWithExpression(String param0); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 654c9eef8..5d5493717 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -32,6 +32,7 @@ import org.springframework.data.repository.query.ParameterAccessor; * * @author Oliver Gierke * @author Christoh Strobl + * @author Thomas Darimont */ class StubParameterAccessor implements MongoParameterAccessor { @@ -129,4 +130,12 @@ class StubParameterAccessor implements MongoParameterAccessor { public TextCriteria getFullText() { return null; } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + return this.values; + } } diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml index 06032c18d..8c89b4c86 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml @@ -26,6 +26,12 @@ + + + + + +