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.
This commit is contained in:
Thomas Darimont
2014-06-26 23:12:26 +02:00
committed by Oliver Gierke
parent b09588f922
commit 1879fea1f0
9 changed files with 257 additions and 22 deletions

View File

@@ -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);
}
}

View File

@@ -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<PropertyAccessor> getPropertyAccessors() {
return delegatee.getPropertyAccessors();
List<PropertyAccessor> propertyAccessors = new ArrayList<PropertyAccessor>();
// 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)

View File

@@ -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();

View File

@@ -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());
}
}

View File

@@ -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<SampleAuthentication> auth = new ThreadLocal<SampleAuthentication>() {
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;
}
}
}

View File

@@ -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<User> 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<User> users = repository.findByFirstnameAndCurrentUserWithCustomQuery("Joachim");
assertThat(users, hasSize(1));
assertThat(users.get(0), is(secondUser));
}
private Page<User> executeSpecWithSort(Sort sort) {
flushTestUsers();

View File

@@ -448,7 +448,7 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
* @see DATAJPA-551
*/
Slice<User> findTop2UsersBy(Pageable page);
/**
* @see DATAJPA-506
*/
@@ -462,43 +462,55 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
Optional<User> 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<User> 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<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
/**
* @see DATAJPA-XXX
* @see DATAJPA-564
*/
List<User> queryByLastname(Expression lastname);
/**
* @see DATAJPA-XXX
* @see DATAJPA-564
*/
@Query("select u from User u where u.firstname = ?#{'Oliver'}")
List<User> findOliverBySpELExpressionWithoutArgumentsWithQuestionmark();
/**
* @see DATAJPA-XXX
* @see DATAJPA-564
*/
@Query("select u from User u where u.firstname = :#{'Oliver'}")
List<User> findOliverBySpELExpressionWithoutArgumentsWithColon();
/**
* @see DATAJPA-XXX
* @see DATAJPA-564
*/
@Query("select u from User u where u.age = ?#{[0]}")
List<User> findUsersByAgeForSpELExpressionByIndexedParameter(int age);
/**
* @see DATAJPA-XXX
* @see DATAJPA-564
*/
@Query("select u from User u where u.firstname = :firstname and u.firstname = :#{#firstname}")
List<User> findUsersByFirstnameForSpELExpression(@Param("firstname") String firstname);
/**
* @see DATAJPA-564
*/
@Query("select u from User u where u.emailAddress = ?#{principal.emailAddress}")
List<User> findCurrentUserWithCustomQuery();
/**
* @see DATAJPA-564
*/
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
}

View File

@@ -21,6 +21,8 @@
</constructor-arg>
</bean>
</property>
<property name="expressionEvaluationContextProvider" ref="expressionEvaluationContextProvider"/>
</bean>
<bean id="roleDao" class="org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean">

View File

@@ -24,6 +24,8 @@
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<bean name="expressionEvaluationContextProvider" class="org.springframework.data.jpa.repository.SampleExpressionEvaluationContextProvider"/>
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script execution="INIT" separator="/;" location="classpath:scripts/schema-stored-procedures.sql"/>
</jdbc:embedded-database>