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