diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc index 2b725a820..b558e13bb 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc @@ -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 { @@ -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 { + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "name": "#{#name}" + } + } + ] + } + } + """) + Page 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 { + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "name": "#{#parameter.value}" + } + } + ] + } + } + """) + Page 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 { + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "name": "#{@queryParameter.value}" + } + } + ] + } + } + """) + Page 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 { + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "name": #{#names} + } + } + ] + } + } + """) + Page findByName(Collection 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 { + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "name": #{#parameters.![value]} + } + } + ] + } + } + """) + Page findByName(Collection 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 { + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "name": #{#another.![value]} + } + } + ] + } + } + """) + Page findByName(@Param("another") Collection parameters, Pageable pageable); +} +---- + +==== diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java index 516ea146b..02826cbe5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java @@ -43,6 +43,7 @@ * * @author Christoph Strobl * @author Peter-Josef Meisch + * @author Haibo Liu * @since 3.2 */ abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery { @@ -112,7 +113,7 @@ private Object execute(ElasticsearchParametersParameterAccessor parameterAccesso * @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 resultProcessing) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java index c4343475f..92f4992c4 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java @@ -21,7 +21,7 @@ * @author Christoph Strobl * @since 3.2 */ -class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor +public class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor implements ElasticsearchParameterAccessor { private final Object[] values; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java index 5f290ea93..2f98e8276 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -69,9 +69,9 @@ */ 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; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java index 9807e6a54..8a0ae64d7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -17,9 +17,10 @@ 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 @@ * @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 @@ protected boolean isExistsQuery() { } 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; } + } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java index 6ac03a8ce..66c36ad84 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java @@ -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 ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQuery 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 diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java index bcd33e888..1fd7f1308 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java @@ -40,7 +40,7 @@ public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod query } @Override - protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) { + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) { CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery(); if (tree.isLimiting()) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java index 7b5f82771..2752b5464 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java @@ -34,6 +34,8 @@ 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 @@ * @author Sascha Woo * @author Peter-Josef Meisch * @author Ezequiel AntĂșnez Camacho + * @author Haibo Liu */ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { @@ -96,11 +99,17 @@ private static boolean isQueryDslRepository(Class repositoryInterface) { @Override protected Optional 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 RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, 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); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchCollectionValueToStringConverter.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchCollectionValueToStringConverter.java new file mode 100644 index 000000000..1a0adb1e3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchCollectionValueToStringConverter.java @@ -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. + *

+ * 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. + *

+ * eg: The value part of an elasticsearch terms query should looks like {@code ["hello \"Stranger\"","Another string"]} + * for query + *

+ * {@code
+ *  {
+ *    "bool":{
+ *      "must":{
+ *        "terms":{
+ *          "name": ["hello \"Stranger\"", "Another string"]
+ *        }
+ *      }
+ *    }
+ *  }
+ * }
+ * 
+ * + * @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 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(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchStringValueToStringConverter.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchStringValueToStringConverter.java new file mode 100644 index 000000000..07ad446b1 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchStringValueToStringConverter.java @@ -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 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("\\\"")); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchValueSpELConversionService.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchValueSpELConversionService.java new file mode 100644 index 000000000..e75709b2b --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/ElasticsearchValueSpELConversionService.java @@ -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 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; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java new file mode 100644 index 000000000..41123d250 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java @@ -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 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; + }); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java index d0f410470..a02684db2 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java @@ -323,9 +323,9 @@ record Person( // @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 // ) { } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java index c7b5615ed..b00a8f043 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java @@ -25,6 +25,7 @@ /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.4 */ @ContextConfiguration(classes = { CustomMethodRepositoryELCIntegrationTests.Config.class }) @@ -40,5 +41,13 @@ static class Config { IndexNameProvider indexNameProvider() { return new IndexNameProvider("custom-method-repository"); } + + /** + * a normal bean referenced by SpEL in query + */ + @Bean + QueryParameter queryParameter() { + return new QueryParameter("abc"); + } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java index d21f00114..9bb507598 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java @@ -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.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.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 @@ void shouldReturnSearchHitsForStringQuery() { assertThat(searchHits.getTotalHits()).isEqualTo(20); } + @Test + void shouldReturnSearchHitsForStringQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByStringSpEL("abc"); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldRaiseExceptionForNullStringQuerySpEL() { + List 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 entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + QueryParameter param = new QueryParameter("abc"); + // when + SearchHits searchHits = repository.queryByParameterPropertySpEL(param); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForBeanQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByBeanPropertySpEL(); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByCollectionSpEL(List.of("abc")); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldRaiseExceptionForNullCollectionQuerySpEL() { + List 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 entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByCollectionSpEL(List.of()); + + assertThat(searchHits.getTotalHits()).isEqualTo(0); + } + + @Test + void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + List params = new ArrayList<>(); + params.add(null); + // when + SearchHits searchHits = repository.queryByCollectionSpEL(params); + + assertThat(searchHits.getTotalHits()).isEqualTo(0); + } + + @Test + void shouldIgnoreNullValuesInCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByCollectionSpEL(Arrays.asList("abc", null)); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + QueryParameter param = new QueryParameter("abc"); + // when + SearchHits searchHits = repository.queryByParameterPropertyCollectionSpEL(List.of(param)); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + QueryParameter param = new QueryParameter("abc"); + // when + SearchHits searchHits = repository.queryByParameterPropertyCollectionSpELWithParamAnnotation( + List.of(param)); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + @Test // DATAES-372 void shouldReturnHighlightsOnAnnotatedMethod() { List entities = createSampleEntities("abc", 2); @@ -1940,6 +2072,105 @@ public interface SampleCustomMethodRepository extends ElasticsearchRepository 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 queryByStringSpEL(@Nullable String type); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "type": "#{#parameter.value}" + } + } + ] + } + } + """) + SearchHits queryByParameterPropertySpEL(QueryParameter parameter); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "type": "#{@queryParameter.value}" + } + } + ] + } + } + """) + SearchHits 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 queryByCollectionSpEL(@Nullable Collection types); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "type": #{#parameters.![value]} + } + } + ] + } + } + """) + SearchHits queryByParameterPropertyCollectionSpEL(Collection parameters); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "type": #{#e.![value]} + } + } + ] + } + } + """) + SearchHits queryByParameterPropertyCollectionSpELWithParamAnnotation( + @Param("e") Collection parameters); + @Query(""" { "bool":{ diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/QueryParameter.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/QueryParameter.java new file mode 100644 index 000000000..86b842b58 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/QueryParameter.java @@ -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) { +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java index b7e9e84a3..60821db50 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java @@ -32,6 +32,8 @@ 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.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 void shouldReplaceRepeatedParametersCorrectly() throws Exception { .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 anotherString = List.of("hello \"Stranger\"", "Another string"); + List 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 ages = List.of(1, 2, 3); + List 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 anotherString = List.of(); + List 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 anotherString = List.of(); + List 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 anotherString = List.of("abc"); + List 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 anotherString = List.of(new QueryParameter("hello \"Stranger\""), + new QueryParameter("Another string")); + List 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 @@ void shouldUseConverterOnParameters() throws NoSuchMethodException { } 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 @@ private interface SampleRepository extends Repository { @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 names); + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names} + } + } + } + } + """) + Person findByNamesSpEL(ArrayList names); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "age": #{#ages} + } + } + } + } + """) + Person findByAgesSpEL(ArrayList ages); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names.![value]} + } + } + } + } + """) + Person findByNamesParameterSpEL(ArrayList 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); diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java index f400f3904..e649fe792 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java @@ -22,6 +22,7 @@ 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.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.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 @@ /** * @author Christoph Strobl * @author Peter-Josef Meisch + * @author Haibo Liu */ @ExtendWith(MockitoExtension.class) public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase { @@ -71,10 +75,7 @@ public void setUp() { @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 void bindsSimplePropertyCorrectly() throws Exception { } @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 anotherString = List.of("hello \"Stranger\"", "Another string"); + List 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 ages = List.of(1, 2, 3); + List 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 anotherString = List.of(); + List 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 anotherString = List.of(); + List 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 anotherString = List.of("abc"); + List 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 anotherString = List.of(new QueryParameter("hello \"Stranger\""), + new QueryParameter("Another string")); + List 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 @@ private interface SampleRepository extends Repository { @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }") Mono findByName(String name); - @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?#{[0]}' } } } }") + @Query(""" + { + "bool":{ + "must":{ + "term":{ + "name": "#{#name}" + } + } + } + } + """) + Mono findByNameSpEL(String name); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names} + } + } + } + } + """) + Flux findByNamesSpEL(List names); + + @Query(""" + { + "bool":{ + "must":{ + "term":{ + "name": "#{#param.value}" + } + } + } + } + """) + Flux findByParameterPropertySpEL(QueryParameter param); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names.![value]} + } + } + } + } + """) + Flux findByNamesParameterSpEL(List names); + + @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '#{[0]}' } } } }") Flux findByNameWithExpression(String param0); @Query(value = "name:(?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)") @@ -228,6 +475,18 @@ Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String @Query("{ 'bool' : { 'must' : { 'terms' : { 'ages' : ?0 } } } }") Flux findByAges(List ages); + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "age": #{#ages} + } + } + } + } + """) + Flux findByAgesSpEL(List ages); } /** diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java deleted file mode 100644 index 12177a283..000000000 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java index eaef4cba4..b5aec9c92 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java @@ -19,12 +19,14 @@ 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 @@ static class Config { IndexNameProvider indexNameProvider() { return new IndexNameProvider("simple-reactive-repository"); } + + /** + * a normal bean referenced by SpEL in query + */ + @Bean + QueryParameter queryParameter() { + return new QueryParameter("message"); + } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java index fb5a1ee93..f7ebbc2fe 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java @@ -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 @@ * @author Christoph Strobl * @author Peter-Josef Meisch * @author Jens Schauder + * @author Haibo Liu */ @SpringIntegrationTest abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { @@ -234,6 +239,167 @@ void shouldReturnFluxOfSearchHitForStringQuery() { .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 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 @@ interface ReactiveSampleEntityRepository extends ReactiveCrudRepository 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> queryByStringSpEL(@Nullable String message); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "message": "#{#parameter.value}" + } + } + ] + } + } + """) + Flux> queryByParameterPropertySpEL(QueryParameter parameter); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "message": "#{@queryParameter.value}" + } + } + ] + } + } + """) + Flux> 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> queryByCollectionSpEL(@Nullable Collection messages); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "message": #{#parameters.![value]} + } + } + ] + } + } + """) + Flux> queryByParameterPropertyCollectionSpEL(Collection parameters); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "message": #{#e.![value]} + } + } + ] + } + } + """) + Flux> queryByParameterPropertyCollectionSpELWithParamAnnotation( + @Param("e") Collection parameters); + Mono findFirstByMessageLike(String message); Mono countAllByMessage(String message); diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java b/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java index 130c35c9a..0b19964af 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java +++ b/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java @@ -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 */