From 1879fea1f03ff4328fa06b2bb9742f5bbdabf38c Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Thu, 26 Jun 2014 23:12:26 +0200 Subject: [PATCH] DATAJPA-564 - Expose invocation arguments as well as delegate root object properties via root expressions. We now use the rootObject of the delegates context as the rootObject for the expression evaluation as well as the arguments of the current method call are available via positional indices as well via the special variable called "args". Added minimalistic Security Context infrastructure that mimics that from Spring Security to ease testing. Referenced appropriate JIRA issues. --- .../config/JpaRepositoryConfigExtension.java | 4 +- .../query/ExpressionAwareParameterBinder.java | 50 +++++++- .../JavaConfigUserRepositoryTests.java | 1 + ...leExpressionEvaluationContextProvider.java | 32 ++++++ .../data/jpa/repository/SampleSecurity.java | 107 ++++++++++++++++++ .../jpa/repository/UserRepositoryTests.java | 53 +++++++-- .../jpa/repository/sample/UserRepository.java | 28 +++-- src/test/resources/application-context.xml | 2 + src/test/resources/infrastructure.xml | 2 + 9 files changed, 257 insertions(+), 22 deletions(-) create mode 100644 src/test/java/org/springframework/data/jpa/repository/SampleExpressionEvaluationContextProvider.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/SampleSecurity.java 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 @@ + +