diff --git a/core/src/main/java/org/apache/struts2/conversion/impl/XWorkBasicConverter.java b/core/src/main/java/org/apache/struts2/conversion/impl/XWorkBasicConverter.java index a19ae2068f..db37cea9ba 100644 --- a/core/src/main/java/org/apache/struts2/conversion/impl/XWorkBasicConverter.java +++ b/core/src/main/java/org/apache/struts2/conversion/impl/XWorkBasicConverter.java @@ -29,6 +29,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -83,6 +84,8 @@ public Object convertValue(Map context, Object o, Member member, return value; } + Object input = trimSurroundingWhitespace(value, toType); + if (toType == String.class) { /* the code below has been disabled as it causes sideffects in Struts2 (XW-512) // if input (value) is a number then use special conversion method (XW-490) @@ -94,54 +97,54 @@ public Object convertValue(Map context, Object o, Member member, } }*/ // okay use default string conversion - result = doConvertToString(context, value); + result = doConvertToString(context, input); } else if (toType == boolean.class) { - result = doConvertToBoolean(value); + result = doConvertToBoolean(input); } else if (toType == Boolean.class) { - result = doConvertToBoolean(value); + result = "".equals(input) ? null : doConvertToBoolean(input); } else if (toType.isArray()) { - result = doConvertToArray(context, o, member, propertyName, value, toType); + result = doConvertToArray(context, o, member, propertyName, input, toType); } else if (Date.class.isAssignableFrom(toType)) { - result = doConvertToDate(context, value, toType); + result = doConvertToDate(context, input, toType); } else if (LocalDate.class.isAssignableFrom(toType)) { - result = doConvertToDate(context, value, toType); + result = doConvertToDate(context, input, toType); } else if (LocalDateTime.class.isAssignableFrom(toType)) { - result = doConvertToDate(context, value, toType); + result = doConvertToDate(context, input, toType); } else if (LocalTime.class.isAssignableFrom(toType)) { - result = doConvertToDate(context, value, toType); + result = doConvertToDate(context, input, toType); } else if (OffsetDateTime.class.isAssignableFrom(toType)) { - result = doConvertToDate(context, value, toType); + result = doConvertToDate(context, input, toType); } else if (Calendar.class.isAssignableFrom(toType)) { - result = doConvertToCalendar(context, value); + result = doConvertToCalendar(context, input); } else if (Collection.class.isAssignableFrom(toType)) { - result = doConvertToCollection(context, o, member, propertyName, value, toType); + result = doConvertToCollection(context, o, member, propertyName, input, toType); } else if (toType == Character.class) { - result = doConvertToCharacter(value); + result = doConvertToCharacter(input); } else if (toType == char.class) { - result = doConvertToCharacter(value); + result = doConvertToCharacter(input); } else if (Number.class.isAssignableFrom(toType) || toType.isPrimitive()) { - result = doConvertToNumber(context, value, toType); + result = doConvertToNumber(context, input, toType); } else if (toType == Class.class) { - result = doConvertToClass(value); + result = doConvertToClass(input); } if (result == null) { - if (value instanceof Object[] array) { + if (input instanceof Object[] array) { if (array.length >= 1) { - value = array[0]; + input = array[0]; } else { - value = null; + input = null; } // let's try to convert the first element only - result = convertValue(context, o, member, propertyName, value, toType); - } else if (!"".equals(value)) { // we've already tried the types we know - result = super.convertValue(context, value, toType); + result = convertValue(context, o, member, propertyName, input, toType); + } else if (!"".equals(input)) { // we've already tried the types we know + result = super.convertValue(context, input, toType); } - if (result == null && value != null && !"".equals(value)) { - throw new TypeConversionException("Cannot create type " + toType + " from value " + value); + if (result == null && input != null && !"".equals(input)) { + throw new TypeConversionException("Cannot create type " + toType + " from value " + input); } } @@ -159,9 +162,24 @@ private Object doConvertToCalendar(Map context, Object value) { return result; } + private Object trimSurroundingWhitespace(Object value, Class toType) { + if (toType.isArray() || Collection.class.isAssignableFrom(toType) || toType == Character.class || toType == char.class) { + return value; + } + if (value instanceof String str) { + return str.trim(); + } + if (value instanceof String[] values) { + return Arrays.stream(values).map(item -> item == null ? null : item.trim()).toArray(String[]::new); + } + return value; + } + private Object doConvertToCharacter(Object value) { if (value instanceof String cStr) { - return !cStr.isEmpty() ? cStr.charAt(0) : null; + String trimmed = cStr.trim(); + String source = trimmed.isEmpty() ? cStr : trimmed; + return !source.isEmpty() ? source.charAt(0) : null; } return null; } diff --git a/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java b/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java index 753d28ef4f..a89a12adc0 100644 --- a/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java +++ b/core/src/test/java/org/apache/struts2/conversion/impl/XWorkConverterTest.java @@ -719,6 +719,72 @@ public void testStringToEnum() { assertEquals(OgnlRuntime.NoConversionPossible, converter.convertValue(context, null, null, null, "red", FurColor.class)); } + public void testStringToNumberIgnoresSurroundingWhitespace() { + assertEquals(42L, converter.convertValue(context, null, null, null, " 42 ", Long.class)); + assertEquals(42L, converter.convertValue(context, null, null, null, new String[]{"\t42\n"}, Long.class)); + assertEquals(42, converter.convertValue(context, null, null, null, " 42 ", Integer.class)); + assertEquals((short) 42, converter.convertValue(context, null, null, null, " 42 ", Short.class)); + assertEquals(4.2f, converter.convertValue(context, null, null, null, " 4.2 ", Float.class)); + assertEquals(4.2, converter.convertValue(context, null, null, null, " 4.2 ", Double.class)); + assertEquals(4.2, converter.convertValue(context, null, null, null, " 4.2 ", double.class)); + assertEquals(new BigInteger("42"), converter.convertValue(context, null, null, null, " 42 ", BigInteger.class)); + assertEquals(new BigDecimal("4.2"), converter.convertValue(context, null, null, null, " 4.2 ", BigDecimal.class)); + } + + public void testStringToEnumIgnoresSurroundingWhitespace() { + assertEquals(FurColor.BLACK, converter.convertValue(context, null, null, null, " BLACK ", FurColor.class)); + assertEquals(FurColor.BLACK, converter.convertValue(context, null, null, null, new String[]{"BLACK\t"}, FurColor.class)); + } + + public void testStringToBooleanIgnoresSurroundingWhitespace() { + assertEquals(Boolean.TRUE, converter.convertValue(context, null, null, null, " true ", Boolean.class)); + assertEquals(Boolean.TRUE, converter.convertValue(context, null, null, null, " true ", boolean.class)); + } + + public void testStringToCharacterIgnoresSurroundingWhitespace() { + assertEquals('x', converter.convertValue(context, null, null, null, " x", Character.class)); + assertEquals('x', converter.convertValue(context, null, null, null, " x", char.class)); + } + + public void testStringToJavaTimeIgnoresSurroundingWhitespace() { + assertEquals(LocalDate.of(2001, 1, 10), converter.convertValue(context, null, null, null, " 2001-01-10 ", LocalDate.class)); + assertEquals(LocalDateTime.of(2001, 1, 10, 1, 2, 3), + converter.convertValue(context, null, null, null, " 2001-01-10T01:02:03 ", LocalDateTime.class)); + } + + public void testStringArrayToArrayIgnoresSurroundingWhitespace() { + assertArrayEquals(new Long[]{1L, 2L}, (Long[]) converter.convertValue(context, null, null, null, new String[]{" 1 ", "2 "}, Long[].class)); + } + + public void testWhitespaceOnlyConvertsToNullForBoxedTypes() { + assertNull(converter.convertValue(context, null, null, null, " ", Long.class)); + assertNull(converter.convertValue(context, null, null, null, " ", Integer.class)); + assertNull(converter.convertValue(context, null, null, null, " ", Double.class)); + assertNull(converter.convertValue(context, null, null, null, " ", BigDecimal.class)); + assertNull(converter.convertValue(context, null, null, null, " ", FurColor.class)); + assertNull(converter.convertValue(context, null, null, null, " ", Boolean.class)); + assertNull(converter.convertValue(context, null, null, null, " ", LocalDate.class)); + } + + public void testWhitespaceOnlyIsConversionErrorForIntegralPrimitives() { + assertEquals(OgnlRuntime.NoConversionPossible, converter.convertValue(context, null, null, null, " ", long.class)); + assertEquals(OgnlRuntime.NoConversionPossible, converter.convertValue(context, null, null, null, " ", int.class)); + } + + public void testStringTargetsKeepSurroundingWhitespace() { + assertEquals(" keep ", converter.convertValue(context, null, null, null, " keep ", String.class)); + assertEquals(" keep ", converter.convertValue(context, null, null, null, " keep ", Object.class)); + assertArrayEquals(new String[]{" keep "}, (String[]) converter.convertValue(context, null, null, null, new String[]{" keep "}, String[].class)); + } + + public void testCustomTypeConverterReceivesUntrimmedValue() { + XWorkConverter freshConverter = container.inject(XWorkConverter.class); + freshConverter.setTypeConverterHolder(new StrutsTypeConverterHolder()); + freshConverter.registerConverter(FurColor.class.getName(), (ctx, target, member, property, value, toType) -> value); + + assertEquals(" BLACK ", freshConverter.convertValue(context, null, null, null, " BLACK ", FurColor.class)); + } + // Testing for null result on non-primitive Number types supplied as empty String or public void testNotPrimitiveDefaultsToNull() { assertNull(converter.convertValue(context, null, null, null, null, Double.class));