Skip to content

Commit bc657eb

Browse files
committed
Fix SpEL vararg method invocation for strings containing commas
Prior to this commit, if a SpEL expression invoked a method or registered function that declares a String varargs argument, there were sometimes issues with converting the input arguments into the varargs array argument. Specifically, if the expression supplied a single String argument containing a comma for the varargs (such as "a,b"), SpEL's ReflectionHelper.convertArguments() method incorrectly converted that single String to an array via the ConversionService, which indirectly converted that String using the StringToArrayConverter, which converts a comma-delimited String to an array. Thus, "a,b" effectively got converted to a two-dimensional array ["a", "b"] instead of simply ["a,b"]. This commit fixes this bug by avoiding use of the TypeConverter and ConversionService for single arguments supplied as varargs when the single argument's type matches the varargs array component type. Closes gh-27582
1 parent 9b96777 commit bc657eb

File tree

3 files changed

+63
-19
lines changed

3 files changed

+63
-19
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
*
3939
* @author Andy Clement
4040
* @author Juergen Hoeller
41+
* @author Sam Brannen
4142
* @since 3.0
4243
*/
4344
public abstract class ReflectionHelper {
@@ -281,25 +282,32 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe
281282
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
282283
conversionOccurred |= (argument != arguments[i]);
283284
}
285+
284286
MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition);
287+
288+
// If the target is varargs and there is just one more argument, then convert it here.
285289
if (varargsPosition == arguments.length - 1) {
286-
// If the target is varargs and there is just one more argument
287-
// then convert it here
288-
TypeDescriptor targetType = new TypeDescriptor(methodParam);
289290
Object argument = arguments[varargsPosition];
291+
TypeDescriptor targetType = new TypeDescriptor(methodParam);
290292
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
291-
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
292-
// Three outcomes of that previous line:
293-
// 1) the input argument was already compatible (ie. array of valid type) and nothing was done
294-
// 2) the input argument was correct type but not in an array so it was made into an array
295-
// 3) the input argument was the wrong type and got converted and put into an array
293+
// If the argument type is equal to the varargs element type, there is no need
294+
// to convert it or wrap it in an array. For example, using StringToArrayConverter
295+
// to convert a String containing a comma would result in the String being split
296+
// and repackaged in an array when it should be used as-is.
297+
if (!sourceType.equals(targetType.getElementTypeDescriptor())) {
298+
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
299+
}
300+
// Three outcomes of the above if-block:
301+
// 1) the input argument was correct type but not wrapped in an array, and nothing was done.
302+
// 2) the input argument was already compatible (i.e., array of valid type), and nothing was done.
303+
// 3) the input argument was the wrong type and got converted and wrapped in an array.
296304
if (argument != arguments[varargsPosition] &&
297305
!isFirstEntryInArray(argument, arguments[varargsPosition])) {
298306
conversionOccurred = true; // case 3
299307
}
300308
}
309+
// Otherwise, convert remaining arguments to the varargs element type.
301310
else {
302-
// Convert remaining arguments to the varargs element type
303311
TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
304312
Assert.state(targetType != null, "No element type");
305313
for (int i = varargsPosition; i < arguments.length; i++) {

spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@
4646
*
4747
* @author Andy Clement
4848
* @author Phillip Webb
49+
* @author Sam Brannen
4950
*/
5051
public class MethodInvocationTests extends AbstractExpressionTests {
5152

@@ -233,26 +234,54 @@ public void testAddingMethodResolvers() {
233234

234235
@Test
235236
public void testVarargsInvocation01() {
236-
// Calling 'public int aVarargsMethod(String... strings)'
237-
//evaluate("aVarargsMethod('a','b','c')", 3, Integer.class);
238-
//evaluate("aVarargsMethod('a')", 1, Integer.class);
237+
// Calling 'public int aVarargsMethod(String... strings)' - returns number of arguments
238+
evaluate("aVarargsMethod('a','b','c')", 3, Integer.class);
239+
evaluate("aVarargsMethod('a')", 1, Integer.class);
239240
evaluate("aVarargsMethod()", 0, Integer.class);
240241
evaluate("aVarargsMethod(1,2,3)", 3, Integer.class); // all need converting to strings
241242
evaluate("aVarargsMethod(1)", 1, Integer.class); // needs string conversion
242243
evaluate("aVarargsMethod(1,'a',3.0d)", 3, Integer.class); // first and last need conversion
243-
// evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class);
244+
evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class);
244245
}
245246

246247
@Test
247248
public void testVarargsInvocation02() {
248-
// Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int+length_of_strings
249+
// Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int + length_of_strings
249250
evaluate("aVarargsMethod2(5,'a','b','c')", 8, Integer.class);
250251
evaluate("aVarargsMethod2(2,'a')", 3, Integer.class);
251252
evaluate("aVarargsMethod2(4)", 4, Integer.class);
252253
evaluate("aVarargsMethod2(8,2,3)", 10, Integer.class);
253254
evaluate("aVarargsMethod2(9)", 9, Integer.class);
254255
evaluate("aVarargsMethod2(2,'a',3.0d)", 4, Integer.class);
255-
// evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class);
256+
evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class);
257+
}
258+
259+
@Test
260+
public void testVarargsInvocation03() {
261+
// Calling 'public int aVarargsMethod3(String str1, String... strings)' - returns all strings concatenated with "-"
262+
263+
// No conversion necessary
264+
evaluate("aVarargsMethod3('x')", "x", String.class);
265+
evaluate("aVarargsMethod3('x', 'a')", "x-a", String.class);
266+
evaluate("aVarargsMethod3('x', 'a', 'b', 'c')", "x-a-b-c", String.class);
267+
268+
// Conversion necessary
269+
evaluate("aVarargsMethod3(9)", "9", String.class);
270+
evaluate("aVarargsMethod3(8,2,3)", "8-2-3", String.class);
271+
evaluate("aVarargsMethod3('2','a',3.0d)", "2-a-3.0", String.class);
272+
evaluate("aVarargsMethod3('8',new String[]{'a','b','c'})", "8-a-b-c", String.class);
273+
274+
// Individual string contains a comma with multiple varargs arguments
275+
evaluate("aVarargsMethod3('foo', ',', 'baz')", "foo-,-baz", String.class);
276+
evaluate("aVarargsMethod3('foo', 'bar', ',baz')", "foo-bar-,baz", String.class);
277+
evaluate("aVarargsMethod3('foo', 'bar,', 'baz')", "foo-bar,-baz", String.class);
278+
279+
// Individual string contains a comma with single varargs argument.
280+
// Reproduces https://github.com/spring-projects/spring-framework/issues/27582
281+
evaluate("aVarargsMethod3('foo', ',')", "foo-,", String.class);
282+
evaluate("aVarargsMethod3('foo', ',bar')", "foo-,bar", String.class);
283+
evaluate("aVarargsMethod3('foo', 'bar,')", "foo-bar,", String.class);
284+
evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class);
256285
}
257286

258287
@Test

spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
///CLOVER:OFF
2929
@SuppressWarnings("unused")
3030
public class Inventor {
31+
3132
private String name;
3233
public String _name;
3334
public String _name_;
@@ -202,8 +203,14 @@ public int aVarargsMethod2(int i, String... strings) {
202203
return strings.length + i;
203204
}
204205

205-
public Inventor(String... strings) {
206+
public String aVarargsMethod3(String str1, String... strings) {
207+
if (ObjectUtils.isEmpty(strings)) {
208+
return str1;
209+
}
210+
return str1 + "-" + String.join("-", strings);
211+
}
206212

213+
public Inventor(String... strings) {
207214
}
208215

209216
public boolean getSomeProperty() {

0 commit comments

Comments
 (0)