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 53eb6f748ab22aaad5a9dbb781f67e6166288911 Author: Lukasz Lenart <[email protected]> AuthorDate: Sat Oct 24 16:18:20 2026 +0200 WW-5810 fix(javatemplates): render java.time in the interceptor's calendar DateTimeFormatter always uses the ISO calendar, while DateTextFieldInterceptor parses with SimpleDateFormat, which follows the default locale's calendar. Under th-TH a LocalDate of 2026-10-24 rendered year 2026 and bound back as 1483-11-02. java.time values are now converted to a Date and formatted with SimpleDateFormat like the Date path; a field the type does not have still renders empty. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../views/java/simple/DateTextFieldHandler.java | 48 +++++++++++++++++----- .../views/java/simple/DateTextFieldTest.java | 38 +++++++++++++++++ 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java index 007d4f2b5..edc255faf 100644 --- a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java +++ b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java @@ -20,9 +20,12 @@ package org.apache.struts2.views.java.simple; import java.io.IOException; import java.text.SimpleDateFormat; -import java.time.format.DateTimeFormatter; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; -import java.time.temporal.UnsupportedTemporalTypeException; import java.util.Date; import java.util.Map; @@ -104,17 +107,40 @@ public class DateTextFieldHandler extends AbstractTagHandler implements TagGener } protected String formatValue(Object value, DateWord dateWord) { - if (value instanceof Date date) { - return new SimpleDateFormat(dateWord.getDateType()).format(date); + Date date = toDate(value); + if (date == null || (value instanceof TemporalAccessor temporal && !temporal.isSupported(toField(dateWord)))) { + return null; } - if (value instanceof TemporalAccessor temporal) { - try { - return DateTimeFormatter.ofPattern(dateWord.getDateType()).format(temporal); - } catch (UnsupportedTemporalTypeException e) { - return null; - } + // DateTextFieldInterceptor parses with SimpleDateFormat, so render in its calendar (Buddhist under th-TH) + return new SimpleDateFormat(dateWord.getDateType()).format(date); + } + + private static Date toDate(Object value) { + LocalDateTime dateTime; + if (value instanceof Date date) { + return date; + } else if (value instanceof LocalDate localDate) { + dateTime = localDate.atStartOfDay(); + } else if (value instanceof LocalDateTime localDateTime) { + dateTime = localDateTime; + } else if (value instanceof LocalTime localTime) { + dateTime = localTime.atDate(LocalDate.EPOCH); + } else { + return null; } - return null; + return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + private static ChronoField toField(DateWord dateWord) { + return switch (dateWord) { + case S -> ChronoField.MILLI_OF_SECOND; + case s -> ChronoField.SECOND_OF_MINUTE; + case m -> ChronoField.MINUTE_OF_HOUR; + case H -> ChronoField.HOUR_OF_DAY; + case d -> ChronoField.DAY_OF_MONTH; + case M -> ChronoField.MONTH_OF_YEAR; + case y -> ChronoField.YEAR; + }; } diff --git a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java index 07c24bf98..8545ae44a 100644 --- a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java +++ b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java @@ -20,7 +20,12 @@ */ package org.apache.struts2.views.java.simple; +import org.apache.struts2.ActionContext; import org.apache.struts2.components.DateTextField; +import org.apache.struts2.conversion.impl.DateConverter; +import org.apache.struts2.dispatcher.HttpParameters; +import org.apache.struts2.interceptor.DateTextFieldInterceptor; +import org.apache.struts2.mock.MockActionInvocation; import org.apache.struts2.components.UIBean; import org.apache.struts2.interceptor.csp.CspNonceSource; import org.apache.struts2.interceptor.csp.StrutsCspNonceReader; @@ -30,6 +35,11 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class DateTextFieldTest extends AbstractCommonAttributesTest { @@ -91,6 +101,34 @@ public class DateTextFieldTest extends AbstractCommonAttributesTest { + "</div>"), output); } + public void testLocalDateRoundTripsUnderABuddhistCalendarLocale() throws Exception { + Locale previous = Locale.getDefault(Locale.Category.FORMAT); + Locale.setDefault(Locale.Category.FORMAT, Locale.forLanguageTag("th-TH")); + try { + String output = render("yyyy-MM-dd", LocalDate.of(2026, 10, 24)); + + assertEquals(LocalDate.of(2026, 10, 24), submit(output, LocalDate.class)); + } finally { + Locale.setDefault(Locale.Category.FORMAT, previous); + } + } + + private Object submit(String output, Class<?> toType) throws Exception { + Map<String, Object> params = new HashMap<>(); + Matcher field = Pattern.compile("name=\"(__[a-z]+_name)\" value=\"([^\"]*)\"").matcher(output); + while (field.find()) { + params.put(field.group(1), new String[]{field.group(2)}); + } + ActionContext actionContext = ActionContext.of().withParameters(HttpParameters.create(params).build()); + MockActionInvocation invocation = new MockActionInvocation(); + invocation.setInvocationContext(actionContext); + + new DateTextFieldInterceptor().intercept(invocation); + + Object bound = actionContext.getParameters().get("name").getObject(); + return new DateConverter().convertValue(actionContext.getContextMap(), null, null, null, bound, toType); + } + private String render(String format, Object value) { tag.setId("id"); tag.setName("name");
