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));

Reply via email to