From 804e1546f149174f6e88d69b77bd3fa9e8b9a76d Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sun, 6 Feb 2022 12:23:39 +0100 Subject: [PATCH 1/5] WW-5016 Adds support for LocalDate and adjusts tests to use the new Java 8 API --- .../org/apache/struts2/components/Date.java | 44 ++-- .../struts2/views/jsp/ui/DateTagTest.java | 228 +++++++++++------- 2 files changed, 165 insertions(+), 107 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/components/Date.java b/core/src/main/java/org/apache/struts2/components/Date.java index 5f65d9c7b9..ec9bb1997d 100644 --- a/core/src/main/java/org/apache/struts2/components/Date.java +++ b/core/src/main/java/org/apache/struts2/components/Date.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.Writer; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -40,7 +41,7 @@ /** * - * + *

* Format Date object in different ways. *

* The date tag will allow you to format a Date in a quick and easy way. @@ -59,6 +60,12 @@ *

* *

+ * Note: Since Struts 2.6 a new Java 8 API has been used to format the Date, it's based on + * DateTimeFormatter + * which uses a bit different patterns. + *

+ * + *

* Configurable attributes are: *

* @@ -130,8 +137,8 @@ * if one is not found DateFormat.MEDIUM format will be used * * - * - * + *

+ *

* * *

Examples

@@ -145,9 +152,8 @@ * * * Date - * */ -@StrutsTag(name="date", tldBodyContent="empty", tldTagClass="org.apache.struts2.views.jsp.DateTag", description="Render a formatted date.") +@StrutsTag(name = "date", tldBodyContent = "empty", tldTagClass = "org.apache.struts2.views.jsp.DateTag", description = "Render a formatted date.") public class Date extends ContextBean { private static final Logger LOG = LogManager.getLogger(Date.class); @@ -292,6 +298,8 @@ public boolean end(Writer writer, String body) { date = Instant.ofEpochMilli((long) dateObject).atZone(tz); } else if (dateObject instanceof LocalDateTime) { date = ((LocalDateTime) dateObject).atZone(tz); + } else if (dateObject instanceof LocalDate) { + date = ((LocalDate) dateObject).atStartOfDay(tz); } else if (dateObject instanceof Instant) { date = ((Instant) dateObject).atZone(tz); } else { @@ -300,18 +308,18 @@ public boolean end(Writer writer, String body) { String developerNotification = ""; if (tp != null) { developerNotification = findProviderInStack().getText( - "devmode.notification", - "Developer Notification:\n{0}", - new String[]{ - "Expression [" + name + "] passed to tag which was evaluated to [" + dateObject + "](" - + (dateObject != null ? dateObject.getClass() : "null") + ") isn't supported!" - } + "devmode.notification", + "Developer Notification:\n{0}", + new String[]{ + "Expression [" + name + "] passed to tag which was evaluated to [" + dateObject + "](" + + (dateObject != null ? dateObject.getClass() : "null") + ") isn't supported!" + } ); } LOG.warn(developerNotification); } else { LOG.debug("Expression [{}] passed to tag which was evaluated to [{}]({}) isn't supported!", - name, dateObject, (dateObject != null ? dateObject.getClass() : "null")); + name, dateObject, (dateObject != null ? dateObject.getClass() : "null")); } } @@ -338,11 +346,11 @@ public boolean end(Writer writer, String body) { // returned string is the same as input = // DATETAG_PROPERTY if (globalFormat != null - && !DATETAG_PROPERTY.equals(globalFormat)) { + && !DATETAG_PROPERTY.equals(globalFormat)) { dtf = DateTimeFormatter.ofPattern(globalFormat, ActionContext.getContext().getLocale()); } else { dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - .withLocale(ActionContext.getContext().getLocale()); + .withLocale(ActionContext.getContext().getLocale()); } } else { dtf = DateTimeFormatter.ofPattern(format, ActionContext.getContext().getLocale()); @@ -378,17 +386,17 @@ private ZoneId getTimeZone() { return tz; } - @StrutsTagAttribute(description="Date or DateTime format pattern", rtexprvalue=false) + @StrutsTagAttribute(description = "Date or DateTime format pattern") public void setFormat(String format) { this.format = format; } - @StrutsTagAttribute(description="Whether to print out the date nicely", type="Boolean", defaultValue="false") + @StrutsTagAttribute(description = "Whether to print out the date nicely", type = "Boolean", defaultValue = "false") public void setNice(boolean nice) { this.nice = nice; } - @StrutsTagAttribute(description = "The specific timezone in which to format the date", required = false) + @StrutsTagAttribute(description = "The specific timezone in which to format the date") public void setTimezone(String timezone) { this.timezone = timezone; } @@ -400,7 +408,7 @@ public String getName() { return name; } - @StrutsTagAttribute(description="The date value to format", required=true) + @StrutsTagAttribute(description = "The date value to format", required = true) public void setName(String name) { this.name = name; } diff --git a/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTagTest.java index 6f6e31eccc..05e267e05c 100644 --- a/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTagTest.java +++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTagTest.java @@ -19,35 +19,33 @@ package org.apache.struts2.views.jsp.ui; import com.opensymphony.xwork2.ActionContext; - import org.apache.struts2.TestAction; +import org.apache.struts2.components.Component; +import org.apache.struts2.components.DateTextField; import org.apache.struts2.views.jsp.AbstractTagTest; import org.apache.struts2.views.jsp.DateTag; +import javax.servlet.jsp.JspException; import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; -import java.util.TimeZone; -import org.apache.struts2.components.Component; -import org.apache.struts2.components.DateTextField; /** * Unit test for {@link org.apache.struts2.components.Date}. - * */ public class DateTagTest extends AbstractTagTest { private DateTag tag; - public void testCustomFormat() throws Exception { + public void testCustomFormatForDateTime() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setName("myDate"); @@ -62,13 +60,55 @@ public void testCustomFormat() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); + } + + public void testCustomFormatForLong() throws Exception { + String format = "yyyy/MM/dd"; + long now = new Date().getTime(); + String formatted = DateTimeFormatter.ofPattern(format).format(Instant.ofEpochMilli(now).atZone(ZoneId.systemDefault())); + context.put("myDate", now); + + tag.setName("myDate"); + tag.setNice(false); + tag.setFormat(format); + tag.doStartTag(); + tag.doEndTag(); + assertEquals(formatted, writer.toString()); + + // Basic sanity check of clearTagStateForTagPoolingServers() behaviour for Struts Tags after doEndTag(). + DateTag freshTag = new DateTag(); + freshTag.setPageContext(pageContext); + assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", + strutsBodyTagsAreReflectionEqual(tag, freshTag)); + } + + public void testCustomFormatForDate() throws Exception { + String format = "yyyy/MM/dd"; + LocalDate now = LocalDate.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); + context.put("myDate", now); + + tag.setName("myDate"); + tag.setNice(false); + tag.setFormat(format); + tag.doStartTag(); + tag.doEndTag(); + assertEquals(formatted, writer.toString()); + + // Basic sanity check of clearTagStateForTagPoolingServers() behaviour for Struts Tags after doEndTag(). + DateTag freshTag = new DateTag(); + freshTag.setPageContext(pageContext); + assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormat_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setPerformClearTagStateForTagPoolingServers(true); // Explicitly request tag state clearing. @@ -86,13 +126,13 @@ public void testCustomFormat_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomGlobalFormatFormat() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); ((TestAction) action).setText(org.apache.struts2.components.Date.DATETAG_PROPERTY, format); @@ -106,10 +146,8 @@ public void testCustomGlobalFormatFormat() throws Exception { public void testCustomFormatWithTimezone() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = Calendar.getInstance(TimeZone.getTimeZone("GMT+1")).getTime(); - SimpleDateFormat sdf = new SimpleDateFormat(format); - sdf.setTimeZone(TimeZone.getTimeZone("GMT+1")); - String formatted = sdf.format(now); + LocalDateTime now = LocalDateTime.now(ZoneId.of("GMT+1")); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setName("myDate"); @@ -126,15 +164,13 @@ public void testCustomFormatWithTimezone() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatWithTimezone_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = Calendar.getInstance(TimeZone.getTimeZone("GMT+1")).getTime(); - SimpleDateFormat sdf = new SimpleDateFormat(format); - sdf.setTimeZone(TimeZone.getTimeZone("GMT+1")); - String formatted = sdf.format(now); + LocalDateTime now = LocalDateTime.now(ZoneId.of("GMT+1")); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setPerformClearTagStateForTagPoolingServers(true); // Explicitly request tag state clearing. @@ -153,15 +189,13 @@ public void testCustomFormatWithTimezone_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatWithTimezoneAsExpression() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = Calendar.getInstance(TimeZone.getTimeZone("GMT+2")).getTime(); - SimpleDateFormat sdf = new SimpleDateFormat(format); - sdf.setTimeZone(TimeZone.getTimeZone("GMT+2")); - String formatted = sdf.format(now); + LocalDateTime now = LocalDateTime.now(ZoneId.of("GMT+2")); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); context.put("myTimezone", "GMT+2"); @@ -178,15 +212,13 @@ public void testCustomFormatWithTimezoneAsExpression() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatWithTimezoneAsExpression_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = Calendar.getInstance(TimeZone.getTimeZone("GMT+2")).getTime(); - SimpleDateFormat sdf = new SimpleDateFormat(format); - sdf.setTimeZone(TimeZone.getTimeZone("GMT+2")); - String formatted = sdf.format(now); + LocalDateTime now = LocalDateTime.now(ZoneId.of("GMT+2")); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); context.put("myTimezone", "GMT+2"); @@ -206,13 +238,13 @@ public void testCustomFormatWithTimezoneAsExpression_clearTagStateSet() throws E freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatCalendar() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; Calendar calendar = Calendar.getInstance(); - String formatted = new SimpleDateFormat(format).format(calendar.getTime()); + String formatted = DateTimeFormatter.ofPattern(format).format(calendar.toInstant().atZone(ZoneId.systemDefault())); context.put("myDate", calendar); tag.setName("myDate"); @@ -227,13 +259,13 @@ public void testCustomFormatCalendar() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatCalendar_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; Calendar calendar = Calendar.getInstance(); - String formatted = new SimpleDateFormat(format).format(calendar.getTime()); + String formatted = DateTimeFormatter.ofPattern(format).format(calendar.toInstant().atZone(ZoneId.systemDefault())); context.put("myDate", calendar); tag.setPerformClearTagStateForTagPoolingServers(true); // Explicitly request tag state clearing. @@ -251,13 +283,13 @@ public void testCustomFormatCalendar_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatLong() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; Date date = new Date(); - String formatted = new SimpleDateFormat(format).format(date); + String formatted = DateTimeFormatter.ofPattern(format).format(date.toInstant().atZone(ZoneId.systemDefault())); // long context.put("myDate", date.getTime()); @@ -273,13 +305,13 @@ public void testCustomFormatLong() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatLong_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; Date date = new Date(); - String formatted = new SimpleDateFormat(format).format(date); + String formatted = DateTimeFormatter.ofPattern(format).format(date.toInstant().atZone(ZoneId.systemDefault())); // long context.put("myDate", date.getTime()); @@ -298,7 +330,7 @@ public void testCustomFormatLong_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatLocalDateTime() throws Exception { @@ -332,7 +364,7 @@ public void testCustomFormatInstant() throws Exception { public void testDefaultFormat() throws Exception { Date now = new Date(); String formatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, - ActionContext.getContext().getLocale()).format(now); + ActionContext.getContext().getLocale()).format(now); context.put("myDate", now); tag.setName("myDate"); @@ -346,13 +378,13 @@ public void testDefaultFormat() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testDefaultFormat_clearTagStateSet() throws Exception { Date now = new Date(); String formatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, - ActionContext.getContext().getLocale()).format(now); + ActionContext.getContext().getLocale()).format(now); context.put("myDate", now); tag.setPerformClearTagStateForTagPoolingServers(true); // Explicitly request tag state clearing. @@ -369,13 +401,13 @@ public void testDefaultFormat_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatAndComponent() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setName("myDate"); @@ -388,7 +420,7 @@ public void testCustomFormatAndComponent() throws Exception { org.apache.struts2.components.Date component = (org.apache.struts2.components.Date) tag.getComponent(); assertEquals("myDate", component.getName()); assertEquals(format, component.getFormat()); - assertEquals(false, component.isNice()); + assertFalse(component.isNice()); tag.doEndTag(); @@ -399,13 +431,13 @@ public void testCustomFormatAndComponent() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testCustomFormatAndComponent_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setPerformClearTagStateForTagPoolingServers(true); // Explicitly request tag state clearing. @@ -420,7 +452,7 @@ public void testCustomFormatAndComponent_clearTagStateSet() throws Exception { org.apache.struts2.components.Date component = (org.apache.struts2.components.Date) tag.getComponent(); assertEquals("myDate", component.getName()); assertEquals(format, component.getFormat()); - assertEquals(false, component.isNice()); + assertFalse(component.isNice()); tag.doEndTag(); @@ -432,13 +464,13 @@ public void testCustomFormatAndComponent_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testSetId() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setName("myDate"); @@ -454,13 +486,13 @@ public void testSetId() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testSetId_clearTagStateSet() throws Exception { String format = "yyyy/MM/dd hh:mm:ss"; - Date now = new Date(); - String formatted = new SimpleDateFormat(format).format(now); + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format).format(now); context.put("myDate", now); tag.setPerformClearTagStateForTagPoolingServers(true); // Explicitly request tag state clearing. @@ -479,7 +511,7 @@ public void testSetId_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureNiceHour() throws Exception { @@ -501,7 +533,7 @@ public void testFutureNiceHour() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureNiceHour_clearTagStateSet() throws Exception { @@ -526,7 +558,7 @@ public void testFutureNiceHour_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testPastNiceHour() throws Exception { @@ -548,7 +580,7 @@ public void testPastNiceHour() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testPastNiceHour_clearTagStateSet() throws Exception { @@ -573,7 +605,7 @@ public void testPastNiceHour_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureNiceHourMinSec() throws Exception { @@ -596,7 +628,7 @@ public void testFutureNiceHourMinSec() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureNiceHourMinSec_clearTagStateSet() throws Exception { @@ -622,7 +654,7 @@ public void testFutureNiceHourMinSec_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testPastNiceHourMin() throws Exception { @@ -645,7 +677,7 @@ public void testPastNiceHourMin() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testPastNiceHourMin_clearTagStateSet() throws Exception { @@ -671,7 +703,7 @@ public void testPastNiceHourMin_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureLessOneMin() throws Exception { @@ -693,7 +725,7 @@ public void testFutureLessOneMin() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureLessOneMin_clearTagStateSet() throws Exception { @@ -718,7 +750,7 @@ public void testFutureLessOneMin_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureLessOneHour() throws Exception { @@ -740,7 +772,7 @@ public void testFutureLessOneHour() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureLessOneHour_clearTagStateSet() throws Exception { @@ -765,7 +797,7 @@ public void testFutureLessOneHour_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureLessOneYear() throws Exception { @@ -787,7 +819,7 @@ public void testFutureLessOneYear() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureLessOneYear_clearTagStateSet() throws Exception { @@ -812,7 +844,7 @@ public void testFutureLessOneYear_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureTwoYears() throws Exception { @@ -838,7 +870,7 @@ public void testFutureTwoYears() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testFutureTwoYears_clearTagStateSet() throws Exception { @@ -867,7 +899,7 @@ public void testFutureTwoYears_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testNoDateObjectInContext() throws Exception { @@ -884,7 +916,7 @@ public void testNoDateObjectInContext() throws Exception { freshTag.setPageContext(pageContext); assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } public void testNoDateObjectInContext_clearTagStateSet() throws Exception { @@ -904,7 +936,7 @@ public void testNoDateObjectInContext_clearTagStateSet() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(tag, freshTag)); + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } /** @@ -912,10 +944,8 @@ public void testNoDateObjectInContext_clearTagStateSet() throws Exception { * since that tag does not have its own unit tests, and it also appears to be * a broken tag. The code coverage tests can be moved if the tag is fixed, or * removed if the tag is dropped. - * - * @throws Exception */ - public void testDateTextFieldTag_artificialCoverageTest() throws Exception { + public void testDateTextFieldTag_artificialCoverageTest() throws JspException { final String format = "yyyy/MM/dd hh:mm:ss"; DateTextFieldTag dateTextFieldTag = createDateTextFieldTag(); dateTextFieldTag.setFormat(format); @@ -939,20 +969,40 @@ public void testDateTextFieldTag_artificialCoverageTest() throws Exception { freshTag.setPageContext(pageContext); assertTrue("Tag state after doEndTag() and explicit tag state clearing is inequal to new Tag with pageContext/parent set. " + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", - strutsBodyTagsAreReflectionEqual(dateTextFieldTag, freshTag)); + strutsBodyTagsAreReflectionEqual(dateTextFieldTag, freshTag)); + } + + public void testNewJava8Format() throws Exception { + String format = "EEEE MMMM dd, hh:mm a"; + LocalDateTime now = LocalDateTime.now(); + String formatted = DateTimeFormatter.ofPattern(format, ActionContext.getContext().getLocale()).format(now); + context.put("myDate", now); + + tag.setName("myDate"); + tag.setNice(false); + tag.setFormat(format); + tag.doStartTag(); + tag.doEndTag(); + assertEquals(formatted, writer.toString()); + + // Basic sanity check of clearTagStateForTagPoolingServers() behaviour for Struts Tags after doEndTag(). + DateTag freshTag = new DateTag(); + freshTag.setPageContext(pageContext); + assertFalse("Tag state after doEndTag() under default tag clear state is equal to new Tag with pageContext/parent set. " + + "May indicate that clearTagStateForTagPoolingServers() calls are not working properly.", + strutsBodyTagsAreReflectionEqual(tag, freshTag)); } /** * Utility method to create a new {@link DateTextFieldTag} instance for code coverage tests. - * + *

* Note: There is no datetextfield.ftl template for the tag, so it does not appear that it can - * actually be used in practice. We can perform basic coverage tests from within this - * unit test class until the {@link DateTextFieldTag} is fixed or removed. - * + * actually be used in practice. We can perform basic coverage tests from within this + * unit test class until the {@link DateTextFieldTag} is fixed or removed. + * * @return a basic {@link DateTextFieldTag} instance - * @throws Exception */ - private DateTextFieldTag createDateTextFieldTag() throws Exception { + private DateTextFieldTag createDateTextFieldTag() { DateTextFieldTag tag = new DateTextFieldTag(); tag.setPageContext(pageContext); tag.setName("myDate"); From e3dff7691e72a30cf6ebcad1bbace48e56f56380 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sun, 20 Feb 2022 13:29:46 +0100 Subject: [PATCH 2/5] WW-5016 Introduces different format adapters to allow use different APIs --- .../StrutsDefaultConfigurationProvider.java | 6 ++ .../org/apache/struts2/StrutsConstants.java | 3 + .../org/apache/struts2/components/Date.java | 78 ++++++++-------- .../components/date/DateFormatter.java | 33 +++++++ .../date/DateTimeFormatterAdapter.java | 47 ++++++++++ .../date/SimpleDateFormatAdapter.java | 48 ++++++++++ .../config/StrutsBeanSelectionProvider.java | 5 +- .../org/apache/struts2/default.properties | 6 ++ core/src/main/resources/struts-default.xml | 5 +- .../apache/struts2/components/DateTest.java | 90 +++++++++++++++++++ 10 files changed, 280 insertions(+), 41 deletions(-) create mode 100644 core/src/main/java/org/apache/struts2/components/date/DateFormatter.java create mode 100644 core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java create mode 100644 core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java create mode 100644 core/src/test/java/org/apache/struts2/components/DateTest.java diff --git a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java index 400674d2de..a5dcf99cbb 100644 --- a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java @@ -56,6 +56,9 @@ import com.opensymphony.xwork2.conversion.impl.DefaultConversionAnnotationProcessor; import com.opensymphony.xwork2.conversion.impl.DefaultConversionFileProcessor; import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker; +import org.apache.struts2.components.date.DateFormatter; +import org.apache.struts2.components.date.DateTimeFormatterAdapter; +import org.apache.struts2.components.date.SimpleDateFormatAdapter; import org.apache.struts2.conversion.StrutsConversionPropertiesProcessor; import com.opensymphony.xwork2.conversion.impl.DefaultObjectTypeDeterminer; import org.apache.struts2.conversion.StrutsTypeConverterCreator; @@ -218,6 +221,9 @@ public void register(ContainerBuilder builder, LocatableProperties props) , Scope.SINGLETON) .factory(ValueSubstitutor.class, EnvsValueSubstitutor.class, Scope.SINGLETON) + + .factory(DateFormatter.class, "simpleDateFormat", SimpleDateFormatAdapter.class, Scope.SINGLETON) + .factory(DateFormatter.class, "dateTimeFormatter", DateTimeFormatterAdapter.class, Scope.SINGLETON) ; props.setProperty(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE.toString()); diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index 789b7c3886..64ac93b047 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -18,6 +18,7 @@ */ package org.apache.struts2; +import org.apache.struts2.components.date.DateFormatter; import org.apache.struts2.dispatcher.mapper.CompositeActionMapper; /** @@ -384,4 +385,6 @@ public final class StrutsConstants { public static final String STRUTS_CHAINING_COPY_MESSAGES = "struts.chaining.copyMessages"; public static final String STRUTS_OBJECT_FACTORY_CLASSLOADER = "struts.objectFactory.classloader"; + /** See {@link org.apache.struts2.components.Date#setDateFormatter(DateFormatter)} */ + public static final String STRUTS_DATE_FORMATTER = "struts.date.formatter"; } diff --git a/core/src/main/java/org/apache/struts2/components/Date.java b/core/src/main/java/org/apache/struts2/components/Date.java index ec9bb1997d..0560057b83 100644 --- a/core/src/main/java/org/apache/struts2/components/Date.java +++ b/core/src/main/java/org/apache/struts2/components/Date.java @@ -18,11 +18,13 @@ */ package org.apache.struts2.components; -import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.TextProvider; +import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.util.ValueStack; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.components.date.DateFormatter; import org.apache.struts2.views.annotations.StrutsTag; import org.apache.struts2.views.annotations.StrutsTagAttribute; @@ -33,8 +35,6 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -157,6 +157,7 @@ public class Date extends ContextBean { private static final Logger LOG = LogManager.getLogger(Date.class); + /** * Property name to fall back when no format is specified */ @@ -208,17 +209,18 @@ public class Date extends ContextBean { private String timezone; + private DateFormatter dateFormatter; + public Date(ValueStack stack) { super(stack); } - private TextProvider findProviderInStack() { - for (Object o : getStack().getRoot()) { - if (o instanceof TextProvider) { - return (TextProvider) o; - } - } - return null; + /** + * An instance of {@link DateFormatter} + */ + @Inject + public void setDateFormatter(DateFormatter dateFormatter) { + this.dateFormatter = dateFormatter; } /** @@ -286,6 +288,8 @@ public String formatTime(TextProvider tp, ZonedDateTime date) { @Override public boolean end(Writer writer, String body) { + TextProvider textProvider = findProviderInStack(); + ZonedDateTime date = null; final ZoneId tz = getTimeZone(); // find the name on the valueStack @@ -304,10 +308,9 @@ public boolean end(Writer writer, String body) { date = ((Instant) dateObject).atZone(tz); } else { if (devMode) { - TextProvider tp = findProviderInStack(); String developerNotification = ""; - if (tp != null) { - developerNotification = findProviderInStack().getText( + if (textProvider != null) { + developerNotification = textProvider.getText( "devmode.notification", "Developer Notification:\n{0}", new String[]{ @@ -329,33 +332,11 @@ public boolean end(Writer writer, String body) { } String msg; if (date != null) { - TextProvider tp = findProviderInStack(); - if (tp != null) { + if (textProvider != null) { if (nice) { - msg = formatTime(tp, date); + msg = formatTime(textProvider, date); } else { - DateTimeFormatter dtf; - if (format == null) { - String globalFormat = null; - - // if the format is not specified, fall back using the - // defined property DATETAG_PROPERTY - globalFormat = tp.getText(DATETAG_PROPERTY); - - // if tp.getText can not find the property then the - // returned string is the same as input = - // DATETAG_PROPERTY - if (globalFormat != null - && !DATETAG_PROPERTY.equals(globalFormat)) { - dtf = DateTimeFormatter.ofPattern(globalFormat, ActionContext.getContext().getLocale()); - } else { - dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - .withLocale(ActionContext.getContext().getLocale()); - } - } else { - dtf = DateTimeFormatter.ofPattern(format, ActionContext.getContext().getLocale()); - } - msg = dtf.format(date); + msg = formatDate(textProvider, date); } if (msg != null) { try { @@ -373,6 +354,18 @@ public boolean end(Writer writer, String body) { return super.end(writer, ""); } + private String formatDate(TextProvider textProvider, ZonedDateTime date) { + // if the format is not specified, fall back using the defined property DATETAG_PROPERTY + String globalFormat = textProvider.getText(Date.DATETAG_PROPERTY); + if (DATETAG_PROPERTY.equals(globalFormat)) { + // if tp.getText can not find the property then the + // returned string is the same as input = DATETAG_PROPERTY + globalFormat = null; + } + + return dateFormatter.format(date, format, globalFormat); + } + private ZoneId getTimeZone() { ZoneId tz = ZoneId.systemDefault(); if (timezone != null) { @@ -386,6 +379,15 @@ private ZoneId getTimeZone() { return tz; } + private TextProvider findProviderInStack() { + for (Object o : getStack().getRoot()) { + if (o instanceof TextProvider) { + return (TextProvider) o; + } + } + return null; + } + @StrutsTagAttribute(description = "Date or DateTime format pattern") public void setFormat(String format) { this.format = format; diff --git a/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java b/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java new file mode 100644 index 0000000000..7ad276a257 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.components.date; + +import java.time.temporal.TemporalAccessor; + +/** + * Allows defines a wrapper around different formatting APIs, like old SimpleDateFormat + * and new DateTimeFormatter introduced in Java 8 Date/Time API + * + * New instance will be injected using {@link org.apache.struts2.StrutsConstants#STRUTS_DATE_FORMATTER} + */ +public interface DateFormatter { + + String format(TemporalAccessor temporal, String format, String defaultFormat); + +} diff --git a/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java b/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java new file mode 100644 index 0000000000..64bb4b1659 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.components.date; + +import com.opensymphony.xwork2.ActionContext; + +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +public class DateTimeFormatterAdapter implements DateFormatter { + + @Override + public String format(TemporalAccessor temporal, String format, String defaultFormat) { + DateTimeFormatter dtf; + Locale locale = ActionContext.getContext().getLocale(); + if (format == null) { + if (defaultFormat != null) { + dtf = DateTimeFormatter.ofPattern(defaultFormat, locale); + } else { + dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + .withLocale(locale); + } + } else { + dtf = DateTimeFormatter.ofPattern(format, locale); + } + return dtf.format(temporal); + } + +} diff --git a/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java b/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java new file mode 100644 index 0000000000..e9f29f0be7 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.components.date; + +import com.opensymphony.xwork2.ActionContext; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.Locale; + +public class SimpleDateFormatAdapter implements DateFormatter { + + @Override + public String format(TemporalAccessor temporal, String format, String defaultFormat) { + DateFormat df; + Locale locale = ActionContext.getContext().getLocale(); + if (format == null) { + if (defaultFormat != null) { + df = new SimpleDateFormat(defaultFormat, locale); + } else { + df = SimpleDateFormat.getDateInstance(DateFormat.MEDIUM, locale); + } + } else { + df = new SimpleDateFormat(format, locale); + } + return df.format(new Date(Instant.from(temporal).toEpochMilli())); + } + +} diff --git a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java index ade6aa0b07..69aa9258ed 100644 --- a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java +++ b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java @@ -54,13 +54,12 @@ import com.opensymphony.xwork2.util.TextParser; import com.opensymphony.xwork2.util.ValueStackFactory; import com.opensymphony.xwork2.util.location.LocatableProperties; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; import com.opensymphony.xwork2.util.reflection.ReflectionContextFactory; import com.opensymphony.xwork2.util.reflection.ReflectionProvider; import com.opensymphony.xwork2.validator.ActionValidatorManager; import org.apache.struts2.StrutsConstants; import org.apache.struts2.components.UrlRenderer; +import org.apache.struts2.components.date.DateFormatter; import org.apache.struts2.dispatcher.DispatcherErrorHandler; import org.apache.struts2.dispatcher.StaticContentLoader; import org.apache.struts2.dispatcher.mapper.ActionMapper; @@ -422,6 +421,8 @@ public void register(ContainerBuilder builder, LocatableProperties props) { alias(NotExcludedAcceptedPatternsChecker.class, StrutsConstants.STRUTS_NOT_EXCLUDED_ACCEPTED_PATTERNS_CHECKER , builder, props, Scope.SINGLETON); + alias(DateFormatter.class, StrutsConstants.STRUTS_DATE_FORMATTER, builder, props, Scope.SINGLETON); + switchDevMode(props); } diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 571dcf59e6..57b692a323 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -243,4 +243,10 @@ struts.handle.exception=true ### NOTE: The sample line below is *INTENTIONALLY* commented out, as this feature is disabled by default. # struts.ognl.expressionMaxLength=256 +### Defines which named instance of DateFormatter to use, there are two instances: +### - simpleDateFormatter (based on SimpleDateFormat) +### - dateTimeFormatter (based on Java 8 Date/Time API) +### These formatters are using a slightly different patterns, please check JavaDocs for from details and WW-5016 +struts.date.formatter=dateTimeFormatter + ### END SNIPPET: complete_file diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml index 9dd8fbfa30..96b17b413c 100644 --- a/core/src/main/resources/struts-default.xml +++ b/core/src/main/resources/struts-default.xml @@ -216,7 +216,7 @@ - + @@ -228,6 +228,9 @@ + + + diff --git a/core/src/test/java/org/apache/struts2/components/DateTest.java b/core/src/test/java/org/apache/struts2/components/DateTest.java new file mode 100644 index 0000000000..2b9ca8bd71 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/components/DateTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.components; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.util.ValueStack; +import com.opensymphony.xwork2.util.ValueStackFactory; +import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.components.date.SimpleDateFormatAdapter; + +import java.io.StringWriter; +import java.io.Writer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Map; + +public class DateTest extends StrutsInternalTestCase { + + private Map context; + private ValueStack stack; + + public void testSupportSimpleDateTimeFormat() { + // given + Date date = new Date(stack); + date.setDateFormatter(new SimpleDateFormatAdapter()); + + String format = "EEEE MMMM dd, hh:mm aa"; + java.util.Date now = new java.util.Date(); + + String expected = new SimpleDateFormat(format, ActionContext.getContext().getLocale()).format(now); + context.put("myDate", now); + + Writer writer = new StringWriter(); + + // when + date.setFormat(format); + date.setName("myDate"); + date.setNice(false); + date.start(writer); + date.end(writer, ""); + + // then + assertEquals(expected, writer.toString()); + } + + public void testDefaultFormat() { + // given + Date date = new Date(stack); + date.setDateFormatter(new SimpleDateFormatAdapter()); + + java.util.Date now = new java.util.Date(); + + String expected = SimpleDateFormat.getDateInstance(DateFormat.MEDIUM, ActionContext.getContext().getLocale()).format(now); + context.put("myDate", now); + + Writer writer = new StringWriter(); + + // when + date.setName("myDate"); + date.setNice(false); + date.start(writer); + date.end(writer, ""); + + // then + assertEquals(expected, writer.toString()); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + stack = container.getInstance(ValueStackFactory.class).createValueStack(); + context = stack.getContext(); + } +} From 5648721d07b483427ed00c4315b91f45cd359918 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sun, 20 Feb 2022 13:33:49 +0100 Subject: [PATCH 3/5] WW-5016 Fixes typo in instance name --- .../config/providers/StrutsDefaultConfigurationProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java index a5dcf99cbb..f932373678 100644 --- a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java @@ -222,7 +222,7 @@ public void register(ContainerBuilder builder, LocatableProperties props) .factory(ValueSubstitutor.class, EnvsValueSubstitutor.class, Scope.SINGLETON) - .factory(DateFormatter.class, "simpleDateFormat", SimpleDateFormatAdapter.class, Scope.SINGLETON) + .factory(DateFormatter.class, "simpleDateFormatter", SimpleDateFormatAdapter.class, Scope.SINGLETON) .factory(DateFormatter.class, "dateTimeFormatter", DateTimeFormatterAdapter.class, Scope.SINGLETON) ; From 4746a49d177baee9d99acd243c2aec627828b2bc Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 21 Feb 2022 08:47:36 +0100 Subject: [PATCH 4/5] WW-5016 Improves description of different patterns --- core/src/main/resources/org/apache/struts2/default.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 57b692a323..4456d10af5 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -246,7 +246,7 @@ struts.handle.exception=true ### Defines which named instance of DateFormatter to use, there are two instances: ### - simpleDateFormatter (based on SimpleDateFormat) ### - dateTimeFormatter (based on Java 8 Date/Time API) -### These formatters are using a slightly different patterns, please check JavaDocs for from details and WW-5016 +### These formatters are using a slightly different patterns, please check JavaDocs of both and more details is in WW-5016 struts.date.formatter=dateTimeFormatter ### END SNIPPET: complete_file From 59932a51799890582859938a355546b75b5f6834 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 21 Feb 2022 19:31:52 +0100 Subject: [PATCH 5/5] WW-5016 Reduces calls to TextProvider --- .../org/apache/struts2/components/Date.java | 19 ++++++++++--------- .../components/date/DateFormatter.java | 11 +++++++++-- .../date/DateTimeFormatterAdapter.java | 10 +++------- .../date/SimpleDateFormatAdapter.java | 8 ++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/components/Date.java b/core/src/main/java/org/apache/struts2/components/Date.java index 0560057b83..ad174fd111 100644 --- a/core/src/main/java/org/apache/struts2/components/Date.java +++ b/core/src/main/java/org/apache/struts2/components/Date.java @@ -23,7 +23,6 @@ import com.opensymphony.xwork2.util.ValueStack; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.struts2.StrutsConstants; import org.apache.struts2.components.date.DateFormatter; import org.apache.struts2.views.annotations.StrutsTag; import org.apache.struts2.views.annotations.StrutsTagAttribute; @@ -355,15 +354,17 @@ public boolean end(Writer writer, String body) { } private String formatDate(TextProvider textProvider, ZonedDateTime date) { - // if the format is not specified, fall back using the defined property DATETAG_PROPERTY - String globalFormat = textProvider.getText(Date.DATETAG_PROPERTY); - if (DATETAG_PROPERTY.equals(globalFormat)) { - // if tp.getText can not find the property then the - // returned string is the same as input = DATETAG_PROPERTY - globalFormat = null; + String useFormat = format; + if (useFormat == null) { + // if the format is not specified, fall back using the defined property DATETAG_PROPERTY + useFormat = textProvider.getText(DATETAG_PROPERTY); + if (DATETAG_PROPERTY.equals(useFormat)) { + // if tp.getText can not find the property then the + // returned string is the same as input = DATETAG_PROPERTY + useFormat = null; + } } - - return dateFormatter.format(date, format, globalFormat); + return dateFormatter.format(date, useFormat); } private ZoneId getTimeZone() { diff --git a/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java b/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java index 7ad276a257..282daaa061 100644 --- a/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java +++ b/core/src/main/java/org/apache/struts2/components/date/DateFormatter.java @@ -23,11 +23,18 @@ /** * Allows defines a wrapper around different formatting APIs, like old SimpleDateFormat * and new DateTimeFormatter introduced in Java 8 Date/Time API - * + *

* New instance will be injected using {@link org.apache.struts2.StrutsConstants#STRUTS_DATE_FORMATTER} */ public interface DateFormatter { - String format(TemporalAccessor temporal, String format, String defaultFormat); + /** + * Formats provided temporal with the given format + * + * @param temporal Java 8 {@link TemporalAccessor} + * @param format implementation specific format + * @return a string representation of the formatted `temporal` + */ + String format(TemporalAccessor temporal, String format); } diff --git a/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java b/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java index 64bb4b1659..05767ab1b3 100644 --- a/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java +++ b/core/src/main/java/org/apache/struts2/components/date/DateTimeFormatterAdapter.java @@ -28,16 +28,12 @@ public class DateTimeFormatterAdapter implements DateFormatter { @Override - public String format(TemporalAccessor temporal, String format, String defaultFormat) { + public String format(TemporalAccessor temporal, String format) { DateTimeFormatter dtf; Locale locale = ActionContext.getContext().getLocale(); if (format == null) { - if (defaultFormat != null) { - dtf = DateTimeFormatter.ofPattern(defaultFormat, locale); - } else { - dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - .withLocale(locale); - } + dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + .withLocale(locale); } else { dtf = DateTimeFormatter.ofPattern(format, locale); } diff --git a/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java b/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java index e9f29f0be7..38f3c52969 100644 --- a/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java +++ b/core/src/main/java/org/apache/struts2/components/date/SimpleDateFormatAdapter.java @@ -30,15 +30,11 @@ public class SimpleDateFormatAdapter implements DateFormatter { @Override - public String format(TemporalAccessor temporal, String format, String defaultFormat) { + public String format(TemporalAccessor temporal, String format) { DateFormat df; Locale locale = ActionContext.getContext().getLocale(); if (format == null) { - if (defaultFormat != null) { - df = new SimpleDateFormat(defaultFormat, locale); - } else { - df = SimpleDateFormat.getDateInstance(DateFormat.MEDIUM, locale); - } + df = SimpleDateFormat.getDateInstance(DateFormat.MEDIUM, locale); } else { df = new SimpleDateFormat(format, locale); }