Merge branch '6.1.x'

This commit is contained in:
Sam Brannen 2024-07-12 17:37:51 +02:00
commit 09d8e4458c
6 changed files with 182 additions and 17 deletions

View File

@ -43,6 +43,7 @@ import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
@ -603,6 +604,12 @@ class DefaultConversionServiceTests {
assertThat(result).containsExactly(1, 2, 3);
}
@Test
void convertIntArrayToStringArray() {
String[] result = conversionService.convert(new int[] {1, 2, 3}, String[].class);
assertThat(result).containsExactly("1", "2", "3");
}
@Test
void convertIntegerArrayToIntegerArray() {
Integer[] result = conversionService.convert(new Integer[] {1, 2, 3}, Integer[].class);
@ -615,6 +622,12 @@ class DefaultConversionServiceTests {
assertThat(result).containsExactly(1, 2, 3);
}
@Test
void convertIntArrayToIntegerArray() {
Integer[] result = conversionService.convert(new int[] {1, 2}, Integer[].class);
assertThat(result).containsExactly(1, 2);
}
@Test
void convertObjectArrayToIntegerArray() {
Integer[] result = conversionService.convert(new Object[] {1, 2, 3}, Integer[].class);
@ -627,15 +640,34 @@ class DefaultConversionServiceTests {
assertThat(result).containsExactly(1, 2, 3);
}
@Disabled("Primitive array to Object[] conversion is not currently supported")
@Test
void convertByteArrayToWrapperArray() {
void convertIntArrayToObjectArray() {
Object[] result = conversionService.convert(new int[] {1, 2}, Object[].class);
assertThat(result).containsExactly(1, 2);
}
@Test
void convertIntArrayToFloatArray() {
Float[] result = conversionService.convert(new int[] {1, 2}, Float[].class);
assertThat(result).containsExactly(1.0F, 2.0F);
}
@Test
void convertIntArrayToPrimitiveFloatArray() {
float[] result = conversionService.convert(new int[] {1, 2}, float[].class);
assertThat(result).containsExactly(1.0F, 2.0F);
}
@Test
void convertPrimitiveByteArrayToByteWrapperArray() {
byte[] byteArray = {1, 2, 3};
Byte[] converted = conversionService.convert(byteArray, Byte[].class);
assertThat(converted).isEqualTo(new Byte[]{1, 2, 3});
}
@Test
void convertArrayToArrayAssignable() {
void convertIntArrayToIntArray() {
int[] result = conversionService.convert(new int[] {1, 2, 3}, int[].class);
assertThat(result).containsExactly(1, 2, 3);
}

View File

@ -383,11 +383,15 @@ public abstract class ReflectionHelper {
conversionOccurred |= (argument != arguments[i]);
}
Class<?> varArgClass = methodHandleType.lastParameterType();
ResolvableType varArgResolvableType = ResolvableType.forClass(varArgClass);
TypeDescriptor targetType = new TypeDescriptor(varArgResolvableType, varArgClass.componentType(), null);
TypeDescriptor componentTypeDesc = targetType.getElementTypeDescriptor();
Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
Class<?> varargsArrayClass = methodHandleType.lastParameterType();
// We use the wrapper type for a primitive varargs array, since we eventually
// need an Object array in order to invoke the MethodHandle in
// FunctionReference#executeFunctionViaMethodHandle().
Class<?> varargsComponentClass = ClassUtils.resolvePrimitiveIfNecessary(varargsArrayClass.componentType());
TypeDescriptor varargsArrayType = TypeDescriptor.array(TypeDescriptor.valueOf(varargsComponentClass));
Assert.state(varargsArrayType != null, "Array type must not be null for a varargs array");
TypeDescriptor varargsComponentType = varargsArrayType.getElementTypeDescriptor();
Assert.state(varargsComponentType != null, "Component type must not be null for a varargs array");
// If the target is varargs and there is just one more argument, then convert it here.
if (varargsPosition == arguments.length - 1) {
@ -395,17 +399,21 @@ public abstract class ReflectionHelper {
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
if (argument == null) {
// Perform the equivalent of GenericConversionService.convertNullSource() for a single argument.
if (componentTypeDesc.getObjectType() == Optional.class) {
if (varargsComponentType.getObjectType() == Optional.class) {
arguments[varargsPosition] = Optional.empty();
conversionOccurred = true;
}
}
// If the argument type is assignable to the varargs component type, there is no need to
// convert it or wrap it in an array. For example, using StringToArrayConverter to
// convert a String containing a comma would result in the String being split and
// repackaged in an array when it should be used as-is.
else if (!sourceType.isAssignableTo(componentTypeDesc)) {
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
// convert it. For example, using StringToArrayConverter to convert a String containing a
// comma would result in the String being split and repackaged in an array when it should
// be used as-is. Similarly, if the argument is an array that is assignable to the varargs
// array type, there is no need to convert it.
else if (!sourceType.isAssignableTo(varargsComponentType) ||
(sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType))) {
TypeDescriptor targetTypeToUse = (sourceType.isArray() ? varargsArrayType : varargsComponentType);
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
}
// Possible outcomes of the above if-else block:
// 1) the input argument was null, and nothing was done.
@ -423,7 +431,7 @@ public abstract class ReflectionHelper {
for (int i = varargsPosition; i < arguments.length; i++) {
Object argument = arguments[i];
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
arguments[i] = converter.convertValue(argument, sourceType, componentTypeDesc);
arguments[i] = converter.convertValue(argument, sourceType, varargsComponentType);
conversionOccurred |= (argument != arguments[i]);
}
}

View File

@ -23,6 +23,7 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
@ -245,6 +246,7 @@ class MethodInvocationTests extends AbstractExpressionTests {
evaluate("aVarargsMethod(1,'a',3.0d)", "[1, a, 3.0]", String.class); // first and last need conversion
evaluate("aVarargsMethod(new String[]{'a','b','c'})", "[a, b, c]", String.class);
evaluate("aVarargsMethod(new String[]{})", "[]", String.class);
evaluate("aVarargsMethod(new int[]{1, 2, 3})", "[1, 2, 3]", String.class); // needs int[] to String[] conversion
evaluate("aVarargsMethod(null)", "[null]", String.class);
evaluate("aVarargsMethod(null,'a')", "[null, a]", String.class);
evaluate("aVarargsMethod('a',null,'b')", "[a, null, b]", String.class);
@ -320,6 +322,7 @@ class MethodInvocationTests extends AbstractExpressionTests {
// Conversion necessary
evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
evaluate("formatObjectVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class);
// Individual string contains a comma with multiple varargs arguments
evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
@ -333,6 +336,34 @@ class MethodInvocationTests extends AbstractExpressionTests {
evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
}
@Test
void testVarargsWithPrimitiveArrayType() {
// Calling 'public String formatPrimitiveVarargs(String format, int... nums)' -> effectively String.format(format, args)
// No var-args and no conversion necessary
evaluate("formatPrimitiveVarargs(9)", "9", String.class);
// No var-args but conversion necessary
evaluate("formatPrimitiveVarargs('7')", "7", String.class);
// No conversion necessary
evaluate("formatPrimitiveVarargs('x -> %s', 9)", "x -> 9", String.class);
evaluate("formatPrimitiveVarargs('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class);
evaluate("formatPrimitiveVarargs('x -> %s', new int[]{1})", "x -> 1", String.class);
evaluate("formatPrimitiveVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class);
// Conversion necessary
evaluate("formatPrimitiveVarargs('x -> %s %s', '2', '3')", "x -> 2 3", String.class);
evaluate("formatPrimitiveVarargs('x -> %s %s', '2', 3.0d)", "x -> 2 3", String.class);
}
@Disabled("Primitive array to Object[] conversion is not currently supported")
@Test
void testVarargsWithPrimitiveArrayToObjectArrayConversion() {
evaluate("formatObjectVarargs('x -> %s %s %s', new short[]{1, 2, 3})", "x -> 1 2 3", String.class); // short[] to Object[]
evaluate("formatObjectVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to Object[]
}
@Test
void testVarargsOptionalInvocation() {
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'

View File

@ -66,6 +66,8 @@ class TestScenarioCreator {
TestScenarioCreator.class.getDeclaredMethod("varargsFunction", String[].class));
testContext.registerFunction("varargsFunction2",
TestScenarioCreator.class.getDeclaredMethod("varargsFunction2", int.class, String[].class));
testContext.registerFunction("varargsObjectFunction",
TestScenarioCreator.class.getDeclaredMethod("varargsObjectFunction", Object[].class));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
@ -106,6 +108,11 @@ class TestScenarioCreator {
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
// #formatObjectVarargs(format, args...)
MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class));
testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs);
// #add(int, int)
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"add", MethodType.methodType(int.class, int.class, int.class));
@ -160,6 +167,10 @@ class TestScenarioCreator {
return i + "-" + Arrays.toString(strings);
}
public static String varargsObjectFunction(Object... args) {
return Arrays.toString(args);
}
public static String message(String template, String... args) {
return template.formatted((Object[]) args);
}
@ -168,6 +179,14 @@ class TestScenarioCreator {
return String.format(format, args);
}
public static String formatPrimitiveVarargs(String format, int... nums) {
Object[] args = new Object[nums.length];
for (int i = 0; i < nums.length; i++) {
args[i] = nums[i];
}
return String.format(format, args);
}
public static int add(int x, int y) {
return x + y;
}

View File

@ -16,6 +16,7 @@
package org.springframework.expression.spel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -80,9 +81,11 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#varargsFunction(new String[0])", "[]", String.class);
evaluate("#varargsFunction('a')", "[a]", String.class);
evaluate("#varargsFunction('a','b','c')", "[a, b, c]", String.class);
evaluate("#varargsFunction(new String[]{'a','b','c'})", "[a, b, c]", String.class);
// Conversion from int to String
evaluate("#varargsFunction(25)", "[25]", String.class);
evaluate("#varargsFunction('b',25)", "[b, 25]", String.class);
evaluate("#varargsFunction(new int[]{1, 2, 3})", "[1, 2, 3]", String.class);
// Strings that contain a comma
evaluate("#varargsFunction('a,b')", "[a,b]", String.class);
evaluate("#varargsFunction('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
@ -103,6 +106,21 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
// null values
evaluate("#varargsFunction2(9,null)", "9-[null]", String.class);
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
evaluate("#varargsObjectFunction()", "[]", String.class);
evaluate("#varargsObjectFunction(new String[0])", "[]", String.class);
evaluate("#varargsObjectFunction('a')", "[a]", String.class);
evaluate("#varargsObjectFunction('a','b','c')", "[a, b, c]", String.class);
evaluate("#varargsObjectFunction(new String[]{'a','b','c'})", "[a, b, c]", String.class);
// Conversion from int to String
evaluate("#varargsObjectFunction(25)", "[25]", String.class);
evaluate("#varargsObjectFunction('b',25)", "[b, 25]", String.class);
// Strings that contain a comma
evaluate("#varargsObjectFunction('a,b')", "[a,b]", String.class);
evaluate("#varargsObjectFunction('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
// null values
evaluate("#varargsObjectFunction(null)", "[null]", String.class);
evaluate("#varargsObjectFunction('a',null,'b')", "[a, null, b]", String.class);
}
@Test // gh-33013
@ -110,17 +128,25 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
// No var-args and no conversion necessary
evaluate("#message('x')", "x", String.class);
evaluate("#formatObjectVarargs('x')", "x", String.class);
// No var-args but conversion necessary
evaluate("#message(9)", "9", String.class);
evaluate("#formatObjectVarargs(9)", "9", String.class);
// No conversion necessary
evaluate("#add(3, 4)", 7, Integer.class);
evaluate("#message('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
evaluate("#formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
evaluate("#message('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class); // Object[] instanceof Object[]
evaluate("#message('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class); // String[] instanceof Object[]
evaluate("#message('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] instanceof Object[]
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class); // Integer instanceof Object
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0F)", "x -> a 3.0", String.class); // String/Float instanceof Object
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
@ -131,9 +157,12 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
// Conversion necessary
evaluate("#add('2', 5.0)", 7, Integer.class);
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
evaluate("#add('2', 5.0)", 7, Integer.class); // String/Double to Integer
evaluate("#messageStatic('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class); // Integer to String
evaluate("#messageStatic('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to String[]
evaluate("#messageStatic('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to String[]
evaluate("#messageStatic('x -> %s %s %s', new short[]{1, 2, 3})", "x -> 1 2 3", String.class); // short[] to String[]
evaluate("#formatObjectVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to String[]
// Individual string contains a comma with multiple varargs arguments
evaluate("#formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
@ -147,6 +176,44 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
}
@Test
void functionWithPrimitiveVarargsViaMethodHandle() {
// Calling 'public String formatPrimitiveVarargs(String format, int... nums)' -> effectively String.format(format, args)
// No var-args and no conversion necessary
evaluate("#formatPrimitiveVarargs(9)", "9", String.class);
// No var-args but conversion necessary
evaluate("#formatPrimitiveVarargs('7')", "7", String.class);
// No conversion necessary
evaluate("#formatPrimitiveVarargs('x -> %s', 9)", "x -> 9", String.class);
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class);
evaluate("#formatPrimitiveVarargs('x -> %s', new int[]{1})", "x -> 1", String.class);
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class);
// Conversion necessary
evaluate("#formatPrimitiveVarargs('x -> %s %s', '2', '3')", "x -> 2 3", String.class); // String to int
evaluate("#formatPrimitiveVarargs('x -> %s %s', '2', 3.0F)", "x -> 2 3", String.class); // String/Float to int
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to int[]
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new String[]{'1', '2', '3'})", "x -> 1 2 3", String.class); // String[] to int[]
}
@Disabled("Primitive array to Object[] conversion is not currently supported")
@Test
void functionFromMethodWithVarargsAndPrimitiveArrayToObjectArrayConversion() {
evaluate("#varargsObjectFunction(new short[]{1, 2, 3})", "[1, 2, 3]", String.class); // short[] to Object[]
evaluate("#varargsObjectFunction(new int[]{1, 2, 3})", "[1, 2, 3]", String.class); // int[] to Object[]
}
@Disabled("Primitive array to Object[] conversion is not currently supported")
@Test
void functionFromMethodHandleWithVarargsAndPrimitiveArrayToObjectArrayConversion() {
evaluate("#message('x -> %s %s %s', new short[]{1, 2, 3})", "x -> 1 2 3", String.class); // short[] to Object[]
evaluate("#message('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to Object[]
evaluate("#formatObjectVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to Object[]
}
@Test
void functionMethodMustBeStatic() throws Exception {
SpelExpressionParser parser = new SpelExpressionParser();

View File

@ -221,6 +221,14 @@ public class Inventor {
return String.format(format, args);
}
public String formatPrimitiveVarargs(String format, int... nums) {
Object[] args = new Object[nums.length];
for (int i = 0; i < nums.length; i++) {
args[i] = nums[i];
}
return String.format(format, args);
}
public Inventor(String... strings) {
if (strings.length > 0) {