Skip to content

Commit c55c644

Browse files
committed
Support single String argument for varargs invocations in SpEL
Prior to this commit, the Spring Expression Language (SpEL) incorrectly split single String arguments by comma for Object... varargs method and constructor invocations. This commit addresses this by checking if the single argument type is already "assignable" to the varargs component type instead of "equal" to the varargs component type. Closes gh-33013
1 parent a4fcd30 commit c55c644

File tree

6 files changed

+122
-11
lines changed

6 files changed

+122
-11
lines changed

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,11 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe
308308
conversionOccurred = true;
309309
}
310310
}
311-
// If the argument type is equal to the varargs component type, there is no need to
311+
// If the argument type is assignable to the varargs component type, there is no need to
312312
// convert it or wrap it in an array. For example, using StringToArrayConverter to
313313
// convert a String containing a comma would result in the String being split and
314314
// repackaged in an array when it should be used as-is.
315-
else if (!sourceType.equals(componentTypeDesc)) {
315+
else if (!sourceType.isAssignableTo(componentTypeDesc)) {
316316
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
317317
}
318318
// Possible outcomes of the above if-else block:
@@ -384,7 +384,7 @@ public static boolean convertAllMethodHandleArguments(TypeConverter converter, O
384384
ResolvableType varArgResolvableType = ResolvableType.forClass(varArgClass);
385385
TypeDescriptor varArgComponentType = new TypeDescriptor(varArgResolvableType, varArgClass, null);
386386
TypeDescriptor componentTypeDesc = varArgComponentType.getElementTypeDescriptor();
387-
// TODO Determine why componentTypeDesc is null.
387+
// TODO Determine why componentTypeDesc can be null.
388388
// Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
389389

390390
// If the target is varargs and there is just one more argument, then convert it here.
@@ -398,11 +398,11 @@ public static boolean convertAllMethodHandleArguments(TypeConverter converter, O
398398
conversionOccurred = true;
399399
}
400400
}
401-
// If the argument type is equal to the varargs component type, there is no need to
401+
// If the argument type is assignable to the varargs component type, there is no need to
402402
// convert it or wrap it in an array. For example, using StringToArrayConverter to
403403
// convert a String containing a comma would result in the String being split and
404404
// repackaged in an array when it should be used as-is.
405-
else if (!sourceType.equals(componentTypeDesc)) {
405+
else if (componentTypeDesc != null && !sourceType.isAssignableTo(componentTypeDesc)) {
406406
arguments[varargsPosition] = converter.convertValue(argument, sourceType, varArgComponentType);
407407
}
408408
// Possible outcomes of the above if-else block:

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

+40
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,46 @@ void testVarargsInvocation03() {
293293
evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class);
294294
}
295295

296+
@Test // gh-33013
297+
void testVarargsWithObjectArrayType() {
298+
// Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
299+
300+
// No var-args and no conversion necessary
301+
evaluate("formatObjectVarargs('x')", "x", String.class);
302+
303+
// No var-args but conversion necessary
304+
evaluate("formatObjectVarargs(9)", "9", String.class);
305+
306+
// No conversion necessary
307+
evaluate("formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
308+
evaluate("formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
309+
evaluate("formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
310+
evaluate("formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
311+
evaluate("formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
312+
evaluate("formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
313+
evaluate("formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
314+
evaluate("formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
315+
evaluate("formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
316+
evaluate("formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
317+
evaluate("formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
318+
evaluate("formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
319+
320+
// Conversion necessary
321+
evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
322+
evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
323+
324+
// Individual string contains a comma with multiple varargs arguments
325+
evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
326+
evaluate("formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class);
327+
evaluate("formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class);
328+
329+
// Individual string contains a comma with single varargs argument.
330+
evaluate("formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class);
331+
evaluate("formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class);
332+
evaluate("formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class);
333+
evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
334+
}
335+
296336
@Test
297337
void testVarargsOptionalInvocation() {
298338
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'

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

+13-5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import static org.assertj.core.api.Assertions.assertThatException;
7070
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
7171
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
72+
import static org.assertj.core.api.InstanceOfAssertFactories.list;
7273

7374
/**
7475
* Reproduction tests cornering various reported SpEL issues.
@@ -1436,13 +1437,20 @@ void SPR12502() {
14361437
assertThat(expression.getValue(new NamedUser())).isEqualTo(NamedUser.class.getName());
14371438
}
14381439

1439-
@Test
1440-
void SPR12522() {
1440+
@Test // gh-17127, SPR-12522
1441+
void arraysAsListWithNoArguments() {
1442+
SpelExpressionParser parser = new SpelExpressionParser();
1443+
Expression expression = parser.parseExpression("T(java.util.Arrays).asList()");
1444+
List<?> value = expression.getValue(List.class);
1445+
assertThat(value).isEmpty();
1446+
}
1447+
1448+
@Test // gh-33013
1449+
void arraysAsListWithSingleEmptyStringArgument() {
14411450
SpelExpressionParser parser = new SpelExpressionParser();
14421451
Expression expression = parser.parseExpression("T(java.util.Arrays).asList('')");
1443-
Object value = expression.getValue();
1444-
assertThat(value).isInstanceOf(List.class);
1445-
assertThat(((List<?>) value)).isEmpty();
1452+
List<?> value = expression.getValue(List.class);
1453+
assertThat(value).asInstanceOf(list(String.class)).containsExactly("");
14461454
}
14471455

14481456
@Test

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ private static void populateMethodHandles(StandardEvaluationContext testContext)
100100
MethodHandle messageStaticFullyBound = messageStaticPartiallyBound
101101
.bindTo(new String[] { "prerecorded", "3", "Oh Hello World", "ignored"});
102102
testContext.registerFunction("messageStaticBound", messageStaticFullyBound);
103-
}
103+
104+
// #formatObjectVarargs(format, args...)
105+
MethodHandle formatObjectVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
106+
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
107+
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
108+
}
104109

105110
/**
106111
* Register some variables that can be referenced from the tests
@@ -154,4 +159,8 @@ public static String message(String template, String... args) {
154159
return template.formatted((Object[]) args);
155160
}
156161

162+
public static String formatObjectVarargs(String format, Object... args) {
163+
return String.format(format, args);
164+
}
165+
157166
}

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

+49
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.expression.spel;
1818

19+
import org.junit.jupiter.api.Disabled;
1920
import org.junit.jupiter.api.Test;
2021

2122
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -101,6 +102,54 @@ void functionWithVarargs() {
101102
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
102103
}
103104

105+
@Disabled("Disabled until bugs are reported and fixed")
106+
@Test
107+
void functionWithVarargsViaMethodHandle_CurrentlyFailing() {
108+
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
109+
110+
// No var-args and no conversion necessary
111+
evaluate("#formatObjectVarargs('x')", "x", String.class);
112+
113+
// No var-args but conversion necessary
114+
evaluate("#formatObjectVarargs(9)", "9", String.class);
115+
116+
// No conversion necessary
117+
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
118+
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
119+
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
120+
evaluate("#formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
121+
evaluate("#formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
122+
evaluate("#formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
123+
evaluate("#formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
124+
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
125+
}
126+
127+
@Test // gh-33013
128+
void functionWithVarargsViaMethodHandle() {
129+
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
130+
131+
// No conversion necessary
132+
evaluate("#formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
133+
evaluate("#formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
134+
evaluate("#formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
135+
evaluate("#formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
136+
137+
// Conversion necessary
138+
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
139+
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
140+
141+
// Individual string contains a comma with multiple varargs arguments
142+
evaluate("#formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
143+
evaluate("#formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class);
144+
evaluate("#formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class);
145+
146+
// Individual string contains a comma with single varargs argument.
147+
evaluate("#formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class);
148+
evaluate("#formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class);
149+
evaluate("#formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class);
150+
evaluate("#formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
151+
}
152+
104153
@Test
105154
void functionMethodMustBeStatic() throws Exception {
106155
SpelExpressionParser parser = new SpelExpressionParser();

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

+5
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ public String aVarargsMethod3(String str1, String... strings) {
217217
return str1 + "-" + String.join("-", strings);
218218
}
219219

220+
public String formatObjectVarargs(String format, Object... args) {
221+
return String.format(format, args);
222+
}
223+
224+
220225
public Inventor(String... strings) {
221226
if (strings.length > 0) {
222227
this.name = strings[0];

0 commit comments

Comments
 (0)