Skip to content

Unify conversion services for value replacement when querying elastic… #2834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.repository.query;

import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
Expand Down Expand Up @@ -64,11 +65,12 @@ protected boolean isExistsQuery() {
}

protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService();
String queryString = new StringQueryUtil(conversionService)
.replacePlaceholders(this.queryString, parameterAccessor);

QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider);
evaluationContextProvider, conversionService);
var query = new StringQuery(evaluator.evaluate());
query.addSort(parameterAccessor.getSort());
return query;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.repository.query;

import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
Expand Down Expand Up @@ -56,8 +57,10 @@ protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor paramet
getElasticsearchOperations().getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.query, parameterAccessor);

ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider);
evaluationContextProvider, conversionService);
return new StringQuery(evaluator.evaluate());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@
*/
package org.springframework.data.elasticsearch.repository.support;

import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchQueryValueConversionService;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.util.NumberUtils;

/**
* @author Peter-Josef Meisch
* @author Niklas Herder
* @author Haibo Liu
*/
final public class StringQueryUtil {

Expand All @@ -35,7 +36,7 @@ final public class StringQueryUtil {
private final ConversionService conversionService;

public StringQueryUtil(ConversionService conversionService) {
this.conversionService = conversionService;
this.conversionService = ElasticsearchQueryValueConversionService.getInstance(conversionService);
}

public String replacePlaceholders(String input, ParameterAccessor accessor) {
Expand All @@ -46,7 +47,7 @@ public String replacePlaceholders(String input, ParameterAccessor accessor) {

String placeholder = Pattern.quote(matcher.group()) + "(?!\\d+)";
int index = NumberUtils.parseNumber(matcher.group(1), Integer.class);
String replacement = Matcher.quoteReplacement(getParameterWithIndex(accessor, index));
String replacement = Matcher.quoteReplacement(getParameterWithIndex(accessor, index, input));
result = result.replaceAll(placeholder, replacement);
// need to escape backslashes that are not escapes for quotes so that they are sent as double-backslashes
// to Elasticsearch
Expand All @@ -55,47 +56,16 @@ public String replacePlaceholders(String input, ParameterAccessor accessor) {
return result;
}

private String getParameterWithIndex(ParameterAccessor accessor, int index) {
private String getParameterWithIndex(ParameterAccessor accessor, int index, String input) {

Object parameter = accessor.getBindableValue(index);
String parameterValue = "null";

if (parameter != null) {
parameterValue = convert(parameter);
}

return parameterValue;

}

private String convert(Object parameter) {
if (Collection.class.isAssignableFrom(parameter.getClass())) {
Collection<?> collectionParam = (Collection<?>) parameter;
StringBuilder sb = new StringBuilder("[");
sb.append(collectionParam.stream().map(o -> {
if (o instanceof String) {
return "\"" + convert(o) + "\"";
} else {
return convert(o);
}
}).collect(Collectors.joining(",")));
sb.append("]");
return sb.toString();
} else {
String parameterValue = "null";
if (conversionService.canConvert(parameter.getClass(), String.class)) {
String converted = conversionService.convert(parameter, String.class);

if (converted != null) {
parameterValue = converted;
}
} else {
parameterValue = parameter.toString();
}

parameterValue = parameterValue.replaceAll("\"", Matcher.quoteReplacement("\\\""));
return parameterValue;
String value = conversionService.convert(parameter, String.class);
if (value == null) {
throw new ConversionException(String.format(
"Parameter value can't be null for placeholder at index '%s' in query '%s' when querying elasticsearch",
index, input));
}
return value;
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchQueryValueConversionService;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchCollectionValueToStringConverter;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchStringValueToStringConverter;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
Expand Down Expand Up @@ -53,13 +57,14 @@ public class QueryStringSpELEvaluator {
private final TypeConverter elasticsearchSpELTypeConverter;

public QueryStringSpELEvaluator(String queryString, ElasticsearchParametersParameterAccessor parameterAccessor,
QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider) {
QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider,
ConversionService conversionService) {
this.queryString = queryString;
this.parameterAccessor = parameterAccessor;
this.queryMethod = queryMethod;
this.evaluationContextProvider = evaluationContextProvider;
this.elasticsearchSpELTypeConverter = new StandardTypeConverter(
ElasticsearchValueSpELConversionService.CONVERSION_SERVICE_LAZY);
ElasticsearchQueryValueConversionService.getInstance(conversionService));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support.spel;
package org.springframework.data.elasticsearch.repository.support.value;

import java.util.Collection;
import java.util.Collections;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.value;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
* A {@link ConversionService} using custom converters to handle query values in elasticsearch query. If the value to be
* converted beyond the scope of custom converters, it'll delegate to the {@link #delegate delegated conversion service}.
* <p>
* This is a better solution for converting query values in elasticsearch query, because it has all the capability the
* {@link #delegate delegated conversion service} has, especially for user-registered {@link Converter}s.
*
* @since 5.3
* @author Haibo Liu
*/
public class ElasticsearchQueryValueConversionService implements ConversionService {

private static final Map<ConversionService, ElasticsearchQueryValueConversionService> CACHE = new ConcurrentHashMap<>();

private final GenericConversionService valueConversionService = new GenericConversionService();

private final ConversionService delegate;

private ElasticsearchQueryValueConversionService(ConversionService delegate) {
Assert.notNull(delegate, "delegated ConversionService must not be null");
this.delegate = delegate;

// register elasticsearch custom type converters for conversion service
valueConversionService.addConverter(new ElasticsearchCollectionValueToStringConverter(this));
valueConversionService.addConverter(new ElasticsearchStringValueToStringConverter());
}

/**
* Get a {@link ElasticsearchQueryValueConversionService} with this conversion service as delegated.
*
* @param conversionService conversion service as delegated
* @return a conversion service having the capability to convert query values in elasticsearch query
*/
public static ElasticsearchQueryValueConversionService getInstance(ConversionService conversionService) {
return CACHE.computeIfAbsent(conversionService, ElasticsearchQueryValueConversionService::new);
}

@Override
public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
return canConvert(TypeDescriptor.valueOf(sourceType), TypeDescriptor.valueOf(targetType));
}

@Override
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
return valueConversionService.canConvert(sourceType, targetType)
|| delegate.canConvert(sourceType, targetType);
}

@SuppressWarnings("unchecked")
@Override
@Nullable
public <T> T convert(@Nullable Object source, Class<T> targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}

@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
if (valueConversionService.canConvert(sourceType, targetType)) {
return valueConversionService.convert(source, sourceType, targetType);
} else {
return delegate.convert(source, sourceType, targetType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support.spel;
package org.springframework.data.elasticsearch.repository.support.value;

import java.util.Collections;
import java.util.Set;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
*/
package org.springframework.data.elasticsearch.repositories.custommethod;

import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
Expand Down Expand Up @@ -49,5 +52,11 @@ IndexNameProvider indexNameProvider() {
QueryParameter queryParameter() {
return new QueryParameter("abc");
}

@Bean
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(List.of(SamplePropertyToStringConverter.INSTANCE,
StringToSamplePropertyConverter.INSTANCE));
}
}
}
Loading