diff --git a/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java index 299704a4b..8394d1a6f 100644 --- a/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java +++ b/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java @@ -131,4 +131,6 @@ public @interface EnableJpaRepositories { * repositories infrastructure. */ boolean considerNestedRepositories() default false; + + String expressionEvaluationContextProviderRef() default "expressionEvaluationContextProvider"; } diff --git a/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 38d3b12aa..eacd50d71 100644 --- a/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -27,6 +27,7 @@ import javax.persistence.metamodel.Metamodel; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.FieldRetrievingFactoryBean; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -37,10 +38,12 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostPro import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; +import org.springframework.data.jpa.repository.support.StandardExpressionEvaluationContextProvider; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * JPA specific configuration extension parsing custom attributes from the XML namespace and @@ -56,6 +59,8 @@ import org.springframework.util.Assert; */ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { + private static final String DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER = "expressionEvaluationContextProvider"; + public static final String JPA_MAPPING_CONTEXT_BEAN_NAME = "jpaMapppingContext"; private static final Class PAB_POST_PROCESSOR = PersistenceAnnotationBeanPostProcessor.class; @@ -96,6 +101,11 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi } builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME); + + String expressionEvaluationContextProviderRef = source.getAttribute("expressionEvaluationContextProviderRef"); + if (StringUtils.hasText(expressionEvaluationContextProviderRef)) { + builder.addPropertyReference("expressionEvaluationContextProvider", expressionEvaluationContextProviderRef); + } } /** @@ -158,6 +168,14 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi registerWithSourceAndGeneratedBeanName(registry, new RootBeanDefinition(PAB_POST_PROCESSOR), source); } + + if (!registry.containsBeanDefinition(DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER)) { + registry.registerBeanDefinition( + DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER, + BeanDefinitionBuilder.rootBeanDefinition(FieldRetrievingFactoryBean.class) + .addPropertyValue("targetClass", StandardExpressionEvaluationContextProvider.class) + .addPropertyValue("targetField", "INSTANCE").getBeanDefinition()); + } } /** diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 4df319c22..fd7bb221d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -19,6 +19,7 @@ import javax.persistence.EntityManager; import javax.persistence.Query; import javax.persistence.TypedQuery; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.util.Assert; @@ -33,6 +34,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final StringQuery query; private final StringQuery countQuery; + private final ExpressionEvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and @@ -41,13 +43,17 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param method must not be {@literal null}. * @param em must not be {@literal null}. * @param queryString must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. */ - public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString) { + public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, + ExpressionEvaluationContextProvider evaluationContextProvider) { super(method, em); Assert.hasText(queryString, "Query string must not be null or empty!"); + Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!"); + this.evaluationContextProvider = evaluationContextProvider; this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation()); this.countQuery = new StringQuery(method.getCountQuery() != null ? method.getCountQuery() : QueryUtils.createCountQueryFor(this.query.getQueryString(), method.getCountQueryProjection())); @@ -74,7 +80,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { */ @Override protected ParameterBinder createBinder(Object[] values) { - return new StringQueryParameterBinder(getQueryMethod().getParameters(), values, query); + return new SpelExpressionStringQueryParameterBinder(getQueryMethod().getParameters(), values, query, + evaluationContextProvider); } /** diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java index c42931a47..6242b380c 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java @@ -20,6 +20,7 @@ import javax.persistence.EntityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; @@ -40,12 +41,14 @@ enum JpaQueryFactory { * * @param queryMethod must not be {@literal null}. * @param em must not be {@literal null}. + * @param evaluationContextProvider * @return the {@link RepositoryQuery} derived from the annotation or {@code null} if no annotation found. */ - AbstractJpaQuery fromQueryAnnotation(JpaQueryMethod queryMethod, EntityManager em) { + AbstractJpaQuery fromQueryAnnotation(JpaQueryMethod queryMethod, EntityManager em, + ExpressionEvaluationContextProvider evaluationContextProvider) { LOG.debug("Looking up query for method {}", queryMethod.getName()); - return fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery()); + return fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), evaluationContextProvider); } /** @@ -54,16 +57,18 @@ enum JpaQueryFactory { * @param method must not be {@literal null}. * @param em must not be {@literal null}. * @param queryString must not be {@literal null} or empty. + * @param evaluationContextProvider * @return */ - AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString) { + AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString, + ExpressionEvaluationContextProvider evaluationContextProvider) { if (queryString == null) { return null; } - return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString) : // - new SimpleJpaQuery(method, em, queryString); + return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString, evaluationContextProvider) : // + new SimpleJpaQuery(method, em, queryString, evaluationContextProvider); } /** diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 071f61b4d..ceab37b15 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import javax.persistence.EntityManager; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -104,15 +105,19 @@ public final class JpaQueryLookupStrategy { */ private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy { - public DeclaredQueryLookupStrategy(EntityManager em, QueryExtractor extractor) { + private final ExpressionEvaluationContextProvider evaluationContextProvider; + + public DeclaredQueryLookupStrategy(EntityManager em, QueryExtractor extractor, + ExpressionEvaluationContextProvider evaluationContextProvider) { super(em, extractor); + this.evaluationContextProvider = evaluationContextProvider; } @Override protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) { - RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em); + RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em, this.evaluationContextProvider); if (null != query) { return query; @@ -126,7 +131,8 @@ public final class JpaQueryLookupStrategy { String name = method.getNamedQueryName(); if (namedQueries.hasQuery(name)) { - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name)); + return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), + this.evaluationContextProvider); } query = NamedQuery.lookupFrom(method, em); @@ -149,21 +155,22 @@ public final class JpaQueryLookupStrategy { */ private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLookupStrategy { - private final DeclaredQueryLookupStrategy strategy; + private final DeclaredQueryLookupStrategy lookupStrategy; private final CreateQueryLookupStrategy createStrategy; - public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor) { + public CreateIfNotFoundQueryLookupStrategy(EntityManager em, QueryExtractor extractor, + CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy) { super(em, extractor); - this.strategy = new DeclaredQueryLookupStrategy(em, extractor); - this.createStrategy = new CreateQueryLookupStrategy(em, extractor); + this.createStrategy = createStrategy; + this.lookupStrategy = lookupStrategy; } @Override protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) { try { - return strategy.resolveQuery(method, em, namedQueries); + return lookupStrategy.resolveQuery(method, em, namedQueries); } catch (IllegalStateException e) { return createStrategy.resolveQuery(method, em, namedQueries); } @@ -175,21 +182,20 @@ public final class JpaQueryLookupStrategy { * * @param em * @param key + * @param evaluationContextProvider * @return */ - public static QueryLookupStrategy create(EntityManager em, Key key, QueryExtractor extractor) { + public static QueryLookupStrategy create(EntityManager em, Key key, QueryExtractor extractor, + ExpressionEvaluationContextProvider evaluationContextProvider) { - if (key == null) { - return new CreateIfNotFoundQueryLookupStrategy(em, extractor); - } - - switch (key) { + switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) { case CREATE: return new CreateQueryLookupStrategy(em, extractor); case USE_DECLARED_QUERY: - return new DeclaredQueryLookupStrategy(em, extractor); + return new DeclaredQueryLookupStrategy(em, extractor, evaluationContextProvider); case CREATE_IF_NOT_FOUND: - return new CreateIfNotFoundQueryLookupStrategy(em, extractor); + return new CreateIfNotFoundQueryLookupStrategy(em, extractor, new CreateQueryLookupStrategy(em, extractor), + new DeclaredQueryLookupStrategy(em, extractor, evaluationContextProvider)); default: throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 0a0ebdd53..dae3a834b 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query; import javax.persistence.EntityManager; import javax.persistence.Query; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.RepositoryQuery; @@ -36,10 +37,12 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { * @param method must not be {@literal null}. * @param em must not be {@literal null}. * @param queryString must not be {@literal null} or empty. + * @param evaluationContextProvider */ - public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString) { + public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, + ExpressionEvaluationContextProvider evaluationContextProvider) { - super(method, em, queryString); + super(method, em, queryString, evaluationContextProvider); Parameters parameters = method.getParameters(); boolean hasPagingOrSortingParameter = parameters.hasPageableParameter() || parameters.hasSortParameter(); diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java index c759e2ee4..bc4448b50 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java @@ -161,4 +161,13 @@ public class ParameterBinder { return result; } + + /** + * Returns the values to bind. + * + * @return + */ + Object[] getValues() { + return values; + } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index c3e62aba8..1be6d9fb5 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query; import javax.persistence.EntityManager; import javax.persistence.Query; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; /** @@ -36,8 +37,9 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * @param method must not be {@literal null}. * @param em must not be {@literal null}. */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em) { - this(method, em, method.getAnnotatedQuery()); + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, + ExpressionEvaluationContextProvider evaluationContextProvider) { + this(method, em, method.getAnnotatedQuery(), evaluationContextProvider); } /** @@ -47,9 +49,10 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * @param em must not be {@literal null}. * @param queryString must not be {@literal null} or empty. */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString) { + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, + ExpressionEvaluationContextProvider evaluationContextProvider) { - super(method, em, queryString); + super(method, em, queryString, evaluationContextProvider); validateQuery(getQuery().getQueryString(), String.format("Validation failed for query for method %s!", method)); diff --git a/src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java new file mode 100644 index 000000000..6123b3fd0 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java @@ -0,0 +1,205 @@ +/* + * Copyright 2014 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.jpa.repository.query; + +import java.util.List; + +import javax.persistence.Query; + +import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; + +/** + * A {@link StringQueryParameterBinder} that is able to bind synthetic query parameters. + * + * @author Thomas Darimont + */ +class SpelExpressionStringQueryParameterBinder extends StringQueryParameterBinder { + + private final StringQuery query; + private final ExpressionEvaluationContextProvider evaluationContextProvider; + + /** + * Creates a new {@link SpelExpressionStringQueryParameterBinder}. + * + * @param parameters must not be {@literal null} + * @param values must not be {@literal null} + * @param query must not be {@literal null} + * @param evaluationContextProvider must not be {@literal null} + */ + public SpelExpressionStringQueryParameterBinder(JpaParameters parameters, Object[] values, StringQuery query, + ExpressionEvaluationContextProvider evaluationContextProvider) { + + super(parameters, values, query); + + Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!"); + + this.query = query; + this.evaluationContextProvider = evaluationContextProvider; + } + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query) + */ + @Override + public T bind(T jpaQuery) { + return potentiallyBindExpressionParameters(super.bind(jpaQuery)); + } + + /** + * @param jpaQuery must not be {@literal null} + * @return + */ + private T potentiallyBindExpressionParameters(T jpaQuery) { + + for (ParameterBinding binding : query.getParameterBindings()) { + + if (binding.isExpression()) { + + Expression expr = new SpelExpressionParser().parseExpression(binding.getExpression()); + + EvaluationContext delegatee = evaluationContextProvider.getEvaluationContext(); + StandardEvaluationContext evalContext = new DelegatingStandardEvaluationContext(getValues(), delegatee); + Object actualValue = expr.getValue(evalContext, String.class); + + if (binding.getName() != null) { + jpaQuery.setParameter(binding.getName(), binding.prepare(actualValue)); + } else { + jpaQuery.setParameter(binding.getPosition(), binding.prepare(actualValue)); + } + } + } + + return jpaQuery; + } + + /** + * A {@link StandardEvaluationContext} that delegates to the given {@link EvaluationContext}. Variables are first + * looked-up locally and if not the lookup is performed against the delegatee. + * + * @author Thomas Darimont + */ + static class DelegatingStandardEvaluationContext extends StandardEvaluationContext { + + private final EvaluationContext delegatee; + + /** + * Creates a new {@link DelegatingStandardEvaluationContext}. + * + * @param values must not be {@literal null} + * @param delegatee must not be {@literal null} + */ + public DelegatingStandardEvaluationContext(Object[] values, EvaluationContext delegatee) { + + super(values); + + Assert.notNull(delegatee, "EvaluationContext delegatee must not be null!"); + + this.delegatee = delegatee; + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getConstructorResolvers() + */ + @Override + public List getConstructorResolvers() { + return delegatee.getConstructorResolvers(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getMethodResolvers() + */ + @Override + public List getMethodResolvers() { + return delegatee.getMethodResolvers(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getPropertyAccessors() + */ + @Override + public List getPropertyAccessors() { + return delegatee.getPropertyAccessors(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getTypeLocator() + */ + @Override + public TypeLocator getTypeLocator() { + return delegatee.getTypeLocator(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getTypeConverter() + */ + @Override + public TypeConverter getTypeConverter() { + return delegatee.getTypeConverter(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getTypeComparator() + */ + @Override + public TypeComparator getTypeComparator() { + return delegatee.getTypeComparator(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getOperatorOverloader() + */ + @Override + public OperatorOverloader getOperatorOverloader() { + return delegatee.getOperatorOverloader(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#getBeanResolver() + */ + @Override + public BeanResolver getBeanResolver() { + return delegatee.getBeanResolver(); + } + + /* (non-Javadoc) + * @see org.springframework.expression.spel.support.StandardEvaluationContext#lookupVariable(java.lang.String) + */ + @Override + public Object lookupVariable(String name) { + + Object result = super.lookupVariable(name); + if (result != null) { + return result; + } + + return delegatee.lookupVariable(name); + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index d23a521c5..8a9a4815d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -142,6 +142,8 @@ class StringQuery { INSTANCE; + private static final String EXPRESSION_PREFIX = "__$synthetic$__"; + private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile("\\?(\\d+)"); private static final Pattern PARAMETER_BINDING_PATTERN; private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type! " + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same binding."; @@ -161,13 +163,15 @@ class StringQuery { builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords builder.append(")?"); builder.append("(?: )?"); // some whitespace - builder.append("\\(?"); // optional braces around paramters + builder.append("\\(?"); // optional braces around parameters builder.append("("); - builder.append("%?(\\?(\\d+))%?"); // position parameter + builder.append("%?(\\?(\\d+))%?"); // position parameter and parameter index builder.append("|"); // or - builder.append("%?(:([\\p{L}\\w]+))%?"); // named parameter; + builder.append("%?(:([\\p{L}\\w]+))%?"); // named parameter and the parameter name + builder.append("|"); // or + builder.append("%?((:|\\?)#\\{([^}]+)\\})%?"); // expression parameter and expression builder.append(")"); - builder.append("\\)?"); // optional braces around paramters + builder.append("\\)?"); // optional braces around parameters PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); } @@ -182,8 +186,18 @@ class StringQuery { private final String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings) { - Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(query); String result = query; + Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(query); + + int greatestParameterIndex = determineGreatestParameterIndexIfPresent(query); + + boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1; + + /* + * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to + * not mix-up with the actual parameter indices. + */ + int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; while (matcher.find()) { @@ -191,30 +205,46 @@ class StringQuery { String parameterName = parameterIndexString != null ? null : matcher.group(6); Integer parameterIndex = parameterIndexString == null ? null : Integer.valueOf(parameterIndexString); String typeSource = matcher.group(1); + String expression = null; + String replacement = null; + + if (parameterName == null && parameterIndex == null) { + expressionParameterIndex++; + + if (parametersShouldBeAccessedByIndex) { + parameterIndex = expressionParameterIndex; + replacement = "?" + parameterIndex; + } else { + parameterName = EXPRESSION_PREFIX + expressionParameterIndex; + replacement = ":" + parameterName; + } + + expression = matcher.group(9); + } switch (ParameterBindingType.of(typeSource)) { case LIKE: Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - String replacement = matcher.group(3); + replacement = replacement != null ? replacement : matcher.group(3); if (parameterIndex != null) { - checkAndRegister(new LikeParameterBinding(parameterIndex, likeType), bindings); + checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings); } else { - checkAndRegister(new LikeParameterBinding(parameterName, likeType), bindings); - replacement = matcher.group(5); + checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings); + + replacement = expression != null ? ":" + parameterName : matcher.group(5); } - result = StringUtils.replace(result, matcher.group(2), replacement); break; case IN: if (parameterIndex != null) { - checkAndRegister(new InParameterBinding(parameterIndex), bindings); + checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings); } else { - checkAndRegister(new InParameterBinding(parameterName), bindings); + checkAndRegister(new InParameterBinding(parameterName, expression), bindings); } result = query; @@ -223,14 +253,32 @@ class StringQuery { case AS_IS: // fall-through we don't need a special parameter binding for the given parameter. default: - bindings.add(parameterIndex != null ? new ParameterBinding(parameterIndex) : new ParameterBinding( - parameterName)); + bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression) + : new ParameterBinding(parameterName, null, expression)); } + + if (replacement != null) { + result = StringUtils.replace(result, matcher.group(2), replacement); + } + } return result; } + private int determineGreatestParameterIndexIfPresent(String query) { + + Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); + + int greatestParameterIndex = -1; + while (parameterIndexMatcher.find()) { + String parameterIndexString = parameterIndexMatcher.group(1); + greatestParameterIndex = Math.max(greatestParameterIndex, Integer.parseInt(parameterIndexString)); + } + + return greatestParameterIndex; + } + private static void checkAndRegister(ParameterBinding binding, List bindings) { for (ParameterBinding existing : bindings) { @@ -304,6 +352,7 @@ class StringQuery { static class ParameterBinding { private final String name; + private final String expression; private final Integer position; /** @@ -312,11 +361,7 @@ class StringQuery { * @param name must not be {@literal null}. */ public ParameterBinding(String name) { - - Assert.notNull(name, "Name must not be null!"); - - this.name = name; - this.position = null; + this(name, null, null); } /** @@ -325,11 +370,30 @@ class StringQuery { * @param position must not be {@literal null}. */ public ParameterBinding(Integer position) { + this(null, position, null); + } - Assert.notNull(position, "Position must not be null!"); + /** + * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression + * information. + * + * @param name + * @param position + * @param expression + */ + ParameterBinding(String name, Integer position, String expression) { - this.name = null; + if (name == null) { + Assert.notNull(position, "Position must not be null!"); + } + + if (position == null) { + Assert.notNull(name, "Name must not be null!"); + } + + this.name = name; this.position = position; + this.expression = expression; } /** @@ -368,6 +432,13 @@ class StringQuery { return position; } + /** + * @return {@literal true} if this parameter binding is a synthetic SpEL expression. + */ + public boolean isExpression() { + return this.expression != null; + } + /* * (non-Javadoc) * @see java.lang.Object#hashCode() @@ -379,6 +450,7 @@ class StringQuery { result += nullSafeHashCode(this.name); result += nullSafeHashCode(this.position); + result += nullSafeHashCode(this.expression); return result; } @@ -396,7 +468,8 @@ class StringQuery { ParameterBinding that = (ParameterBinding) obj; - return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position); + return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position) + && nullSafeEquals(this.expression, that.expression); } /* @@ -405,7 +478,8 @@ class StringQuery { */ @Override public String toString() { - return String.format("ParameterBinding [name: %s, position: %d]", getName(), getPosition()); + return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(), + getExpression()); } /** @@ -415,6 +489,10 @@ class StringQuery { public Object prepare(Object valueToBind) { return valueToBind; } + + public String getExpression() { + return expression; + } } /** @@ -429,18 +507,20 @@ class StringQuery { * Creates a new {@link InParameterBinding} for the parameter with the given name. * * @param name + * @param expression */ - public InParameterBinding(String name) { - super(name); + public InParameterBinding(String name, String expression) { + super(name, null, expression); } /** * Creates a new {@link InParameterBinding} for the parameter with the given position. * * @param position + * @param expression */ - public InParameterBinding(int position) { - super(position); + public InParameterBinding(int position, String expression) { + super(null, position, expression); } /* @@ -486,8 +566,20 @@ class StringQuery { * @param type must not be {@literal null}. */ public LikeParameterBinding(String name, Type type) { + this(name, type, null); + } - super(name); + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter + * binding input. + * + * @param name must not be {@literal null} or empty. + * @param type must not be {@literal null}. + * @param expression may be {@literal null}. + */ + public LikeParameterBinding(String name, Type type, String expression) { + + super(name, null, expression); Assert.hasText(name, "Name must not be null or empty!"); Assert.notNull(type, "Type must not be null!"); @@ -505,8 +597,19 @@ class StringQuery { * @param type must not be {@literal null}. */ public LikeParameterBinding(int position, Type type) { + this(position, type, null); + } - super(position); + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}. + * + * @param position + * @param type must not be {@literal null}. + * @param expression may be {@literal null}. + */ + public LikeParameterBinding(int position, Type type, String expression) { + + super(null, position, expression); Assert.isTrue(position > 0, "Position must be greater than zero!"); Assert.notNull(type, "Type must not be null!"); diff --git a/src/main/java/org/springframework/data/jpa/repository/support/ExpressionEvaluationContextProvider.java b/src/main/java/org/springframework/data/jpa/repository/support/ExpressionEvaluationContextProvider.java new file mode 100644 index 000000000..f8c2a68dd --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/ExpressionEvaluationContextProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 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.jpa.repository.support; + +import org.springframework.expression.EvaluationContext; + +/** + * Provides a way to access a centrally defined potentially shared {@link EvaluationContext}. + * + * @author Thomas Darimont + */ +public interface ExpressionEvaluationContextProvider { + + /** + * Returns the {@link EvaluationContext}. + * + * @return + */ + EvaluationContext getEvaluationContext(); +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index eb417336a..655b5c9f0 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -41,6 +41,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private final EntityManager entityManager; private final QueryExtractor extractor; private final CrudMethodMetadataPostProcessor lockModePostProcessor; + private final ExpressionEvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link JpaRepositoryFactory}. @@ -48,12 +49,23 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { * @param entityManager must not be {@literal null} */ public JpaRepositoryFactory(EntityManager entityManager) { + this(entityManager, StandardExpressionEvaluationContextProvider.INSTANCE); + } + + /** + * Creates a new {@link JpaRepositoryFactory}. + * + * @param entityManager must not be {@literal null} + * @param evaluationContextProvider must not be {@literal null} + */ + public JpaRepositoryFactory(EntityManager entityManager, ExpressionEvaluationContextProvider evaluationContextProvider) { Assert.notNull(entityManager); this.entityManager = entityManager; this.extractor = PersistenceProvider.fromEntityManager(entityManager); this.lockModePostProcessor = CrudMethodMetadataPostProcessor.INSTANCE; + this.evaluationContextProvider = evaluationContextProvider; addRepositoryProxyPostProcessor(lockModePostProcessor); } @@ -132,7 +144,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { @Override protected QueryLookupStrategy getQueryLookupStrategy(Key key) { - return JpaQueryLookupStrategy.create(entityManager, key, extractor); + return JpaQueryLookupStrategy.create(entityManager, key, extractor, evaluationContextProvider); } /* diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index 68ec233f9..2a47c5033 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -39,6 +39,8 @@ public class JpaRepositoryFactoryBean, S, ID extends private EntityManager entityManager; + private ExpressionEvaluationContextProvider expressionEvaluationContextProvider = StandardExpressionEvaluationContextProvider.INSTANCE; + /** * The {@link EntityManager} to be used. * @@ -58,6 +60,14 @@ public class JpaRepositoryFactoryBean, S, ID extends super.setMappingContext(mappingContext); } + /** + * @param expressionEvaluationContextProvider the expressionEvaluationContextProvider to set + */ + public void setExpressionEvaluationContextProvider( + ExpressionEvaluationContextProvider expressionEvaluationContextProvider) { + this.expressionEvaluationContextProvider = expressionEvaluationContextProvider; + } + /* * (non-Javadoc) * @@ -76,7 +86,7 @@ public class JpaRepositoryFactoryBean, S, ID extends * @return */ protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { - return new JpaRepositoryFactory(entityManager); + return new JpaRepositoryFactory(entityManager, expressionEvaluationContextProvider); } /* diff --git a/src/main/java/org/springframework/data/jpa/repository/support/StandardExpressionEvaluationContextProvider.java b/src/main/java/org/springframework/data/jpa/repository/support/StandardExpressionEvaluationContextProvider.java new file mode 100644 index 000000000..9942d2b21 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/StandardExpressionEvaluationContextProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 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.jpa.repository.support; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Default implementation of {@link ExpressionEvaluationContextProvider} that always creates a new + * {@link EvaluationContext}. + * + * @author Thomas Darimont + */ +public enum StandardExpressionEvaluationContextProvider implements ExpressionEvaluationContextProvider { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider#getEvaluationContext() + */ + @Override + public StandardEvaluationContext getEvaluationContext() { + return new StandardEvaluationContext(); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index d38d3716d..97edc3351 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -1588,6 +1588,32 @@ public class UserRepositoryTests { assertThat(result.isPresent(), is(true)); assertThat(result.get(), is(firstUser)); } + + /** + * @see DATAJPA-XXX + */ + @Test + public void shouldFindUserByFirstnameAndLastnameWithSpelExpressionInStringBasedQuery() { + + flushTestUsers(); + List users = repository.findByFirstnameAndLastnameWithSpelExpression("Oliver", "ierk"); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(firstUser)); + } + + /** + * @see DATAJPA-XXX + */ + @Test + public void shouldFindUserByLastnameWithSpelExpressionInStringBasedQuery() { + + flushTestUsers(); + List users = repository.findByLastnameWithSpelExpression("ierk"); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(firstUser)); + } private Page executeSpecWithSort(Sort sort) { diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index 8a2fdd7fc..b79b37cfe 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -36,6 +36,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.support.StandardExpressionEvaluationContextProvider; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; @@ -72,7 +73,8 @@ public class JpaQueryLookupStrategyUnitTests { @Test public void invalidAnnotatedQueryCausesException() throws Exception { - QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor); + QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor, + StandardExpressionEvaluationContextProvider.INSTANCE); Method method = UserRepository.class.getMethod("findByFoo", String.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -93,7 +95,8 @@ public class JpaQueryLookupStrategyUnitTests { @Test public void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() throws Exception { - QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor); + QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor, + StandardExpressionEvaluationContextProvider.INSTANCE); Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Pageable.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 78251ee1b..6f64ba09a 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -44,6 +44,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.jpa.repository.support.DefaultJpaEntityMetadata; import org.springframework.data.jpa.repository.support.JpaEntityMetadata; +import org.springframework.data.jpa.repository.support.StandardExpressionEvaluationContextProvider; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.RepositoryQuery; @@ -95,7 +96,8 @@ public class SimpleJpaQueryUnitTests { when(method.getEntityInformation()).thenReturn((JpaEntityMetadata) new DefaultJpaEntityMetadata(User.class)); when(em.createQuery("foo", Long.class)).thenReturn(query); - SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u"); + SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", + StandardExpressionEvaluationContextProvider.INSTANCE); assertThat(jpaQuery.createCountQuery(new Object[] {}), is(query)); } @@ -111,7 +113,8 @@ public class SimpleJpaQueryUnitTests { Method method = UserRepository.class.getMethod("findAllPaged", Pageable.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor); - AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u"); + AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", + StandardExpressionEvaluationContextProvider.INSTANCE); jpaQuery.createCountQuery(new Object[] { new PageRequest(1, 10) }); verify(query, times(0)).setFirstResult(anyInt()); @@ -124,7 +127,8 @@ public class SimpleJpaQueryUnitTests { Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor); - AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em); + AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em, + StandardExpressionEvaluationContextProvider.INSTANCE); assertThat(jpaQuery instanceof NativeJpaQuery, is(true)); @@ -205,7 +209,8 @@ public class SimpleJpaQueryUnitTests { private RepositoryQuery createJpaQuery(Method method) { JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, extractor); - return JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em); + return JpaQueryFactory.INSTANCE.fromQueryAnnotation(queryMethod, em, + StandardExpressionEvaluationContextProvider.INSTANCE); } interface SampleRepository { diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 9638f5276..b64aa5a44 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -447,7 +447,7 @@ public interface UserRepository extends JpaRepository, JpaSpecifi * @see DATAJPA-551 */ Slice findTop2UsersBy(Pageable page); - + /** * @see DATAJPA-506 */ @@ -459,4 +459,16 @@ public interface UserRepository extends JpaRepository, JpaSpecifi */ @Query("select u from User u where u.emailAddress = ?1") Optional findOptionalByEmailAddress(String emailAddress); + + /** + * @see DATAJPA-XXX + */ + @Query("select u from User u where u.firstname = ?#{[0]} and u.firstname = ?1 and u.lastname like %?#{[1]}% and u.lastname like %?2%") + List findByFirstnameAndLastnameWithSpelExpression(String firstname, String lastname); + + /** + * @see DATAJPA-XXX + */ + @Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%") + List findByLastnameWithSpelExpression(@Param("lastname") String lastname); }