Skip to content

Commit e22ee22

Browse files
christophstroblodrotbohm
authored andcommitted
DATAKV-115 - Make derived finder execution thread-safe.
We now make sure separate SpelExpression and EvaluationContext for each and every derived finder execution to assert the expression context cannot be accidentally overridden. Original pull request: #16.
1 parent 3c52aff commit e22ee22

File tree

6 files changed

+175
-37
lines changed

6 files changed

+175
-37
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.keyvalue.core;
17+
18+
import org.springframework.expression.EvaluationContext;
19+
import org.springframework.expression.spel.standard.SpelExpression;
20+
import org.springframework.expression.spel.support.StandardEvaluationContext;
21+
22+
/**
23+
* {@link SpelCriteria} allows to pass on a {@link SpelExpression} and {@link EvaluationContext} to the actual query
24+
* processor. This decouples the {@link SpelExpression} from the context it is used in.
25+
*
26+
* @author Christoph Strobl
27+
*/
28+
public class SpelCriteria {
29+
30+
private final SpelExpression expression;
31+
private final EvaluationContext context;
32+
33+
/**
34+
* Creates new {@link SpelCriteria}.
35+
*
36+
* @param expression must not be {@literal null}.
37+
* @param context can be {@literal null} and will be defaulted to {@link StandardEvaluationContext}.
38+
*/
39+
public SpelCriteria(SpelExpression expression, EvaluationContext context) {
40+
41+
this.expression = expression;
42+
this.context = context == null ? new StandardEvaluationContext() : context;
43+
}
44+
45+
/**
46+
* @return never {@literal null}.
47+
*/
48+
public EvaluationContext getContext() {
49+
return context;
50+
}
51+
52+
/**
53+
* @return never {@literal null}.
54+
*/
55+
public SpelExpression getExpression() {
56+
return expression;
57+
}
58+
59+
}

src/main/java/org/springframework/data/keyvalue/core/SpelCriteriaAccessor.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014 the original author or authors.
2+
* Copyright 2014-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
1919
import org.springframework.expression.spel.standard.SpelExpression;
2020
import org.springframework.expression.spel.standard.SpelExpressionParser;
21+
import org.springframework.expression.spel.support.StandardEvaluationContext;
2122
import org.springframework.util.Assert;
2223

2324
/**
@@ -26,7 +27,7 @@
2627
* @author Christoph Strobl
2728
* @author Oliver Gierke
2829
*/
29-
class SpelCriteriaAccessor implements CriteriaAccessor<SpelExpression> {
30+
class SpelCriteriaAccessor implements CriteriaAccessor<SpelCriteria> {
3031

3132
private final SpelExpressionParser parser;
3233

@@ -40,20 +41,25 @@ public SpelCriteriaAccessor(SpelExpressionParser parser) {
4041
}
4142

4243
@Override
43-
public SpelExpression resolve(KeyValueQuery<?> query) {
44+
public SpelCriteria resolve(KeyValueQuery<?> query) {
4445

4546
if (query.getCritieria() == null) {
4647
return null;
4748
}
4849

4950
if (query.getCritieria() instanceof SpelExpression) {
50-
return (SpelExpression) query.getCritieria();
51+
return new SpelCriteria((SpelExpression) query.getCritieria(), new StandardEvaluationContext());
5152
}
5253

5354
if (query.getCritieria() instanceof String) {
54-
return parser.parseRaw((String) query.getCritieria());
55+
return new SpelCriteria(parser.parseRaw((String) query.getCritieria()), new StandardEvaluationContext());
56+
}
57+
58+
if (query.getCritieria() instanceof SpelCriteria) {
59+
return (SpelCriteria) query.getCritieria();
5560
}
5661

5762
throw new IllegalArgumentException("Cannot create SpelCriteria for " + query.getCritieria());
5863
}
64+
5965
}

src/main/java/org/springframework/data/keyvalue/core/SpelQueryEngine.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014 the original author or authors.
2+
* Copyright 2014-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@
3535
* @author Oliver Gierke
3636
* @param <T>
3737
*/
38-
class SpelQueryEngine<T extends KeyValueAdapter> extends QueryEngine<KeyValueAdapter, SpelExpression, Comparator<?>> {
38+
class SpelQueryEngine<T extends KeyValueAdapter> extends QueryEngine<KeyValueAdapter, SpelCriteria, Comparator<?>> {
3939

4040
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
4141

@@ -51,7 +51,7 @@ public SpelQueryEngine() {
5151
* @see org.springframework.data.keyvalue.core.QueryEngine#execute(java.lang.Object, java.lang.Object, int, int, java.io.Serializable)
5252
*/
5353
@Override
54-
public Collection<?> execute(SpelExpression criteria, Comparator<?> sort, int offset, int rows, Serializable keyspace) {
54+
public Collection<?> execute(SpelCriteria criteria, Comparator<?> sort, int offset, int rows, Serializable keyspace) {
5555
return sortAndFilterMatchingRange(getAdapter().getAllOf(keyspace), criteria, sort, offset, rows);
5656
}
5757

@@ -60,12 +60,12 @@ public Collection<?> execute(SpelExpression criteria, Comparator<?> sort, int of
6060
* @see org.springframework.data.keyvalue.core.QueryEngine#count(java.lang.Object, java.io.Serializable)
6161
*/
6262
@Override
63-
public long count(SpelExpression criteria, Serializable keyspace) {
63+
public long count(SpelCriteria criteria, Serializable keyspace) {
6464
return filterMatchingRange(getAdapter().getAllOf(keyspace), criteria, -1, -1).size();
6565
}
6666

6767
@SuppressWarnings({ "unchecked", "rawtypes" })
68-
private List<?> sortAndFilterMatchingRange(Iterable<?> source, SpelExpression criteria, Comparator sort, int offset,
68+
private List<?> sortAndFilterMatchingRange(Iterable<?> source, SpelCriteria criteria, Comparator sort, int offset,
6969
int rows) {
7070

7171
List<?> tmp = IterableConverter.toList(source);
@@ -76,7 +76,7 @@ private List<?> sortAndFilterMatchingRange(Iterable<?> source, SpelExpression cr
7676
return filterMatchingRange(tmp, criteria, offset, rows);
7777
}
7878

79-
private static <S> List<S> filterMatchingRange(Iterable<S> source, SpelExpression criteria, int offset, int rows) {
79+
private static <S> List<S> filterMatchingRange(Iterable<S> source, SpelCriteria criteria, int offset, int rows) {
8080

8181
List<S> result = new ArrayList<S>();
8282

@@ -90,10 +90,11 @@ private static <S> List<S> filterMatchingRange(Iterable<S> source, SpelExpressio
9090

9191
if (!matches) {
9292
try {
93-
matches = criteria.getValue(candidate, Boolean.class);
93+
matches = criteria.getExpression().getValue(criteria.getContext(), candidate, Boolean.class);
9494
} catch (SpelEvaluationException e) {
95-
criteria.getEvaluationContext().setVariable("it", candidate);
96-
matches = criteria.getValue() == null ? false : criteria.getValue(Boolean.class);
95+
criteria.getContext().setVariable("it", candidate);
96+
matches = criteria.getExpression().getValue(criteria.getContext()) == null ? false : criteria.getExpression()
97+
.getValue(criteria.getContext(), Boolean.class);
9798
}
9899
}
99100

src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2015 the original author or authors.
2+
* Copyright 2014-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import org.springframework.data.domain.Sort;
2424
import org.springframework.data.keyvalue.core.IterableConverter;
2525
import org.springframework.data.keyvalue.core.KeyValueOperations;
26+
import org.springframework.data.keyvalue.core.SpelCriteria;
2627
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
2728
import org.springframework.data.repository.query.EvaluationContextProvider;
2829
import org.springframework.data.repository.query.ParameterAccessor;
@@ -93,7 +94,7 @@ public QueryMethod getQueryMethod() {
9394
public Object execute(Object[] parameters) {
9495

9596
ParameterAccessor accessor = new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters);
96-
KeyValueQuery<?> query = prepareQuery(parameters, accessor);
97+
KeyValueQuery<?> query = prepareQuery(parameters);
9798
ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(accessor);
9899

99100
return processor.processResult(doExecute(parameters, query));
@@ -114,8 +115,8 @@ protected Object doExecute(Object[] parameters, KeyValueQuery<?> query) {
114115

115116
Iterable<?> result = this.keyValueOperations.find(query, queryMethod.getEntityInformation().getJavaType());
116117

117-
long count = queryMethod.isSliceQuery() ? 0
118-
: keyValueOperations.count(query, queryMethod.getEntityInformation().getJavaType());
118+
long count = queryMethod.isSliceQuery() ? 0 : keyValueOperations.count(query, queryMethod.getEntityInformation()
119+
.getJavaType());
119120

120121
return new PageImpl(IterableConverter.toList(result), page, count);
121122

@@ -133,14 +134,24 @@ protected Object doExecute(Object[] parameters, KeyValueQuery<?> query) {
133134
}
134135

135136
@SuppressWarnings({ "rawtypes", "unchecked" })
136-
private KeyValueQuery<?> prepareQuery(Object[] parameters, ParameterAccessor accessor) {
137+
protected KeyValueQuery<?> prepareQuery(Object[] parameters) {
138+
139+
ParametersParameterAccessor accessor = new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters);
137140

138141
if (this.query == null) {
139142
this.query = createQuery(accessor);
140143
}
141144

142145
KeyValueQuery<?> q = new KeyValueQuery(this.query.getCritieria());
143146

147+
if (this.query.getCritieria() instanceof SpelExpression) {
148+
149+
EvaluationContext context = this.evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(),
150+
parameters);
151+
SpelCriteria spelCriteria = new SpelCriteria((SpelExpression) this.query.getCritieria(), context);
152+
q = new KeyValueQuery(spelCriteria);
153+
}
154+
144155
if (accessor.getPageable() != null) {
145156
q.setOffset(accessor.getPageable().getOffset());
146157
q.setRows(accessor.getPageable().getPageSize());
@@ -153,12 +164,6 @@ private KeyValueQuery<?> prepareQuery(Object[] parameters, ParameterAccessor acc
153164

154165
q.setSort(sort != null ? sort : query.getSort());
155166

156-
if (q.getCritieria() instanceof SpelExpression) {
157-
EvaluationContext context = this.evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(),
158-
parameters);
159-
((SpelExpression) q.getCritieria()).setEvaluationContext(context);
160-
}
161-
162167
return q;
163168
}
164169

src/test/java/org/springframework/data/keyvalue/core/SpelQueryEngineUnitTests.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package org.springframework.data.keyvalue.core;
1717

1818
import static org.hamcrest.Matchers.*;
19-
import static org.hamcrest.Matchers.contains;
2019
import static org.junit.Assert.*;
2120
import static org.mockito.Matchers.*;
2221
import static org.mockito.Mockito.*;
@@ -33,14 +32,12 @@
3332
import org.mockito.Mock;
3433
import org.mockito.runners.MockitoJUnitRunner;
3534
import org.springframework.data.annotation.Id;
36-
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
3735
import org.springframework.data.keyvalue.repository.query.SpelQueryCreator;
3836
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3937
import org.springframework.data.repository.core.RepositoryMetadata;
4038
import org.springframework.data.repository.query.ParametersParameterAccessor;
4139
import org.springframework.data.repository.query.QueryMethod;
4240
import org.springframework.data.repository.query.parser.PartTree;
43-
import org.springframework.expression.spel.standard.SpelExpression;
4441
import org.springframework.expression.spel.support.StandardEvaluationContext;
4542

4643
/**
@@ -77,8 +74,8 @@ public void queriesEntitiesWithNullProperty() throws Exception {
7774

7875
doReturn(people).when(adapter).getAllOf(anyString());
7976

80-
assertThat((Collection<Person>) engine.execute(createQueryForMethodWithArgs("findByFirstname", "bob"), null, -1, -1,
81-
anyString()), contains(BOB_WITH_FIRSTNAME));
77+
assertThat((Collection<Person>) engine.execute(createQueryForMethodWithArgs("findByFirstname", "bob"), null, -1,
78+
-1, anyString()), contains(BOB_WITH_FIRSTNAME));
8279
}
8380

8481
/**
@@ -92,7 +89,7 @@ public void countsEntitiesWithNullProperty() throws Exception {
9289
assertThat(engine.count(createQueryForMethodWithArgs("findByFirstname", "bob"), anyString()), is(1L));
9390
}
9491

95-
private static SpelExpression createQueryForMethodWithArgs(String methodName, Object... args) throws Exception {
92+
private static SpelCriteria createQueryForMethodWithArgs(String methodName, Object... args) throws Exception {
9693

9794
List<Class<?>> types = new ArrayList<Class<?>>(args.length);
9895

@@ -105,13 +102,10 @@ private static SpelExpression createQueryForMethodWithArgs(String methodName, Ob
105102
doReturn(method.getReturnType()).when(metadata).getReturnedDomainClass(method);
106103

107104
PartTree partTree = new PartTree(method.getName(), method.getReturnType());
108-
SpelQueryCreator creator = new SpelQueryCreator(partTree, new ParametersParameterAccessor(
109-
new QueryMethod(method, metadata, new SpelAwareProxyProjectionFactory()).getParameters(), args));
105+
SpelQueryCreator creator = new SpelQueryCreator(partTree, new ParametersParameterAccessor(new QueryMethod(method,
106+
metadata, new SpelAwareProxyProjectionFactory()).getParameters(), args));
110107

111-
KeyValueQuery<SpelExpression> query = creator.createQuery();
112-
query.getCritieria().setEvaluationContext(new StandardEvaluationContext(args));
113-
114-
return query.getCritieria();
108+
return new SpelCriteria(creator.createQuery().getCritieria(), new StandardEvaluationContext(args));
115109
}
116110

117111
static interface PersonRepository {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2015-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.keyvalue.repository.query;
17+
18+
import static org.hamcrest.core.IsNot.*;
19+
import static org.hamcrest.core.IsSame.*;
20+
import static org.junit.Assert.*;
21+
import static org.mockito.Matchers.*;
22+
import static org.mockito.Mockito.*;
23+
24+
import java.lang.reflect.Method;
25+
import java.util.List;
26+
27+
import org.junit.Test;
28+
import org.junit.runner.RunWith;
29+
import org.mockito.Mock;
30+
import org.mockito.runners.MockitoJUnitRunner;
31+
import org.springframework.data.keyvalue.Person;
32+
import org.springframework.data.keyvalue.core.KeyValueOperations;
33+
import org.springframework.data.projection.ProjectionFactory;
34+
import org.springframework.data.repository.core.RepositoryMetadata;
35+
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
36+
import org.springframework.data.repository.query.QueryMethod;
37+
38+
/**
39+
* @author Christoph Strobl
40+
*/
41+
@RunWith(MockitoJUnitRunner.class)
42+
public class KeyValuePartTreeQueryUnitTests {
43+
44+
@Mock KeyValueOperations kvOpsMock;
45+
@Mock RepositoryMetadata metadataMock;
46+
@Mock ProjectionFactory projectionFactoryMock;
47+
48+
/**
49+
* @see DATAKV-115
50+
*/
51+
@Test
52+
@SuppressWarnings({ "unchecked", "rawtypes" })
53+
public void spelExpressionAndContextShouldNotBeReused() throws NoSuchMethodException, SecurityException {
54+
55+
when(metadataMock.getDomainType()).thenReturn((Class) Person.class);
56+
when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Person.class);
57+
58+
QueryMethod qm = new QueryMethod(Repo.class.getMethod("findByFirstname", String.class), metadataMock,
59+
projectionFactoryMock);
60+
61+
KeyValuePartTreeQuery query = new KeyValuePartTreeQuery(qm, DefaultEvaluationContextProvider.INSTANCE, kvOpsMock,
62+
SpelQueryCreator.class);
63+
64+
Object[] args = new Object[] { "foo" };
65+
66+
assertThat(query.prepareQuery(args).getCritieria(), not(sameInstance(query.prepareQuery(args).getCritieria())));
67+
}
68+
69+
static interface Repo {
70+
71+
List<Person> findByFirstname(String firstname);
72+
}
73+
}

0 commit comments

Comments
 (0)