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 eacd50d71..382580a4b 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 @@ -56,6 +56,7 @@ import org.springframework.util.StringUtils; * @author Oliver Gierke * @author Eberhard Wolff * @author Gil Markham + * @author Thomas Darimont */ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { @@ -104,7 +105,8 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi String expressionEvaluationContextProviderRef = source.getAttribute("expressionEvaluationContextProviderRef"); if (StringUtils.hasText(expressionEvaluationContextProviderRef)) { - builder.addPropertyReference("expressionEvaluationContextProvider", expressionEvaluationContextProviderRef); + builder.addPropertyReference(DEFAULT_EXPRESSION_EVALUATION_CONTEXT_PROVIDER, + expressionEvaluationContextProviderRef); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ExpressionAwareParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/ExpressionAwareParameterBinder.java index b370666c0..f73097d60 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/ExpressionAwareParameterBinder.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/ExpressionAwareParameterBinder.java @@ -15,10 +15,12 @@ */ package org.springframework.data.jpa.repository.query; +import java.util.ArrayList; import java.util.List; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; +import org.springframework.expression.AccessException; import org.springframework.expression.BeanResolver; import org.springframework.expression.ConstructorResolver; import org.springframework.expression.EvaluationContext; @@ -29,7 +31,9 @@ import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypeComparator; import org.springframework.expression.TypeConverter; import org.springframework.expression.TypeLocator; +import org.springframework.expression.TypedValue; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; @@ -143,11 +147,15 @@ class ExpressionAwareParameterBinder extends ParameterBinder { evalContext.setVariable(param.getName(), getValues()[param.getIndex()]); } } + + evalContext.setVariable("args", getValues()); } /** * 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. + * looked-up locally and if not the lookup is performed against the delegatee. Property references at the expression + * root are resolved against the delegatees rootObject first potentially followed by a second lookup against the + * current rootObject. * * @author Thomas Darimont */ @@ -158,12 +166,11 @@ class ExpressionAwareParameterBinder extends ParameterBinder { /** * Creates a new {@link DelegatingStandardEvaluationContext}. * - * @param values must not be {@literal null} + * @param objects * @param delegatee must not be {@literal null} */ - public DelegatingStandardEvaluationContext(Object[] values, EvaluationContext delegatee) { - - super(values); + public DelegatingStandardEvaluationContext(Object[] parameterValues, EvaluationContext delegatee) { + super(parameterValues); Assert.notNull(delegatee, "EvaluationContext delegatee must not be null!"); @@ -191,7 +198,38 @@ class ExpressionAwareParameterBinder extends ParameterBinder { */ @Override public List getPropertyAccessors() { - return delegatee.getPropertyAccessors(); + + List propertyAccessors = new ArrayList(); + + // First we want to lookup properties against the delegatees root object + propertyAccessors.add(newDelegatingPropertyAccessor(delegatee.getRootObject())); + + // Second we want to loopup properties against the current root object + propertyAccessors.addAll(delegatee.getPropertyAccessors()); + + return propertyAccessors; + } + + /** + * Returns a {@link ReflectivePropertyAccessor} that always looks up properties at the expression root against the + * delegates root object first. + * + * @param delegateRoot + * @return + */ + private ReflectivePropertyAccessor newDelegatingPropertyAccessor(final TypedValue delegateRoot) { + + return new ReflectivePropertyAccessor() { + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return super.canRead(context, delegateRoot.getValue(), name); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return super.read(context, delegateRoot.getValue(), name); + } + }; } /* (non-Javadoc) diff --git a/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java index 28c712e95..af737e3c2 100644 --- a/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java @@ -65,6 +65,7 @@ public class JavaConfigUserRepositoryTests extends UserRepositoryTests { factory.setRepositoryInterface(UserRepository.class); factory.setCustomImplementation(new UserRepositoryImpl()); factory.setNamedQueries(namedQueries()); + factory.setExpressionEvaluationContextProvider(new SampleExpressionEvaluationContextProvider()); factory.afterPropertiesSet(); return factory.getObject(); diff --git a/src/test/java/org/springframework/data/jpa/repository/SampleExpressionEvaluationContextProvider.java b/src/test/java/org/springframework/data/jpa/repository/SampleExpressionEvaluationContextProvider.java new file mode 100644 index 000000000..2d5913691 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/SampleExpressionEvaluationContextProvider.java @@ -0,0 +1,32 @@ +/* + * 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; + +import org.springframework.data.jpa.repository.SampleSecurity.SampleSecurityContextHolder; +import org.springframework.data.jpa.repository.support.ExpressionEvaluationContextProvider; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * @author Thomas Darimont + */ +public class SampleExpressionEvaluationContextProvider implements ExpressionEvaluationContextProvider { + + @Override + public EvaluationContext getEvaluationContext() { + return new StandardEvaluationContext(SampleSecurityContextHolder.getCurrent()); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/SampleSecurity.java b/src/test/java/org/springframework/data/jpa/repository/SampleSecurity.java new file mode 100644 index 000000000..dfc8cf41f --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/SampleSecurity.java @@ -0,0 +1,107 @@ +/* + * 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; + +/** + * Minimalistic thread-scoped security context analogous to Spring Security for testing. + * + * @author Thomas Darimont + */ +public class SampleSecurity { + + /** + * @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/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 03cd91848..5c8656ad8 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -58,6 +58,7 @@ import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.SpecialUser; import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.SampleSecurity.SampleSecurityContextHolder; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -105,6 +106,8 @@ public class UserRepositoryTests { fourthUser = new User("kevin", "raymond", "no@gmail.com"); fourthUser.setAge(31); adminRole = new Role("admin"); + + SampleSecurityContextHolder.clear(); } @Test @@ -1592,7 +1595,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldFindUserByFirstnameAndLastnameWithSpelExpressionInStringBasedQuery() { @@ -1605,7 +1608,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldFindUserByLastnameWithSpelExpressionInStringBasedQuery() { @@ -1618,7 +1621,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldFindBySpELExpressionWithoutArgumentsWithQuestionmark() { @@ -1631,7 +1634,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldFindBySpELExpressionWithoutArgumentsWithColon() { @@ -1644,7 +1647,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldFindUsersByAgeForSpELExpression() { @@ -1657,7 +1660,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldfindUsersByFirstnameForSpELExpressionWithParameterNameVariableReference() { @@ -1670,7 +1673,7 @@ public class UserRepositoryTests { } /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Test public void shouldFindUserByLastnameWithSpelExpressionInDerivedQuery() { @@ -1684,6 +1687,42 @@ public class UserRepositoryTests { assertThat(users.get(0), is(firstUser)); } + /** + * @see DATAJPA-564 + */ + @Test + public void shouldFindCurrentUserWithCustomQueryDependingOnSecurityContext() { + + flushTestUsers(); + + SampleSecurityContextHolder.getCurrent().setPrincipal(secondUser); + List users = repository.findCurrentUserWithCustomQuery(); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(secondUser)); + + SampleSecurityContextHolder.getCurrent().setPrincipal(firstUser); + users = repository.findCurrentUserWithCustomQuery(); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(firstUser)); + } + + /** + * @see DATAJPA-564 + */ + @Test + public void shouldFindByFirstnameAndCurrentUserWithCustomQuery() { + + flushTestUsers(); + + SampleSecurityContextHolder.getCurrent().setPrincipal(secondUser); + List users = repository.findByFirstnameAndCurrentUserWithCustomQuery("Joachim"); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(secondUser)); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); 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 eb7909090..8b4b80bf0 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 @@ -448,7 +448,7 @@ public interface UserRepository extends JpaRepository, JpaSpecifi * @see DATAJPA-551 */ Slice findTop2UsersBy(Pageable page); - + /** * @see DATAJPA-506 */ @@ -462,43 +462,55 @@ public interface UserRepository extends JpaRepository, JpaSpecifi Optional findOptionalByEmailAddress(String emailAddress); /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @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 + * @see DATAJPA-564 */ @Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%") List findByLastnameWithSpelExpression(@Param("lastname") String lastname); /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ List queryByLastname(Expression lastname); /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Query("select u from User u where u.firstname = ?#{'Oliver'}") List findOliverBySpELExpressionWithoutArgumentsWithQuestionmark(); /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Query("select u from User u where u.firstname = :#{'Oliver'}") List findOliverBySpELExpressionWithoutArgumentsWithColon(); /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Query("select u from User u where u.age = ?#{[0]}") List findUsersByAgeForSpELExpressionByIndexedParameter(int age); /** - * @see DATAJPA-XXX + * @see DATAJPA-564 */ @Query("select u from User u where u.firstname = :firstname and u.firstname = :#{#firstname}") List findUsersByFirstnameForSpELExpression(@Param("firstname") String firstname); + + /** + * @see DATAJPA-564 + */ + @Query("select u from User u where u.emailAddress = ?#{principal.emailAddress}") + List findCurrentUserWithCustomQuery(); + + /** + * @see DATAJPA-564 + */ + @Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}") + List findByFirstnameAndCurrentUserWithCustomQuery(String firstname); } diff --git a/src/test/resources/application-context.xml b/src/test/resources/application-context.xml index d99d31cad..2dea00963 100644 --- a/src/test/resources/application-context.xml +++ b/src/test/resources/application-context.xml @@ -21,6 +21,8 @@ + + diff --git a/src/test/resources/infrastructure.xml b/src/test/resources/infrastructure.xml index db96fca62..06daffcdd 100644 --- a/src/test/resources/infrastructure.xml +++ b/src/test/resources/infrastructure.xml @@ -24,6 +24,8 @@ + +