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 @@ + + + + + +