This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 6b2c31575 WW-3666 Trim surrounding whitespace before converting to
non-String types (#2039)
6b2c31575 is described below
commit 6b2c315751b825638ead43bf14bdb4b93442e807
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 17 10:43:44 2026 +0200
WW-3666 Trim surrounding whitespace before converting to non-String types
(#2039)
* WW-3666 feat(core): trim surrounding whitespace before converting to
non-String types
Padded request values such as " 42 " or " ACTIVE " were rejected by the
locale-aware number, enum and java.time paths, while primitive int/long
already trimmed. Boolean and Character silently misread them (" true " was
false, " x" was ' ').
XWorkBasicConverter now trims String and String[] values for scalar targets.
String/Object targets and converters registered for a type still receive the
raw value; arrays and collections trim per element. Whitespace-only input
follows the existing "" path, so boxed Boolean blank becomes null and a
whitespace-only Character keeps its first character.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* WW-3666 refactor(core): keep the trimmed value in a local instead of
reassigning the parameter
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../conversion/impl/XWorkBasicConverter.java | 66 ++++++++++++++--------
.../conversion/impl/XWorkConverterTest.java | 66 ++++++++++++++++++++++
2 files changed, 108 insertions(+), 24 deletions(-)
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 a19ae2068..db37cea9b 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.LocalDate;
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 class XWorkBasicConverter extends DefaultTypeConverter
{
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 class XWorkBasicConverter extends
DefaultTypeConverter {
}
}*/
// 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 @@ public class XWorkBasicConverter extends
DefaultTypeConverter {
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 753d28ef4..a89a12adc 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 class XWorkConverterTest extends XWorkTestCase {
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));