Skip to content

Commit 40972b2

Browse files
committed
Escape strings with quotes in custom query parameters.
Original Pull Request #1793 Closes #1790 (cherry picked from commit f8fbf77)
1 parent 85af546 commit 40972b2

File tree

5 files changed

+126
-70
lines changed

5 files changed

+126
-70
lines changed

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

+2-45
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
*/
1616
package org.springframework.data.elasticsearch.repository.query;
1717

18-
import java.util.regex.Matcher;
19-
import java.util.regex.Pattern;
20-
2118
import org.springframework.core.convert.support.GenericConversionService;
2219
import org.springframework.data.domain.PageRequest;
2320
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
@@ -26,11 +23,11 @@
2623
import org.springframework.data.elasticsearch.core.convert.DateTimeConverters;
2724
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
2825
import org.springframework.data.elasticsearch.core.query.StringQuery;
26+
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
2927
import org.springframework.data.repository.query.ParametersParameterAccessor;
3028
import org.springframework.data.util.StreamUtils;
3129
import org.springframework.util.Assert;
3230
import org.springframework.util.ClassUtils;
33-
import org.springframework.util.NumberUtils;
3431

3532
/**
3633
* ElasticsearchStringQuery
@@ -43,25 +40,8 @@
4340
*/
4441
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
4542

46-
private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
4743
private String query;
4844

49-
private final GenericConversionService conversionService = new GenericConversionService();
50-
51-
{
52-
if (!conversionService.canConvert(java.util.Date.class, String.class)) {
53-
conversionService.addConverter(DateTimeConverters.JavaDateConverter.INSTANCE);
54-
}
55-
if (ClassUtils.isPresent("org.joda.time.DateTimeZone", ElasticsearchStringQuery.class.getClassLoader())) {
56-
if (!conversionService.canConvert(org.joda.time.ReadableInstant.class, String.class)) {
57-
conversionService.addConverter(DateTimeConverters.JodaDateTimeConverter.INSTANCE);
58-
}
59-
if (!conversionService.canConvert(org.joda.time.LocalDateTime.class, String.class)) {
60-
conversionService.addConverter(DateTimeConverters.JodaLocalDateTimeConverter.INSTANCE);
61-
}
62-
}
63-
}
64-
6545
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
6646
String query) {
6747
super(queryMethod, elasticsearchOperations);
@@ -118,31 +98,8 @@ public Object execute(Object[] parameters) {
11898
}
11999

120100
protected StringQuery createQuery(ParametersParameterAccessor parameterAccessor) {
121-
String queryString = replacePlaceholders(this.query, parameterAccessor);
101+
String queryString = StringQueryUtil.replacePlaceholders(this.query, parameterAccessor);
122102
return new StringQuery(queryString);
123103
}
124104

125-
private String replacePlaceholders(String input, ParametersParameterAccessor accessor) {
126-
127-
Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input);
128-
String result = input;
129-
while (matcher.find()) {
130-
131-
String placeholder = Pattern.quote(matcher.group()) + "(?!\\d+)";
132-
int index = NumberUtils.parseNumber(matcher.group(1), Integer.class);
133-
result = result.replaceAll(placeholder, getParameterWithIndex(accessor, index));
134-
}
135-
return result;
136-
}
137-
138-
private String getParameterWithIndex(ParametersParameterAccessor accessor, int index) {
139-
Object parameter = accessor.getBindableValue(index);
140-
if (parameter == null) {
141-
return "null";
142-
}
143-
if (conversionService.canConvert(parameter.getClass(), String.class)) {
144-
return conversionService.convert(parameter, String.class);
145-
}
146-
return parameter.toString();
147-
}
148105
}

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

+2-24
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,11 @@
1515
*/
1616
package org.springframework.data.elasticsearch.repository.query;
1717

18-
import java.util.regex.Matcher;
19-
import java.util.regex.Pattern;
20-
2118
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
2219
import org.springframework.data.elasticsearch.core.query.StringQuery;
20+
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
2321
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2422
import org.springframework.expression.spel.standard.SpelExpressionParser;
25-
import org.springframework.util.NumberUtils;
26-
import org.springframework.util.ObjectUtils;
2723

2824
/**
2925
* @author Christoph Strobl
@@ -32,7 +28,6 @@
3228
*/
3329
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
3430

35-
private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
3631
private final String query;
3732

3833
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
@@ -52,27 +47,10 @@ public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQuery
5247

5348
@Override
5449
protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) {
55-
String queryString = replacePlaceholders(this.query, parameterAccessor);
50+
String queryString = StringQueryUtil.replacePlaceholders(this.query, parameterAccessor);
5651
return new StringQuery(queryString);
5752
}
5853

59-
private String replacePlaceholders(String input, ElasticsearchParameterAccessor accessor) {
60-
61-
Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input);
62-
String result = input;
63-
while (matcher.find()) {
64-
65-
String placeholder = Pattern.quote(matcher.group()) + "(?!\\d+)";
66-
int index = NumberUtils.parseNumber(matcher.group(1), Integer.class);
67-
result = result.replaceAll(placeholder, getParameterWithIndex(accessor, index));
68-
}
69-
return result;
70-
}
71-
72-
private String getParameterWithIndex(ElasticsearchParameterAccessor accessor, int index) {
73-
return ObjectUtils.nullSafeToString(accessor.getBindableValue(index));
74-
}
75-
7654
@Override
7755
boolean isCountQuery() {
7856
return queryMethod.hasCountQueryAnnotation();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.repository.support;
17+
18+
import java.util.regex.Matcher;
19+
import java.util.regex.Pattern;
20+
21+
import org.springframework.core.convert.support.GenericConversionService;
22+
import org.springframework.data.elasticsearch.core.convert.DateTimeConverters;
23+
import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery;
24+
import org.springframework.data.repository.query.ParameterAccessor;
25+
import org.springframework.util.ClassUtils;
26+
import org.springframework.util.NumberUtils;
27+
28+
/**
29+
* @author Peter-Josef Meisch
30+
*/
31+
final public class StringQueryUtil {
32+
33+
private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
34+
private static final GenericConversionService conversionService = new GenericConversionService();
35+
36+
{
37+
if (!conversionService.canConvert(java.util.Date.class, String.class)) {
38+
conversionService.addConverter(DateTimeConverters.JavaDateConverter.INSTANCE);
39+
}
40+
if (ClassUtils.isPresent("org.joda.time.DateTimeZone", ElasticsearchStringQuery.class.getClassLoader())) {
41+
if (!conversionService.canConvert(org.joda.time.ReadableInstant.class, String.class)) {
42+
conversionService.addConverter(DateTimeConverters.JodaDateTimeConverter.INSTANCE);
43+
}
44+
if (!conversionService.canConvert(org.joda.time.LocalDateTime.class, String.class)) {
45+
conversionService.addConverter(DateTimeConverters.JodaLocalDateTimeConverter.INSTANCE);
46+
}
47+
}
48+
}
49+
50+
private StringQueryUtil() {}
51+
52+
public static String replacePlaceholders(String input, ParameterAccessor accessor) {
53+
54+
Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input);
55+
String result = input;
56+
while (matcher.find()) {
57+
58+
String placeholder = Pattern.quote(matcher.group()) + "(?!\\d+)";
59+
int index = NumberUtils.parseNumber(matcher.group(1), Integer.class);
60+
result = result.replaceAll(placeholder, Matcher.quoteReplacement(getParameterWithIndex(accessor, index)));
61+
}
62+
return result;
63+
}
64+
65+
private static String getParameterWithIndex(ParameterAccessor accessor, int index) {
66+
67+
Object parameter = accessor.getBindableValue(index);
68+
String parameterValue = "null";
69+
70+
// noinspection ConstantConditions
71+
if (parameter != null) {
72+
73+
if (conversionService.canConvert(parameter.getClass(), String.class)) {
74+
String converted = conversionService.convert(parameter, String.class);
75+
76+
if (converted != null) {
77+
parameterValue = converted;
78+
}
79+
} else {
80+
parameterValue = parameter.toString();
81+
}
82+
}
83+
84+
parameterValue = parameterValue.replaceAll("\"", Matcher.quoteReplacement("\\\""));
85+
return parameterValue;
86+
87+
}
88+
89+
}

src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Map;
2626

2727
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.DisplayName;
2829
import org.junit.jupiter.api.Test;
2930
import org.junit.jupiter.api.extension.ExtendWith;
3031
import org.mockito.Mock;
@@ -37,6 +38,7 @@
3738
import org.springframework.data.elasticsearch.annotations.MultiField;
3839
import org.springframework.data.elasticsearch.annotations.Query;
3940
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
41+
import org.springframework.data.elasticsearch.core.SearchHits;
4042
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
4143
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
4244
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
@@ -82,6 +84,17 @@ public void shouldReplaceRepeatedParametersCorrectly() throws Exception {
8284
.isEqualTo("name:(zero, eleven, one, two, three, four, five, six, seven, eight, nine, ten, eleven, zero, one)");
8385
}
8486

87+
@Test // #1790
88+
@DisplayName("should escape Strings in query parameters")
89+
void shouldEscapeStringsInQueryParameters() throws Exception {
90+
91+
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByPrefix", "hello \"Stranger\"");
92+
93+
assertThat(query).isInstanceOf(StringQuery.class);
94+
assertThat(((StringQuery) query).getSource())
95+
.isEqualTo("{\"bool\":{\"must\": [{\"match\": {\"prefix\": {\"name\" : \"hello \\\"Stranger\\\"\"}}]}}");
96+
}
97+
8598
private org.springframework.data.elasticsearch.core.query.Query createQuery(String methodName, String... args)
8699
throws NoSuchMethodException {
87100

@@ -90,7 +103,6 @@ private org.springframework.data.elasticsearch.core.query.Query createQuery(Stri
90103
ElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
91104
return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
92105
}
93-
94106
private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
95107
return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery());
96108
}
@@ -110,6 +122,9 @@ private interface SampleRepository extends Repository<Person, String> {
110122
@Query(value = "name:(?0, ?11, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?0, ?1)")
111123
Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5,
112124
String arg6, String arg7, String arg8, String arg9, String arg10, String arg11);
125+
126+
@Query("{\"bool\":{\"must\": [{\"match\": {\"prefix\": {\"name\" : \"?0\"}}]}}")
127+
SearchHits<Book> findByPrefix(String prefix);
113128
}
114129

115130
/**

src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java

+17
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import org.junit.jupiter.api.BeforeEach;
3131
import org.junit.jupiter.api.Disabled;
32+
import org.junit.jupiter.api.DisplayName;
3233
import org.junit.jupiter.api.Test;
3334
import org.junit.jupiter.api.extension.ExtendWith;
3435
import org.mockito.Mock;
@@ -41,6 +42,7 @@
4142
import org.springframework.data.elasticsearch.annotations.MultiField;
4243
import org.springframework.data.elasticsearch.annotations.Query;
4344
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
45+
import org.springframework.data.elasticsearch.core.SearchHit;
4446
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
4547
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
4648
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
@@ -119,6 +121,17 @@ public void shouldReplaceRepeatedParametersCorrectly() throws Exception {
119121
.isEqualTo("name:(zero, eleven, one, two, three, four, five, six, seven, eight, nine, ten, eleven, zero, one)");
120122
}
121123

124+
@Test // #1790
125+
@DisplayName("should escape Strings in query parameters")
126+
void shouldEscapeStringsInQueryParameters() throws Exception {
127+
128+
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByPrefix", "hello \"Stranger\"");
129+
130+
assertThat(query).isInstanceOf(StringQuery.class);
131+
assertThat(((StringQuery) query).getSource())
132+
.isEqualTo("{\"bool\":{\"must\": [{\"match\": {\"prefix\": {\"name\" : \"hello \\\"Stranger\\\"\"}}]}}");
133+
}
134+
122135
private org.springframework.data.elasticsearch.core.query.Query createQuery(String methodName, String... args)
123136
throws NoSuchMethodException {
124137

@@ -163,6 +176,10 @@ Person findWithQuiteSomeParameters(String arg0, String arg1, String arg2, String
163176
@Query(value = "name:(?0, ?11, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?0, ?1)")
164177
Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5,
165178
String arg6, String arg7, String arg8, String arg9, String arg10, String arg11);
179+
180+
@Query("{\"bool\":{\"must\": [{\"match\": {\"prefix\": {\"name\" : \"?0\"}}]}}")
181+
Flux<SearchHit<Book>> findByPrefix(String prefix);
182+
166183
}
167184

168185
/**

0 commit comments

Comments
 (0)