mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-24 13:02:10 +00:00
Add support for SpEL in @Query
.
Original Pull Request #2826 Closes #2083
This commit is contained in:
parent
c6041fb659
commit
e1a2412651
@ -316,7 +316,7 @@ Repository methods can be defined to have the following return types for returni
|
||||
|
||||
.Declare query on the method using the `@Query` annotation.
|
||||
====
|
||||
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.
|
||||
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.
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@ -361,3 +361,202 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[elasticsearch.query-methods.at-query.spel]]
|
||||
=== Using SpEL Expressions
|
||||
|
||||
.Declare query on the method using the `@Query` annotation with SpEL expression.
|
||||
====
|
||||
https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
|
||||
|
||||
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"name": "#{#name}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Page<Book> findByName(String name, Pageable pageable);
|
||||
}
|
||||
----
|
||||
|
||||
If for example the function is called with the parameter _John_, it would produce the following query body:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"name": "John"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
.accessing parameter property.
|
||||
====
|
||||
Supposing that we have the following class as query parameter type:
|
||||
[source,java]
|
||||
----
|
||||
public record QueryParameter(String value) {
|
||||
}
|
||||
----
|
||||
|
||||
It's easy to access the parameter by `#` symbol, then reference the property `value` with a simple `.`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"name": "#{#parameter.value}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Page<Book> findByName(QueryParameter parameter, Pageable pageable);
|
||||
}
|
||||
----
|
||||
|
||||
We can pass `new QueryParameter("John")` as the parameter now, and it will produce the same query string as above.
|
||||
====
|
||||
|
||||
.accessing bean property.
|
||||
====
|
||||
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"name": "#{@queryParameter.value}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Page<Book> findByName(Pageable pageable);
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
.SpEL and `Collection` param.
|
||||
====
|
||||
`Collection` parameter is also supported and is as easy to use as normal `String`, such as the following `terms` query:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"name": #{#names}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Page<Book> findByName(Collection<String> names, Pageable pageable);
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: collection values should not be quoted when declaring the elasticsearch json query.
|
||||
|
||||
A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"name": ["name1", "name2"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
.access property in the `Collection` param.
|
||||
====
|
||||
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/collection-projection.html[SpEL Collection Projection] is convenient to use when values in the `Collection` parameter is not plain `String`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"name": #{#parameters.![value]}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
|
||||
}
|
||||
----
|
||||
This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
|
||||
====
|
||||
|
||||
.alter parameter name by using `@Param`
|
||||
====
|
||||
When accessing the parameter by SpEL, it's also useful to alter the parameter name to another one by `@Param` annotation in Sping Data:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
interface BookRepository extends ElasticsearchRepository<Book, String> {
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"name": #{#another.![value]}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Page<Book> findByName(@Param("another") Collection<QueryParameter> parameters, Pageable pageable);
|
||||
}
|
||||
----
|
||||
|
||||
====
|
||||
|
@ -43,6 +43,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @since 3.2
|
||||
*/
|
||||
abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery {
|
||||
@ -112,7 +113,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
|
||||
* @param accessor must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor);
|
||||
protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor);
|
||||
|
||||
private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
|
||||
Converter<Object, Object> resultProcessing) {
|
||||
|
@ -21,7 +21,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
* @author Christoph Strobl
|
||||
* @since 3.2
|
||||
*/
|
||||
class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
|
||||
public class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
|
||||
implements ElasticsearchParameterAccessor {
|
||||
|
||||
private final Object[] values;
|
||||
|
@ -69,9 +69,9 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
public class ElasticsearchQueryMethod extends QueryMethod {
|
||||
|
||||
// the following 2 variables exits in the base class, but are private. We need them for
|
||||
// the following 2 variables exist in the base class, but are private. We need them for
|
||||
// correct handling of return types (SearchHits), so we have our own values here.
|
||||
// Alas this means that we have to copy code that initializes these variables and in the
|
||||
// This means that we have to copy code that initializes these variables and in the
|
||||
// base class uses them in order to use our variables
|
||||
protected final Method method;
|
||||
protected final Class<?> unwrappedReturnType;
|
||||
|
@ -17,9 +17,10 @@ package org.springframework.data.elasticsearch.repository.query;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.query.BaseQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.Query;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
|
||||
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@ -30,16 +31,21 @@ import org.springframework.util.Assert;
|
||||
* @author Mark Paluch
|
||||
* @author Taylor Ono
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
|
||||
|
||||
private final String queryString;
|
||||
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
|
||||
String queryString) {
|
||||
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
super(queryMethod, elasticsearchOperations);
|
||||
Assert.notNull(queryString, "Query cannot be empty");
|
||||
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
|
||||
|
||||
this.queryString = queryString;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -58,12 +64,14 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue
|
||||
}
|
||||
|
||||
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
|
||||
|
||||
String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
|
||||
.replacePlaceholders(this.queryString, parameterAccessor);
|
||||
|
||||
var query = new StringQuery(queryString);
|
||||
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
|
||||
evaluationContextProvider);
|
||||
var query = new StringQuery(evaluator.evaluate());
|
||||
query.addSort(parameterAccessor.getSort());
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,19 +16,23 @@
|
||||
package org.springframework.data.elasticsearch.repository.query;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.query.BaseQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
|
||||
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Taylor Ono
|
||||
* @author Haibo Liu
|
||||
* @since 3.2
|
||||
*/
|
||||
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
|
||||
|
||||
private final String query;
|
||||
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
|
||||
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
|
||||
@ -43,14 +47,17 @@ public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsea
|
||||
|
||||
super(queryMethod, operations);
|
||||
this.query = query;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) {
|
||||
String queryString = new StringQueryUtil(
|
||||
getElasticsearchOperations().getElasticsearchConverter().getConversionService()).replacePlaceholders(this.query,
|
||||
parameterAccessor);
|
||||
return new StringQuery(queryString);
|
||||
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
|
||||
String queryString = new StringQueryUtil(getElasticsearchOperations().getElasticsearchConverter().getConversionService())
|
||||
.replacePlaceholders(this.query, parameterAccessor);
|
||||
|
||||
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
|
||||
evaluationContextProvider);
|
||||
return new StringQuery(evaluator.evaluate());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -40,7 +40,7 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) {
|
||||
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
|
||||
CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery();
|
||||
|
||||
if (tree.isLimiting()) {
|
||||
|
@ -34,6 +34,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.expression.TypeConverter;
|
||||
import org.springframework.expression.spel.support.StandardTypeConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@ -54,6 +56,7 @@ import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;
|
||||
* @author Sascha Woo
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Ezequiel Antúnez Camacho
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
@ -96,11 +99,17 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
|
||||
@Override
|
||||
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
return Optional.of(new ElasticsearchQueryLookupStrategy());
|
||||
return Optional.of(new ElasticsearchQueryLookupStrategy(evaluationContextProvider));
|
||||
}
|
||||
|
||||
private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
ElasticsearchQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @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 +124,11 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
if (namedQueries.hasQuery(namedQueryName)) {
|
||||
String namedQuery = namedQueries.getQuery(namedQueryName);
|
||||
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery);
|
||||
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery,
|
||||
evaluationContextProvider);
|
||||
} else if (queryMethod.hasAnnotatedQuery()) {
|
||||
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery());
|
||||
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
|
||||
evaluationContextProvider);
|
||||
}
|
||||
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations);
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2024 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
|
||||
*
|
||||
* https://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.elasticsearch.repository.support.spel;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Convert a collection into string for value part of the elasticsearch query.
|
||||
* <p>
|
||||
* If the value is type {@link String}, it should be wrapped with square brackets, with each element quoted therefore
|
||||
* escaped(by {@link ElasticsearchStringValueToStringConverter}) if quotations exist in the original element.
|
||||
* <p>
|
||||
* eg: The value part of an elasticsearch terms query should looks like {@code ["hello \"Stranger\"","Another string"]}
|
||||
* for query
|
||||
* <pre>
|
||||
* {@code
|
||||
* {
|
||||
* "bool":{
|
||||
* "must":{
|
||||
* "terms":{
|
||||
* "name": ["hello \"Stranger\"", "Another string"]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @since 5.3
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
public class ElasticsearchCollectionValueToStringConverter implements GenericConverter {
|
||||
|
||||
private static final String DELIMITER = ",";
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public ElasticsearchCollectionValueToStringConverter(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(Collection.class, String.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return "[]";
|
||||
}
|
||||
Collection<?> sourceCollection = (Collection<?>) source;
|
||||
if (sourceCollection.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
StringJoiner sb = new StringJoiner(DELIMITER, "[", "]");
|
||||
for (Object sourceElement : sourceCollection) {
|
||||
// ignore the null value in collection
|
||||
if (Objects.isNull(sourceElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object targetElement = this.conversionService.convert(
|
||||
sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType);
|
||||
if (sourceElement instanceof String) {
|
||||
sb.add("\"" + targetElement + "\"");
|
||||
} else {
|
||||
sb.add(String.valueOf(targetElement));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2024 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
|
||||
*
|
||||
* https://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.elasticsearch.repository.support.spel;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/**
|
||||
* Values in elasticsearch query may contain quotations and should be escaped when converting.
|
||||
* Note that the converter should only be used in this situation, rather than common string to string conversions.
|
||||
*
|
||||
* @since 5.3
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
public class ElasticsearchStringValueToStringConverter implements GenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(String.class, String.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return escape(source);
|
||||
}
|
||||
|
||||
private String escape(@Nullable Object source) {
|
||||
// escape the quotes in the string, because the string should already be quoted manually
|
||||
return String.valueOf(source).replaceAll("\"", Matcher.quoteReplacement("\\\""));
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2024 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
|
||||
*
|
||||
* https://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.elasticsearch.repository.support.spel;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.util.Lazy;
|
||||
|
||||
/**
|
||||
* To supply a {@link ConversionService} with custom converters to handle SpEL values in elasticsearch query.
|
||||
*
|
||||
* @since 5.3
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
public class ElasticsearchValueSpELConversionService {
|
||||
|
||||
public static final Lazy<ConversionService> CONVERSION_SERVICE_LAZY = Lazy.of(
|
||||
ElasticsearchValueSpELConversionService::buildSpELConversionService);
|
||||
|
||||
private static ConversionService buildSpELConversionService() {
|
||||
// register elasticsearch custom type converter for conversion service
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
ConverterRegistry converterRegistry = (ConverterRegistry) conversionService;
|
||||
converterRegistry.addConverter(new ElasticsearchCollectionValueToStringConverter(conversionService));
|
||||
converterRegistry.addConverter(new ElasticsearchStringValueToStringConverter());
|
||||
return conversionService;
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2024 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
|
||||
*
|
||||
* https://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.elasticsearch.repository.support.spel;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.convert.ConversionException;
|
||||
import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.TypeConverter;
|
||||
import org.springframework.expression.common.CompositeStringExpression;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.support.StandardTypeConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* To evaluate the SpEL expressions of the query string.
|
||||
*
|
||||
* @author Haibo Liu
|
||||
* @since 5.3
|
||||
*/
|
||||
public class QueryStringSpELEvaluator {
|
||||
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
private static final Map<String, Expression> QUERY_EXPRESSIONS = new ConcurrentHashMap<>();
|
||||
|
||||
private final String queryString;
|
||||
private final ElasticsearchParametersParameterAccessor parameterAccessor;
|
||||
private final QueryMethod queryMethod;
|
||||
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
private final TypeConverter elasticsearchSpELTypeConverter;
|
||||
|
||||
public QueryStringSpELEvaluator(String queryString, ElasticsearchParametersParameterAccessor parameterAccessor,
|
||||
QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
this.queryString = queryString;
|
||||
this.parameterAccessor = parameterAccessor;
|
||||
this.queryMethod = queryMethod;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.elasticsearchSpELTypeConverter = new StandardTypeConverter(ElasticsearchValueSpELConversionService.CONVERSION_SERVICE_LAZY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the SpEL parts of the query string.
|
||||
*
|
||||
* @return a plain string with values evaluated
|
||||
*/
|
||||
public String evaluate() {
|
||||
Expression expr = getQueryExpression(queryString);
|
||||
if (expr != null) {
|
||||
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getParameters(),
|
||||
parameterAccessor.getValues());
|
||||
if (context instanceof StandardEvaluationContext standardEvaluationContext) {
|
||||
standardEvaluationContext.setTypeConverter(elasticsearchSpELTypeConverter);
|
||||
}
|
||||
|
||||
String parsed = parseExpressions(expr, context);
|
||||
Assert.notNull(parsed, "Query parsed by SpEL should not be null");
|
||||
return parsed;
|
||||
}
|
||||
return queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Expression#getValue(EvaluationContext, Class)} is not used because the value part in SpEL should be converted
|
||||
* by {@link ElasticsearchStringValueToStringConverter} or
|
||||
* {@link ElasticsearchCollectionValueToStringConverter} to
|
||||
* escape the quotations, but other literal parts in SpEL expression should not be processed with these converters.
|
||||
* So we just get the string value from {@link LiteralExpression} directly rather than
|
||||
* {@link LiteralExpression#getValue(EvaluationContext, Class)}.
|
||||
*/
|
||||
private String parseExpressions(Expression rootExpr, EvaluationContext context) {
|
||||
StringBuilder parsed = new StringBuilder();
|
||||
if (rootExpr instanceof LiteralExpression literalExpression) {
|
||||
// get the string literal directly
|
||||
parsed.append(literalExpression.getExpressionString());
|
||||
} else if (rootExpr instanceof SpelExpression spelExpression) {
|
||||
// evaluate the value
|
||||
String value = spelExpression.getValue(context, String.class);
|
||||
if (value == null) {
|
||||
throw new ConversionException(String.format(
|
||||
"Parameter value can't be null for SpEL expression '%s' in method '%s' when querying elasticsearch",
|
||||
spelExpression.getExpressionString(), queryMethod.getName()));
|
||||
}
|
||||
parsed.append(value);
|
||||
} else if (rootExpr instanceof CompositeStringExpression compositeStringExpression) {
|
||||
// parse one by one for composite expression
|
||||
Expression[] expressions = compositeStringExpression.getExpressions();
|
||||
|
||||
for (Expression exp : expressions) {
|
||||
parsed.append(parseExpressions(exp, context));
|
||||
}
|
||||
} else {
|
||||
// no more
|
||||
parsed.append(rootExpr.getValue(context, String.class));
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Expression getQueryExpression(String queryString) {
|
||||
return QUERY_EXPRESSIONS.computeIfAbsent(queryString, f -> {
|
||||
Expression expr = PARSER.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION);
|
||||
return expr instanceof LiteralExpression ? null : expr;
|
||||
});
|
||||
}
|
||||
}
|
@ -323,9 +323,9 @@ public abstract class SearchTemplateIntegrationTests {
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}-student")
|
||||
record Student( //
|
||||
@Nullable @Id String id, //
|
||||
@Field(type = FieldType.Text) String firstName, //
|
||||
@Field(type = FieldType.Text) String lastName //
|
||||
@Nullable @Id String id, //
|
||||
@Field(type = FieldType.Text) String firstName, //
|
||||
@Field(type = FieldType.Text) String lastName //
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @since 4.4
|
||||
*/
|
||||
@ContextConfiguration(classes = { CustomMethodRepositoryELCIntegrationTests.Config.class })
|
||||
@ -40,5 +41,13 @@ public class CustomMethodRepositoryELCIntegrationTests extends CustomMethodRepos
|
||||
IndexNameProvider indexNameProvider() {
|
||||
return new IndexNameProvider("custom-method-repository");
|
||||
}
|
||||
|
||||
/**
|
||||
* a normal bean referenced by SpEL in query
|
||||
*/
|
||||
@Bean
|
||||
QueryParameter queryParameter() {
|
||||
return new QueryParameter("abc");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package org.springframework.data.elasticsearch.repositories.custommethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
|
||||
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
|
||||
|
||||
@ -52,6 +53,7 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.SearchHit;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.data.elasticsearch.core.SearchPage;
|
||||
import org.springframework.data.elasticsearch.core.convert.ConversionException;
|
||||
import org.springframework.data.elasticsearch.core.geo.GeoBox;
|
||||
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
@ -63,6 +65,7 @@ import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@ -1522,6 +1525,135 @@ public abstract class CustomMethodRepositoryIntegrationTests {
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForStringQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByStringSpEL("abc");
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRaiseExceptionForNullStringQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByStringSpEL(null));
|
||||
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo("Parameter value can't be null for SpEL expression '#type' in method 'queryByStringSpEL'" +
|
||||
" when querying elasticsearch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForParameterPropertyQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
QueryParameter param = new QueryParameter("abc");
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByParameterPropertySpEL(param);
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForBeanQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByBeanPropertySpEL();
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForCollectionQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(List.of("abc"));
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRaiseExceptionForNullCollectionQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByCollectionSpEL(null));
|
||||
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo("Parameter value can't be null for SpEL expression '#types' in method 'queryByCollectionSpEL'" +
|
||||
" when querying elasticsearch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnSearchHitsForEmptyCollectionQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(List.of());
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
List<String> params = new ArrayList<>();
|
||||
params.add(null);
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(params);
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreNullValuesInCollectionQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByCollectionSpEL(Arrays.asList("abc", null));
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
QueryParameter param = new QueryParameter("abc");
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByParameterPropertyCollectionSpEL(List.of(param));
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 20);
|
||||
repository.saveAll(entities);
|
||||
|
||||
QueryParameter param = new QueryParameter("abc");
|
||||
// when
|
||||
SearchHits<SampleEntity> searchHits = repository.queryByParameterPropertyCollectionSpELWithParamAnnotation(
|
||||
List.of(param));
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test // DATAES-372
|
||||
void shouldReturnHighlightsOnAnnotatedMethod() {
|
||||
List<SampleEntity> entities = createSampleEntities("abc", 2);
|
||||
@ -1940,6 +2072,105 @@ public abstract class CustomMethodRepositoryIntegrationTests {
|
||||
@Highlight(fields = { @HighlightField(name = "type") })
|
||||
SearchHits<SampleEntity> queryByString(String type);
|
||||
|
||||
/**
|
||||
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
|
||||
* will not accept a null parameter as query value.
|
||||
*/
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"type": "#{#type}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
SearchHits<SampleEntity> queryByStringSpEL(@Nullable String type);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"type": "#{#parameter.value}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
SearchHits<SampleEntity> queryByParameterPropertySpEL(QueryParameter parameter);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"type": "#{@queryParameter.value}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
SearchHits<SampleEntity> queryByBeanPropertySpEL();
|
||||
|
||||
/**
|
||||
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
|
||||
* will not accept a null parameter as query value.
|
||||
*/
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"type": #{#types}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
SearchHits<SampleEntity> queryByCollectionSpEL(@Nullable Collection<String> types);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"type": #{#parameters.![value]}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
SearchHits<SampleEntity> queryByParameterPropertyCollectionSpEL(Collection<QueryParameter> parameters);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"type": #{#e.![value]}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
SearchHits<SampleEntity> queryByParameterPropertyCollectionSpELWithParamAnnotation(
|
||||
@Param("e") Collection<QueryParameter> parameters);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2024 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
|
||||
*
|
||||
* https://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.elasticsearch.repositories.custommethod;
|
||||
|
||||
/**
|
||||
* Used as a parameter referenced by SpEL in query method tests.
|
||||
*
|
||||
* @param value content
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
public record QueryParameter(String value) {
|
||||
}
|
@ -32,6 +32,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
@ -42,15 +44,18 @@ import org.springframework.data.elasticsearch.annotations.Query;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.SearchHits;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Niklas Herder
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
|
||||
@ -83,6 +88,205 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
|
||||
.isEqualTo("name:(zero, eleven, one, two, three, four, five, six, seven, eight, nine, ten, eleven, zero, one)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceParametersSpEL() throws Exception {
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", "Luke");
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "Luke"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceParametersSpELWithQuotes() throws Exception {
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL",
|
||||
"hello \"world\"");
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "hello \\"world\\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUseParameterPropertySpEL() throws Exception {
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByParameterPropertySpEL",
|
||||
new QueryParameter("Luke"));
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "Luke"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of("hello \"Stranger\"", "Another string");
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": ["hello \\"Stranger\\"", "Another string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceNonStringCollectionSpEL() throws Exception {
|
||||
|
||||
final List<Integer> ages = List.of(1, 2, 3);
|
||||
List<Integer> params = new ArrayList<>(ages);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAgesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"age": [1, 2, 3]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceEmptyCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of();
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldBeEmptyWithNullValuesInCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of();
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
// add a null value
|
||||
params.add(null);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldIgnoreNullValuesInCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of("abc");
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
// add a null value
|
||||
params.add(null);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": ["abc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceCollectionParametersSpEL() throws Exception {
|
||||
|
||||
final List<QueryParameter> anotherString = List.of(new QueryParameter("hello \"Stranger\""),
|
||||
new QueryParameter("Another string"));
|
||||
List<QueryParameter> params = new ArrayList<>(anotherString);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesParameterSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": ["hello \\"Stranger\\"", "Another string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test // #1790
|
||||
@DisplayName("should escape Strings in query parameters")
|
||||
void shouldEscapeStringsInQueryParameters() throws Exception {
|
||||
@ -166,7 +370,8 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
|
||||
}
|
||||
|
||||
private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
|
||||
return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery());
|
||||
return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery(),
|
||||
QueryMethodEvaluationContextProvider.DEFAULT);
|
||||
}
|
||||
|
||||
private ElasticsearchQueryMethod getQueryMethod(String name, Class<?>... parameters) throws NoSuchMethodException {
|
||||
@ -187,9 +392,74 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
|
||||
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }")
|
||||
Person findByName(String name);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "#{#name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Person findByNameSpEL(String name);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "#{#param.value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Person findByParameterPropertySpEL(QueryParameter param);
|
||||
|
||||
@Query("{ 'bool' : { 'must' : { 'terms' : { 'name' : ?0 } } } }")
|
||||
Person findByNameIn(ArrayList<String> names);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": #{#names}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Person findByNamesSpEL(ArrayList<String> names);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"age": #{#ages}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Person findByAgesSpEL(ArrayList<Integer> ages);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": #{#names.![value]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Person findByNamesParameterSpEL(ArrayList<QueryParameter> names);
|
||||
|
||||
@Query(value = "name:(?0, ?11, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?0, ?1)")
|
||||
Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5,
|
||||
String arg6, String arg7, String arg8, String arg9, String arg10, String arg11);
|
||||
|
@ -22,6 +22,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@ -29,12 +30,13 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
@ -45,6 +47,7 @@ import org.springframework.data.elasticsearch.annotations.Query;
|
||||
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.SearchHit;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
@ -55,6 +58,7 @@ import org.springframework.lang.Nullable;
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
|
||||
@ -71,10 +75,7 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
|
||||
@Test // DATAES-519
|
||||
public void bindsSimplePropertyCorrectly() throws Exception {
|
||||
|
||||
ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByName", String.class);
|
||||
StubParameterAccessor accessor = new StubParameterAccessor("Luke");
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByName", "Luke");
|
||||
StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }");
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
@ -82,20 +83,214 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
|
||||
}
|
||||
|
||||
@Test // DATAES-519
|
||||
@Disabled("TODO: fix spel query integration")
|
||||
public void bindsExpressionPropertyCorrectly() throws Exception {
|
||||
|
||||
ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByNameWithExpression",
|
||||
String.class);
|
||||
StubParameterAccessor accessor = new StubParameterAccessor("Luke");
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameWithExpression", "Luke");
|
||||
StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }");
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
assertThat(((StringQuery) query).getSource()).isEqualTo(reference.getSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceParametersSpEL() throws Exception {
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", "Luke");
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "Luke"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceParametersSpELWithQuotes() throws Exception {
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL",
|
||||
"hello \"world\"");
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "hello \\"world\\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUseParameterPropertySpEL() throws Exception {
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByParameterPropertySpEL",
|
||||
new QueryParameter("Luke"));
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "Luke"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of("hello \"Stranger\"", "Another string");
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": ["hello \\"Stranger\\"", "Another string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceNonStringCollectionSpEL() throws Exception {
|
||||
|
||||
final List<Integer> ages = List.of(1, 2, 3);
|
||||
List<Integer> params = new ArrayList<>(ages);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAgesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"age": [1, 2, 3]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceEmptyCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of();
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldBeEmptyWithNullValuesInCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of();
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
// add a null value
|
||||
params.add(null);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldIgnoreNullValuesInCollectionSpEL() throws Exception {
|
||||
|
||||
final List<String> anotherString = List.of("abc");
|
||||
List<String> params = new ArrayList<>(anotherString);
|
||||
// add a null value
|
||||
params.add(null);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": ["abc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReplaceCollectionParametersSpEL() throws Exception {
|
||||
|
||||
final List<QueryParameter> anotherString = List.of(new QueryParameter("hello \"Stranger\""),
|
||||
new QueryParameter("Another string"));
|
||||
List<QueryParameter> params = new ArrayList<>(anotherString);
|
||||
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesParameterSpEL", params);
|
||||
String expected = """
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": ["hello \\"Stranger\\"", "Another string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertThat(query).isInstanceOf(StringQuery.class);
|
||||
JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE);
|
||||
}
|
||||
|
||||
@Test // DATAES-552
|
||||
public void shouldReplaceLotsOfParametersCorrectly() throws Exception {
|
||||
|
||||
@ -205,7 +400,59 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
|
||||
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }")
|
||||
Mono<Person> findByName(String name);
|
||||
|
||||
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?#{[0]}' } } } }")
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "#{#name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Mono<Person> findByNameSpEL(String name);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": #{#names}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<Person> findByNamesSpEL(List<String> names);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"term":{
|
||||
"name": "#{#param.value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<Person> findByParameterPropertySpEL(QueryParameter param);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"name": #{#names.![value]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<Person> findByNamesParameterSpEL(List<QueryParameter> names);
|
||||
|
||||
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '#{[0]}' } } } }")
|
||||
Flux<Person> findByNameWithExpression(String param0);
|
||||
|
||||
@Query(value = "name:(?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)")
|
||||
@ -228,6 +475,18 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
|
||||
@Query("{ 'bool' : { 'must' : { 'terms' : { 'ages' : ?0 } } } }")
|
||||
Flux<Person> findByAges(List<Integer> ages);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":{
|
||||
"terms":{
|
||||
"age": #{#ages}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<Person> findByAgesSpEL(List<Integer> ages);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2024 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
|
||||
*
|
||||
* https://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.elasticsearch.repository.query;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
|
||||
/**
|
||||
* Simple {@link ParameterAccessor} that returns the given parameters unfiltered.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
class StubParameterAccessor implements ElasticsearchParameterAccessor {
|
||||
|
||||
private final Object[] values;
|
||||
|
||||
StubParameterAccessor(Object... values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScrollPosition getScrollPosition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParameterAccessor#getPageable()
|
||||
*/
|
||||
@Override
|
||||
public Pageable getPageable() {
|
||||
return Pageable.unpaged();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParameterAccessor#getBindableValue(int)
|
||||
*/
|
||||
@Override
|
||||
public Object getBindableValue(int index) {
|
||||
return values[index];
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParameterAccessor#hasBindableNullValue()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasBindableNullValue() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParameterAccessor#getSort()
|
||||
*/
|
||||
@Override
|
||||
public Sort getSort() {
|
||||
return Sort.unsorted();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParameterAccessor#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Object> iterator() {
|
||||
return Arrays.asList(values).iterator();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.repository.query.ElasticsearchParameterAccessor#getValues()
|
||||
*/
|
||||
@Override
|
||||
public Object[] getValues() {
|
||||
return this.values;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParameterAccessor#findDynamicProjection()
|
||||
*/
|
||||
@Override
|
||||
public Class<?> findDynamicProjection() {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -19,12 +19,14 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
|
||||
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Haibo Liu
|
||||
* @since 4.4
|
||||
*/
|
||||
@ContextConfiguration(classes = { SimpleReactiveElasticsearchRepositoryELCIntegrationTests.Config.class })
|
||||
@ -39,6 +41,14 @@ public class SimpleReactiveElasticsearchRepositoryELCIntegrationTests
|
||||
IndexNameProvider indexNameProvider() {
|
||||
return new IndexNameProvider("simple-reactive-repository");
|
||||
}
|
||||
|
||||
/**
|
||||
* a normal bean referenced by SpEL in query
|
||||
*/
|
||||
@Bean
|
||||
QueryParameter queryParameter() {
|
||||
return new QueryParameter("message");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,10 +16,14 @@
|
||||
package org.springframework.data.elasticsearch.repository.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
|
||||
import static org.springframework.data.elasticsearch.core.query.Query.*;
|
||||
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.convert.ConversionException;
|
||||
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
@ -64,6 +68,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Jens Schauder
|
||||
* @author Haibo Liu
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
|
||||
@ -234,6 +239,167 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForStringQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
repository.queryByStringSpEL("message")
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRaiseExceptionForNullStringQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByStringSpEL(null));
|
||||
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo("Parameter value can't be null for SpEL expression '#message' in method 'queryByStringSpEL'" +
|
||||
" when querying elasticsearch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForParameterPropertyQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
QueryParameter param = new QueryParameter("message");
|
||||
|
||||
repository.queryByParameterPropertySpEL(param)
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForBeanQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
repository.queryByBeanPropertySpEL()
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForCollectionQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
repository.queryByCollectionSpEL(List.of("message"))
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRaiseExceptionForNullCollectionQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByCollectionSpEL(null));
|
||||
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo("Parameter value can't be null for SpEL expression '#messages' in method 'queryByCollectionSpEL'" +
|
||||
" when querying elasticsearch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnSearchHitsForEmptyCollectionQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
repository.queryByCollectionSpEL(List.of())
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(0) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
List<String> params = new ArrayList<>();
|
||||
params.add(null);
|
||||
|
||||
repository.queryByCollectionSpEL(params)
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextCount(0) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreNullValuesInCollectionQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
repository.queryByCollectionSpEL(Arrays.asList("message", null))
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
QueryParameter param = new QueryParameter("message");
|
||||
|
||||
repository.queryByParameterPropertyCollectionSpEL(List.of(param))
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() {
|
||||
bulkIndex(new SampleEntity("id-one", "message"), //
|
||||
new SampleEntity("id-two", "message"), //
|
||||
new SampleEntity("id-three", "message")) //
|
||||
.block();
|
||||
|
||||
QueryParameter param = new QueryParameter("message");
|
||||
|
||||
repository.queryByParameterPropertyCollectionSpELWithParamAnnotation(List.of(param))
|
||||
.as(StepVerifier::create) //
|
||||
.expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))//
|
||||
.expectNextCount(2) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAES-372
|
||||
void shouldReturnHighlightsOnAnnotatedMethod() {
|
||||
|
||||
@ -787,6 +953,105 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
|
||||
@Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }")
|
||||
Flux<SampleEntity> findAllViaAnnotatedQueryByMessageLikePaged(String message, Pageable pageable);
|
||||
|
||||
/**
|
||||
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
|
||||
* will not accept a null parameter as query value.
|
||||
*/
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"message": "#{#message}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<SearchHit<SampleEntity>> queryByStringSpEL(@Nullable String message);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"message": "#{#parameter.value}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<SearchHit<SampleEntity>> queryByParameterPropertySpEL(QueryParameter parameter);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"term":{
|
||||
"message": "#{@queryParameter.value}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<SearchHit<SampleEntity>> queryByBeanPropertySpEL();
|
||||
|
||||
/**
|
||||
* The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters
|
||||
* will not accept a null parameter as query value.
|
||||
*/
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"message": #{#messages}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<SearchHit<SampleEntity>> queryByCollectionSpEL(@Nullable Collection<String> messages);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"message": #{#parameters.![value]}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<SearchHit<SampleEntity>> queryByParameterPropertyCollectionSpEL(Collection<QueryParameter> parameters);
|
||||
|
||||
@Query("""
|
||||
{
|
||||
"bool":{
|
||||
"must":[
|
||||
{
|
||||
"terms":{
|
||||
"message": #{#e.![value]}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""")
|
||||
Flux<SearchHit<SampleEntity>> queryByParameterPropertyCollectionSpELWithParamAnnotation(
|
||||
@Param("e") Collection<QueryParameter> parameters);
|
||||
|
||||
Mono<SampleEntity> findFirstByMessageLike(String message);
|
||||
|
||||
Mono<Long> countAllByMessage(String message);
|
||||
|
@ -16,7 +16,7 @@
|
||||
package org.springframework.data.elasticsearch.utils;
|
||||
|
||||
/**
|
||||
* Class providing an index name with a prefix and a index number
|
||||
* Class providing an index name with a prefix and an index number
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user