Skip to content

Commit 6d5e7e4

Browse files
committed
Add support for SpEL in @query
1 parent 8f745b1 commit 6d5e7e4

File tree

9 files changed

+395
-10
lines changed

9 files changed

+395
-10
lines changed

src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ Repository methods can be defined to have the following return types for returni
316316

317317
.Declare query on the method using the `@Query` annotation.
318318
====
319-
The arguments passed to the method can be inserted into placeholders in the query string. the placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
319+
The arguments passed to the method can be inserted into placeholders in the query string. The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
320320
[source,java]
321321
----
322322
interface BookRepository extends ElasticsearchRepository<Book, String> {

src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,22 @@
1717

1818
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
1919
import org.springframework.data.elasticsearch.core.query.BaseQuery;
20-
import org.springframework.data.elasticsearch.core.query.Query;
2120
import org.springframework.data.elasticsearch.core.query.StringQuery;
2221
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
22+
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
23+
import org.springframework.expression.EvaluationContext;
24+
import org.springframework.expression.Expression;
25+
import org.springframework.expression.ParserContext;
26+
import org.springframework.expression.TypeConverter;
27+
import org.springframework.expression.common.LiteralExpression;
28+
import org.springframework.expression.spel.standard.SpelExpressionParser;
29+
import org.springframework.expression.spel.support.StandardEvaluationContext;
30+
import org.springframework.lang.Nullable;
2331
import org.springframework.util.Assert;
2432

33+
import java.util.Map;
34+
import java.util.concurrent.ConcurrentHashMap;
35+
2536
/**
2637
* ElasticsearchStringQuery
2738
*
@@ -30,16 +41,27 @@
3041
* @author Mark Paluch
3142
* @author Taylor Ono
3243
* @author Peter-Josef Meisch
44+
* @author Haibo Liu
3345
*/
3446
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
3547

3648
private final String queryString;
49+
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
50+
private Map<String, Expression> queryExpressions = new ConcurrentHashMap<>();
51+
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
52+
private final TypeConverter typeConverter;
3753

3854
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
39-
String queryString) {
55+
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
56+
TypeConverter typeConverter) {
4057
super(queryMethod, elasticsearchOperations);
4158
Assert.notNull(queryString, "Query cannot be empty");
59+
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
60+
Assert.notNull(typeConverter, "TypeConverter must not be null");
61+
4262
this.queryString = queryString;
63+
this.evaluationContextProvider = evaluationContextProvider;
64+
this.typeConverter = typeConverter;
4365
}
4466

4567
@Override
@@ -58,12 +80,36 @@ protected boolean isExistsQuery() {
5880
}
5981

6082
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
61-
6283
String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
6384
.replacePlaceholders(this.queryString, parameterAccessor);
6485

65-
var query = new StringQuery(queryString);
86+
var query = new StringQuery(parseSpEL(queryString, parameterAccessor));
6687
query.addSort(parameterAccessor.getSort());
6788
return query;
6889
}
90+
91+
private String parseSpEL(String queryString, ElasticsearchParametersParameterAccessor parameterAccessor) {
92+
Expression expr = getQueryExpression(queryString);
93+
if (expr != null) {
94+
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getParameters(),
95+
parameterAccessor.getValues());
96+
if (context instanceof StandardEvaluationContext standardEvaluationContext) {
97+
standardEvaluationContext.setTypeConverter(typeConverter);
98+
}
99+
100+
String parsed = expr.getValue(context, String.class);
101+
Assert.notNull(parsed, "Query parsed by SpEL should not be null");
102+
return parsed;
103+
}
104+
return queryString;
105+
}
106+
107+
@Nullable
108+
private Expression getQueryExpression(String queryString) {
109+
return queryExpressions.computeIfAbsent(queryString, f -> {
110+
Expression expr = PARSER.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION);
111+
return expr instanceof LiteralExpression ? null : expr;
112+
});
113+
114+
}
69115
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2024 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+
* https://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.elasticsearch.repository.support;
17+
18+
import org.springframework.core.convert.ConversionService;
19+
import org.springframework.core.convert.TypeDescriptor;
20+
import org.springframework.core.convert.converter.GenericConverter;
21+
import org.springframework.lang.Nullable;
22+
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.Set;
26+
import java.util.StringJoiner;
27+
import java.util.regex.Matcher;
28+
29+
/**
30+
* Convert a collection into string for value part of the elasticsearch query.
31+
* The string should be wrapped with square brackets, with each element quoted
32+
* therefore escaped if quotes exist in the original element.
33+
* <p>
34+
* eg: {@code ["hello \"Stranger\"","Another string"]} for terms query
35+
* {@code { 'bool' : { 'must' : { 'terms' : { 'name' : ["hello \"Stranger\"","Another string"] } } } }}
36+
*
37+
* @author Haibo Liu
38+
*/
39+
public class ElasticsearchCollectionToStringConverter implements GenericConverter {
40+
41+
private static final String DELIMITER = ",";
42+
43+
private final ConversionService conversionService;
44+
45+
46+
public ElasticsearchCollectionToStringConverter(ConversionService conversionService) {
47+
this.conversionService = conversionService;
48+
}
49+
50+
@Override
51+
public Set<ConvertiblePair> getConvertibleTypes() {
52+
return Collections.singleton(new ConvertiblePair(Collection.class, String.class));
53+
}
54+
55+
@Override
56+
@Nullable
57+
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
58+
if (source == null) {
59+
return null;
60+
}
61+
Collection<?> sourceCollection = (Collection<?>) source;
62+
if (sourceCollection.isEmpty()) {
63+
return "[]";
64+
}
65+
StringJoiner sb = new StringJoiner(DELIMITER, "[", "]");
66+
for (Object sourceElement : sourceCollection) {
67+
Object targetElement = this.conversionService.convert(
68+
sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType);
69+
sb.add("\"" + escape(targetElement) + "\"");
70+
}
71+
return sb.toString();
72+
}
73+
74+
private String escape(@Nullable Object target) {
75+
// escape the quotes in the string, because the string should already be quoted manually
76+
return String.valueOf(target).replaceAll("\"", Matcher.quoteReplacement("\\\""));
77+
}
78+
}

src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.springframework.data.elasticsearch.repository.support;
1717

18+
import org.springframework.core.convert.ConversionService;
19+
import org.springframework.core.convert.converter.ConverterRegistry;
20+
import org.springframework.core.convert.support.DefaultConversionService;
1821
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
1922
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
2023
import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery;
@@ -34,6 +37,8 @@
3437
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
3538
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3639
import org.springframework.data.repository.query.RepositoryQuery;
40+
import org.springframework.expression.TypeConverter;
41+
import org.springframework.expression.spel.support.StandardTypeConverter;
3742
import org.springframework.lang.Nullable;
3843
import org.springframework.util.Assert;
3944

@@ -54,6 +59,7 @@
5459
* @author Sascha Woo
5560
* @author Peter-Josef Meisch
5661
* @author Ezequiel Antúnez Camacho
62+
* @author Haibo Liu
5763
*/
5864
public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
5965

@@ -96,11 +102,24 @@ private static boolean isQueryDslRepository(Class<?> repositoryInterface) {
96102
@Override
97103
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
98104
QueryMethodEvaluationContextProvider evaluationContextProvider) {
99-
return Optional.of(new ElasticsearchQueryLookupStrategy());
105+
return Optional.of(new ElasticsearchQueryLookupStrategy(evaluationContextProvider));
100106
}
101107

102108
private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
103109

110+
QueryMethodEvaluationContextProvider evaluationContextProvider;
111+
private TypeConverter typeConverter;
112+
113+
ElasticsearchQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) {
114+
this.evaluationContextProvider = evaluationContextProvider;
115+
116+
// register elasticsearch custom type converter for conversion service
117+
ConversionService conversionService = new DefaultConversionService();
118+
ConverterRegistry converterRegistry = (ConverterRegistry) conversionService;
119+
converterRegistry.addConverter(new ElasticsearchCollectionToStringConverter(conversionService));
120+
typeConverter = new StandardTypeConverter(conversionService);
121+
}
122+
104123
/*
105124
* (non-Javadoc)
106125
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
@@ -115,9 +134,11 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
115134

116135
if (namedQueries.hasQuery(namedQueryName)) {
117136
String namedQuery = namedQueries.getQuery(namedQueryName);
118-
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery);
137+
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery,
138+
evaluationContextProvider, typeConverter);
119139
} else if (queryMethod.hasAnnotatedQuery()) {
120-
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery());
140+
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
141+
evaluationContextProvider, typeConverter);
121142
}
122143
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations);
123144
}

src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
/**
2727
* @author Peter-Josef Meisch
28+
* @author Haibo Liu
2829
* @since 4.4
2930
*/
3031
@ContextConfiguration(classes = { CustomMethodRepositoryELCIntegrationTests.Config.class })
@@ -40,5 +41,13 @@ static class Config {
4041
IndexNameProvider indexNameProvider() {
4142
return new IndexNameProvider("custom-method-repository");
4243
}
44+
45+
/**
46+
* a normal bean referenced by SpEL in query
47+
*/
48+
@Bean
49+
QueryParam queryParam() {
50+
return new QueryParam("abc");
51+
}
4352
}
4453
}

0 commit comments

Comments
 (0)