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 1f484a738..11e7d286e 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 @@ -45,6 +45,7 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostPro import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor; +import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; @@ -76,6 +77,7 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi private static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; private static final String ENABLE_DEFAULT_TRANSACTIONS_ATTRIBUTE = "enableDefaultTransactions"; private static final String JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME = "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup"; + private static final String ESCAPE_CHARACTER_PROPERTY = "escapeCharacter"; /* * (non-Javadoc) @@ -132,17 +134,18 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi Optional transactionManagerRef = source.getAttribute("transactionManagerRef"); builder.addPropertyValue("transactionManager", transactionManagerRef.orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); builder.addPropertyValue("entityManager", getEntityManagerBeanDefinitionFor(source, source.getSource())); - builder.addPropertyValue("escapeCharacter", getEscapeCharacter(source).orElse('\\')); + builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\')); builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME); } /** - * XML configurations do not support {@link Character} values. This method catches the exception thrown and returns an {@link Optional#empty()} instead. + * XML configurations do not support {@link Character} values. This method catches the exception thrown and returns an + * {@link Optional#empty()} instead. */ private static Optional getEscapeCharacter(RepositoryConfigurationSource source) { try { - return source.getAttribute("escapeCharacter", Character.class); + return source.getAttribute(ESCAPE_CHARACTER_PROPERTY, Character.class); } catch (IllegalArgumentException ___) { return Optional.empty(); } @@ -209,6 +212,21 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi registerIfNotAlreadyRegistered(() -> new RootBeanDefinition(JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME), registry, JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME, source); + + // EvaluationContextExtension for JPA specific SpEL functions + + registerIfNotAlreadyRegistered(() -> { + + Object value = AnnotationRepositoryConfigurationSource.class.isInstance(config) // + ? config.getRequiredAttribute(ESCAPE_CHARACTER_PROPERTY, Character.class) // + : config.getAttribute(ESCAPE_CHARACTER_PROPERTY).orElse("\\"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(JpaEvaluationContextExtension.class); + builder.addConstructorArgValue(value); + + return builder.getBeanDefinition(); + + }, registry, JpaEvaluationContextExtension.class.getName(), source); } /* diff --git a/src/main/java/org/springframework/data/jpa/repository/support/EscapeCharacter.java b/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java similarity index 58% rename from src/main/java/org/springframework/data/jpa/repository/support/EscapeCharacter.java rename to src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java index 21d9b107d..269fa44ae 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/EscapeCharacter.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java @@ -13,35 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.support; +package org.springframework.data.jpa.repository.query; import lombok.Value; +import java.util.Arrays; +import java.util.List; + import org.springframework.util.Assert; /** - * A Value-class encapsulating an escape character for LIKE queries and the actually usage of it in escaping Strings. + * A value type encapsulating an escape character for LIKE queries and the actually usage of it in escaping + * {@link String}s. * * @author Jens Schauder + * @author Oliver Drotbohm */ @Value(staticConstructor = "of") public class EscapeCharacter { + + private static final List TO_REPLACE = Arrays.asList("_", "%"); + char value; + /** + * Escapes all special like characters ({@code _}, {@code %}) using the configured escape character. + * + * @param value must not be {@literal null}. + * @return + */ public String escape(String value) { Assert.notNull(value, "Value must be not null."); - return value.replace("_", value + "_").replace("%", value + "%"); - } - - // used for SpEL expressions - static String escape(String value, String escape) { - - Assert.hasText(escape, "escape must be a sinlge character String."); - char[] chars = escape.toCharArray(); - Assert.isTrue(chars.length == 1, "escape must be a single character String."); - - return EscapeCharacter.of(chars[0]).escape(value); + return TO_REPLACE.stream() // + .reduce(value, (it, character) -> it.replace(character, this.value + character)); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 1e5955fce..76bd83c95 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -36,7 +36,6 @@ import javax.persistence.metamodel.SingularAttribute; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.AbstractQueryCreator; 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 78909bf36..7b4fc4c5e 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 @@ -22,7 +22,6 @@ import javax.persistence.EntityManager; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java index bd0607282..cba60a049 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java @@ -27,7 +27,6 @@ import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.ParameterExpression; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; diff --git a/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index 38ead40a3..ae6b3e002 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -29,7 +29,6 @@ import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index d45ddc1b4..5f197730e 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -15,7 +15,6 @@ */ package org.springframework.data.jpa.repository.query; -import java.lang.reflect.Method; import java.util.List; import java.util.function.Function; @@ -26,7 +25,6 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter; import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -36,7 +34,6 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; /** * Encapsulates different strategies for the creation of a {@link QueryParameterSetter} from a {@link Query} and a @@ -178,11 +175,7 @@ abstract class QueryParameterSetterFactory { private Object evaluateExpression(Expression expression, Object[] values) { EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, values); - Method escapeMethod = ReflectionUtils.findMethod(EscapeCharacter.class, "escape", String.class, String.class); - Assert.notNull(escapeMethod, "Escape method must not be null."); - - context.setVariable("escape", escapeMethod); return expression.getValue(context, Object.class); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java new file mode 100644 index 000000000..686e430c2 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 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.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.spel.spi.EvaluationContextExtension; + +/** + * {@link EvaluationContextExtension} to register {@link EscapeCharacter} as root object to essentially expose an + * {@code expose(…)} function to SpEL. + * + * @author Oliver Drotbohm + */ +public class JpaEvaluationContextExtension implements EvaluationContextExtension { + + private final EscapeCharacter character; + + /** + * Creates a new {@link JpaEvaluationContextExtension} for the given escape character. + * + * @param escapeCharacter the character to be used to escape parameters for LIKE expression. + */ + public JpaEvaluationContextExtension(char escapeCharacter) { + this.character = EscapeCharacter.of(escapeCharacter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.spel.spi.EvaluationContextExtension#getExtensionId() + */ + @Override + public String getExtensionId() { + return "jpa"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.spel.spi.EvaluationContextExtension#getRootObject() + */ + @Override + public Object getRootObject() { + return character; + } +} 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 5e98c33c8..1fde89b1a 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 @@ -32,6 +32,7 @@ import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.query.AbstractJpaQuery; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.util.JpaMetamodel; 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 ad8dc4c66..b8144f10b 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 @@ -20,6 +20,7 @@ import javax.persistence.PersistenceContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.SimpleEntityPathResolver; diff --git a/src/main/resources/META-INF/spring.schemas b/src/main/resources/META-INF/spring.schemas index 7d65aa229..93521b27d 100644 --- a/src/main/resources/META-INF/spring.schemas +++ b/src/main/resources/META-INF/spring.schemas @@ -3,4 +3,5 @@ http\://www.springframework.org/schema/data/jpa/spring-jpa-1.1.xsd=org/springfra http\://www.springframework.org/schema/data/jpa/spring-jpa-1.2.xsd=org/springframework/data/jpa/repository/config/spring-jpa-1.2.xsd http\://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd=org/springframework/data/jpa/repository/config/spring-jpa-1.3.xsd http\://www.springframework.org/schema/data/jpa/spring-jpa-1.8.xsd=org/springframework/data/jpa/repository/config/spring-jpa-1.8.xsd -http\://www.springframework.org/schema/data/jpa/spring-jpa.xsd=org/springframework/data/jpa/repository/config/spring-jpa-1.8.xsd +http\://www.springframework.org/schema/data/jpa/spring-jpa-1.11.xsd=org/springframework/data/jpa/repository/config/spring-jpa-1.11.xsd +http\://www.springframework.org/schema/data/jpa/spring-jpa.xsd=org/springframework/data/jpa/repository/config/spring-jpa-1.11.xsd diff --git a/src/main/resources/org/springframework/data/jpa/repository/config/spring-jpa-1.11.xsd b/src/main/resources/org/springframework/data/jpa/repository/config/spring-jpa-1.11.xsd new file mode 100644 index 000000000..093f57b60 --- /dev/null +++ b/src/main/resources/org/springframework/data/jpa/repository/config/spring-jpa-1.11.xsd @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java index 5a4f8c0ab..119ba9bcb 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java @@ -31,7 +31,6 @@ import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.HibernateUtils; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; 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 d31554efa..2a3dfe601 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 @@ -35,7 +35,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.NamedQueries; diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java index 96bcf08c0..c6a4f28c9 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java @@ -29,7 +29,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.repository.query.DefaultParameters; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java index 9ddcfc711..19a6c5a29 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java @@ -28,7 +28,6 @@ import org.junit.runner.RunWith; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.parser.Part; diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java index ea7a9f74f..efe0ca170 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java @@ -24,7 +24,6 @@ import javax.persistence.criteria.CriteriaBuilder; import org.junit.Test; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.parser.Part; diff --git a/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index 6a2296a85..7b1b08bec 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -44,7 +44,6 @@ import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.HibernateUtils; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.Temporal; -import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; 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 191b26212..4eaea14aa 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 @@ -559,7 +559,7 @@ public interface UserRepository List findByNamedQueryWithConstructorExpression(); // DATAJPA-1519 - @Query("select u from User u where u.firstname like %?#{#escape([0],'#')}% escape '#'") + @Query("select u from User u where u.firstname like %?#{escape([0])}% escape '\\'") List findContainingEscaped(String namePart); interface RolesAndFirstname {