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

commit 5673d54e2ba162e860b5b19bdb571446e6773508
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 24 16:48:02 2026 +0200

    WW-5812 fix(conversion): select the date format by a strict whole-input 
parse
    
    DateConverter picked the first format that parsed the input leniently,
    then re-parsed it strictly and threw when that failed, never trying the
    remaining formats. Under nl-NL the dd-MM-y short pattern leniently
    accepts "2026-10-24", so a valid ISO date was rejected; a dd-MM-yyyy
    global format rejected it in every locale.
    
    The java.util.Date and java.sql.Timestamp branches now pick the first
    format that strictly parses the whole input, falling back to the
    previous lenient pick when none does. Format order is unchanged and any
    input that converted before converts to the same value, except a
    date-time under a date-only global format, which now keeps its time
    instead of the date-only prefix match dropping it.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../struts2/conversion/impl/DateConverter.java     | 60 +++++++++++++---------
 .../struts2/conversion/impl/DateConverterTest.java | 57 ++++++++++++++++++++
 2 files changed, 92 insertions(+), 25 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java 
b/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java
index 9486b5d35..9cc061a80 100644
--- a/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java
+++ b/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java
@@ -29,6 +29,7 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Member;
 import java.text.DateFormat;
 import java.text.ParseException;
+import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -59,38 +60,15 @@ public class DateConverter extends DefaultTypeConverter {
             if (java.sql.Time.class == toType) {
                 df = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
             } else if (java.sql.Timestamp.class == toType) {
-                Date check;
                 SimpleDateFormat dtfmt = (SimpleDateFormat) 
DateFormat.getDateTimeInstance(DateFormat.SHORT,
                         DateFormat.MEDIUM, locale);
                 SimpleDateFormat fullfmt = new 
SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT, locale);
 
                 SimpleDateFormat dfmt = (SimpleDateFormat) 
DateFormat.getDateInstance(DateFormat.SHORT, locale);
 
-                SimpleDateFormat[] fmts = { fullfmt, dtfmt, dfmt };
-                for (SimpleDateFormat fmt : fmts) {
-                    try {
-                        check = fmt.parse(sa);
-                        df = fmt;
-                        if (check != null) {
-                            break;
-                        }
-                    } catch (ParseException ignore) {
-                    }
-                }
+                df = selectFormat(new DateFormat[] { fullfmt, dtfmt, dfmt }, 
sa);
             } else if (java.util.Date.class == toType) {
-                Date check;
-                DateFormat[] dfs = getDateFormats(ActionContext.of(context), 
locale);
-
-                for (DateFormat df1 : dfs) {
-                    try {
-                        check = df1.parse(sa);
-                        df = df1;
-                        if (check != null) {
-                            break;
-                        }
-                    } catch (ParseException ignore) {
-                    }
-                }
+                df = selectFormat(getDateFormats(ActionContext.of(context), 
locale), sa);
             } else if (java.time.LocalDateTime.class == toType || 
java.time.LocalDate.class == toType
                     || java.time.LocalTime.class == toType) {
                 DateTimeFormatter dtf = null;
@@ -155,6 +133,38 @@ public class DateConverter extends DefaultTypeConverter {
         return result;
     }
 
+    /**
+     * Picks the first format that strictly parses the whole input, so a 
format that only matches leniently, or only
+     * matches a prefix, does not hide a later one that fits (an ISO date 
under a {@code dd-MM-y} locale, for example).
+     * Without such a format it falls back to the first lenient match, which 
the caller then re-parses strictly.
+     */
+    protected DateFormat selectFormat(DateFormat[] formats, String input) {
+        for (DateFormat format : formats) {
+            if (parsesWholeInputStrictly(format, input)) {
+                return format;
+            }
+        }
+        for (DateFormat format : formats) {
+            try {
+                format.parse(input);
+                return format;
+            } catch (ParseException ignore) {
+            }
+        }
+        return null;
+    }
+
+    private static boolean parsesWholeInputStrictly(DateFormat format, String 
input) {
+        boolean lenient = format.isLenient();
+        format.setLenient(false);
+        try {
+            ParsePosition position = new ParsePosition(0);
+            return format.parse(input, position) != null && 
position.getIndex() == input.length();
+        } finally {
+            format.setLenient(lenient);
+        }
+    }
+
     /**
      * {@code DateTextFieldInterceptor} binds a {@link Date} object, so a 
{@code java.time} property needs it converted
      * in the zone the interceptor parsed it in.
diff --git 
a/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java 
b/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java
index 4dda161dc..fdf712704 100644
--- 
a/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java
+++ 
b/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java
@@ -187,6 +187,63 @@ public class DateConverterTest extends 
StrutsInternalTestCase {
         assertTrue(value.toString().startsWith(LOCALTIME_STR));
     }
 
+    public void testIsoDateUnderALocaleWithDashSeparators() {
+        Object value = convertToDate("2026-10-24", 
Locale.forLanguageTag("nl-NL"), null);
+
+        assertEquals(LocalDateTime.of(2026, 10, 24, 0, 0), 
toLocalDateTime(value));
+    }
+
+    public void testIsoDateUnderADateOnlyGlobalFormat() {
+        Object value = convertToDate("2026-10-24", Locale.US, "dd-MM-yyyy");
+
+        assertEquals(LocalDateTime.of(2026, 10, 24, 0, 0), 
toLocalDateTime(value));
+    }
+
+    public void testIsoDateTimeUnderADateOnlyGlobalFormat() {
+        Object value = convertToDate("2026-10-24T10:15:30", Locale.US, 
"dd-MM-yyyy");
+
+        assertEquals(LocalDateTime.of(2026, 10, 24, 10, 15, 30), 
toLocalDateTime(value));
+    }
+
+    public void testDateTimeKeepsItsTimeUnderADateOnlyGlobalFormat() {
+        Object value = convertToDate("24-10-2026 10:15:30", 
Locale.forLanguageTag("nl-NL"), "dd-MM-yyyy");
+
+        assertEquals(LocalDateTime.of(2026, 10, 24, 10, 15, 30), 
toLocalDateTime(value));
+    }
+
+    public void testInvalidTimeIsStillRejected() {
+        try {
+            convertToDate("24-10-2026 25:00:00", 
Locale.forLanguageTag("nl-NL"), null);
+            fail("TypeConversionException expected");
+        } catch (TypeConversionException e) {
+            assertEquals(MESSAGE_PARSE_ERROR, e.getMessage());
+        }
+    }
+
+    public void testSqlTimestampUnderALocaleWithDashSeparators() {
+        ActionContext context = 
ActionContext.of().withLocale(Locale.forLanguageTag("nl-NL"));
+
+        Object value = new 
DateConverter().convertValue(context.getContextMap(), null, null, null, 
"24-10-2026 10:15:30.123", Timestamp.class);
+
+        assertEquals(Timestamp.valueOf("2026-10-24 10:15:30.123"), value);
+    }
+
+    private Object convertToDate(String input, Locale locale, String 
globalFormat) {
+        ValueStack stack = new StubValueStack();
+        Map<String, String> texts = new HashMap<>();
+        if (globalFormat != null) {
+            texts.put(org.apache.struts2.components.Date.DATETAG_PROPERTY, 
globalFormat);
+        }
+        stack.push(new StubTextProvider(texts));
+        ActionContext context = 
ActionContext.of().withLocale(locale).withValueStack(stack);
+
+        return new DateConverter().convertValue(context.getContextMap(), null, 
null, null, input, Date.class);
+    }
+
+    private static LocalDateTime toLocalDateTime(Object date) {
+        return LocalDateTime.ofInstant(((Date) date).toInstant(), 
ZoneId.systemDefault());
+    }
+
     public void testDateObjectToLocalDate() {
         Date date = Date.from(LocalDateTime.of(2020, 3, 20, 10, 
15).atZone(ZoneId.systemDefault()).toInstant());
 

Reply via email to