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 33a4dac3721e1ef763c05181813a9939baafbc05
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 24 15:54:02 2026 +0200

    WW-5810 feat(tags): support java.time types in datetextfield
    
    DateConverter now converts a java.util.Date object into LocalDate,
    LocalDateTime or LocalTime in the server's default time zone. That is
    the value DateTextFieldInterceptor binds, so the JavaTemplates split
    fields can now fill java.time properties; Date targets are unchanged.
    
    The JavaTemplates handler formats java.time values per field instead of
    casting to Date, leaving a field the type does not have empty. The html5
    theme renders the date part of a time-bearing value, which the browser
    otherwise rejects; LocalDate already rendered as ISO and is now pinned.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/struts2/components/DateTextField.java   |  7 ++-
 .../struts2/conversion/impl/DateConverter.java     | 23 ++++++-
 .../struts2/components/ConstraintAction.java       | 22 +++++++
 .../struts2/conversion/impl/DateConverterTest.java | 40 +++++++++++++
 .../interceptor/DateTextFieldInterceptorTest.java  | 30 ++++++++++
 .../struts2/views/jsp/ui/DateTextFieldTest.java    | 27 ++++++++-
 .../struts2/views/jsp/ui/DateTextField-5-html5.txt |  1 +
 .../struts2/views/jsp/ui/DateTextField-6-html5.txt |  1 +
 .../views/java/simple/DateTextFieldHandler.java    | 24 ++++++--
 .../views/java/simple/DateTextFieldTest.java       | 70 ++++++++++++++++++++++
 10 files changed, 236 insertions(+), 9 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/components/DateTextField.java 
b/core/src/main/java/org/apache/struts2/components/DateTextField.java
index aaa04410d..4647b36af 100644
--- a/core/src/main/java/org/apache/struts2/components/DateTextField.java
+++ b/core/src/main/java/org/apache/struts2/components/DateTextField.java
@@ -26,6 +26,8 @@ import 
org.apache.struts2.views.annotations.StrutsTagAttribute;
 
 import java.time.Instant;
 import java.time.ZoneId;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
 import java.util.Date;
 
 import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
@@ -59,8 +61,11 @@ public class DateTextField extends UIBean {
             addParameter("format", findValue(format, String.class));
         }
 
-        if (getAttributes().get(ATTR_NAME_VALUE) instanceof Date date) {
+        Object value = getAttributes().get(ATTR_NAME_VALUE);
+        if (value instanceof Date date) {
             addParameter("isoDate", 
ISO_LOCAL_DATE.format(Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault())));
+        } else if (value instanceof TemporalAccessor temporal && 
temporal.isSupported(ChronoField.EPOCH_DAY)) {
+            addParameter("isoDate", ISO_LOCAL_DATE.format(temporal));
         }
     }
 
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 037ca032f..9486b5d35 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
@@ -30,10 +30,12 @@ import java.lang.reflect.Member;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.OffsetDateTime;
+import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.time.temporal.TemporalAccessor;
@@ -147,12 +149,29 @@ public class DateConverter extends DefaultTypeConverter {
                 throw new TypeConversionException("Could not parse date", e);
             }
 
-        } else if (Date.class.isAssignableFrom(value.getClass())) {
-            result = (Date) value;
+        } else if (value instanceof Date date) {
+            return convertDate(date, toType);
         }
         return result;
     }
 
+    /**
+     * {@code DateTextFieldInterceptor} binds a {@link Date} object, so a 
{@code java.time} property needs it converted
+     * in the zone the interceptor parsed it in.
+     */
+    protected Object convertDate(Date date, Class<?> toType) {
+        // java.sql.Date#toInstant() throws, so go through the epoch millis
+        LocalDateTime dateTime = 
LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), 
ZoneId.systemDefault());
+        if (LocalDate.class == toType) {
+            return dateTime.toLocalDate();
+        } else if (LocalDateTime.class == toType) {
+            return dateTime;
+        } else if (LocalTime.class == toType) {
+            return dateTime.toLocalTime();
+        }
+        return date;
+    }
+
     /**
      * The user defined global date format, see
      * {@link org.apache.struts2.components.Date#DATETAG_PROPERTY}
diff --git 
a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java 
b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
index 8f2a5dc37..59dee88bc 100644
--- a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
+++ b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
@@ -21,6 +21,8 @@ package org.apache.struts2.components;
 import org.apache.struts2.ActionSupport;
 import org.apache.struts2.interceptor.parameter.StrutsParameter;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.Date;
 
 public class ConstraintAction extends ActionSupport {
@@ -37,6 +39,8 @@ public class ConstraintAction extends ActionSupport {
     private int priority;
     private Object attachment;
     private Date birthday;
+    private LocalDate dueDate;
+    private LocalDateTime meetingAt;
 
     public String getUsername() {
         return username;
@@ -118,6 +122,24 @@ public class ConstraintAction extends ActionSupport {
         this.birthday = birthday;
     }
 
+    public LocalDate getDueDate() {
+        return dueDate;
+    }
+
+    @StrutsParameter
+    public void setDueDate(LocalDate dueDate) {
+        this.dueDate = dueDate;
+    }
+
+    public LocalDateTime getMeetingAt() {
+        return meetingAt;
+    }
+
+    @StrutsParameter
+    public void setMeetingAt(LocalDateTime meetingAt) {
+        this.meetingAt = meetingAt;
+    }
+
     public ConstraintUser getOwner() {
         return owner;
     }
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 5130b9f95..4dda161dc 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
@@ -31,6 +31,7 @@ import java.text.DateFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Locale;
@@ -186,6 +187,45 @@ public class DateConverterTest extends 
StrutsInternalTestCase {
         assertTrue(value.toString().startsWith(LOCALTIME_STR));
     }
 
+    public void testDateObjectToLocalDate() {
+        Date date = Date.from(LocalDateTime.of(2020, 3, 20, 10, 
15).atZone(ZoneId.systemDefault()).toInstant());
+
+        Object value = new 
DateConverter().convertValue(ActionContext.of().getContextMap(), null, null, 
null, date, LocalDate.class);
+
+        assertEquals(LocalDate.of(2020, 3, 20), value);
+    }
+
+    public void testSqlDateObjectToLocalDate() {
+        Object value = new 
DateConverter().convertValue(ActionContext.of().getContextMap(), null, null, 
null,
+            java.sql.Date.valueOf(LocalDate.of(2020, 3, 20)), LocalDate.class);
+
+        assertEquals(LocalDate.of(2020, 3, 20), value);
+    }
+
+    public void testDateObjectToLocalDateTime() {
+        Date date = Date.from(LocalDateTime.of(2020, 3, 20, 10, 
15).atZone(ZoneId.systemDefault()).toInstant());
+
+        Object value = new 
DateConverter().convertValue(ActionContext.of().getContextMap(), null, null, 
null, date, LocalDateTime.class);
+
+        assertEquals(LocalDateTime.of(2020, 3, 20, 10, 15), value);
+    }
+
+    public void testDateObjectToLocalTime() {
+        Date date = Date.from(LocalDateTime.of(2020, 3, 20, 10, 
15).atZone(ZoneId.systemDefault()).toInstant());
+
+        Object value = new 
DateConverter().convertValue(ActionContext.of().getContextMap(), null, null, 
null, date, LocalTime.class);
+
+        assertEquals(LocalTime.of(10, 15), value);
+    }
+
+    public void testDateObjectToDateIsUnchanged() {
+        Date date = new Date();
+
+        Object value = new 
DateConverter().convertValue(ActionContext.of().getContextMap(), null, null, 
null, date, Date.class);
+
+        assertSame(date, value);
+    }
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/DateTextFieldInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/DateTextFieldInterceptorTest.java
index afc398580..861a31458 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/DateTextFieldInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/DateTextFieldInterceptorTest.java
@@ -19,6 +19,7 @@
 package org.apache.struts2.interceptor;
 
 import java.text.SimpleDateFormat;
+import java.time.LocalDate;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -28,6 +29,7 @@ import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.mock.MockActionInvocation;
 import org.apache.struts2.dispatcher.HttpParameters;
+import org.apache.struts2.util.ValueStack;
 
 /**
  * Unit test for DateTextFieldInterceptor.
@@ -78,4 +80,32 @@ public class DateTextFieldInterceptorTest extends 
StrutsInternalTestCase {
                assertEquals(date, parameters.get("name").getObject());
        }
 
+       public void testBindsToALocalDateProperty() throws Exception {
+               param.put("__year_birthday", new String[]{"2000"});
+               param.put("__month_birthday", new String[]{"06"});
+               param.put("__day_birthday", new String[]{"15"});
+               
ActionContext.getContext().withParameters(HttpParameters.create(param).build());
+
+               interceptor.intercept(ai);
+
+               Person person = new Person();
+               ValueStack stack = ActionContext.getContext().getValueStack();
+               stack.push(person);
+               stack.setValue("birthday", 
ai.getInvocationContext().getParameters().get("birthday").getObject());
+
+               assertEquals(LocalDate.of(2000, 6, 15), person.getBirthday());
+       }
+
+       public static class Person {
+               private LocalDate birthday;
+
+               public LocalDate getBirthday() {
+                       return birthday;
+               }
+
+               public void setBirthday(LocalDate birthday) {
+                       this.birthday = birthday;
+               }
+       }
+
 }
diff --git 
a/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java 
b/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java
index 0c177f643..b8676f5b7 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java
@@ -23,6 +23,7 @@ import org.apache.struts2.components.ConstraintAction;
 import org.apache.struts2.views.jsp.AbstractUITagTest;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.Date;
 
@@ -61,6 +62,26 @@ public class DateTextFieldTest extends AbstractUITagTest {
         
verify(DateTextFieldTag.class.getResource("DateTextField-1-html5.txt"));
     }
 
+    public void testHtml5RendersALocalDateAsIso() throws Exception {
+        ((ConstraintAction) action).setDueDate(LocalDate.of(2026, 10, 24));
+
+        DateTextFieldTag tag = html5Tag("dueDate");
+        tag.doStartTag();
+        tag.doEndTag();
+
+        
verify(DateTextFieldTag.class.getResource("DateTextField-5-html5.txt"));
+    }
+
+    public void testHtml5RendersTheDatePartOfALocalDateTime() throws Exception 
{
+        ((ConstraintAction) action).setMeetingAt(LocalDateTime.of(2026, 10, 
24, 10, 15));
+
+        DateTextFieldTag tag = html5Tag("meetingAt");
+        tag.doStartTag();
+        tag.doEndTag();
+
+        
verify(DateTextFieldTag.class.getResource("DateTextField-6-html5.txt"));
+    }
+
     public void testHtml5RendersNoValueWithoutADate() throws Exception {
         DateTextFieldTag tag = html5Tag();
         tag.doStartTag();
@@ -109,10 +130,14 @@ public class DateTextFieldTest extends AbstractUITagTest {
     }
 
     private DateTextFieldTag html5Tag() {
+        return html5Tag("birthday");
+    }
+
+    private DateTextFieldTag html5Tag(String name) {
         DateTextFieldTag tag = new DateTextFieldTag();
         tag.setPageContext(pageContext);
         tag.setTheme("html5");
-        tag.setName("birthday");
+        tag.setName(name);
         return tag;
     }
 }
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-5-html5.txt
 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-5-html5.txt
new file mode 100644
index 000000000..693ed003c
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-5-html5.txt
@@ -0,0 +1 @@
+<input type="date" name="dueDate" value="2026-10-24" id="dueDate"/>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-6-html5.txt
 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-6-html5.txt
new file mode 100644
index 000000000..5517aca69
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-6-html5.txt
@@ -0,0 +1 @@
+<input type="date" name="meetingAt" value="2026-10-24" id="meetingAt"/>
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 7c93a6b1f..007d4f2b5 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,6 +20,9 @@ package org.apache.struts2.views.java.simple;
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.UnsupportedTemporalTypeException;
 import java.util.Date;
 import java.util.Map;
 
@@ -42,7 +45,7 @@ public class DateTextFieldHandler extends AbstractTagHandler 
implements TagGener
         if (id == null) {
                id = name;
         }
-        Date date = (Date)params.get("nameValue");
+        Object value = params.get("nameValue");
         
         if (format != null) {
                // Verify if it's correct
@@ -83,10 +86,7 @@ public class DateTextFieldHandler extends AbstractTagHandler 
implements TagGener
                        } else {
                                attr.addDefaultToEmpty("name", 
dateWord.getDescription());
                        }
-                       if (date != null) {
-                               SimpleDateFormat formatter = new 
SimpleDateFormat(dateWord.getDateType());
-                               attr.addIfExists("value", 
formatter.format(date), false);
-                       }
+                       attr.addIfExists("value", formatValue(value, dateWord), 
false);
                        
                        super.start("input", attr);
                        super.end("input");
@@ -102,6 +102,20 @@ 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);
+        }
+        if (value instanceof TemporalAccessor temporal) {
+            try {
+                return 
DateTimeFormatter.ofPattern(dateWord.getDateType()).format(temporal);
+            } catch (UnsupportedTemporalTypeException e) {
+                return null;
+            }
+        }
+        return null;
+    }
     
     
     
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 1cc501441..07c24bf98 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
@@ -25,6 +25,12 @@ import org.apache.struts2.components.UIBean;
 import org.apache.struts2.interceptor.csp.CspNonceSource;
 import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.Date;
+
 public class DateTextFieldTest extends AbstractCommonAttributesTest {
 
     private DateTextField tag;
@@ -45,6 +51,70 @@ public class DateTextFieldTest extends 
AbstractCommonAttributesTest {
         assertEquals(expected, output);
     }
 
+    public void testRenderDateValue() {
+        Date date = Date.from(LocalDate.of(2026, 10, 
24).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+        String output = render("yyyy-MM-dd", date);
+
+        assertEquals(s("<div id='id'>" + year("2026") + "-" + month("10") + 
"-" + day("24") + "</div>"), output);
+    }
+
+    public void testRenderLocalDateValue() {
+        String output = render("yyyy-MM-dd", LocalDate.of(2026, 10, 24));
+
+        assertEquals(s("<div id='id'>" + year("2026") + "-" + month("10") + 
"-" + day("24") + "</div>"), output);
+    }
+
+    public void testRenderLocalDateTimeValue() {
+        String output = render("yyyy-MM-dd HH:mm", LocalDateTime.of(2026, 10, 
24, 9, 5));
+
+        assertEquals(s("<div id='id'>" + year("2026") + "-" + month("10") + 
"-" + day("24") + " "
+            + "<input type='text' class='date_hour' size='2' maxlength='2' 
id='__hour_id' name='__hour_name' value='09'></input>"
+            + ":<input type='text' class='date_minute' size='2' maxlength='2' 
id='__minute_id' name='__minute_name' value='05'></input>"
+            + "</div>"), output);
+    }
+
+    public void testRenderLocalTimeValue() {
+        String output = render("HH:mm", LocalTime.of(9, 5));
+
+        assertEquals(s("<div id='id'>"
+            + "<input type='text' class='date_hour' size='2' maxlength='2' 
id='__hour_id' name='__hour_name' value='09'></input>"
+            + ":<input type='text' class='date_minute' size='2' maxlength='2' 
id='__minute_id' name='__minute_name' value='05'></input>"
+            + "</div>"), output);
+    }
+
+    public void testRenderLocalDateLeavesTimeFieldsEmpty() {
+        String output = render("yyyy-MM-dd HH", LocalDate.of(2026, 10, 24));
+
+        assertEquals(s("<div id='id'>" + year("2026") + "-" + month("10") + 
"-" + day("24") + " "
+            + "<input type='text' class='date_hour' size='2' maxlength='2' 
id='__hour_id' name='__hour_name'></input>"
+            + "</div>"), output);
+    }
+
+    private String render(String format, Object value) {
+        tag.setId("id");
+        tag.setName("name");
+        tag.setFormat(format);
+
+        tag.evaluateParams();
+        map.putAll(tag.getAttributes());
+        map.put("nameValue", value);
+        theme.renderTag(getTagName(), context);
+        return writer.getBuffer().toString();
+    }
+
+    private static String year(String value) {
+        return "<input type='text' class='date_year' size='4' maxlength='4' 
id='__year_id' name='__year_name' value='" + value + "'></input>";
+    }
+
+    private static String month(String value) {
+        return "<input type='text' class='date_month' size='2' maxlength='2' 
id='__month_id' name='__month_name' value='" + value + "'></input>";
+    }
+
+    private static String day(String value) {
+        return "<input type='text' class='date_day' size='2' maxlength='2' 
id='__day_id' name='__day_name' value='" + value + "'></input>";
+    }
+
     @Override
     public void testRenderTextFieldScriptingAttrs() throws Exception { }
 

Reply via email to