From 74108008c12becb7774742f14453b856cde5a33c Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Fri, 6 Nov 2015 11:36:33 +0100 Subject: [PATCH 01/48] Does not force the use of the default time zone for every input. For example, ZonedDateInput does not need it. Fixes #2. --- README.md | 42 ++- build.gradle | 2 +- .../sargue/time/jsptags/FormatSupport.java | 22 +- src/test/java/FormatTagTest.java | 252 +++++++++--------- src/test/java/ParseLocalDateTagTest.java | 2 +- 5 files changed, 179 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index d0ce9bb..dea097e 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,22 @@ This project provides JSP tags for the new java.time package present in Java 8. -The java.time packages are specified in JSR-310 and are based on the Joda-Time library. +The java.time packages are specified in JSR-310 and are based on the Joda-Time +library. This project is forked from and based on the original Joda-Time JSP Tags. ### Project status -The status is currently alpha stage. +The status is currently beta stage. -I started this project because I needed a replacement of Joda-Time JSP Tags after -migration of a project to Java 8. There are some basic test but the project just -uses some basic formatting so everything about locale, zones, parsing is untested. +I started this project because I needed a replacement of Joda-Time JSP Tags +after migration of a project to Java 8. ### About -This library works very similarly to the date-related tags in the jstl fmt library and -almost exactly as the tags in the original Joda-Time JSP Tags. +This library works very similarly to the date-related tags in the jstl fmt +library and almost exactly as the tags in the original Joda-Time JSP Tags. ### Requirements @@ -33,7 +33,7 @@ almost exactly as the tags in the original Joda-Time JSP Tags. Add the dependency to your project: #### Gradle -`compile 'net.sargue:java-time-jsptags:1.1.0'` +`compile 'net.sargue:java-time-jsptags:1.1.1'` #### Maven @@ -41,7 +41,7 @@ Add the dependency to your project: net.sargue java-time-jsptags - 1.1.0 + 1.1.1 ``` @@ -64,9 +64,27 @@ Example: Formats any `java.util.Temporal` like `Instant`, `LocalDateTime`, `LocalDate`, `LocalTime`, etc. The `var` and `scope` attributes can be used to set the value of a variable instead of printing the result. -The time zone may be specified using an attribute, an enclosing `` tag, -preceding `` tag, or via the `net.sargue.time.zoneId` scoped variable. If the -time zone is not specified it will default to the **system default time-zone**. + +###### Time zone (ZoneId) + +A time zone may be necessary to perform some formatting. It depends on the +desired format and the value object. An `Instant` has no time zone so if you +want a _long_ style time format (which outputs the time zone) you will need +to adjust using a time zone. A `ZonedDateTime` has all the information needed +but you may want to change the display time zone. + +The time zone may be specified using an attribute, an enclosing +`` tag, preceding `` tag, or via the +`net.sargue.time.zoneId` scoped variable. + +The time zone will default to the **system default time-zone** if none is +specified and the value object is one of these classes: + +* `Instant` +* `LocalDateTime` +* `LocalTime` +* `OffsetDateTime` +* `OffsetLocalTime` Attributes: diff --git a/build.gradle b/build.gradle index c6be732..1c7dc89 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'signing' group = 'net.sargue' archivesBaseName = 'java-time-jsptags' -version = '1.1.0' +version = '1.1.1' def NEXUS_USERNAME = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : '' def NEXUS_PASSWORD = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : '' diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index 9c140a6..bf91c6d 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -22,7 +22,7 @@ import javax.servlet.jsp.tagext.TagSupport; import java.io.IOException; import java.text.DateFormat; -import java.time.ZoneId; +import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.Locale; @@ -113,11 +113,23 @@ public int doEndTag() throws JspException { } // set formatter timezone - ZoneId tz = this.zoneId; - if (tz == null) { - tz = ZoneIdSupport.getZoneId(pageContext, this); + ZoneId zoneId = this.zoneId; + if (zoneId == null) { + zoneId = ZoneIdSupport.getZoneId(pageContext, this); + } + if (zoneId != null) { + formatter = formatter.withZone(zoneId); + } else { + if (value instanceof Instant || + value instanceof LocalDateTime || + value instanceof OffsetDateTime || + value instanceof OffsetTime || + value instanceof LocalTime) + // these time objects may need a zone to resolve some patterns + // and/or styles, and as there is no zone we revert to the + // system default zone + formatter = formatter.withZone(ZoneId.systemDefault()); } - formatter = formatter.withZone(tz == null ? ZoneId.systemDefault() : tz); // format value String formatted; diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index 7c518b9..f67d8b7 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -1,8 +1,6 @@ import net.sargue.time.jsptags.FormatTag; -import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; @@ -12,189 +10,199 @@ import java.time.*; import java.util.Locale; +import static org.junit.Assert.assertEquals; + /** - * Basic tests. Just checking there are no exceptions (I'm not checking the output). + * Basic format tests. * * @author Sergi Baila * @link http://blog.agilelogicsolutions.com/2011/02/unit-testing-jsp-custom-tag-using.html */ public class FormatTagTest { - private FormatTag formatTag; - private MockPageContext mockPageContext; + private MockServletContext mockServletContext; @Before public void setup() throws UnsupportedEncodingException { Locale.setDefault(Locale.forLanguageTag("ca")); - // mock ServletContext - MockServletContext mockServletContext = new MockServletContext(); - // mock PageContext - mockPageContext = new MockPageContext(mockServletContext); - mockPageContext.getRequest().setCharacterEncoding("UTF-8"); - mockPageContext.getResponse().setCharacterEncoding("UTF-8"); - formatTag = new FormatTag(); - formatTag.setPageContext(mockPageContext); - } - - @After - public void print() throws UnsupportedEncodingException { - System.out.println(((MockHttpServletResponse) mockPageContext.getResponse()).getContentAsString()); + mockServletContext = new MockServletContext(); } @Test public void dayOfWeekTest() throws IOException, JspException { - for (DayOfWeek o : DayOfWeek.values()) { - mockPageContext.getOut().println("DayOfWeek: " + o); - formatPattern(o, "E EE EEE EEEE e ee eee eeee"); - } + testPattern("dl. dl. dl. dilluns 1 01 dl. dilluns", + DayOfWeek.MONDAY, + "E EE EEE EEEE e ee eee eeee"); + testPattern("dt. dt. dt. dimarts 2 02 dt. dimarts", + DayOfWeek.TUESDAY, + "E EE EEE EEEE e ee eee eeee"); } @Test public void instantTest() throws JspException, IOException { - Object o = Instant.now(); - mockPageContext.getOut().println("Instant: " + o); - format(o); - formatPattern(o, "G u y D M d Q Y w W E e F a h K k H m s S A n N VV z O X x Z"); - formatStyle(o, "S-"); - formatStyle(o, "M-"); - formatStyle(o, "L-"); - formatStyle(o, "F-"); - formatStyle(o, "-S"); - formatStyle(o, "-M"); - formatStyle(o, "-L"); - formatStyle(o, "-F"); + Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); + testBasic("06/11/2015", instant); + testStyle("06/11/15", instant, "S-"); + testStyle("06/11/2015", instant, "M-"); + testStyle("6 / de novembre / 2015", instant, "L-"); + testStyle("divendres, 6 / de novembre / 2015", instant, "F-"); + testStyle("10:45", instant, "-S"); + testStyle("10:45:33", instant, "-M"); + testStyle("10:45:33 CET", instant, "-L"); + testStyle("10:45:33 CET", instant, "-F"); } @Test public void localDateTest() throws IOException, JspException { - Object o = LocalDate.now(); - mockPageContext.getOut().println("LocalDate: " + o); - format(o); - formatPattern(o, "dd/MM/yyyy"); - formatStyle(o, "S-"); - formatStyle(o, "M-"); - formatStyle(o, "L-"); - formatStyle(o, "F-"); + LocalDate localDate = LocalDate.parse("2015-11-06"); + testBasic("06/11/2015", localDate); + testPattern("06/11/2015", localDate, "dd/MM/yyyy"); + testStyle("06/11/15", localDate, "S-"); + testStyle("06/11/2015", localDate, "M-"); + testStyle("6 / de novembre / 2015", localDate, "L-"); + testStyle("divendres, 6 / de novembre / 2015", localDate, "F-"); } @Test public void localTimeTest() throws IOException, JspException { - Object o = LocalTime.now(); - mockPageContext.getOut().println("LocalTime: " + o); - formatPattern(o, "HH:mm:ss"); - formatStyle(o, "-S"); - formatStyle(o, "-M"); - formatStyle(o, "-L"); - formatStyle(o, "-F"); + LocalTime localTime = LocalTime.parse("10:53:55.913"); + testPattern("10:53:55", localTime, "HH:mm:ss"); + testStyle("10:53", localTime, "-S"); + testStyle("10:53:55", localTime, "-M"); + testStyle("10:53:55 CET", localTime, "-L"); + testStyle("10:53:55 CET", localTime, "-F"); } @Test public void localDateTimeTest() throws IOException, JspException { - Object o = LocalDateTime.now(); - mockPageContext.getOut().println("LocalDateTime: " + o); - format(o); - formatPattern(o, "dd/MM/yyyy HH:mm:ss"); - formatStyle(o, "S-"); - formatStyle(o, "M-"); - formatStyle(o, "L-"); - formatStyle(o, "F-"); - formatStyle(o, "-S"); - formatStyle(o, "-M"); - formatStyle(o, "-L"); - formatStyle(o, "-F"); + LocalDateTime localDateTime = + LocalDateTime.parse("2015-11-06T10:55:53.456"); + testPattern("06/11/2015 10:55:53", localDateTime, + "dd/MM/yyyy HH:mm:ss"); + testBasic("06/11/2015", localDateTime); + testStyle("06/11/15", localDateTime, "S-"); + testStyle("06/11/2015", localDateTime, "M-"); + testStyle("6 / de novembre / 2015", localDateTime, "L-"); + testStyle("divendres, 6 / de novembre / 2015", localDateTime, "F-"); + testStyle("10:55", localDateTime, "-S"); + testStyle("10:55:53", localDateTime, "-M"); + testStyle("10:55:53 CET", localDateTime, "-L"); + testStyle("10:55:53 CET", localDateTime, "-F"); } @Test public void monthTest() throws IOException, JspException { - for (Month o : Month.values()) { - mockPageContext.getOut().println("Month: " + o); - formatPattern(o, "M MM MMM MMMM L LL LLL LLLL"); - } - + testPattern("4 04 d’abr. d’abril 4 04 abr. abril", + Month.APRIL, + "M MM MMM MMMM L LL LLL LLLL"); } @Test public void monthDayTest() throws IOException, JspException { - MonthDay o = MonthDay.now(); - mockPageContext.getOut().println("MonthDay: " + o); - formatPattern(o, "M d"); + MonthDay monthDay = MonthDay.parse("--11-06"); + testPattern("11 6", monthDay, "M d"); } @Test public void offsetDateTimeTest() throws IOException, JspException { - Object o = OffsetDateTime.now(); - mockPageContext.getOut().println("OffsetDateTime: " + o); - format(o); - formatPattern(o, "G u y D M d Q Y w W E e F a h K k H m s S A n N VV z O X x Z"); - formatStyle(o, "S-"); - formatStyle(o, "M-"); - formatStyle(o, "L-"); - formatStyle(o, "F-"); - formatStyle(o, "-S"); - formatStyle(o, "-M"); - formatStyle(o, "-L"); - formatStyle(o, "-F"); + OffsetDateTime offsetDateTime = + OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); + testBasic("06/11/2015", offsetDateTime); + testStyle("06/11/15", offsetDateTime, "S-"); + testStyle("06/11/2015", offsetDateTime, "M-"); + testStyle("6 / de novembre / 2015", offsetDateTime, "L-"); + testStyle("divendres, 6 / de novembre / 2015", offsetDateTime, "F-"); + testStyle("10:58", offsetDateTime, "-S"); + testStyle("10:58:21", offsetDateTime, "-M"); + testStyle("10:58:21 CET", offsetDateTime, "-L"); + testStyle("10:58:21 CET", offsetDateTime, "-F"); } @Test public void offsetTimeTest() throws IOException, JspException { - Object o = OffsetTime.now(); - mockPageContext.getOut().println("OffsetTime: " + o); - formatPattern(o, "HH:mm:ss"); - formatStyle(o, "-S"); - formatStyle(o, "-M"); - formatStyle(o, "-L"); - formatStyle(o, "-F"); + OffsetTime offsetTime = OffsetTime.parse("11:01:39.810+01:00"); + testPattern("11:01:39", offsetTime, "HH:mm:ss"); + testStyle("11:01", offsetTime, "-S"); + testStyle("11:01:39", offsetTime, "-M"); + testStyle("11:01:39 CET", offsetTime, "-L"); + testStyle("11:01:39 CET", offsetTime, "-F"); } @Test public void yearTest() throws IOException, JspException { - Year o = Year.now(); - mockPageContext.getOut().println("Year: " + o); - formatPattern(o, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG"); + Year year = Year.parse("2015"); + testPattern("2015 15 2015 2015 2015 15 2015 2015 dC dC dC AD", + year, + "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG"); } @Test public void yearMonthTest() throws IOException, JspException { - YearMonth o = YearMonth.now(); - mockPageContext.getOut().println("YearMonth: " + o); - formatPattern(o, "G u y M L Q QQ QQQ QQQQ"); + YearMonth yearMonth = YearMonth.parse("2015-11"); + testPattern("dC 2015 2015 11 11 4 04 4T 4t trimestre", + yearMonth, + "G u y M L Q QQ QQQ QQQQ"); } @Test public void zonedDateTime() throws IOException, JspException { - Object o = ZonedDateTime.now(); - mockPageContext.getOut().println("ZonedDateTime: " + o); - format(o); - formatPattern(o, "G u y D M d Q Y w W E e F a h K k H m s S A n N VV z O X x Z"); - formatStyle(o, "S-"); - formatStyle(o, "M-"); - formatStyle(o, "L-"); - formatStyle(o, "F-"); - formatStyle(o, "-S"); - formatStyle(o, "-M"); - formatStyle(o, "-L"); - formatStyle(o, "-F"); - } - - private void format(Object o) throws JspException, IOException { - format(o, null, null); - } - - private void formatPattern(Object o, String pattern) throws JspException, IOException { - format(o, pattern, null); - } - - private void formatStyle(Object o, String style) throws JspException, IOException { - mockPageContext.getOut().print("Style: " + style + " => "); - format(o, null, style); - } + ZonedDateTime zonedDateTime = + ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); + testBasic("06/11/2015", zonedDateTime); + testStyle("06/11/15", zonedDateTime, "S-"); + testStyle("06/11/2015", zonedDateTime, "M-"); + testStyle("6 / de novembre / 2015", zonedDateTime, "L-"); + testStyle("divendres, 6 / de novembre / 2015", zonedDateTime, "F-"); + testStyle("11:04", zonedDateTime, "-S"); + testStyle("11:04:47", zonedDateTime, "-M"); + testStyle("11:04:47 CET", zonedDateTime, "-L"); + testStyle("11:04:47 CET", zonedDateTime, "-F"); + + ZonedDateTime pstZonedDateTime = + zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); + System.out.println(pstZonedDateTime); + testBasic("06/11/2015", pstZonedDateTime); + testStyle("06/11/15", pstZonedDateTime, "S-"); + testStyle("06/11/2015", pstZonedDateTime, "M-"); + testStyle("6 / de novembre / 2015", pstZonedDateTime, "L-"); + testStyle("divendres, 6 / de novembre / 2015", pstZonedDateTime, "F-"); + testStyle("02:04", pstZonedDateTime, "-S"); + testStyle("02:04:47", pstZonedDateTime, "-M"); + testStyle("02:04:47 PST", pstZonedDateTime, "-L"); + testStyle("02:04:47 PST", pstZonedDateTime, "-F"); } + + private void testBasic(String expected, Object o) + throws JspException, IOException + { + assertEquals(expected, format(o, null, null)); + } + + private void testStyle(String expected, Object o, String style) + throws JspException, IOException + { + assertEquals(expected, format(o, null, style)); + } + + private void testPattern(String expected, Object data, String pattern) + throws JspException, IOException + { + assertEquals(expected, format(data, pattern, null)); + } + + private String format(Object o, String pattern, String style) + throws JspException, IOException + { + MockPageContext mockPageContext = new MockPageContext( + mockServletContext); + mockPageContext.getRequest().setCharacterEncoding("UTF-8"); + mockPageContext.getResponse().setCharacterEncoding("UTF-8"); + FormatTag formatTag = new FormatTag(); + formatTag.setPageContext(mockPageContext); - private void format(Object o, String pattern, String style) throws JspException, IOException { formatTag.setPattern(pattern); formatTag.setStyle(style); formatTag.setValue(o); formatTag.doEndTag(); - mockPageContext.getOut().println(); + return mockPageContext.getContentAsString(); } } diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index e6e1c4d..bb9c466 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -14,7 +14,7 @@ import java.util.Locale; /** - * Basic tests. + * Basic parse tests. * * @author Sergi Baila * @link http://blog.agilelogicsolutions.com/2011/02/unit-testing-jsp-custom-tag-using.html From a1c7dc4feebd4592998b6cc2519d76631035a942 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Fri, 6 Nov 2015 12:11:00 +0100 Subject: [PATCH 02/48] Fix for Travis CI (setting default time zone for tests). Fixes #2. --- README.md | 6 +- src/test/java/FormatTagTest.java | 180 +++++++++++++++---------------- 2 files changed, 89 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index dea097e..d6e6124 100644 --- a/README.md +++ b/README.md @@ -234,8 +234,12 @@ Build is based on gradle. See build.gradle included in the repository. ### Changelog +##### v1.1.1 +Fixed issue [#2](https://github.com/sargue/java-time-jsptags/issues/2), better +support of time zones on formatting. + ##### v1.1.0 -Fixed issue #1, added more parse tags. +Fixed issue [#1](https://github.com/sargue/java-time-jsptags/issues/1), added more parse tags. ##### v1.0.0 Some tests added. Minor refactorings and no functionality changed. diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index f67d8b7..61a1ef9 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -9,6 +9,7 @@ import java.io.UnsupportedEncodingException; import java.time.*; import java.util.Locale; +import java.util.TimeZone; import static org.junit.Assert.assertEquals; @@ -25,168 +26,155 @@ public class FormatTagTest { @Before public void setup() throws UnsupportedEncodingException { Locale.setDefault(Locale.forLanguageTag("ca")); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); mockServletContext = new MockServletContext(); } @Test public void dayOfWeekTest() throws IOException, JspException { - testPattern("dl. dl. dl. dilluns 1 01 dl. dilluns", - DayOfWeek.MONDAY, - "E EE EEE EEEE e ee eee eeee"); - testPattern("dt. dt. dt. dimarts 2 02 dt. dimarts", - DayOfWeek.TUESDAY, - "E EE EEE EEEE e ee eee eeee"); + assertEquals("dl. dl. dl. dilluns 1 01 dl. dilluns", format( + DayOfWeek.MONDAY, "E EE EEE EEEE e ee eee eeee", null)); + assertEquals("dt. dt. dt. dimarts 2 02 dt. dimarts", format( + DayOfWeek.TUESDAY, "E EE EEE EEEE e ee eee eeee", null)); } @Test public void instantTest() throws JspException, IOException { Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); - testBasic("06/11/2015", instant); - testStyle("06/11/15", instant, "S-"); - testStyle("06/11/2015", instant, "M-"); - testStyle("6 / de novembre / 2015", instant, "L-"); - testStyle("divendres, 6 / de novembre / 2015", instant, "F-"); - testStyle("10:45", instant, "-S"); - testStyle("10:45:33", instant, "-M"); - testStyle("10:45:33 CET", instant, "-L"); - testStyle("10:45:33 CET", instant, "-F"); + assertEquals("06/11/2015", format(instant, null, null)); + assertEquals("06/11/15", format(instant, null, "S-")); + assertEquals("06/11/2015", format(instant, null, "M-")); + assertEquals("6 / de novembre / 2015", format(instant, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", + format(instant, null, "F-")); + assertEquals("10:45", format(instant, null, "-S")); + assertEquals("10:45:33", format(instant, null, "-M")); + assertEquals("10:45:33 CET", format(instant, null, "-L")); + assertEquals("10:45:33 CET", format(instant, null, "-F")); } @Test public void localDateTest() throws IOException, JspException { LocalDate localDate = LocalDate.parse("2015-11-06"); - testBasic("06/11/2015", localDate); - testPattern("06/11/2015", localDate, "dd/MM/yyyy"); - testStyle("06/11/15", localDate, "S-"); - testStyle("06/11/2015", localDate, "M-"); - testStyle("6 / de novembre / 2015", localDate, "L-"); - testStyle("divendres, 6 / de novembre / 2015", localDate, "F-"); + assertEquals("06/11/2015", format(localDate, null, null)); + assertEquals("06/11/2015", format(localDate, "dd/MM/yyyy", null)); + assertEquals("06/11/15", format(localDate, null, "S-")); + assertEquals("06/11/2015", format(localDate, null, "M-")); + assertEquals("6 / de novembre / 2015", format(localDate, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", + format(localDate, null, "F-")); } @Test public void localTimeTest() throws IOException, JspException { LocalTime localTime = LocalTime.parse("10:53:55.913"); - testPattern("10:53:55", localTime, "HH:mm:ss"); - testStyle("10:53", localTime, "-S"); - testStyle("10:53:55", localTime, "-M"); - testStyle("10:53:55 CET", localTime, "-L"); - testStyle("10:53:55 CET", localTime, "-F"); + assertEquals("10:53:55", format(localTime, "HH:mm:ss", null)); + assertEquals("10:53", format(localTime, null, "-S")); + assertEquals("10:53:55", format(localTime, null, "-M")); + assertEquals("10:53:55 CET", format(localTime, null, "-L")); + assertEquals("10:53:55 CET", format(localTime, null, "-F")); } @Test public void localDateTimeTest() throws IOException, JspException { LocalDateTime localDateTime = LocalDateTime.parse("2015-11-06T10:55:53.456"); - testPattern("06/11/2015 10:55:53", localDateTime, - "dd/MM/yyyy HH:mm:ss"); - testBasic("06/11/2015", localDateTime); - testStyle("06/11/15", localDateTime, "S-"); - testStyle("06/11/2015", localDateTime, "M-"); - testStyle("6 / de novembre / 2015", localDateTime, "L-"); - testStyle("divendres, 6 / de novembre / 2015", localDateTime, "F-"); - testStyle("10:55", localDateTime, "-S"); - testStyle("10:55:53", localDateTime, "-M"); - testStyle("10:55:53 CET", localDateTime, "-L"); - testStyle("10:55:53 CET", localDateTime, "-F"); + assertEquals("06/11/2015 10:55:53", + format(localDateTime, "dd/MM/yyyy HH:mm:ss", null)); + assertEquals("06/11/2015", format(localDateTime, null, null)); + assertEquals("06/11/15", format(localDateTime, null, "S-")); + assertEquals("06/11/2015", format(localDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", format(localDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", + format(localDateTime, null, "F-")); + assertEquals("10:55", format(localDateTime, null, "-S")); + assertEquals("10:55:53", format(localDateTime, null, "-M")); + assertEquals("10:55:53 CET", format(localDateTime, null, "-L")); + assertEquals("10:55:53 CET", format(localDateTime, null, "-F")); } @Test public void monthTest() throws IOException, JspException { - testPattern("4 04 d’abr. d’abril 4 04 abr. abril", - Month.APRIL, - "M MM MMM MMMM L LL LLL LLLL"); + assertEquals("4 04 d’abr. d’abril 4 04 abr. abril", + format(Month.APRIL, "M MM MMM MMMM L LL LLL LLLL", null)); } @Test public void monthDayTest() throws IOException, JspException { MonthDay monthDay = MonthDay.parse("--11-06"); - testPattern("11 6", monthDay, "M d"); + assertEquals("11 6", format(monthDay, "M d", null)); } @Test public void offsetDateTimeTest() throws IOException, JspException { OffsetDateTime offsetDateTime = OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); - testBasic("06/11/2015", offsetDateTime); - testStyle("06/11/15", offsetDateTime, "S-"); - testStyle("06/11/2015", offsetDateTime, "M-"); - testStyle("6 / de novembre / 2015", offsetDateTime, "L-"); - testStyle("divendres, 6 / de novembre / 2015", offsetDateTime, "F-"); - testStyle("10:58", offsetDateTime, "-S"); - testStyle("10:58:21", offsetDateTime, "-M"); - testStyle("10:58:21 CET", offsetDateTime, "-L"); - testStyle("10:58:21 CET", offsetDateTime, "-F"); + assertEquals("06/11/2015", format(offsetDateTime, null, null)); + assertEquals("06/11/15", format(offsetDateTime, null, "S-")); + assertEquals("06/11/2015", format(offsetDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", + format(offsetDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", + format(offsetDateTime, null, "F-")); + assertEquals("10:58", format(offsetDateTime, null, "-S")); + assertEquals("10:58:21", format(offsetDateTime, null, "-M")); + assertEquals("10:58:21 CET", format(offsetDateTime, null, "-L")); + assertEquals("10:58:21 CET", format(offsetDateTime, null, "-F")); } @Test public void offsetTimeTest() throws IOException, JspException { OffsetTime offsetTime = OffsetTime.parse("11:01:39.810+01:00"); - testPattern("11:01:39", offsetTime, "HH:mm:ss"); - testStyle("11:01", offsetTime, "-S"); - testStyle("11:01:39", offsetTime, "-M"); - testStyle("11:01:39 CET", offsetTime, "-L"); - testStyle("11:01:39 CET", offsetTime, "-F"); + assertEquals("11:01:39", format(offsetTime, "HH:mm:ss", null)); + assertEquals("11:01", format(offsetTime, null, "-S")); + assertEquals("11:01:39", format(offsetTime, null, "-M")); + assertEquals("11:01:39 CET", format(offsetTime, null, "-L")); + assertEquals("11:01:39 CET", format(offsetTime, null, "-F")); } @Test public void yearTest() throws IOException, JspException { Year year = Year.parse("2015"); - testPattern("2015 15 2015 2015 2015 15 2015 2015 dC dC dC AD", - year, - "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG"); + assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC AD", format( + year, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG", null)); } @Test public void yearMonthTest() throws IOException, JspException { YearMonth yearMonth = YearMonth.parse("2015-11"); - testPattern("dC 2015 2015 11 11 4 04 4T 4t trimestre", - yearMonth, - "G u y M L Q QQ QQQ QQQQ"); + assertEquals("dC 2015 2015 11 11 4 04 4T 4t trimestre", format( + yearMonth, "G u y M L Q QQ QQQ QQQQ", null)); } @Test public void zonedDateTime() throws IOException, JspException { ZonedDateTime zonedDateTime = ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); - testBasic("06/11/2015", zonedDateTime); - testStyle("06/11/15", zonedDateTime, "S-"); - testStyle("06/11/2015", zonedDateTime, "M-"); - testStyle("6 / de novembre / 2015", zonedDateTime, "L-"); - testStyle("divendres, 6 / de novembre / 2015", zonedDateTime, "F-"); - testStyle("11:04", zonedDateTime, "-S"); - testStyle("11:04:47", zonedDateTime, "-M"); - testStyle("11:04:47 CET", zonedDateTime, "-L"); - testStyle("11:04:47 CET", zonedDateTime, "-F"); + assertEquals("06/11/2015", format(zonedDateTime, null, null)); + assertEquals("06/11/15", format(zonedDateTime, null, "S-")); + assertEquals("06/11/2015", format(zonedDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", format(zonedDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", + format(zonedDateTime, null, "F-")); + assertEquals("11:04", format(zonedDateTime, null, "-S")); + assertEquals("11:04:47", format(zonedDateTime, null, "-M")); + assertEquals("11:04:47 CET", format(zonedDateTime, null, "-L")); + assertEquals("11:04:47 CET", format(zonedDateTime, null, "-F")); ZonedDateTime pstZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); System.out.println(pstZonedDateTime); - testBasic("06/11/2015", pstZonedDateTime); - testStyle("06/11/15", pstZonedDateTime, "S-"); - testStyle("06/11/2015", pstZonedDateTime, "M-"); - testStyle("6 / de novembre / 2015", pstZonedDateTime, "L-"); - testStyle("divendres, 6 / de novembre / 2015", pstZonedDateTime, "F-"); - testStyle("02:04", pstZonedDateTime, "-S"); - testStyle("02:04:47", pstZonedDateTime, "-M"); - testStyle("02:04:47 PST", pstZonedDateTime, "-L"); - testStyle("02:04:47 PST", pstZonedDateTime, "-F"); } - - private void testBasic(String expected, Object o) - throws JspException, IOException - { - assertEquals(expected, format(o, null, null)); - } - - private void testStyle(String expected, Object o, String style) - throws JspException, IOException - { - assertEquals(expected, format(o, null, style)); - } - - private void testPattern(String expected, Object data, String pattern) - throws JspException, IOException - { - assertEquals(expected, format(data, pattern, null)); + assertEquals("06/11/2015", format(pstZonedDateTime, null, null)); + assertEquals("06/11/15", format(pstZonedDateTime, null, "S-")); + assertEquals("06/11/2015", format(pstZonedDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", + format(pstZonedDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", + format(pstZonedDateTime, null, "F-")); + assertEquals("02:04", format(pstZonedDateTime, null, "-S")); + assertEquals("02:04:47", format(pstZonedDateTime, null, "-M")); + assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-L")); + assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-F")); } private String format(Object o, String pattern, String style) From 0f73c0625c8e5f62ec511e0604d907a3cbcccead Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Fri, 6 Nov 2015 12:30:18 +0100 Subject: [PATCH 03/48] Maven central repository name fix. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1c7dc89..6f5f5a6 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ uploadArchives { } pom.project { - name name + name 'Java 8 java.time JSP tags' packaging 'jar' description 'JSP tag support for Java 8 java.time (JSR-310)' url 'https://github.com/sargue/java-time-jsptags' From 76d8ce023b21286a3ee6f037bd0ac4161ace13ec Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Fri, 1 Apr 2016 10:28:34 +0200 Subject: [PATCH 04/48] Changed dependencies from 'compile' to 'compileOnly' (provided) --- .gitignore | 69 ++++++++-- README.md | 15 ++- build.gradle | 14 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 +++++++++++++++++++++++ gradlew.bat | 90 +++++++++++++ 7 files changed, 332 insertions(+), 22 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat diff --git a/.gitignore b/.gitignore index b0f0bcb..f7a0232 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,57 @@ -/bin/ -/target/ -*.log -/tests/ -.checkstyle -.classpath -.project -/.settings/ -/nbproject/ -.idea +### Gradle template +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + *.iml -/out -/.gradle -/build + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties diff --git a/README.md b/README.md index d6e6124..1cab938 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ This project is forked from and based on the original Joda-Time JSP Tags. ### Project status -The status is currently beta stage. - -I started this project because I needed a replacement of Joda-Time JSP Tags +I started this project because I needed a replacement of Joda-Time JSP Tags after migration of a project to Java 8. ### About @@ -33,7 +31,7 @@ library and almost exactly as the tags in the original Joda-Time JSP Tags. Add the dependency to your project: #### Gradle -`compile 'net.sargue:java-time-jsptags:1.1.1'` +`compile 'net.sargue:java-time-jsptags:1.1.2'` #### Maven @@ -41,7 +39,7 @@ Add the dependency to your project: net.sargue java-time-jsptags - 1.1.1 + 1.1.2 ``` @@ -234,6 +232,13 @@ Build is based on gradle. See build.gradle included in the repository. ### Changelog +##### v1.1.2 +I have changed the gradle build to use the gradle wrapper and gradle version +2.12 which finally includes a compile-only (like *provided*) configuration. +I have updated the build script acordingly. It shouldn't break any build but I +detected that including this library before this change leaked some undesired +jar files (like the JSTL API). + ##### v1.1.1 Fixed issue [#2](https://github.com/sargue/java-time-jsptags/issues/2), better support of time zones on formatting. diff --git a/build.gradle b/build.gradle index 6f5f5a6..31f027e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,11 @@ apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' +apply plugin: 'idea' group = 'net.sargue' archivesBaseName = 'java-time-jsptags' -version = '1.1.1' +version = '1.1.2' def NEXUS_USERNAME = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : '' def NEXUS_PASSWORD = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : '' @@ -17,10 +18,15 @@ repositories { mavenCentral() } +configurations { + testCompile.extendsFrom compileOnly +} + dependencies { - compile 'javax.servlet:javax.servlet-api:3.0.1' - compile 'javax.servlet.jsp:javax.servlet.jsp-api:2.2.1' - compile 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' + compileOnly 'javax.servlet:javax.servlet-api:3.0.1' + compileOnly 'javax.servlet.jsp:javax.servlet.jsp-api:2.2.1' + compileOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' + testCompile 'junit:junit:4.12' testCompile 'org.springframework:spring-test:4.1.7.RELEASE' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..941144813d241db74e1bf25b6804c679fbe7f0a3 GIT binary patch literal 53636 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 DXC5Z7 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9d2227d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Apr 01 10:00:03 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 9ab9237de89f5e09bff0ce335ea8b9ebe204821d Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Fri, 1 Apr 2016 11:08:59 +0200 Subject: [PATCH 05/48] permission access for travis --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 2ce1c6d9a3cd34a248f7f04b81e76d45045d3de3 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Fri, 22 Apr 2016 09:11:47 +0200 Subject: [PATCH 06/48] Update README.md --- README.md | 70 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 1cab938..617e3b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![Build Status](https://travis-ci.org/sargue/java-time-jsptags.svg?branch=master)](https://travis-ci.org/sargue/java-time-jsptags) -## Java 8 java.time JSP tags +Java 8 java.time JSP tags +========================= This project provides JSP tags for the new java.time package present in Java 8. @@ -9,31 +10,35 @@ library. This project is forked from and based on the original Joda-Time JSP Tags. -### Project status +Project status +-------------- I started this project because I needed a replacement of Joda-Time JSP Tags after migration of a project to Java 8. -### About +About +----- This library works very similarly to the date-related tags in the jstl fmt library and almost exactly as the tags in the original Joda-Time JSP Tags. -### Requirements +Requirements +------------ * Java 8 * Servlet 2.4 * JSP 2.0 * JSTL 1.1 -### Usage +Usage +----- Add the dependency to your project: -#### Gradle +### Gradle `compile 'net.sargue:java-time-jsptags:1.1.2'` -#### Maven +### Maven ```xml @@ -43,7 +48,7 @@ Add the dependency to your project: ``` -#### Tag library declaration +### Tag library declaration Declare the library as follows in your jsp pages: @@ -51,9 +56,15 @@ Declare the library as follows in your jsp pages: <%@ taglib uri="http://sargue.net/jsptags/time" prefix="javatime" %> ``` -#### Tags +### Javadocs -##### <javatime:format> +You can [browse online the javadocs](http://www.javadoc.io/doc/net.sargue/java-time-jsptags) thanks to the great [javadoc.io](http://javadoc.io) service. + +Tags +==== + +<javatime:format> +----------------------- Example: ``` @@ -63,7 +74,7 @@ Example: Formats any `java.util.Temporal` like `Instant`, `LocalDateTime`, `LocalDate`, `LocalTime`, etc. The `var` and `scope` attributes can be used to set the value of a variable instead of printing the result. -###### Time zone (ZoneId) +### Time zone (ZoneId) A time zone may be necessary to perform some formatting. It depends on the desired format and the value object. An `Instant` has no time zone so if you @@ -96,7 +107,8 @@ Attributes: | pattern | The pattern to use for formatting (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for formatting. See comment above for fallback and defaults. | -##### <javatime:parseInstant> +<javatime:parseInstant> +----------------------------- Example: ``` @@ -120,7 +132,8 @@ Attributes: | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | -##### <javatime:parseLocalDateTime> +<javatime:parseLocalDateTime> +----------------------------------- Example: ``` @@ -144,7 +157,8 @@ Attributes: | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | -##### <javatime:parseLocalDate> +<javatime:parseLocalDate> +------------------------------- Example: ``` @@ -168,7 +182,8 @@ Attributes: | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | -##### <javatime:parseLocalTime> +<javatime:parseLocalTime> +------------------------------- Example: ``` @@ -192,7 +207,8 @@ Attributes: | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | -##### <javatime:zoneId> +<javatime:zoneId> +----------------------- Example: ``` @@ -208,7 +224,8 @@ The `` tag may override this value with an explicit `zoneId` a |:-----------------|:----------------------------------------------| | value (required) | The default time zone for nested tags to use. | -##### <javatime:setZoneId> +<javatime:setZoneId> +-------------------------- Example: ``` @@ -226,34 +243,37 @@ a `zoneId` attribute and is not nested within a `` tag. | var | The scoped variable to set. | | scope | The scope of the variable to set. | -### Build +Build +===== Build is based on gradle. See build.gradle included in the repository. -### Changelog +Changelog +--------- -##### v1.1.2 +### v1.1.2 I have changed the gradle build to use the gradle wrapper and gradle version 2.12 which finally includes a compile-only (like *provided*) configuration. I have updated the build script acordingly. It shouldn't break any build but I detected that including this library before this change leaked some undesired jar files (like the JSTL API). -##### v1.1.1 +### v1.1.1 Fixed issue [#2](https://github.com/sargue/java-time-jsptags/issues/2), better support of time zones on formatting. -##### v1.1.0 +### v1.1.0 Fixed issue [#1](https://github.com/sargue/java-time-jsptags/issues/1), added more parse tags. -##### v1.0.0 +### v1.0.0 Some tests added. Minor refactorings and no functionality changed. Some documentation. Moved to gradle build. Preparing to publish to Maven Central. -##### v0.1 +### v0.1 First released version just with some refactoring, no tests, no documentation. -### Contributing +Contributing +============ If you found any bug please report it to the GitHub issues page. From 3f7c1d3c75ffb2ba5d22a13f79507bd6ff66d551 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Mon, 13 Jun 2016 15:20:24 +0200 Subject: [PATCH 07/48] updated docs Closes #4 --- README.md | 8 ++++---- .../net/sargue/time/jsptags/Resources.properties | 0 .../net/sargue/time/jsptags/Resources_ja.properties | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename src/main/{java => resources}/net/sargue/time/jsptags/Resources.properties (100%) rename src/main/{java => resources}/net/sargue/time/jsptags/Resources_ja.properties (100%) diff --git a/README.md b/README.md index 617e3b6..f12b92c 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Attributes: | Attribute | Description | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------| -| value | Required unless value is nested within tag. Must be a string which can be parsed into a `java.time.Instant` according to the parsing options specified. | +| value | Required. Must be a string which can be parsed into a `java.time.Instant` according to the parsing options specified. | | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | @@ -149,7 +149,7 @@ Attributes: | Attribute | Description | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------| -| value | Required unless value is nested within tag. Must be a string which can be parsed into a `java.time.LocalDateTime` according to the parsing options specified. | +| value | Required. Must be a string which can be parsed into a `java.time.LocalDateTime` according to the parsing options specified. | | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | @@ -174,7 +174,7 @@ Attributes: | Attribute | Description | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------| -| value | Required unless value is nested within tag. Must be a string which can be parsed into a `java.time.LocalDate` according to the parsing options specified. | +| value | Required. Must be a string which can be parsed into a `java.time.LocalDate` according to the parsing options specified. | | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | @@ -199,7 +199,7 @@ Attributes: | Attribute | Description | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------| -| value | Required unless value is nested within tag. Must be a string which can be parsed into a `java.time.LocalTime` according to the parsing options specified. | +| value | Required. Must be a string which can be parsed into a `java.time.LocalTime` according to the parsing options specified. | | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | diff --git a/src/main/java/net/sargue/time/jsptags/Resources.properties b/src/main/resources/net/sargue/time/jsptags/Resources.properties similarity index 100% rename from src/main/java/net/sargue/time/jsptags/Resources.properties rename to src/main/resources/net/sargue/time/jsptags/Resources.properties diff --git a/src/main/java/net/sargue/time/jsptags/Resources_ja.properties b/src/main/resources/net/sargue/time/jsptags/Resources_ja.properties similarity index 100% rename from src/main/java/net/sargue/time/jsptags/Resources_ja.properties rename to src/main/resources/net/sargue/time/jsptags/Resources_ja.properties From e155cc36ab5d0f18a1693a60cbfa106d331c8f71 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Mon, 13 Jun 2016 15:34:34 +0200 Subject: [PATCH 08/48] updating documentation Closes #3 --- README.md | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f12b92c..c6b5320 100644 --- a/README.md +++ b/README.md @@ -60,19 +60,15 @@ Declare the library as follows in your jsp pages: You can [browse online the javadocs](http://www.javadoc.io/doc/net.sargue/java-time-jsptags) thanks to the great [javadoc.io](http://javadoc.io) service. -Tags -==== +### *Style* and *pattern* attributes -<javatime:format> ------------------------ +Most tags have the attributes `style` and `pattern` which control the formatter beneath the tag. -Example: -``` - -``` +The `style` expected value is two characters, one for date, one for time, from S=Short, M=Medium, L=Long, F=Full, -=None. +They directly map to the enum [`FormatStyle`](https://docs.oracle.com/javase/8/docs/api/java/time/format/FormatStyle.html) -Formats any `java.util.Temporal` like `Instant`, `LocalDateTime`, `LocalDate`, `LocalTime`, etc. -The `var` and `scope` attributes can be used to set the value of a variable instead of printing the result. +The `pattern` attribute is for complete control over your formatting. +The syntax is explain in the [`DateTimeFormatter`](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) ### Time zone (ZoneId) @@ -95,6 +91,20 @@ specified and the value object is one of these classes: * `OffsetDateTime` * `OffsetLocalTime` +Tags +==== + +<javatime:format> +----------------------- + +Example: +``` + +``` + +Formats any `java.util.Temporal` like `Instant`, `LocalDateTime`, `LocalDate`, `LocalTime`, etc. +The `var` and `scope` attributes can be used to set the value of a variable instead of printing the result. + Attributes: | Attribute | Description | @@ -103,7 +113,7 @@ Attributes: | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for formatting. | -| style | The style to use for formatting (see java.time format documentation for recognized style strings) | +| style | The style to use for formatting (two characters, one for date, one for time, from S=Short, M=Medium, L=Long, F=Full, -=None)| | pattern | The pattern to use for formatting (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for formatting. See comment above for fallback and defaults. | @@ -128,7 +138,7 @@ Attributes: | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | -| style | The style to use for parsing (see java.time format documentation for recognized style strings) | +| style | The style to use for parsing (two characters, one for date, one for time, from S=Short, M=Medium, L=Long, F=Full, -=None)| | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | @@ -153,7 +163,7 @@ Attributes: | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | -| style | The style to use for parsing (see java.time format documentation for recognized style strings) | +| style | The style to use for parsing (two characters, one for date, one for time, from S=Short, M=Medium, L=Long, F=Full, -=None)| | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | @@ -178,7 +188,7 @@ Attributes: | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | -| style | The style to use for parsing (see java.time format documentation for recognized style strings) | +| style | The style to use for parsing (two characters, one for date, one for time, from S=Short, M=Medium, L=Long, F=Full, -=None)| | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | @@ -203,7 +213,7 @@ Attributes: | var | The scoped variable to set. | | scope | The scope of the variable to set. | | locale | The locale to use for parsing. | -| style | The style to use for parsing (see java.time format documentation for recognized style strings) | +| style | The style to use for parsing (two characters, one for date, one for time, from S=Short, M=Medium, L=Long, F=Full, -=None)| | pattern | The pattern to use for parsing (see java.time format documentation for recognized pattern strings) | | zoneId | The time zone to use for parsing. See comment above for fallback and defaults. | From 6daf37c03ac5fa4d295961c2b5578b5f4579d749 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Mon, 13 Jun 2016 15:38:12 +0200 Subject: [PATCH 09/48] minor README update --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c6b5320..3897693 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Project status I started this project because I needed a replacement of Joda-Time JSP Tags after migration of a project to Java 8. +The library is pretty stable right now. Should you have any problem +please file an issue. There is no planned development for this library, +just bugfix manteinance. + About ----- @@ -68,7 +72,7 @@ The `style` expected value is two characters, one for date, one for time, from S They directly map to the enum [`FormatStyle`](https://docs.oracle.com/javase/8/docs/api/java/time/format/FormatStyle.html) The `pattern` attribute is for complete control over your formatting. -The syntax is explain in the [`DateTimeFormatter`](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +The syntax is explained in the [`DateTimeFormatter`](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) ### Time zone (ZoneId) From 5547155cca72fabc0596b9030c807f533b19855f Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Mon, 13 Jun 2016 15:38:56 +0200 Subject: [PATCH 10/48] README typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3897693..ee0504c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ after migration of a project to Java 8. The library is pretty stable right now. Should you have any problem please file an issue. There is no planned development for this library, -just bugfix manteinance. +just bugfix maintenance. About ----- From 3f2e4616f59474928b3da1157e98a83a59bf4674 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Thu, 5 Jan 2017 09:52:39 +0100 Subject: [PATCH 11/48] fix resource bundle location, closes #5 --- src/main/java/net/sargue/time/jsptags/Resources.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/sargue/time/jsptags/Resources.java b/src/main/java/net/sargue/time/jsptags/Resources.java index 33c2622..776d5af 100644 --- a/src/main/java/net/sargue/time/jsptags/Resources.java +++ b/src/main/java/net/sargue/time/jsptags/Resources.java @@ -46,7 +46,7 @@ public class Resources { // Static data /** The location of our resources. */ - private static final String RESOURCE_LOCATION = "Resources"; + private static final String RESOURCE_LOCATION = "net.sargue.time.jsptags.Resources"; /** Our class-wide ResourceBundle. */ private static ResourceBundle rb = ResourceBundle.getBundle(RESOURCE_LOCATION); From 4a4b0248c008b19fe7a04f1eabb5b291011bfe29 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Thu, 5 Jan 2017 10:11:02 +0100 Subject: [PATCH 12/48] publish v1.1.3, updated gradle, publish via bintray's jcenter --- README.md | 7 +- build.gradle | 91 ++++++++++++------------ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 1 + 4 files changed, 54 insertions(+), 47 deletions(-) create mode 100644 settings.gradle diff --git a/README.md b/README.md index ee0504c..816574f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Usage Add the dependency to your project: ### Gradle -`compile 'net.sargue:java-time-jsptags:1.1.2'` +`compile 'net.sargue:java-time-jsptags:1.1.3'` ### Maven @@ -48,7 +48,7 @@ Add the dependency to your project: net.sargue java-time-jsptags - 1.1.2 + 1.1.3 ``` @@ -265,6 +265,9 @@ Build is based on gradle. See build.gradle included in the repository. Changelog --------- +### v1.1.3 +Fixed issue [#5](https://github.com/sargue/java-time-jsptags/issues/5) about error messages. + ### v1.1.2 I have changed the gradle build to use the gradle wrapper and gradle version 2.12 which finally includes a compile-only (like *provided*) configuration. diff --git a/build.gradle b/build.gradle index 31f027e..259e0d3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,21 +1,20 @@ +plugins { + id "com.jfrog.bintray" version "1.7.3" +} + apply plugin: 'java' apply plugin: 'maven' -apply plugin: 'signing' -apply plugin: 'idea' group = 'net.sargue' archivesBaseName = 'java-time-jsptags' -version = '1.1.2' - -def NEXUS_USERNAME = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : '' -def NEXUS_PASSWORD = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : '' +version = '1.1.3' sourceCompatibility = 1.8 compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { - mavenCentral() + jcenter() } configurations { @@ -52,49 +51,53 @@ artifacts { archives javadocJar, sourcesJar } -signing { - sign configurations.archives -} - -uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD) - } - - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD) +install { + repositories.mavenInstaller { + pom.project { + name 'Java 8 java.time JSP tags' + description 'JSP tag support for Java 8 java.time (JSR-310)' + url 'https://github.com/sargue/java-time-jsptags' + + scm { + connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' + developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' + url 'git@github.com:sargue/java-time-jsptags.git' } - pom.project { - name 'Java 8 java.time JSP tags' - packaging 'jar' - description 'JSP tag support for Java 8 java.time (JSR-310)' - url 'https://github.com/sargue/java-time-jsptags' - - scm { - connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' - developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' - url 'git@github.com:sargue/java-time-jsptags.git' + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' } + } - licenses { - license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } + developers { + developer { + id 'sargue' + name 'Sergi Baila' + email 'sargue@gmail.com' } + } + } + } +} - developers { - developer { - id 'sargue' - name 'Sergi Baila' - email 'sargue@gmail.com' - } - } +bintray { + user = project.hasProperty('BINTRAY_USER') ? BINTRAY_USER : '' + key = project.hasProperty('BINTRAY_KEY') ? BINTRAY_KEY : '' + configurations = ['archives'] + pkg { + repo = 'maven' + name = 'net.sargue:java-time-jsptags' + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/sargue/java-time-jsptags' + version { + name = project.version + desc = 'JSP tag support for Java 8 java.time (JSR-310)' + + gpg { + sign = true + passphrase = project.hasProperty('BINTRAY_GPG') ? BINTRAY_GPG : '' } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9d2227d..6222571 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..34b9937 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'java-time-jsptags' \ No newline at end of file From 086ca3f89e51ecfabad020b91f089eaad3ad623a Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Mon, 26 Jun 2017 17:29:28 +0200 Subject: [PATCH 13/48] closes #7 Make Util.createFormatterForStyle public --- src/main/java/net/sargue/time/jsptags/Util.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/sargue/time/jsptags/Util.java b/src/main/java/net/sargue/time/jsptags/Util.java index 075b628..b171b54 100644 --- a/src/main/java/net/sargue/time/jsptags/Util.java +++ b/src/main/java/net/sargue/time/jsptags/Util.java @@ -591,15 +591,18 @@ private static ResourceBundle findMatch(String basename, Locale pref) { */ /** - * Select a format from a two character style pattern. The first character + * Creates a formatter from a two character style pattern. The first character * is the date style, and the second character is the time style. Specify a * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' * for full. A date or time may be ommitted by specifying a style character '-'. * * @param style two characters from the set {"S", "M", "L", "F", "-"} * @throws IllegalArgumentException if the style is invalid + * @return a formatter for the specified style */ - static DateTimeFormatter createFormatterForStyle(String style) throws JspException { + public static DateTimeFormatter createFormatterForStyle(String style) + throws JspException + { if (style == null || style.length() != 2) { throw new JspException("Invalid style specification: " + style); } From a1de0b01152c4d7b901813d87ce500bdbd3e6151 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Tue, 27 Jun 2017 18:48:22 +0200 Subject: [PATCH 14/48] publish v1.1.4 --- README.md | 7 +++++-- build.gradle | 2 +- src/main/java/net/sargue/time/jsptags/Util.java | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 816574f..cd0143d 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Usage Add the dependency to your project: ### Gradle -`compile 'net.sargue:java-time-jsptags:1.1.3'` +`compile 'net.sargue:java-time-jsptags:1.1.4'` ### Maven @@ -48,7 +48,7 @@ Add the dependency to your project: net.sargue java-time-jsptags - 1.1.3 + 1.1.4 ``` @@ -265,6 +265,9 @@ Build is based on gradle. See build.gradle included in the repository. Changelog --------- +### v1.1.4 +Made helper method public [by request](https://github.com/sargue/java-time-jsptags/issues/7). + ### v1.1.3 Fixed issue [#5](https://github.com/sargue/java-time-jsptags/issues/5) about error messages. diff --git a/build.gradle b/build.gradle index 259e0d3..eb0f7f3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'maven' group = 'net.sargue' archivesBaseName = 'java-time-jsptags' -version = '1.1.3' +version = '1.1.4' sourceCompatibility = 1.8 compileJava.options.encoding = 'UTF-8' diff --git a/src/main/java/net/sargue/time/jsptags/Util.java b/src/main/java/net/sargue/time/jsptags/Util.java index b171b54..acd28fb 100644 --- a/src/main/java/net/sargue/time/jsptags/Util.java +++ b/src/main/java/net/sargue/time/jsptags/Util.java @@ -597,7 +597,7 @@ private static ResourceBundle findMatch(String basename, Locale pref) { * for full. A date or time may be ommitted by specifying a style character '-'. * * @param style two characters from the set {"S", "M", "L", "F", "-"} - * @throws IllegalArgumentException if the style is invalid + * @throws JspException if the style is invalid * @return a formatter for the specified style */ public static DateTimeFormatter createFormatterForStyle(String style) From 41899e104130fb02c63124c3b2056f08f4818736 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 21 Jan 2022 21:50:34 -0600 Subject: [PATCH 15/48] Upgrade to gradle 7.3 --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 68 +++++++++++++---------- gradlew.bat | 14 ++--- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..740092e660c7c8c4f466792e06a3c2af15f87163 100644 GIT binary patch delta 24979 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 delta 24404 zcmZ6yV~{4%(k?_CR%EWq zwQ8^eG_(X1K}i-A3rhK@h=wW z-var+w|@)l-{t6H$q4%Y#3aoS!~7@T|2>iPM*Y8bDU}RcSpV~KbT4cT?O#5pf4Rhf zQj|x~Q}}S90jl1vZszt%E{;y-E^gN5uK%9AyoKG}tjrzUtWAyG99e9B>eAG^&9T$yopyQ&TndPhHDSgY7}SaFdt@2UYsvovM6hQY%B{Mq1UrL0-%J+!&Aag0Ivj>%$Olh+WUm0cdiS z?U>B9gwLep3a{oH$SU3>BzLWbH`VP>sp|(csc>soL`co2PgE!)-r9)f)ArJP&tcms zy`pLWPsVHTfQg)(Cs+-vbX}1f)h+|79Meop2sm7Yrk^0dZwI1OUj8bR0|8K}epXf4 zu3|`3-d$s=8EG>n#G!h-NX>i##ZwR(YibM@tx_CUGiJK>w98bUKNEK(#PEELO2M+8 z_mG_jHfG~On?s^}OZWu1UPFzafMN5AZw+n$aP3?!kf?{yv-g5agg@5OpZxIMKXAGV z{&eEfj8^K=fC#L7dFd}n8xIx<5;ZMbSM9Xql*mw3zG~**6|G#=7gq*Z&T_Sjh8DsA z=aJ{SB~qQ@JlS*uG>rCnq)#~oIBlJj5TbE&ojPL>J3Z1IA%qX)E*a&xlL6wNJ%wBa z(_ac(W85l+P8V>e6;UoGwR!~e|r=h2HYW!9|V8T?*9Ja5d8UEg$F`BP9bV9 zY5_qm9a%b&^h7eidpj0G5^#&221q+!LKkXYTMKiOr}q;nl+u;mm28fOQj=1YiYZqO6t1Q}WEald0aE z`6#-(k$sYfs+^@Ulb6#{fA9-GrapY{D%F)0GJ1DRBRgd7)aQN3&DUD)q#agjA<#K)9>Fv$96-RcK{* zW<(nJYL%pEO|`l-<(8SbvvW95fArdnZZVoYn+W5o^0MjMPn)fX6v;BD=Gq`vDmp`< zY!y4;Wn-hd(BIZs+4|6#0&ud5&8x!kWvRLu_}bZKV*b1zc(a=>XBH6adgRPvB_O9b zSV)kqbR|Q;TZW?#E4>6C2N}mk%2hobGPA-OM=qp-%&z=JB!UJ$oM&$yQ;DtRzJ$0+ z=9u5S(H=n^l$|);p%cz=Voa^m&-@$pWd~27hi`1lv+I)iM|e$i7O;(j8|uGUa{@QY zptEvkBR`&D@pA9KI_kbOs6Mo%Y;K|?e{Lbnuf3wCTR`;gyM`C8E8Cuvz$F)CGRz0% zDrH?q!b;0%yvrhirPZL_%2LL%*0AI58`d04^~gdnq=4Pis2a>CD1*&7nB3_6V1`d^ z5zjbi8Z*}$sR__ae!wxv zNE%cP=p{aumuoh>{kWyZG4VTVCN5&RiFX~ri-y8;bbcmDuQm%`wKLOOv@?l=%hE8b z?;^d;6}Mw$5qop4?z<5Q7*Fst?~Qo-eis^xnD~51icLte1mtH>hlQ4ZW;-JcALS=2 zVW6`&OjW%?i$5z`+Z4mcM9eEDIhd!;yex*F5bzY-JDNq&bf@;Im8c0Z*R@|HlU-}I zk78n>;Pdb#uhs^G{>%hd`cT5O^Gt;^%7;)XWRzcJ)6P^-O)#OfkoMbiB&&z}T~&k3 z#MsN1=APXI0nqEx2s*ummWQvAPpf+iYiO>n;s{WaR>f0l%&Wr>wjp544O&-GqxomB z^%>UvTPihuaL*GEGx|GIT^rs`(}s-()2nM_f+W@$viu=_^C5g`Nxp+lA&q%l1MQ3L z@j;Q0QRJN^VtGi4m|Sr>+5ff=WHH~d#i<}>cj$sq1eorsr|LVWdh*&B)Ngk>NP@#_ zF5eTVkksR3H)1*|48{G<4JAdKXTB~$+x5D9FUkO(WCgt&yroW^ALL|$7fcAERtpW* zD_7!%@9K%#p;pUFKQf>;2!LOm6`&@7g6x-sWNhmnV?4}K6ZnRkWv#@vf~m_I+v?=J z&J&?B01%X4@<{%JHn_^Ct`# z!aw`zXvyHt8i@3%@5y|la@`n|<5X#ehRl2j0q~W*bJh`lemGtt!zJ+qk0(m9?vI3Y zhHA(iA@R2@YJwP+;O`fL?jKPF3@$&fM;TfjVt98Cx zFCmw0yKNu3G@WI2XB)e6yHpaJhMDN1lkb)AzdOPVyn#g0KicsD0R%+0@)I4 zRQbOfgR(!q^~pbCg8i>CF#RVg51$Zl<%+6~_SZhgY}jMEL`g;_3<|{hPh=pv9SqHa zOc^4X5?ye&ykkn38K)gbD>{0?yTtbpT>P-60C&hR*5KTP=U=K9PtKy*m@NyR+rBgR z`Hwu;`Ro0^Umuu(-|yPP46&77qN1o0Y`G(q(VXn>eiF#=1IT;s)koYR1Ia@E><)v#8*u7t)73J!TvZ|f4K0VZjLEEN zk_(kRv;IOO*{~30O|EcT+3lCE6F2h}Gd0WF!m`??iBNN78{`ES^g#PMqy`{r)=6vz z$2J^umu|9X*{AAYhSPiu_rgD0MLvy}+>#eg`a^Qs;kr)erd}^5^_vyxjj#$TsRx%n z60$F{s`x#^=bUvru1gbu3h#_)S{@doWjpK8jNo|7*5qfai^im7XZ1gOJB1Z!ZuMXR zUXK}@y?lC$i1!n?Gv+YMq?(8}uoNFRzU0%pt31s(&YR_<1PBB&%qgVA<$qUVOJ-%D=^Jy=h8n-1d2z(VhLf&^ANME=#J9guNJ^r1BtFp7*uaPtM>A zf8Hy^uTq*f=jk4xOu&npA)U z>vq`ETZR*VQsmjl^5@ukSUR!>+?ZTE1W!Z48lT}<5wg*7F4g_D0zes&<{Xo!6E-mn`YaGsKrcsI!(N||1$$XJxNb+yfb)|AA+fxfu*hLls# zYVRHT8np)?X%~=)2o^S+c*EL8+xZ3E1)Fqo2m29oPG|CIM1)3Fe1-5D3er!KkEE!DnOdXhbgWm^3U2Dx_1g!wChG&hzVsJ-6n>#DwQFa2HgnH-9jY)K3UZ9qN%*;DFx=ApM*Ay6rT}uaJG5ch-9| zoUHpf9e;o3zd3<(#|DnLqK61_M^*4397d@7h(wjcPpITJ+s&F#MgcL-Bg%>Rx@43P znx`sw$~&nyx-7L4O&H_Ceq`|pQvk~c6*$6}qhn`3kb>AlRcAk(f}~MP@oxOlGqWE* zyz%>HIK#-J7-c`|aK`b)u!fO)b>*LII77^weWZ&VW8no$d;GYx};{|vGxqCcq z4Mw&f#RA9YujaPeY<{faY5qf0mu$%1J52Cz#ld50Z?(Y-g@Ck*tsms!m2sJA*lzwh zdNlV3#sf?C?L_xRY?lnU;#<6?Ymqh2v_5lNTH5ZsyoH^z0Esp^`J8x!Qjcy7$4}Vq z9#+DqDxdcJ2TKXxU8+<&dAE7bfx2~6N9oSrcFsNM;O?_4?v+ICh1`EKDWYOetk*De z?50hy+6SVi2myNFOX#NJzq~H%R(&y@*Q)$l%`h2l=TOK`7cvOykIQZk`=+~(t-$>! zsLtbS3Wqgao=21IJ8d@?Zvd)-y^W_ZytwrUR7zOKMfMsInwT$XM1siUuD^!1{xrL* z5*oO>FF!2dvGiS2&4eeL&IM0>a$mtE=5*)eWzF26Gy_Uq#~2)vyi{|>G_^dyopJDu z!g{YgZ}a15K1EfO2y&ZcUW*xW%>E!Cyi7u)V>&61dh-#CCaA$3kJIE2v!lCu>5Qsk z9gFWdXpbu1M@NIxJ<6;XV^Lv`_BzzOSDk;s30g`t(*zLLz`mD78%90b_7)$^^jLOo zyvw5vr~{aTDzv*(6kf?>*Y(7i`<+A$-Adl`qW6yxd*4e$6C@ra(E&pR>b1w}=zw2_ z=&fFo=@$&VMrbbOHJD&8w#81s4X{Ve!6ELMg@tw9yxJ`KrrP_jmY;>{G^Z)$Clnb{ zJEg|;sXL{Lc>+!P#u8LJ!~1GG2la_PR(Lo(S@*mCz6qh!Yn6Yz??LwWLUHBWkW#I@E=-~=c7s&2Ff z_56D)d}K`v1K~T~?gQX~QvI$qH7DX8n{G#Z(Uu=@li^2Z2v|ZW6>cr_=;9=pfwMD@3q<6w8i78*7uY~7KIP(QdfGAv9 zcNw{a46XNUS(nr0WuO?&B;+1RRqlW*Ray-!hyI;;<*gj!<{G`XZ66rN18dO1)1S+C zcV=x!-ly(yYI?iZ!{I7D|A9vvuenJ+84*jyL~QeMh{3HtS>kY9Z;Mg0oPE5)m` z6}&T*DFA%-Y$&H2(&L=MCoc>Dpu$6d-c~q&piO?F)I302^ zMBPGDq~|(h59Wr0!8t+im+V9n1^}MV9aT}q=C^i=;+Co0EYXW(W-!yu^(a*t6#K^x z_+;lVfJkmaSyW?FtSCAjfh->MKv0QmTvk<=(%lMwYyEoaEt{;zm}uIoqGJ)yDX|it z^gX*0YmhUddPuKj(0&geCFLiI{oDNlRmz&c+On?1I7IwxSmX&mI$4flD!|##-wjmX zH43$MaS}5Bp%EY3aS(Hvc)$$=0 zQ+^-gkAUBX=suAlIPj9~3Py38846;C^QFkAekkiz*4QhH%4#oJuT{QDoP?EkY8S!V z7v*=0E`g5I=o3=em^$CpA|R)LIEXQ4%cLUiU4X znz9um@s8lcL4ub~+(1gaXCk^Z^GuGtCcT|Y7OT^Vky-6m(p}W3O)#NXSC(F&+OY9& ze=TZjYg^T@b-cRi>wA0M+y10^16X))=9nQJF%EoNociXT{ms4eGc@@AT$YToA_gE8 z3e07UQO1$P0SvQ)LD+z;$0jRq3Rwsm?!gcra^}IQ22suJlG#!~^(ItHvv3Yb@-Yf| zJGHVeU?4%+(kv&wvS8*PpRR%CjpYxQLEUiX;qS5e6n$h$xm6E8fU}2>LG`B0fvb;Q zK57%oRkEv{^=8>&81J7%!Z0UeM}f2FWlI571sRjBfUZd2^w>Q7LX-NJCm8Q~x&v1) z^}zX)ClIe1RWJ9z^bpLk%QEZ4a%HMoWSFy0@49C;YQLbNf=Abuf^}(0*}%EumnA!> z`>sx&sCd@tW!HUe$bLWQuAhIwAZ_t39w>3A4XQz?M3+eOSU5VoIO@a{_0M{OB%*YGVvRy}%Jp6_8 z;ZmKhzWZb;vV8*7NtXjG$M3mc#mljKN@Qtlp4hfCk;<4b*pt6`_{+DOS38xh_zpo5 z0Z(;orsZwX7s*Qri&w|&++7>yx|zK%4~@Urnl@vx>8q@Ik~T_BmH3blFOukehEng` zD%LMqQsh|mxnmqjkfjKr$l-QbaArzJ-P&s)bn__jwv0W>QM%2?Olha^DsmohnxZ;M zQhDdpJf=zum9ef-EqrDAIVnlxQ1Kp_0mIdsy-m=n(JmFO9v3m`47T#(Srn3DFX6eu zVGW|5PZJIlZPXZ`caaDsLrk&?HqYKAkwQq~VAf2i+Sn0tgiVV7v>@d&~Jd#|fd{jUOn!X0kqAWZILM%e-~ z)vK>G3z?hNTJBBv7^KD?&nH_prhEue^jJmvRxc97Rb1<<@DePkOg;FBKEoCumvSx| zMtmyEc3VCQxCFrdyIE8V5;OYTfZS~w?Vjl*e*D-MeF-Z|6~C5Nx7Jq5RS4&5F?`0s zaGwd83*_x(@JTNkWEMNG1XiD(xJ2{N8)U#p5SrTZ&S7FrwWcE3DX~R+LTe7E$p}-T zWihoCJFjVXnhaKnK2vgejC46Ad`%N(Ve?2dZmigf90mA9WNp$)JJhrp0E6`0v(Y$| z6US#mI!T%qPf=T&XsdR81XX#r`5Jl;N(JZKt%H>R49i`VgkjRdeJYkKwXtY1brIEU z1S|aMW%EEc2|TE;m>MEIN{utNfS3G$EV%7~>O9s{;PQB}l0Y=IN())BcG_oE&Vk)@ zqr9-KqSX!;EtXQ(O=BDjU9rQl!p~p|c&1!I(CRnLl*Ybh_HRH!7-^4xNE+>f!0oY9>Wb*wWtIxvFr6 zaJ2f|Ym-&J^z9N9!bSc6Z_^cD6T5gTq4eH$fEn&PnVLuLu27IgY3-q zurq1J#93BU*yh4QZrRj7Gkq`sHv>BEz+#?VD%owewB;{fRo6#pk$o0Z zYw31_jVqeuilOi8AP=HsbkGAd_Ee~(5)}WP6A7RN-PKj0u&%Lc36}@<@ zU-LkluyXf^)O?3dD?l43JPo*B6U}O2NLu(L^RMO;5>)+w5-fXM ze&m1x6fEWh{}$@OmW3=DJj1qIpsaQd|HYsha2C;B?7Tvpe9aEPh)BOsU>wWS<}r@CNX<1wZd zkYtEbWsrWFW{fd~;X^Yp<*TF`DM*%XFfE4*Flem$0?p$1JhtU1fBfS`gVHX_?~MZ9 zr0W=o<)h+MKG~&Zf}$Wfl7-I!;rpCPzL+_L_sfvSgoBe9nd-sJ9quG=O7QnO`cKl7 z;AuhSmudj0DZc`FD>dviJruv^gii0#m(sxj9`_H=P^ESGn+IU>RFI-~@H zEoia2BFYuDV7!hucM1b;={KVfF*l>Mzs86W`~#~$cWO@A1($V{+@BbK4+Trn(ZFCE zEKzma=pg+Of&f|G@1z6fXq}~(EJu)^b?rOstl@XF;4iIgokF|k$JOMfqwj$SfMRTF z!DF&aT}#L>=rgI}YpQox{?_0l9S=Q37f5AEiCc7ll@v3K$V>-FV>Yzh2QK(qsj(`aCVMTgjW|V%7JPkR1Cj+b)-xGGFD6*;Yx zC7;aL}VdX2Z^Mj@8hD+XUrxMOU1$p#L8gttaCA+ zgloZJfu6b8b@3?f$6+&d0*ss7zcrU_-Y4Fk^~s zpRl7K`r7HVsZ1T>_i;$SaQ2o4!87<1Ep0VKWo1D*zvO5_Tw)6xPkM+8xmjH?wi^5- z=7vkXd#Cw{p15HtfKj;Ek0+;J&R3XRNcNm#V?Ctp#Eo|sde%Z{-ro(=Vzw;HjdRcM znZ5Sar z425OS@ZAYtfMU0`TF!-UCKM8g8iGPd;!&|J@%>UzCR<|i3*y@QV2RLvam3-;n!%TC zucyPVXoMxtgK|AkL6L-V;3qfi}KjsyA&K+b6Yl0^-7+ zBPI zZ>_F$gvH~+H?T=_)C%8xF~xd?h$_Ll<(9@kC;M8Tq|0eq$jb6HUGhppCro+>A`Az5 zz0!?h&DM7B#iq_S`hn`USL(eU~ikBR?Gx#^y-os-u%;#F-Y)lNgC>@1C?u-&o9f-{-qia{)&rJ zd|TbTgi-+=E)$$B_Q4&(b=!wIyp^f_!HqvO0I>d$xiGCPAV%=5yN(by8uh_$$)*4v zr+^uWUAXsW?%tCaCvEVnrGV#1V%nj+?i8-+Tk7CF%iC2JrL0NRd32 zgx-koD$cs%ht{DJ8!>w&f&xYaNqc@StvoK=!F}i_zdYHt5Xzr|mj_ zW7P1}U^=*z5dY|YT3>1WMwf=dY{M(GM%cFZ!awUZ*a6u{J7L*pPVR!kFbK{NUFd^) z_(F1tl5)6N$Ha;7F!Gpw)1C4kB_3j_b`fbBIy%OOJC^8ORt{Z=PM;Snj^RvYm z0Hb{$q!(&luzPi-dn_TIN8C4?rLA8`p7Yubd~bxmMrh9ipem!!Di&~`RdKh(0q5Ss z;U4*V7G<1s_U|Yg1sYx#$lce@OP^JJUByf*iif?BRQtXQu1vT-VH^o1gY(SU5Uui% zHew;x2w`lFX%q{(9$F`pVcyyEA&DxDUA;r`5jK3W?)mC1G)>UC1`fkDE^8zQwt>CP zK}Ncdcsn;qTEAApL`AqB{*u5w15mX3VvW4ODSXkXui44IGOUqT(2CLn^`R~L~3d7x=YYDZXJ4kdiViXWMcE>~m-p9X^+P_QVg!gd05*dCUf`+!9yGFE&%3?T$`d z!ihb2oD!}}OcqO31d12R3QUA9a6`}_M|!ryx!-{T0vCHQXKM#AsfudIQ3Ua0w;zhpS$Hc0k|&bQg1eIScI z_|CPU>F0~y5}EfQ%l1Ro2&okQ0?7b`9KK1d{6l%J2!$_5)sO&l6h+<}Hv1*}L+!kc zvp)zm;xuMJe@Kd2`=`$U%j^B|yN zZDiTPHDKKl6sp($zK+=@Pdh{-6}i7=XUnybDPPa?g}0F#k=z7)fV4*#JFA}}l@UwS zTw~UiTu{KF+bs$hvD8_%o{qqG!;Af8VwI4JnW>DC-wHbJ6kMfE?}CL_=cE0q zx?h}F6WV$?3LpYKUrvTMU|ozgY4eLPZa+Cc<8kqC2Ur(hjNgV^Gq^_h((D15*e{J| ziL~SuRu7icAHtKHy0hPnEiE~p7k9&+uJ_z?zc1(bJUIyO=6YOqplg|2^K+Ot@?N21 zg}v$co;G55gdQCTc7KUi!aCJ9pDq3c{cpW$cTg^Btgh z@M@GnJ7zE^5VV|81yvc`LN@5IMyzC`be__fMn(y-^h!F^^K=I5PuxFS8T}lL;SGNB z`=5+|Rsf40^=2WQkE@8DSy{ZUb2ELW-yidijKIA?gy!$gni7l_WW9{W8WOapW;?^Z zvH#=)W9e{6X6$LL!{Kiq0N+7!#EVuMQU()Q39>E9nz@~k>Ja3MloMydd*f)t_{+P} znB=?G5ao-~hl{QRW-^Kjh@Wb9(dIG;U(tOykn@-vGjJU9tPyS33`fh_ihUm0PiB_d z3;gQRdp{A{jXpgs5gu!u8=HL0mYbL(FVN6Et%PCxmmjrm0ya^u}arR|;W{;gZB99yb zV|!71DZZPh*^W^Wu(KGz>|K}UP!V3HhoD6sjKdjhh2(<0k&e@ns{t6C)3P*ruJ`oN3s(VOjJ+p1=#`SrkBVA zEK?=%3tHFnlI%7RI&^w%%qRe*_C|cti_6f6xBlGMs=h zoFEZSfa)zf#0k?s>Zx}91XRO4yvA}}YXo9pIg^_YgHw*bQgzJ%yVXN?t^$SO4}p1C z3&CDOrC^;3u(#(O=Lds?Yz{#NpMT(wcnt~s_4lLoJT4cMR`_?@?29^$n<|x_&n_m# zG;NzwW30ON*hF(3$@-{HM|l>$;M5kK>Q7@d0gWP8L-Hn~S`4<@ol_Ge8mYchG-q&f zcm0iyT$!aObyiDf$x(kBT`ld&LCZ{j)x#IBXam$bpm4eS{s&TIb+#hfC`hM?e5c;o zqUKWWT|Mr2N}R`LE8E#fporJ6hAq~2e|iW_?9e7Dus@nZC8RV3syknA9@m&kZ&j9z zq_6l9LNEWPg<@BYx`Y*bwLC(IVB4nb6&ZPlnVzMZNQ*Rq9V%bE{=yG*{rvA5;?I-N z0e##U;HoudiEiNO+|E#*_i3FbreBM{fQ?U+8M+4b_n60<~6B_>To3Aj6<8~;qI8^kV{4)y+^_Ltf(}x%Pju9 z*y|R(Hqtn7gLUKWMwif=`x+Z#Z}K*Oj&F_X=u+*i_W=2L{srA1__lsld_O$^$SbC` zI2_O+^9#N;eg1OR2=ah|g+VRdr9$o%zH*-8ui%k44gvEFp73(cy8Ne(@T|-I-Y2h% z5D2V4=GTeB+tZdw&lS55-uF1#`9S>dawoOcLALzQt3-+g1Vr`!xth=cX`0?fc^llG~}N>B#MuX2qv>(@CXOaVA#nma>c2CU4oaH?BiBHwmC%sHkX)@kr3b z#OpeQFCZcVi!nxV?>+?{hCm(f-)7}FTDI-s0oOTieQ$kt@BZVypTAuZ1!0W(P#%xz z19p%Hbwj9r4P82ptiR@VzXN&^s9;W$>Pl^@Y`rjljA0 z>4$yIxX6ZmPo6)`yapQmhyoF(-b9-pjkEm806Z6-!uIP6K{rDrXiSU_aV)_ zr~)HScqxa+zrDx;B}b#jL%&glbK@1G^Ae`7RarjWfC`WaSen5s$2Qk2Y)PanU_h~@=G}DS74{b!PI>6=~5r;o!??IWb!$bk0gpd!YUu=N|)Rp^7xcJz&H#qng_;?%qJUbn}R$fk(mR@FZ z!j*R{_6=)ED=Me}eC27mQVI@gttI=?rS$Y>539-8dL#4v!1_6>1WA#fQ>C*Bj``E* z-r4i^xn0~jK3h+>R>NP9`hB83C8Qz4{9!2kyx>Q{%jL{d#nCqBoF+bt(pNGHwAw4y znW%VZ)fmc`c>i(_L_3oUm;k}^cI@-cT;v3Oa&xXkpOqAcj5UGbpVws7Ji_xiyiklZOw zN)_lUKkzdEdRrUPg{fDgXpo4pd;U%SH?#ki__(D!1yVC2mEAKcf ziP?%A6<3jWUI|f|E`G^yA4+X&60<4ngTV&$DV)^jgX`G-N?RxW5*B;P%pspoj6&JJ zA>T1CJ4AhOpuBZ@Sq^uF9a;MvqpGmDpRktt(!voyG+)eK2Q52Tmng{u`EpB50d@Yl zJhNo(kwP%wSEb^4IwoT9ue4q|wS}cG5tX%u?L^cb*&IgxZgRAg6b1aOT zJV2Psx?a_5DLEL5@5@<;@nBquQ%E3SMI@U~;|--IO+G1)`0P8;pms?S}?_gzUw zz3UtRLi?R=lhhva`>du9)Q3n##(+D#Ik>^{5sYPim^| ztH@eYZ0z#Y0;!70jsrU;oAp@Lzir?z>kNkGRA_ldXln*EDJYcfX| ztGc!1C-xf5q$=Op1BQ9@oxHeGno(4J>KoAkt=$}9n4uAG3z<&K*>0*_4vBKu8e04F zOV*aNbQ3su8Mo)-j_Q1I`%y#tEjF0i{4^fN<}_oCvZG~QP0<+#Wi$u}GU(J;G(UlY zD%v}$4f;Adnh%tGB}eX^(xV09`UK;N7V_2aF-~9V99O+Li_&o!R&1 zTD!}%s0*r+B3_&yhMvC@+fHDARXKjhaoNRv*pjX}SSCB;(x02}`$-0~$5P?#dInz{ z(bM1m5F&pW@>Lvp&!G((bwr=F-Iii~{rT{7)@_ZBZDe4aIjff=!OmEEwVe6QM60ci zRu-DIs|=K>8YF|u|HyE+Fxz^w$Z}@`@KiBHzm|?PGUP2%PE+AyMU)6|)VzOpUXH#4 zjfo@2k3VCGYs@dEabfS0;A{)efr+EnbEUV3v90p@nG0@{zd1~j=C6x*BTt^Z?WyxE#TR*_;m#gD(B>{nVLA{LZHp& zdS7dWQ2L}0`2;H6zLgrw-$LL$*XH|Nd#?D!oQSJ#(V2GXUt8zwq-j1P&~oQa_j%2~ z?5QXuQ0?_!Meky%8RjwcR%UPj&IIK{YswFJGLFne?HH=7DfDBzltNQK7JKI|qe1rc zcn>xHWa42=tbi{Y3~f4x_V6MgbN{e&uvp`Kx+sppL`RKLxK|yMyAVDtW;Ewc zlfA9!di`2I*(g!yAN2|}rfY#an(DN9jZqWcE_QJ3Se*E_#(wH*`T#Ejj3_$}ZJErQ zf>(H#c_`Xgq++(`M_+hboP5J->m)wD%F6JtimvHB=UJVpC&7cD$>N~2r;P^~GsiNs z=)RV0*T;?ukR`yVHp#Yb#Kd}((ennziSHMH-yWm=eAa;>iSf6aW~!yCL9e9{JS@;k zCHR~}Eb;O^PnLT6oU2L$1|~!s#HAcO=0sh(whj00UNhDO2GlD9!=3XZlW>H%6@h8b z3S(!B%;->P^1>Vu%uQRU9J9gzW9hhOT63&ItZwnsxvCqcSBY6x#_C2Sl)S(Lv#S`B zBur3hS+4%i1i`uqMYZ{xjl-#zIytc+B9x@m;d84X%qY^pJaV_m@+P2N>*@#8PCG+8= zM}vzbPAi`li;o+CZ#Pr;+aZ3fGmxf#E?$SggsW99{5Gx-WkFQRL%RCh5U4rwHsrny zF)ohb5${gmGgS%7GQB`wXX57bc3{4cg`Jp_PPhzJ0$~vpRgTmaj>>5M6gZkDOAtru zGe2yX)XFh$g;yMCEn7lmJ716&k5j(DY`U$krC)GUN3B_a^f04vJA2)#Z<5MZH+F^x zu-3)4;@A)VI9jVP>8WwY>`x7aHxFNUqz%!c7i~MP;4)XPJnfzU49l6L8EhSQ4Pyj& zhny-R)K8sc?FfWI&C|DD9kjYGh(&Z?OVHT>io7y z)z*g4bPE^irt~lfi~wlR)nTNUmd;~3#}w1>lfDcCJ$c8`eW>p%%i`-%fyX;@cd2{u zS6n?fnR}b-j+_Y)9N`bigW?An6XHW%_J}=Z5I<)C0QROYbf8}p3Spun&RbWcq*_UG zjAyKk0W&lWOto7Wd{sl)IC4{M+!6E;19X^UrN%|PZ)#5Pg1pU(yS`c9bEZ36u>WS~ zr#Z+F+I+C8?q^6L?ID!+pu2h@%Xa4p|Mr80+tBNsX8y2w=g`(UFGf09LN)`sjb{sz zQiq*Mpx_#bn+fWc|@D(PCdp$&VX| z?B|Q~h^KLrepIMb-1v^GY$4)`pB!Vt8twqzqgH%LzP_|BRqayBhMwX=La{JDovek^ zMZ;vecQef!wu+@kxlwBqox|>V^)tS(caIaW>;q(2?CN)DxVl zC9h4~n?|vsFdSlPI+s3{OCMKH&*F=!2pdmJZM-3IwT9;-di%0s#CcT2 z(t(`taee95PyvE>`%1KxxV(FCsbN)ezyBln-()xxr?jT|9~l+_|Hp=D{=S% zCy)VoT9967RiyJ!>ORKD~WS~aZQ6r+9*VcNQD+b_Px0n7xxz$J2%Ql z=_VB$QOibpy+BQBuH*5VZ?CwWK8?{Urw+om_Xi(v zM0_(ypBdM4I+b2goMibV`M-V zKm`v1OPaW2Mzc5bn_y-U0+>YZ_*;2^OP4X3^Vf!pxm~h zEWS@*1SNB@=C?j~vKNl4gbPt6^!AlBw6)Zf@DEoEiEr-jZ!Ts8G30>HlG5zp#7WlK zgTP*0LW23(pE8xDxr-leMq#T+vlfp-{8N5!^$X{5;wZq^JOEiyN%vAB zgz?yiDbW6YJ5r&ZTie2x>vfXHY*UrwN`X| zpcx)Ky??5%8C+vMFvez*kgKl64V2HD5v4UxMR*jjlTyu52F73kP8{iOzT;Jq%=QXl z0(`kwph0QwEhWspl%O>{CU|k0zYEvT#$1wWQ)nCjh2=&VL$$s7_Vt+BZ^UPHo8x1j3MSkkO%kyv%oM_7L{2ajSM z@*W;iPXeW@@ukI0haqDlk?}iNtT;aH=s8#Px#S;b_5~P=yIe$W+Gd?@r;~eOx9Pxb zIw96=uzu0wIE=~T!~+f~G)iFg$>0Y@mfh6rd9m4l(s)bdsZBWISg6^)U z?;>6G3J<(!Y!)nW|0d8@9=^SYgbN-i!u-@8GQQKM*qn;oETxp}{^<-KIK0F8_gk!u z)DZ%F#e*F@#;w{tBmCbn09d+DXM^h6iB>G;+;QVBA9h04A0~ippsS0@@RM`S^AT&y zE{A-RVuyroinM7gIy>>h&vY1rP#xr>dabo|3@&zofow+{x(J<7wRr{PtUFbXTZhJILrJnF#u5vVv(>red6?SQLefcoxYNdy^Cx znsQN;Op_L}EE_X%SS;4l=)WsLeW=ZH{G`^?Z`4 z#K_VJOp}TXoV7_B8YbK}XBYLM-ifw=4n2c~)mt_(E;-n1zvGHKi^pX(bc6gP~L>ydf7lUP4j@+Mt%* z+njSBYx5E2y2`YF%_|;U+stFplop4%_PSn+f4Auo5_~kz6s$g5vSi|DXsWYJz>+bd zGIf=v#-NY^Gh?j2|DHjNom@k!MuyWr)a4jpc`nip|L<5(c3(x8oC_EH{aVGMt^qn)WbUs4o^Vm z+H=Q1p(KYOjy^L3s~G;_ZEo3`e_-#%Qbg5~LBG=Ydg)@ef->eMmr)`^G8r=K{sdF9 z&IHip6;#PKiiY;}LxJtap=@f*iE+F`g9)cl+Bky!v!nzO z(jP2&K&XR7b&3B7zOu&_y}V#CNdB+?()I%qa@06p*&z19n844BW_uci zT9YZ=%1g##5s{|vGNyzcLKR!HApC1!W8sJdS|UxWF>U0PkTKdCKN9ifw>;5rMH6?F z?|%YI2M9P9=d^F=JS5;(;kwi*y?IUaD{R!+$VRTQf|?*RuZ+0bTGT~_)FQN4^1;Pj z22ePiCirFa0r|rAkMg*{~)~t>ve*EmKY}efdR{h(yJco|55j zzLzgFOILg@ocK!Gy|_viX-Hq%YbY7*edI_YM%ZR=E)ud|yIEH;VE~=j-QO;+e_jF) z*ePiV7rUJ6ZV!H*^TvKChB9&OTX-ZA!hd zXD%D#)*L8MUnf661w#4WuOpZmv6~-7;i)+VNi%c%2zmSZi0i^{MTZQE`S_N?Qo1I% zgx_zi=opPog8U!Vg*JKK+pK78_$as59mHLQY#r%w{wi3O)-yk_GyApFW{BWB?Gazr zDmOp2fyS!@jZSc`CszaKZPnM z6mSI+Fg%!(3g+m`G)6Z&4mNordxIGpTjvp;5c|amH8SG!TGwc0?kfm$Xf1YWnstq^ z$B3rusBm4#q|9h&<3i%(B>A#Y*{L&`7D>J$kMSuk84KQ23C4$IK-r@2^p!92?3>CT z%!#h0SL{grkpD9DE6{_rMfoU*vCiGN!&fQLsN+CMmL1miIFrzZ`i{+GPWli=MKT)^ zKka~Tyj?kOl|(rf9}}5%+#$<$%W{Ki$F?41qdMvs9>m1kMfIT zmT;WNHe7&d^FC z^dPlTt-rivFC#(FKYRk>1vM9ZIe1Tb@bajNT(-7Q;}=!r*YOmO;4I;ORLyX`{hph> zE(a@K1^I=SthpiZwJ<~244;pS_D4Gq6bbnr?i>l5z1;Wah=nBP!gCSxq%hm;DO14! zok$&D7JcORS>}o+5gVyhZF`UYFJGB#bRYRC_j({v{ES8kFN$=rNxo++357bJi{ud> z2e#V|Mw?eF-{sf8S6=w0Nf=fxWOS^p^<>7{o6nq;LF9)|08*lsWSn;IM!Sr4K*%=_ zgqUY14ErG8re0Y~A(|$62PCrk%pNYN^xk5Pr#+MeG~^xhaE#Q*uR+^vr7j#Y4Z1eR z!Ry1;_X5AiQ8<*IsH&<&EqI41MW}+tezGjVm#let_|sq~fww#Gx}axI199^}R1fEs z$%Iw)GIYRlH$c2u&bo_c+o!zB?2gqz($AVcnCFK)>ELjBZ>Ls67N#;CmD0y%ZiP=Z zCyJIX&y-|S3Jx0u@v;coRJSEWhfNxkZ>FxVA4*va?~*q32JF|Ucxm+2P?oOyvABNp z)9WzHy2rG`8+MN*eC^X=PCo46ZBf%=Yp*&j*<+qiU+;!TdeU>4 zH0>u2~6LPUQ+}sWS1r+kZMb%h<2%W{9{TN#AxgbG zxfXX&(J+&}>U@jze9l7Np$1~@@ET&iW92@7oehK)t{E+^Z(MPQn%E<_m)ZVYNs}9M z=s-a3LJID3dRt;H;q_60COg-M@#To;Ynm`vDMuO3qLL)52+x4>)F7Mf;`BoBhSp^% zanS36h7TS2I|O_WKfwpIio2Hb2KXr6sTqazkS~=MoZM(QG>W8z_l8Q=majjT1WJML z)`OtFU*WSt{=1n?)|ugNiZ^mv;qsGXM+8CF?Dk6G2F7#%6Z+|U|{KUn?$2z zH#c&o0ehBkBc0j1dgZ8Hxs+{4ar!(Gw%tr*x8GlzL~cr2W`gtOc-}QJz^M9`2Y7{y z^l1mjUVeVrQ_3DzdBqVsXV>TT?Jo1aq^XH?wK4Upf`Md7WF`;0( zD|$HzaTx}TMuhXt9Ch0jABi$$+^S%F6pT(>YILNNSM+kM z;pp;?iX-yG#(w4ytKB%?#j!N=R6yN&u6BUlwB*3elqr~Q8ixCtUHUW)-$P*usf9&c4y$m z=T$|+ud>CzLedT;d=@T#DU>A8C!D8rbbnsQ^1uP-Ab+VlA3l+7(Ha|&CQF;Uxm21# zyE%I!H(b(FZrfTgO0XxnUYJO;Mt4O=S95_i*g&$#XTieS-l_p+MHx<&wPsKgb6Y%s zf`K8h{$=oiPPUHdOcOl#fY5x|LQ01&JzNhu6Bt0u`KS7a3g{4l3g~jpDQjRPW5$vBFP!EkFVCU{K z2HRHDi^`BmfoP0*V{^%!wmC`SE8m@u^_doHDfxfJ1v7y-!{Sp1UwUpUSYQuiPZ%+??%q`t}xpr@@F9G%vhTr0I zqzAQmScWS#JR=B(nL(F$-3*r9HM+7$@DT>7Ex3I0VU&@pT-HMm_*mOipn)1)2m`St zTD}*k8+4RQde+}YNmzA&UnBUDXPDc|9XCj*B8 zSaU;4^`Wl$emL{`y8@~DWqKM-0#T_n@p>fb`Y0Csc>@$o4u9SVg^Pc)=P>ih=wVz` z9#ocV2|>fI5Sg576=Um-5N0N3*`=XHIeK&Jbz zcgrk#KNfenvVFB~&Il+xxnV5lko>eTWjiW;fQg(_jkr)b7t!@qlO+N{v6XFSRuN|T zUM-K@bW+jBS)5ni)sj`b_R-yP#*8^0+2L;}*>AiW)?}$l6vH+avyN*rt3lau zOU5R*R>1tNb5|LQkC(e+SAWgD^HEBrOG~Dn<7#!b=MPni&&^6i-_@|;Y!-5yAp9~} zL?4^*u0pgfje0L0xife-38A^qyz>?`P?VF$a6o>e50={ZYOm^zNb4LiIIFE9o)4lr zK_ftL7rfo_MZf5rLl>B#?+&@3hRMwwr}oU9uBl=r4ydF{7yqP7(O5#pwoJO1hxKsW zE-mpupNmbd<3w&cRMuanz6~WQ&b0T`iZU;n=41bnQ?QHH{ZBHoZL$4FZO=H`Tze-C zO_d}ZR$vcxbXPB#zWrPhR^=fJ_m_C?n;f8cJHz{mXNTYTxF2_)1{WlBh2nPjr()=F zl1VWuk7Q4vp>Op13NLq2w9i;}oZ9`~FFKv!jO`l4C8c~?YU%Beeug3 zuU=dmQ*Ud%KHR{+L`VJ{bBh>cW}c;LTYU9PU?RP@jX`VS% z;2Yu|ciWirHe@j6xvPIpH$MHgI@AeqvG`FU`7^awVfP*1ROv$u9n96gOqnu8ye~CL zfFaesH}q&^nLtcwE*H7!=pm;zO3RXjCG@(E{Xw5eE=_NI(EY{qz8lZLK!x~Ifr!4@ zV)It6sA8QVyE{Bsvz*dj z*xwYn zd6Pp3N$PIVY^x7$sFQ^Ifo)hBezY9|tXxUKQUkv@dtK)7!lj+!y16|;e=h2fA*Kih zu;ePDi7|YHWVj3YVQWT^+G!yj`)Jx|<1Dcq39c)TQ}j~9YQ#C|k)U1r zG8Z9adGWU_k^C^9*Kt>sAeKSace@YH^9*t5wuE0mewV!*?#gR9itnTqR?QE~Jl0K_ z0tXn+=TyCRbYHsmaaYf7E10?orHMZih@=JAziWCtf&}wr?0tcye>JQIl?l+)ka5%S zvQPOk&IaLorFCgOFg^A*DN?Cazw7r7OY8GPte>~Oe7ELgV`u7sX~>I+-uJbl^Redg ziKE@&QLC>N=^m!4bz_Q~Zx5S;J;n3Sgwr0xkyF$jvQHio>3!q1lDuInd)-A=QS2o* zmZ0{MEVNYD=VAFc@4W}*)x_ag0xB_uCX=;nYTvV#3B17dF|Ic{H9n{kMpmF zT;0e1A(V0rwLiM9;}fl?NNE@fAL%VI82pNrMOVOb&%Wa-xy7VF0$7uM%T7@qx@5Zu zx!M2RQ%wCiFA=z|rw-VBd0sAa`G^K;)33JWgv2u z;Ph=gu>a2}DHjpf{(F?HV2llHoO8JU=s3wi3()`0D+_$B{qI4AwWVyb6JnwzI(g)$qS%uKh=l3hP+m1@QMIkFB=)8TX$E z(*KCDP7qE1BJO%v-Eo5c-#~a?J2}A#VErU95(tiL$Gd=Ll3nN>+iZYp3^=)dD*%@8 zLYT4t$%TucLzl`28#)nY-qbUu$`C@LQ+YQUcK$zoikW#29mGO$3Ig)*!J{l` z{{mtr<3k62qdEnqv9Q3kyYMm0H%FY9&ks5kC>138ixo3@6*_A(1JGjv={=Vd?JXHr6 zo&Wb&4yPYEpvxyKp3498$pZ)-bGA_YX8^#p{%r>ZJfjOA9pk|65L*dc2MtfIgJ)`h zwtE@?e&U2*VPt^Ec4A;KpW4qrl_5b!r?vsl@FM>_!~Yp2n3FR)bnG@TRrCF4+TZgk zx~#1wFv~LYoek;}@e=`H8ah3I<%EBalQHUG1#j*7?Ww=!tp7xT-^!3=D|lS9DExl^ z?|3_%zMT3_;_dt$IEN<}ys(c9-A>(W#4P!M<^y1jUKWFl3VJGkzmSOv-4UmZmxfOm zY3u+A2;6g+37zpH7x`xupi|TVR$&arpAPUh_RMhE5$6Af!zmf{;0f6?r&D#>gB<8& z%#NTp0|3!9V4eg9S(VqR^867x3^MJ36S4{4Q?i%=E_AZ9i4_aT33ql>hJfw?mhobY z!xPgm=GilOz|))e0R_^30nGedlAZxs=MdC?YFH12!!kJkm%uvF6R9BX6bQJC6o_MK z3_Yz+;H=)VCq0VZ5D_P%DmB0>c)(r4$rSkCQ5ljDb=vR(SMI+%t?|?~iZOr$C;Z32 z?<>NI1-B{XN3sD%2w=SEe{{faWyp5sDdPuQn!j4BGHkmU4}ekuGcZAmuK*APexa8d z-E2ReMSV+P;}aN|wq!7rbmjqp%Lr#4_8;@)`0$YLTvvDrEmFZBvSZN{^?r9VbJOZ&<%lrjhN$MyyDam*+_PbfD1g0 zUZxs&(%@B$ib3RwrtR8O6!|d55LT`tI* \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..f955316 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line From 7704884f80b352ff6b27a86acc54c6acb4e057a6 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 21 Jan 2022 21:50:46 -0600 Subject: [PATCH 16/48] Remove dos line endings --- .travis.yml | 8 +- build.gradle | 208 +++++++++++------------ src/test/java/ParseLocalDateTagTest.java | 104 ++++++------ 3 files changed, 160 insertions(+), 160 deletions(-) diff --git a/.travis.yml b/.travis.yml index f116b97..b5300d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -language: java -install: /bin/true -jdk: -- oraclejdk8 +language: java +install: /bin/true +jdk: +- oraclejdk8 diff --git a/build.gradle b/build.gradle index eb0f7f3..c7ee116 100644 --- a/build.gradle +++ b/build.gradle @@ -1,104 +1,104 @@ -plugins { - id "com.jfrog.bintray" version "1.7.3" -} - -apply plugin: 'java' -apply plugin: 'maven' - -group = 'net.sargue' -archivesBaseName = 'java-time-jsptags' -version = '1.1.4' - -sourceCompatibility = 1.8 -compileJava.options.encoding = 'UTF-8' -compileTestJava.options.encoding = 'UTF-8' - -repositories { - jcenter() -} - -configurations { - testCompile.extendsFrom compileOnly -} - -dependencies { - compileOnly 'javax.servlet:javax.servlet-api:3.0.1' - compileOnly 'javax.servlet.jsp:javax.servlet.jsp-api:2.2.1' - compileOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' - - testCompile 'junit:junit:4.12' - testCompile 'org.springframework:spring-test:4.1.7.RELEASE' -} - -jar { - manifest { - attributes 'Implementation-Title': 'Java 8 java.time JSP tags', - 'Implementation-Version': version - } -} - -task javadocJar(type: Jar) { - classifier = 'javadoc' - from javadoc -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource -} - -artifacts { - archives javadocJar, sourcesJar -} - -install { - repositories.mavenInstaller { - pom.project { - name 'Java 8 java.time JSP tags' - description 'JSP tag support for Java 8 java.time (JSR-310)' - url 'https://github.com/sargue/java-time-jsptags' - - scm { - connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' - developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' - url 'git@github.com:sargue/java-time-jsptags.git' - } - - licenses { - license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - - developers { - developer { - id 'sargue' - name 'Sergi Baila' - email 'sargue@gmail.com' - } - } - } - } -} - -bintray { - user = project.hasProperty('BINTRAY_USER') ? BINTRAY_USER : '' - key = project.hasProperty('BINTRAY_KEY') ? BINTRAY_KEY : '' - configurations = ['archives'] - pkg { - repo = 'maven' - name = 'net.sargue:java-time-jsptags' - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/sargue/java-time-jsptags' - version { - name = project.version - desc = 'JSP tag support for Java 8 java.time (JSR-310)' - - gpg { - sign = true - passphrase = project.hasProperty('BINTRAY_GPG') ? BINTRAY_GPG : '' - } - } - } -} +plugins { + id "com.jfrog.bintray" version "1.7.3" +} + +apply plugin: 'java' +apply plugin: 'maven' + +group = 'net.sargue' +archivesBaseName = 'java-time-jsptags' +version = '1.1.4' + +sourceCompatibility = 1.8 +compileJava.options.encoding = 'UTF-8' +compileTestJava.options.encoding = 'UTF-8' + +repositories { + jcenter() +} + +configurations { + testCompile.extendsFrom compileOnly +} + +dependencies { + compileOnly 'javax.servlet:javax.servlet-api:3.0.1' + compileOnly 'javax.servlet.jsp:javax.servlet.jsp-api:2.2.1' + compileOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' + + testCompile 'junit:junit:4.12' + testCompile 'org.springframework:spring-test:4.1.7.RELEASE' +} + +jar { + manifest { + attributes 'Implementation-Title': 'Java 8 java.time JSP tags', + 'Implementation-Version': version + } +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives javadocJar, sourcesJar +} + +install { + repositories.mavenInstaller { + pom.project { + name 'Java 8 java.time JSP tags' + description 'JSP tag support for Java 8 java.time (JSR-310)' + url 'https://github.com/sargue/java-time-jsptags' + + scm { + connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' + developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' + url 'git@github.com:sargue/java-time-jsptags.git' + } + + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + + developers { + developer { + id 'sargue' + name 'Sergi Baila' + email 'sargue@gmail.com' + } + } + } + } +} + +bintray { + user = project.hasProperty('BINTRAY_USER') ? BINTRAY_USER : '' + key = project.hasProperty('BINTRAY_KEY') ? BINTRAY_KEY : '' + configurations = ['archives'] + pkg { + repo = 'maven' + name = 'net.sargue:java-time-jsptags' + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/sargue/java-time-jsptags' + version { + name = project.version + desc = 'JSP tag support for Java 8 java.time (JSR-310)' + + gpg { + sign = true + passphrase = project.hasProperty('BINTRAY_GPG') ? BINTRAY_GPG : '' + } + } + } +} diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index bb9c466..5244e12 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -1,52 +1,52 @@ -import net.sargue.time.jsptags.ParseInstantTag; -import net.sargue.time.jsptags.ParseLocalDateTag; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.mock.web.MockPageContext; -import org.springframework.mock.web.MockServletContext; - -import javax.servlet.jsp.JspException; -import java.io.UnsupportedEncodingException; -import java.time.LocalDate; -import java.util.Locale; - -/** - * Basic parse tests. - * - * @author Sergi Baila - * @link http://blog.agilelogicsolutions.com/2011/02/unit-testing-jsp-custom-tag-using.html - */ - -public class ParseLocalDateTagTest { - - private ParseLocalDateTag parseLocalDateTag; - private MockPageContext mockPageContext; - - @Before - public void setup() throws UnsupportedEncodingException { - Locale.setDefault(Locale.forLanguageTag("ca")); - // mock ServletContext - MockServletContext mockServletContext = new MockServletContext(); - // mock PageContext - mockPageContext = new MockPageContext(mockServletContext); - mockPageContext.getRequest().setCharacterEncoding("UTF-8"); - mockPageContext.getResponse().setCharacterEncoding("UTF-8"); - parseLocalDateTag = new ParseLocalDateTag(); - parseLocalDateTag.setPageContext(mockPageContext); - } - - @Test - public void parsePatternToVar() throws JspException { - parseLocalDateTag.setValue("2015-10-28"); - parseLocalDateTag.setPattern("yyyy-MM-dd"); - parseLocalDateTag.setVar("date"); - parseLocalDateTag.doEndTag(); - Object date = mockPageContext.getAttribute("date"); - Assert.assertTrue(date instanceof LocalDate); - LocalDate localDate = (LocalDate) date; - Assert.assertEquals(LocalDate.of(2015, 10, 28), localDate); - } -} +import net.sargue.time.jsptags.ParseInstantTag; +import net.sargue.time.jsptags.ParseLocalDateTag; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockPageContext; +import org.springframework.mock.web.MockServletContext; + +import javax.servlet.jsp.JspException; +import java.io.UnsupportedEncodingException; +import java.time.LocalDate; +import java.util.Locale; + +/** + * Basic parse tests. + * + * @author Sergi Baila + * @link http://blog.agilelogicsolutions.com/2011/02/unit-testing-jsp-custom-tag-using.html + */ + +public class ParseLocalDateTagTest { + + private ParseLocalDateTag parseLocalDateTag; + private MockPageContext mockPageContext; + + @Before + public void setup() throws UnsupportedEncodingException { + Locale.setDefault(Locale.forLanguageTag("ca")); + // mock ServletContext + MockServletContext mockServletContext = new MockServletContext(); + // mock PageContext + mockPageContext = new MockPageContext(mockServletContext); + mockPageContext.getRequest().setCharacterEncoding("UTF-8"); + mockPageContext.getResponse().setCharacterEncoding("UTF-8"); + parseLocalDateTag = new ParseLocalDateTag(); + parseLocalDateTag.setPageContext(mockPageContext); + } + + @Test + public void parsePatternToVar() throws JspException { + parseLocalDateTag.setValue("2015-10-28"); + parseLocalDateTag.setPattern("yyyy-MM-dd"); + parseLocalDateTag.setVar("date"); + parseLocalDateTag.doEndTag(); + Object date = mockPageContext.getAttribute("date"); + Assert.assertTrue(date instanceof LocalDate); + LocalDate localDate = (LocalDate) date; + Assert.assertEquals(LocalDate.of(2015, 10, 28), localDate); + } +} From bbb481b8ecbaa3e5778ae6e547f0247881c34822 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 21 Jan 2022 22:01:28 -0600 Subject: [PATCH 17/48] Connect to eclipse and fix some classpath issues for updates --- .gitignore | 4 ++ .project | 17 +++++++ build.gradle | 84 ++++++++++++++++++-------------- src/test/java/FormatTagTest.java | 30 +++++++++--- 4 files changed, 91 insertions(+), 44 deletions(-) create mode 100644 .project diff --git a/.gitignore b/.gitignore index f7a0232..5a959fb 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties + +# eclipse +bin/ +.classpath diff --git a/.project b/.project new file mode 100644 index 0000000..5ca4635 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + java-time-jsptags + + + + org.eclipse.jdt.core.javanature + + + + org.eclipse.jdt.core.javabuilder + + + + + + diff --git a/build.gradle b/build.gradle index c7ee116..f7d7b0b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,9 @@ plugins { id "com.jfrog.bintray" version "1.7.3" + id "java" + id "eclipse" } -apply plugin: 'java' -apply plugin: 'maven' - group = 'net.sargue' archivesBaseName = 'java-time-jsptags' version = '1.1.4' @@ -14,7 +13,8 @@ compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { - jcenter() + mavenLocal() + mavenCentral() } configurations { @@ -26,8 +26,8 @@ dependencies { compileOnly 'javax.servlet.jsp:javax.servlet.jsp-api:2.2.1' compileOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' - testCompile 'junit:junit:4.12' - testCompile 'org.springframework:spring-test:4.1.7.RELEASE' + testImplementation 'junit:junit:4.12' + testImplementation 'org.springframework:spring-test:4.1.7.RELEASE' } jar { @@ -51,36 +51,36 @@ artifacts { archives javadocJar, sourcesJar } -install { - repositories.mavenInstaller { - pom.project { - name 'Java 8 java.time JSP tags' - description 'JSP tag support for Java 8 java.time (JSR-310)' - url 'https://github.com/sargue/java-time-jsptags' - - scm { - connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' - developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' - url 'git@github.com:sargue/java-time-jsptags.git' - } - - licenses { - license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - - developers { - developer { - id 'sargue' - name 'Sergi Baila' - email 'sargue@gmail.com' - } - } - } - } -} +// install { +// repositories.mavenInstaller { +// pom.project { +// name 'Java 8 java.time JSP tags' +// description 'JSP tag support for Java 8 java.time (JSR-310)' +// url 'https://github.com/sargue/java-time-jsptags' +// +// scm { +// connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' +// developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' +// url 'git@github.com:sargue/java-time-jsptags.git' +// } +// +// licenses { +// license { +// name 'The Apache License, Version 2.0' +// url 'http://www.apache.org/licenses/LICENSE-2.0.txt' +// } +// } +// +// developers { +// developer { +// id 'sargue' +// name 'Sergi Baila' +// email 'sargue@gmail.com' +// } +// } +// } +// } +// } bintray { user = project.hasProperty('BINTRAY_USER') ? BINTRAY_USER : '' @@ -102,3 +102,15 @@ bintray { } } } + +eclipse { + classpath { + file { + // remove entries added due to issue with testsets + // https://github.com/unbroken-dome/gradle-testsets-plugin/issues/77 + whenMerged { + entries.removeAll{it.kind == "lib" && (it.path.endsWith("build/classes/java/test") || it.path.endsWith("build/resources/test"))} + } + } + } +} \ No newline at end of file diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index 61a1ef9..0c385cd 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -1,17 +1,31 @@ -import net.sargue.time.jsptags.FormatTag; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockPageContext; -import org.springframework.mock.web.MockServletContext; +import static org.junit.Assert.assertEquals; -import javax.servlet.jsp.JspException; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.time.*; +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Locale; import java.util.TimeZone; -import static org.junit.Assert.assertEquals; +import javax.servlet.jsp.JspException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockPageContext; +import org.springframework.mock.web.MockServletContext; + +import net.sargue.time.jsptags.FormatTag; /** * Basic format tests. From b3f85719b64c73fa6384652cd079f3852be9bf73 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 21 Jan 2022 22:09:06 -0600 Subject: [PATCH 18/48] Got everything building --- build.gradle | 11 ++++++----- .../net/sargue/time/jsptags/FormatSupport.java | 16 +++++++++++----- .../java/net/sargue/time/jsptags/FormatTag.java | 3 ++- .../jsptags/JavaTimeTagLibraryValidator.java | 12 +++++++----- src/test/java/ParseLocalDateTagTest.java | 15 +++++++-------- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index f7d7b0b..a511849 100644 --- a/build.gradle +++ b/build.gradle @@ -22,12 +22,13 @@ configurations { } dependencies { - compileOnly 'javax.servlet:javax.servlet-api:3.0.1' - compileOnly 'javax.servlet.jsp:javax.servlet.jsp-api:2.2.1' - compileOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' + implementation(group: "javax.servlet", name: "javax.servlet-api", version: "4.0.1") + implementation(group: "javax.servlet.jsp", name: "javax.servlet.jsp-api", version: "2.3.3") + implementation(group: "javax.servlet.jsp.jstl", name: "javax.servlet.jsp.jstl-api", version: "1.2.2") + - testImplementation 'junit:junit:4.12' - testImplementation 'org.springframework:spring-test:4.1.7.RELEASE' + testImplementation(group: "junit", name: "junit", version: "4.12") + testImplementation(group: "org.springframework", name: "spring-test", version: "4.1.7.RELEASE") } jar { diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index bf91c6d..98f3ff0 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -16,17 +16,23 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspTagException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.TagSupport; import java.io.IOException; import java.text.DateFormat; -import java.time.*; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.Locale; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspTagException; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.tagext.TagSupport; + /** * Support for tag handlers for <formatDate>, the date and time * formatting tag in JSTL 1.0. diff --git a/src/main/java/net/sargue/time/jsptags/FormatTag.java b/src/main/java/net/sargue/time/jsptags/FormatTag.java index 0c09b32..8d82cf1 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatTag.java +++ b/src/main/java/net/sargue/time/jsptags/FormatTag.java @@ -16,10 +16,11 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspTagException; import java.time.ZoneId; import java.util.Locale; +import javax.servlet.jsp.JspTagException; + /** *

* A handler for <format> that supports rtexprvalue-based attributes. diff --git a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java index 3f6e69a..e02ebcd 100644 --- a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java +++ b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java @@ -16,9 +16,9 @@ */ package net.sargue.time.jsptags; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import javax.servlet.jsp.tagext.PageData; import javax.servlet.jsp.tagext.TagLibraryValidator; @@ -26,8 +26,10 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; -import java.io.IOException; -import java.util.*; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; /** *

diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index 5244e12..80d7118 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -1,17 +1,16 @@ -import net.sargue.time.jsptags.ParseInstantTag; -import net.sargue.time.jsptags.ParseLocalDateTag; -import org.junit.After; +import java.io.UnsupportedEncodingException; +import java.time.LocalDate; +import java.util.Locale; + +import javax.servlet.jsp.JspException; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; -import javax.servlet.jsp.JspException; -import java.io.UnsupportedEncodingException; -import java.time.LocalDate; -import java.util.Locale; +import net.sargue.time.jsptags.ParseLocalDateTag; /** * Basic parse tests. From 747972e03a7cba9f6d820bb5f54e69ebcfc0ed2f Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 08:53:33 -0600 Subject: [PATCH 19/48] Migrate to jakarta package names - use sprint-test 6.0.0-M2 to support jakarta package names - upgraded to require Java 17 (needed for new spring mock class) --- build.gradle | 20 +- .../sargue/time/jsptags/FormatSupport.java | 8 +- .../net/sargue/time/jsptags/FormatTag.java | 2 +- .../jsptags/JavaTimeTagLibraryValidator.java | 7 +- .../time/jsptags/ParseLocalDateTimeTag.java | 1 - .../time/jsptags/ParseLocalTimeTag.java | 1 - .../net/sargue/time/jsptags/ParseSupport.java | 9 +- .../sargue/time/jsptags/SetZoneIdIdTag.java | 2 - .../sargue/time/jsptags/SetZoneIdSupport.java | 9 +- .../java/net/sargue/time/jsptags/Util.java | 1238 ++++++++--------- .../sargue/time/jsptags/ZoneIdSupport.java | 13 +- .../net/sargue/time/jsptags/ZoneIdTag.java | 2 +- src/test/java/FormatTagTest.java | 329 +++-- src/test/java/ParseLocalDateTagTest.java | 3 +- 14 files changed, 813 insertions(+), 831 deletions(-) diff --git a/build.gradle b/build.gradle index a511849..5c2ca8a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,19 +2,29 @@ plugins { id "com.jfrog.bintray" version "1.7.3" id "java" id "eclipse" + id "com.github.ben-manes.versions" version "0.41.0" // adds dependencyUpdates task } group = 'net.sargue' archivesBaseName = 'java-time-jsptags' version = '1.1.4' -sourceCompatibility = 1.8 compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' +java { + // set version of Java that the source confirms to. + // The bytecode will be for this version of Java as well, unless targetCompatibility is specified. + sourceCompatibility = JavaVersion.VERSION_17 +} + repositories { mavenLocal() mavenCentral() + // for snapshot release of spring that includes jakarta classes + maven { + url "https://repo.spring.io/milestone" + } } configurations { @@ -22,13 +32,13 @@ configurations { } dependencies { - implementation(group: "javax.servlet", name: "javax.servlet-api", version: "4.0.1") - implementation(group: "javax.servlet.jsp", name: "javax.servlet.jsp-api", version: "2.3.3") - implementation(group: "javax.servlet.jsp.jstl", name: "javax.servlet.jsp.jstl-api", version: "1.2.2") + implementation(group: "jakarta.servlet", name: "jakarta.servlet-api", version: "5.0.0") + implementation(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") + implementation(group: "jakarta.servlet.jsp.jstl", name: "jakarta.servlet.jsp.jstl-api", version: "2.0.0") testImplementation(group: "junit", name: "junit", version: "4.12") - testImplementation(group: "org.springframework", name: "spring-test", version: "4.1.7.RELEASE") + testImplementation(group: "org.springframework", name: "spring-test", version: "6.0.0-M2") } jar { diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index 98f3ff0..37c2ace 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -28,10 +28,10 @@ import java.time.temporal.TemporalAccessor; import java.util.Locale; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspTagException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.TagSupport; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.TagSupport; /** * Support for tag handlers for <formatDate>, the date and time diff --git a/src/main/java/net/sargue/time/jsptags/FormatTag.java b/src/main/java/net/sargue/time/jsptags/FormatTag.java index 8d82cf1..ae01a8c 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatTag.java +++ b/src/main/java/net/sargue/time/jsptags/FormatTag.java @@ -19,7 +19,7 @@ import java.time.ZoneId; import java.util.Locale; -import javax.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.JspTagException; /** *

diff --git a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java index e02ebcd..2d52452 100644 --- a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java +++ b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java @@ -20,9 +20,6 @@ import java.util.ArrayList; import java.util.List; -import javax.servlet.jsp.tagext.PageData; -import javax.servlet.jsp.tagext.TagLibraryValidator; -import javax.servlet.jsp.tagext.ValidationMessage; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; @@ -31,6 +28,10 @@ import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; +import jakarta.servlet.jsp.tagext.PageData; +import jakarta.servlet.jsp.tagext.TagLibraryValidator; +import jakarta.servlet.jsp.tagext.ValidationMessage; + /** *

* A SAX-based TagLibraryValidator for the java.time tags. Currently implements the diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java index 7044fe6..0852d6b 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java @@ -17,7 +17,6 @@ package net.sargue.time.jsptags; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQuery; diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java index b558f58..1c7befd 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java @@ -16,7 +16,6 @@ */ package net.sargue.time.jsptags; -import java.time.LocalDate; import java.time.LocalTime; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQuery; diff --git a/src/main/java/net/sargue/time/jsptags/ParseSupport.java b/src/main/java/net/sargue/time/jsptags/ParseSupport.java index a50e6ef..9faf2b3 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ParseSupport.java @@ -16,10 +16,6 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspTagException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.BodyTagSupport; import java.io.IOException; import java.text.DateFormat; import java.time.ZoneId; @@ -29,6 +25,11 @@ import java.time.temporal.TemporalQuery; import java.util.Locale; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.BodyTagSupport; + /** * Support for tag handlers for the date and time parsing tags. * diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java index 311dabb..cbfc46f 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java @@ -16,8 +16,6 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspTagException; - /** *

* A handler for <setDateTimeZone> that supports rtexprvalue-based diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java index 3e9039b..0f072fd 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java @@ -16,13 +16,14 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.jstl.core.Config; -import javax.servlet.jsp.tagext.TagSupport; import java.time.ZoneId; import java.time.ZoneOffset; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.jstl.core.Config; +import jakarta.servlet.jsp.tagext.TagSupport; + /** * Support for tag handlers for <setDateTimeZone>. * diff --git a/src/main/java/net/sargue/time/jsptags/Util.java b/src/main/java/net/sargue/time/jsptags/Util.java index acd28fb..542ac40 100644 --- a/src/main/java/net/sargue/time/jsptags/Util.java +++ b/src/main/java/net/sargue/time/jsptags/Util.java @@ -16,19 +16,30 @@ */ package net.sargue.time.jsptags; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.jstl.core.Config; -import javax.servlet.jsp.jstl.fmt.LocalizationContext; +import static java.time.format.FormatStyle.FULL; +import static java.time.format.FormatStyle.LONG; +import static java.time.format.FormatStyle.MEDIUM; +import static java.time.format.FormatStyle.SHORT; + import java.text.DateFormat; import java.text.NumberFormat; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; -import java.util.*; - -import static java.time.format.FormatStyle.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; + +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.jstl.core.Config; +import jakarta.servlet.jsp.jstl.fmt.LocalizationContext; /** *

@@ -41,618 +52,599 @@ */ public class Util { - private static final String REQUEST = "request"; - - private static final String SESSION = "session"; - - private static final String APPLICATION = "application"; - - private static final char HYPHEN = '-'; - - private static final char UNDERSCORE = '_'; - - private static final Locale EMPTY_LOCALE = new Locale("", ""); - - static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset"; - - /** - * Converts the given string description of a scope to the corresponding - * PageContext constant. - * - * The validity of the given scope has already been checked by the - * appropriate TLV. - * - * @param scope String description of scope - * - * @return PageContext constant corresponding to given scope description - */ - public static int getScope(String scope) { - int ret = PageContext.PAGE_SCOPE; // default - - if (REQUEST.equalsIgnoreCase(scope)) { - ret = PageContext.REQUEST_SCOPE; - } else if (SESSION.equalsIgnoreCase(scope)) { - ret = PageContext.SESSION_SCOPE; - } else if (APPLICATION.equalsIgnoreCase(scope)) { - ret = PageContext.APPLICATION_SCOPE; - } - return ret; - } - - /** - * HttpServletRequest.getLocales() returns the server's default locale if - * the request did not specify a preferred language. We do not want this - * behavior, because it prevents us from using the fallback locale. We - * therefore need to return an empty Enumeration if no preferred locale has - * been specified. This way, the logic for the fallback locale will be able - * to kick in. - * - * @param request the http request - * @return the locales from the request or an empty enumeration if no - * preferred locale has been specified - */ - public static Enumeration getRequestLocales(HttpServletRequest request) { - Enumeration values = request.getHeaders("accept-language"); - if (values.hasMoreElements()) { - // At least one "accept-language". Simply return - // the enumeration returned by request.getLocales(). - // System.out.println("At least one accept-language"); - return request.getLocales(); - } else { - // No header for "accept-language". Simply return - // the empty enumeration. - // System.out.println("No accept-language"); - return values; - } - } - - /** - * See parseLocale(String, String) for details. - * - * @param locale the locale string to parse - * @return java.util.Locale object corresponding to the given - * locale string, or the null if the locale string is null or empty - */ - public static Locale parseLocale(String locale) { - return parseLocale(locale, null); - } - - /** - * Parses the given locale string into its language and (optionally) country - * components, and returns the corresponding java.util.Locale - * object. - * - * If the given locale string is null or empty, a null value is returned. - * - * @param locale the locale string to parse - * @param variant the variant - * - * @return java.util.Locale object corresponding to the given - * locale string, or the null if the locale string is null or empty - * - * @throws IllegalArgumentException if the given locale does not have a - * language component or has an empty country component - */ - public static Locale parseLocale(String locale, String variant) { - Locale ret; - String language = locale; - String country = null; - int index; - - if (locale == null || locale.isEmpty()) - return null; - - if (((index = locale.indexOf(HYPHEN)) > -1) - || ((index = locale.indexOf(UNDERSCORE)) > -1)) { - language = locale.substring(0, index); - country = locale.substring(index + 1); - } - - if (language.isEmpty()) { - throw new IllegalArgumentException(Resources - .getMessage("LOCALE_NO_LANGUAGE")); - } - - if (country == null) { - if (variant != null) { - ret = new Locale(language, "", variant); - } else { - ret = new Locale(language, ""); - } - } else if (country.length() > 0) { - if (variant != null) { - ret = new Locale(language, country, variant); - } else { - ret = new Locale(language, country); - } - } else { - throw new IllegalArgumentException(Resources - .getMessage("LOCALE_EMPTY_COUNTRY")); - } - - return ret; - } - - /** - * Stores the given locale in the response object of the given page context, - * and stores the locale's associated charset in the - * javax.servlet.jsp.jstl.fmt.request.charset session attribute, which may - * be used by the action in a page invoked by a form - * included in the response to set the request charset to the same as the - * response charset (this makes it possible for the container to decode the - * form parameter values properly, since browsers typically encode form - * field values using the response's charset). - * - * @param pc the page context whose response object is assigned the - * given locale - * @param locale the response locale - */ - static void setResponseLocale(PageContext pc, Locale locale) { - // set response locale - ServletResponse response = pc.getResponse(); - response.setLocale(locale); - - // get response character encoding and store it in session attribute - if (pc.getSession() != null) { - try { - pc.setAttribute(REQUEST_CHAR_SET, response - .getCharacterEncoding(), PageContext.SESSION_SCOPE); - } catch (IllegalStateException ex) { - // invalidated session ignored - } - } - } - - /** - * Returns the formatting locale to use with the given formatting action in - * the given page. - * - * @param pc The page context containing the formatting action @param - * fromTag The formatting action @param format true if the - * formatting action is of type (as opposed to ), and - * false otherwise (if set to true, the formatting - * locale that is returned by this method is used to set the response - * locale). - * - * @param avail the array of available locales - * - * @return the formatting locale to use - */ - static Locale getFormattingLocale(PageContext pc, boolean format, Locale[] avail) { - - LocalizationContext locCtxt; - - // Use locale from default I18N localization context, unless it is null - if ((locCtxt = getLocalizationContext(pc)) != null) { - if (locCtxt.getLocale() != null) { - if (format) { - setResponseLocale(pc, locCtxt.getLocale()); - } - return locCtxt.getLocale(); - } - } - - /* - * Establish formatting locale by comparing the preferred locales (in - * order of preference) against the available formatting locales, and - * determining the best matching locale. - */ - Locale match; - Locale pref = getLocale(pc, Config.FMT_LOCALE); - if (pref != null) { - // Preferred locale is application-based - match = findFormattingMatch(pref, avail); - } else { - // Preferred locales are browser-based - match = findFormattingMatch(pc, avail); - } - if (match == null) { - // Use fallback locale. - pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); - if (pref != null) { - match = findFormattingMatch(pref, avail); - } - } - if (format && (match != null)) { - setResponseLocale(pc, match); - } - - return match; - } - - /** - * Setup the available formatting locales that will be used by - * getFormattingLocale(PageContext). - */ - static Locale[] availableFormattingLocales; - static { - Locale[] dateLocales = DateFormat.getAvailableLocales(); - Set numberLocales = new HashSet<>(Arrays.asList(NumberFormat.getAvailableLocales())); - ArrayList locales = new ArrayList<>(); - for (Locale dateLocale : dateLocales) - if (numberLocales.contains(dateLocale)) - locales.add(dateLocale); - availableFormattingLocales = new Locale[locales.size()]; - availableFormattingLocales = locales.toArray(availableFormattingLocales); - } - - /** - * Returns the locale specified by the named scoped attribute or context - * configuration parameter. - * - *

The named scoped attribute is searched in the page, request, session - * (if valid), and application scope(s) (in this order). If no such - * attribute exists in any of the scopes, the locale is taken from the named - * context configuration parameter. - * - * @param pageContext the page in which to search for the named scoped - * attribute or context configuration parameter @param name the name of the - * scoped attribute or context configuration parameter - * - * @return the locale specified by the named scoped attribute or context - * configuration parameter, or null if no scoped attribute or - * configuration parameter with the given name exists - */ - static Locale getLocale(PageContext pageContext, String name) { - Locale loc = null; - - Object obj = Config.find(pageContext, name); - if (obj != null) { - if (obj instanceof Locale) { - loc = (Locale) obj; - } else { - loc = parseLocale((String) obj); - } - } - - return loc; - } - - // ********************************************************************* - // Private utility methods - - /** - * Determines the client's preferred locales from the request, and compares - * each of the locales (in order of preference) against the available - * locales in order to determine the best matching locale. - * - * @param pageContext Page containing the formatting action @param avail - * Available formatting locales - * - * @return Best matching locale, or null if no match was found - */ - private static Locale findFormattingMatch(PageContext pageContext, - Locale[] avail) { - Locale match = null; - for (Enumeration enum_ = Util - .getRequestLocales((HttpServletRequest) pageContext - .getRequest()); enum_.hasMoreElements();) { - Locale locale = (Locale) enum_.nextElement(); - match = findFormattingMatch(locale, avail); - if (match != null) { - break; - } - } - - return match; - } - - /** - * Returns the best match between the given preferred locale and the given - * available locales. - * - * The best match is given as the first available locale that exactly - * matches the given preferred locale ("exact match"). If no exact match - * exists, the best match is given to an available locale that meets the - * following criteria (in order of priority): - available locale's variant - * is empty and exact match for both language and country - available - * locale's variant and country are empty, and exact match for language. - * - * @param pref the preferred locale @param avail the available formatting - * locales - * - * @return Available locale that best matches the given preferred locale, or - * null if no match exists - */ - private static Locale findFormattingMatch(Locale pref, Locale[] avail) { - Locale match = null; - boolean langAndCountryMatch = false; - for (Locale locale : avail) { - if (pref.equals(locale)) { - // Exact match - match = locale; - break; - } else if (!"".equals(pref.getVariant()) - && "".equals(locale.getVariant()) - && pref.getLanguage().equals(locale.getLanguage()) - && pref.getCountry().equals(locale.getCountry())) { - // Language and country match; different variant - match = locale; - langAndCountryMatch = true; - } else if (!langAndCountryMatch - && pref.getLanguage().equals(locale.getLanguage()) - && ("".equals(locale.getCountry()))) { - // Language match - if (match == null) { - match = locale; - } - } - } - return match; - } - - /** - * Gets the default I18N localization context. - * - * @param pc Page in which to look up the default I18N localization context - * @return the localization context - */ - public static LocalizationContext getLocalizationContext(PageContext pc) { - LocalizationContext locCtxt; - - Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT); - if (obj == null) { - return null; - } - - if (obj instanceof LocalizationContext) { - locCtxt = (LocalizationContext) obj; - } else { - // localization context is a bundle basename - locCtxt = getLocalizationContext(pc, (String) obj); - } - - return locCtxt; - } - - /** - * Gets the resource bundle with the given base name, whose locale is - * determined as follows: - * - * Check if a match exists between the ordered set of preferred locales and - * the available locales, for the given base name. The set of preferred - * locales consists of a single locale (if the - * javax.servlet.jsp.jstl.fmt.locale configuration setting is - * present) or is equal to the client's preferred locales determined from - * the client's browser settings. - * - *

- * If no match was found in the previous step, check if a match exists - * between the fallback locale (given by the - * javax.servlet.jsp.jstl.fmt.fallbackLocale configuration - * setting) and the available locales, for the given base name. - * - * @param pc Page in which the resource bundle with the given base - * name is requested - * @param basename Resource bundle base name - * - * @return Localization context containing the resource bundle with the - * given base name and the locale that led to the resource bundle match, or - * the empty localization context if no resource bundle match was found - */ - public static LocalizationContext getLocalizationContext(PageContext pc, - String basename) { - LocalizationContext locCtxt = null; - ResourceBundle bundle; - - if ((basename == null) || basename.equals("")) { - return new LocalizationContext(); - } - - // Try preferred locales - Locale pref = getLocale(pc, Config.FMT_LOCALE); - if (pref != null) { - // Preferred locale is application-based - bundle = findMatch(basename, pref); - if (bundle != null) { - locCtxt = new LocalizationContext(bundle, pref); - } - } else { - // Preferred locales are browser-based - locCtxt = findMatch(pc, basename); - } - - if (locCtxt == null) { - // No match found with preferred locales, try using fallback locale - pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); - if (pref != null) { - bundle = findMatch(basename, pref); - if (bundle != null) { - locCtxt = new LocalizationContext(bundle, pref); - } - } - } - - if (locCtxt == null) { - // try using the root resource bundle with the given basename - try { - bundle = ResourceBundle.getBundle(basename, EMPTY_LOCALE, - Thread.currentThread().getContextClassLoader()); - if (bundle != null) { - locCtxt = new LocalizationContext(bundle, null); - } - } catch (MissingResourceException mre) { - // do nothing - } - } - - if (locCtxt != null) { - // set response locale - if (locCtxt.getLocale() != null) { - setResponseLocale(pc, locCtxt.getLocale()); - } - } else { - // create empty localization context - locCtxt = new LocalizationContext(); - } - - return locCtxt; - } - - /** - * Determines the client's preferred locales from the request, and compares - * each of the locales (in order of preference) against the available - * locales in order to determine the best matching locale. - * - * @param pageContext the page in which the resource bundle with the given - * base name is requested @param basename the resource bundle's base name - * - * @return the localization context containing the resource bundle with the - * given base name and best matching locale, or null if no - * resource bundle match was found - */ - private static LocalizationContext findMatch(PageContext pageContext, - String basename) { - LocalizationContext locCtxt = null; - - // Determine locale from client's browser settings. - for (Enumeration enum_ = Util - .getRequestLocales((HttpServletRequest) pageContext - .getRequest()); enum_.hasMoreElements();) { - Locale pref = (Locale) enum_.nextElement(); - ResourceBundle match = findMatch(basename, pref); - if (match != null) { - locCtxt = new LocalizationContext(match, pref); - break; - } - } - - return locCtxt; - } - - /** - * Gets the resource bundle with the given base name and preferred locale. - * - * This method calls java.util.ResourceBundle.getBundle(), but ignores its - * return value unless its locale represents an exact or language match with - * the given preferred locale. - * - * @param basename the resource bundle base name @param pref the preferred - * locale - * - * @return the requested resource bundle, or null if no resource - * bundle with the given base name exists or if there is no exact- or - * language-match between the preferred locale and the locale of the bundle - * returned by java.util.ResourceBundle.getBundle(). - */ - private static ResourceBundle findMatch(String basename, Locale pref) { - ResourceBundle match = null; - - try { - ResourceBundle bundle = ResourceBundle.getBundle(basename, pref, - Thread.currentThread().getContextClassLoader()); - Locale avail = bundle.getLocale(); - if (pref.equals(avail)) { - // Exact match - match = bundle; - } else { - /* - * We have to make sure that the match we got is for the - * specified locale. The way ResourceBundle.getBundle() works, - * if a match is not found with (1) the specified locale, it - * tries to match with (2) the current default locale as - * returned by Locale.getDefault() or (3) the root resource - * bundle (basename). We must ignore any match that could have - * worked with (2) or (3). So if an exact match is not found, we - * make the following extra tests: - avail locale must be equal - * to preferred locale - avail country must be empty or equal to - * preferred country (the equality match might have failed on - * the variant) - */ - if (pref.getLanguage().equals(avail.getLanguage()) - && ("".equals(avail.getCountry()) || pref.getCountry() - .equals(avail.getCountry()))) { - /* - * Language match. By making sure the available locale does - * not have a country and matches the preferred locale's - * language, we rule out "matches" based on the container's - * default locale. For example, if the preferred locale is - * "en-US", the container's default locale is "en-UK", and - * there is a resource bundle (with the requested base name) - * available for "en-UK", ResourceBundle.getBundle() will - * return it, but even though its language matches that of - * the preferred locale, we must ignore it, because matches - * based on the container's default locale are not portable - * across different containers with different default - * locales. - */ - match = bundle; - } - } - } catch (MissingResourceException mre) { - throw new IllegalStateException("Shouldn't happen?"); - } - - return match; - } - - /* - This section is based on joda-time DateTimeFormat to handle the two character style pattern missing in Java Time. - */ - - /** - * Creates a formatter from a two character style pattern. The first character - * is the date style, and the second character is the time style. Specify a - * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' - * for full. A date or time may be ommitted by specifying a style character '-'. - * - * @param style two characters from the set {"S", "M", "L", "F", "-"} - * @throws JspException if the style is invalid - * @return a formatter for the specified style - */ - public static DateTimeFormatter createFormatterForStyle(String style) - throws JspException - { - if (style == null || style.length() != 2) { - throw new JspException("Invalid style specification: " + style); - } - FormatStyle dateStyle = selectStyle(style.charAt(0)); - FormatStyle timeStyle = selectStyle(style.charAt(1)); - if (dateStyle == null && timeStyle == null) { - throw new JspException("Style '--' is invalid"); - } - return createFormatterForStyleIndex(dateStyle, timeStyle); - } - - /** - * Gets the formatter for the specified style. - * - * @param dateStyle the date style - * @param timeStyle the time style - * @return the formatter - */ - private static DateTimeFormatter createFormatterForStyleIndex(FormatStyle dateStyle, FormatStyle timeStyle) - throws JspException { - if (dateStyle == null && timeStyle == null) - throw new JspException("Both styles cannot be null."); - else if (dateStyle != null && timeStyle != null) - return DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); - else if (dateStyle == null) - return DateTimeFormatter.ofLocalizedTime(timeStyle); - else - return DateTimeFormatter.ofLocalizedDate(dateStyle); - } - - /** - * Gets the FormatStyle style code from first character. - * - * @param ch the one character style code - * @return the FormatStyle - */ - private static FormatStyle selectStyle(char ch) throws JspException { - switch (ch) { - case 'S': - return SHORT; - case 'M': - return MEDIUM; - case 'L': - return LONG; - case 'F': - return FULL; - case '-': - return null; - default: - throw new JspException("Invalid style character: " + ch); - } - } + private static final String REQUEST = "request"; + + private static final String SESSION = "session"; + + private static final String APPLICATION = "application"; + + private static final char HYPHEN = '-'; + + private static final char UNDERSCORE = '_'; + + private static final Locale EMPTY_LOCALE = new Locale("", ""); + + static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset"; + + /** + * Converts the given string description of a scope to the corresponding + * PageContext constant. + * + * The validity of the given scope has already been checked by the appropriate + * TLV. + * + * @param scope String description of scope + * + * @return PageContext constant corresponding to given scope description + */ + public static int getScope(String scope) { + int ret = PageContext.PAGE_SCOPE; // default + + if (REQUEST.equalsIgnoreCase(scope)) { + ret = PageContext.REQUEST_SCOPE; + } else if (SESSION.equalsIgnoreCase(scope)) { + ret = PageContext.SESSION_SCOPE; + } else if (APPLICATION.equalsIgnoreCase(scope)) { + ret = PageContext.APPLICATION_SCOPE; + } + return ret; + } + + /** + * HttpServletRequest.getLocales() returns the server's default locale if the + * request did not specify a preferred language. We do not want this behavior, + * because it prevents us from using the fallback locale. We therefore need to + * return an empty Enumeration if no preferred locale has been specified. This + * way, the logic for the fallback locale will be able to kick in. + * + * @param request the http request + * @return the locales from the request or an empty enumeration if no preferred + * locale has been specified + */ + public static Enumeration getRequestLocales(HttpServletRequest request) { + Enumeration values = request.getHeaders("accept-language"); + if (values.hasMoreElements()) { + // At least one "accept-language". Simply return + // the enumeration returned by request.getLocales(). + // System.out.println("At least one accept-language"); + return request.getLocales(); + } else { + // No header for "accept-language". Simply return + // the empty enumeration. + // System.out.println("No accept-language"); + return values; + } + } + + /** + * See parseLocale(String, String) for details. + * + * @param locale the locale string to parse + * @return {@link java.util.Locale} object corresponding to the given locale + * string, or the null if the locale string is null or empty + */ + public static Locale parseLocale(String locale) { + return parseLocale(locale, null); + } + + /** + * Parses the given locale string into its language and (optionally) country + * components, and returns the corresponding {@link java.util.Locale} object. + * + * If the given locale string is null or empty, a null value is returned. + * + * @param locale the locale string to parse + * @param variant the variant + * + * @return {@link java.util.Locale} object corresponding to the given locale + * string, or the null if the locale string is null or empty + * + * @throws IllegalArgumentException if the given locale does not have a language + * component or has an empty country component + */ + public static Locale parseLocale(String locale, String variant) { + Locale ret; + String language = locale; + String country = null; + int index; + + if (locale == null || locale.isEmpty()) + return null; + + if (((index = locale.indexOf(HYPHEN)) > -1) || ((index = locale.indexOf(UNDERSCORE)) > -1)) { + language = locale.substring(0, index); + country = locale.substring(index + 1); + } + + if (language.isEmpty()) { + throw new IllegalArgumentException(Resources.getMessage("LOCALE_NO_LANGUAGE")); + } + + if (country == null) { + if (variant != null) { + ret = new Locale(language, "", variant); + } else { + ret = new Locale(language, ""); + } + } else if (country.length() > 0) { + if (variant != null) { + ret = new Locale(language, country, variant); + } else { + ret = new Locale(language, country); + } + } else { + throw new IllegalArgumentException(Resources.getMessage("LOCALE_EMPTY_COUNTRY")); + } + + return ret; + } + + /** + * Stores the given locale in the response object of the given page context, and + * stores the locale's associated charset in the + * javax.servlet.jsp.jstl.fmt.request.charset session attribute, which may be + * used by the action in a page invoked by a form included in + * the response to set the request charset to the same as the response charset + * (this makes it possible for the container to decode the form parameter values + * properly, since browsers typically encode form field values using the + * response's charset). + * + * @param pc the page context whose response object is assigned the given + * locale + * @param locale the response locale + */ + static void setResponseLocale(PageContext pc, Locale locale) { + // set response locale + ServletResponse response = pc.getResponse(); + response.setLocale(locale); + + // get response character encoding and store it in session attribute + if (pc.getSession() != null) { + try { + pc.setAttribute(REQUEST_CHAR_SET, response.getCharacterEncoding(), PageContext.SESSION_SCOPE); + } catch (IllegalStateException ex) { + // invalidated session ignored + } + } + } + + /** + * Returns the formatting locale to use with the given formatting action in the + * given page. + * + * @param pc The page context containing the formatting action @param fromTag + * The formatting action @param format {@code true} if the + * formatting action is of type {@code } (as opposed to + * {@code }), and {@code false} otherwise (if set to + * {@code true}, the formatting locale that is returned by this + * method is used to set the response locale). + * + * @param avail the array of available locales + * + * @return the formatting locale to use + */ + static Locale getFormattingLocale(PageContext pc, boolean format, Locale[] avail) { + + LocalizationContext locCtxt; + + // Use locale from default I18N localization context, unless it is null + if ((locCtxt = getLocalizationContext(pc)) != null) { + if (locCtxt.getLocale() != null) { + if (format) { + setResponseLocale(pc, locCtxt.getLocale()); + } + return locCtxt.getLocale(); + } + } + + /* + * Establish formatting locale by comparing the preferred locales (in order of + * preference) against the available formatting locales, and determining the + * best matching locale. + */ + Locale match; + Locale pref = getLocale(pc, Config.FMT_LOCALE); + if (pref != null) { + // Preferred locale is application-based + match = findFormattingMatch(pref, avail); + } else { + // Preferred locales are browser-based + match = findFormattingMatch(pc, avail); + } + if (match == null) { + // Use fallback locale. + pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); + if (pref != null) { + match = findFormattingMatch(pref, avail); + } + } + if (format && (match != null)) { + setResponseLocale(pc, match); + } + + return match; + } + + /** + * Setup the available formatting locales that will be used by + * getFormattingLocale(PageContext). + */ + static Locale[] availableFormattingLocales; + static { + Locale[] dateLocales = DateFormat.getAvailableLocales(); + Set numberLocales = new HashSet<>(Arrays.asList(NumberFormat.getAvailableLocales())); + ArrayList locales = new ArrayList<>(); + for (Locale dateLocale : dateLocales) + if (numberLocales.contains(dateLocale)) + locales.add(dateLocale); + availableFormattingLocales = new Locale[locales.size()]; + availableFormattingLocales = locales.toArray(availableFormattingLocales); + } + + /** + * Returns the locale specified by the named scoped attribute or context + * configuration parameter. + * + *

+ * The named scoped attribute is searched in the page, request, session (if + * valid), and application scope(s) (in this order). If no such attribute exists + * in any of the scopes, the locale is taken from the named context + * configuration parameter. + * + * @param pageContext the page in which to search for the named scoped attribute + * or context configuration parameter @param name the name of + * the scoped attribute or context configuration parameter + * + * @return the locale specified by the named scoped attribute or context + * configuration parameter, or {@code null} if no scoped attribute or + * configuration parameter with the given name exists + */ + static Locale getLocale(PageContext pageContext, String name) { + Locale loc = null; + + Object obj = Config.find(pageContext, name); + if (obj != null) { + if (obj instanceof Locale) { + loc = (Locale) obj; + } else { + loc = parseLocale((String) obj); + } + } + + return loc; + } + + // ********************************************************************* + // Private utility methods + + /** + * Determines the client's preferred locales from the request, and compares each + * of the locales (in order of preference) against the available locales in + * order to determine the best matching locale. + * + * @param pageContext Page containing the formatting action @param avail + * Available formatting locales + * + * @return Best matching locale, or {@code null} if no match was found + */ + private static Locale findFormattingMatch(PageContext pageContext, Locale[] avail) { + Locale match = null; + for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ + .hasMoreElements();) { + Locale locale = (Locale) enum_.nextElement(); + match = findFormattingMatch(locale, avail); + if (match != null) { + break; + } + } + + return match; + } + + /** + * Returns the best match between the given preferred locale and the given + * available locales. + * + * The best match is given as the first available locale that exactly matches + * the given preferred locale ("exact match"). If no exact match exists, the + * best match is given to an available locale that meets the following criteria + * (in order of priority): - available locale's variant is empty and exact match + * for both language and country - available locale's variant and country are + * empty, and exact match for language. + * + * @param pref the preferred locale @param avail the available formatting + * locales + * + * @return Available locale that best matches the given preferred locale, or + * {@code null} if no match exists + */ + private static Locale findFormattingMatch(Locale pref, Locale[] avail) { + Locale match = null; + boolean langAndCountryMatch = false; + for (Locale locale : avail) { + if (pref.equals(locale)) { + // Exact match + match = locale; + break; + } else if (!"".equals(pref.getVariant()) && "".equals(locale.getVariant()) + && pref.getLanguage().equals(locale.getLanguage()) + && pref.getCountry().equals(locale.getCountry())) { + // Language and country match; different variant + match = locale; + langAndCountryMatch = true; + } else if (!langAndCountryMatch && pref.getLanguage().equals(locale.getLanguage()) + && ("".equals(locale.getCountry()))) { + // Language match + if (match == null) { + match = locale; + } + } + } + return match; + } + + /** + * Gets the default I18N localization context. + * + * @param pc Page in which to look up the default I18N localization context + * @return the localization context + */ + public static LocalizationContext getLocalizationContext(PageContext pc) { + LocalizationContext locCtxt; + + Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT); + if (obj == null) { + return null; + } + + if (obj instanceof LocalizationContext) { + locCtxt = (LocalizationContext) obj; + } else { + // localization context is a bundle basename + locCtxt = getLocalizationContext(pc, (String) obj); + } + + return locCtxt; + } + + /** + * Gets the resource bundle with the given base name, whose locale is determined + * as follows: + * + * Check if a match exists between the ordered set of preferred locales and the + * available locales, for the given base name. The set of preferred locales + * consists of a single locale (if the + * {@link Config#FMT_LOCALE} configuration setting is present) + * or is equal to the client's preferred locales determined from the client's + * browser settings. + * + *

+ * If no match was found in the previous step, check if a match exists between + * the fallback locale (given by the + * {@link Config#FMT_FALLBACK_LOCALE} configuration setting) and + * the available locales, for the given base name. + * + * @param pc Page in which the resource bundle with the given base name is + * requested + * @param basename Resource bundle base name + * + * @return Localization context containing the resource bundle with the given + * base name and the locale that led to the resource bundle match, or + * the empty localization context if no resource bundle match was found + */ + public static LocalizationContext getLocalizationContext(PageContext pc, String basename) { + LocalizationContext locCtxt = null; + ResourceBundle bundle; + + if ((basename == null) || basename.equals("")) { + return new LocalizationContext(); + } + + // Try preferred locales + Locale pref = getLocale(pc, Config.FMT_LOCALE); + if (pref != null) { + // Preferred locale is application-based + bundle = findMatch(basename, pref); + if (bundle != null) { + locCtxt = new LocalizationContext(bundle, pref); + } + } else { + // Preferred locales are browser-based + locCtxt = findMatch(pc, basename); + } + + if (locCtxt == null) { + // No match found with preferred locales, try using fallback locale + pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); + if (pref != null) { + bundle = findMatch(basename, pref); + if (bundle != null) { + locCtxt = new LocalizationContext(bundle, pref); + } + } + } + + if (locCtxt == null) { + // try using the root resource bundle with the given basename + try { + bundle = ResourceBundle.getBundle(basename, EMPTY_LOCALE, + Thread.currentThread().getContextClassLoader()); + if (bundle != null) { + locCtxt = new LocalizationContext(bundle, null); + } + } catch (MissingResourceException mre) { + // do nothing + } + } + + if (locCtxt != null) { + // set response locale + if (locCtxt.getLocale() != null) { + setResponseLocale(pc, locCtxt.getLocale()); + } + } else { + // create empty localization context + locCtxt = new LocalizationContext(); + } + + return locCtxt; + } + + /** + * Determines the client's preferred locales from the request, and compares each + * of the locales (in order of preference) against the available locales in + * order to determine the best matching locale. + * + * @param pageContext the page in which the resource bundle with the given base + * name is requested @param basename the resource bundle's + * base name + * + * @return the localization context containing the resource bundle with the + * given base name and best matching locale, or {@code null} if no + * resource bundle match was found + */ + private static LocalizationContext findMatch(PageContext pageContext, String basename) { + LocalizationContext locCtxt = null; + + // Determine locale from client's browser settings. + for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ + .hasMoreElements();) { + Locale pref = (Locale) enum_.nextElement(); + ResourceBundle match = findMatch(basename, pref); + if (match != null) { + locCtxt = new LocalizationContext(match, pref); + break; + } + } + + return locCtxt; + } + + /** + * Gets the resource bundle with the given base name and preferred locale. + * + * This method calls java.util.ResourceBundle.getBundle(), but ignores its + * return value unless its locale represents an exact or language match with the + * given preferred locale. + * + * @param basename the resource bundle base name @param pref the preferred + * locale + * + * @return the requested resource bundle, or {@code null} if no resource bundle + * with the given base name exists or if there is no exact- or + * language-match between the preferred locale and the locale of the + * bundle returned by java.util.ResourceBundle.getBundle(). + */ + private static ResourceBundle findMatch(String basename, Locale pref) { + ResourceBundle match = null; + + try { + ResourceBundle bundle = ResourceBundle.getBundle(basename, pref, + Thread.currentThread().getContextClassLoader()); + Locale avail = bundle.getLocale(); + if (pref.equals(avail)) { + // Exact match + match = bundle; + } else { + /* + * We have to make sure that the match we got is for the specified locale. The + * way ResourceBundle.getBundle() works, if a match is not found with (1) the + * specified locale, it tries to match with (2) the current default locale as + * returned by Locale.getDefault() or (3) the root resource bundle (basename). + * We must ignore any match that could have worked with (2) or (3). So if an + * exact match is not found, we make the following extra tests: - avail locale + * must be equal to preferred locale - avail country must be empty or equal to + * preferred country (the equality match might have failed on the variant) + */ + if (pref.getLanguage().equals(avail.getLanguage()) + && ("".equals(avail.getCountry()) || pref.getCountry().equals(avail.getCountry()))) { + /* + * Language match. By making sure the available locale does not have a country + * and matches the preferred locale's language, we rule out "matches" based on + * the container's default locale. For example, if the preferred locale is + * "en-US", the container's default locale is "en-UK", and there is a resource + * bundle (with the requested base name) available for "en-UK", + * ResourceBundle.getBundle() will return it, but even though its language + * matches that of the preferred locale, we must ignore it, because matches + * based on the container's default locale are not portable across different + * containers with different default locales. + */ + match = bundle; + } + } + } catch (MissingResourceException mre) { + throw new IllegalStateException("Shouldn't happen?"); + } + + return match; + } + + /* + * This section is based on joda-time DateTimeFormat to handle the two character + * style pattern missing in Java Time. + */ + + /** + * Creates a formatter from a two character style pattern. The first character + * is the date style, and the second character is the time style. Specify a + * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for + * full. A date or time may be ommitted by specifying a style character '-'. + * + * @param style two characters from the set {"S", "M", "L", "F", "-"} + * @throws JspException if the style is invalid + * @return a formatter for the specified style + */ + public static DateTimeFormatter createFormatterForStyle(String style) throws JspException { + if (style == null || style.length() != 2) { + throw new JspException("Invalid style specification: " + style); + } + FormatStyle dateStyle = selectStyle(style.charAt(0)); + FormatStyle timeStyle = selectStyle(style.charAt(1)); + if (dateStyle == null && timeStyle == null) { + throw new JspException("Style '--' is invalid"); + } + return createFormatterForStyleIndex(dateStyle, timeStyle); + } + + /** + * Gets the formatter for the specified style. + * + * @param dateStyle the date style + * @param timeStyle the time style + * @return the formatter + */ + private static DateTimeFormatter createFormatterForStyleIndex(FormatStyle dateStyle, FormatStyle timeStyle) + throws JspException { + if (dateStyle == null && timeStyle == null) + throw new JspException("Both styles cannot be null."); + else if (dateStyle != null && timeStyle != null) + return DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); + else if (dateStyle == null) + return DateTimeFormatter.ofLocalizedTime(timeStyle); + else + return DateTimeFormatter.ofLocalizedDate(dateStyle); + } + + /** + * Gets the FormatStyle style code from first character. + * + * @param ch the one character style code + * @return the FormatStyle + */ + private static FormatStyle selectStyle(char ch) throws JspException { + switch (ch) { + case 'S': + return SHORT; + case 'M': + return MEDIUM; + case 'L': + return LONG; + case 'F': + return FULL; + case '-': + return null; + default: + throw new JspException("Invalid style character: " + ch); + } + } } diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java index a15d920..b5712d1 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java @@ -16,16 +16,17 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspTagException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.jstl.core.Config; -import javax.servlet.jsp.tagext.BodyTagSupport; -import javax.servlet.jsp.tagext.Tag; import java.io.IOException; import java.time.ZoneId; import java.time.ZoneOffset; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.jstl.core.Config; +import jakarta.servlet.jsp.tagext.BodyTagSupport; +import jakarta.servlet.jsp.tagext.Tag; + /** * Support for tag handlers for <timeZone>. * diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java index 7ccdbfc..03e8404 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java @@ -16,7 +16,7 @@ */ package net.sargue.time.jsptags; -import javax.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.JspTagException; /** * A handler for <zoneId>. diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index 0c385cd..72524e8 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -18,13 +18,12 @@ import java.util.Locale; import java.util.TimeZone; -import javax.servlet.jsp.JspException; - import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; +import jakarta.servlet.jsp.JspException; import net.sargue.time.jsptags.FormatTag; /** @@ -35,176 +34,158 @@ */ public class FormatTagTest { - private MockServletContext mockServletContext; - - @Before - public void setup() throws UnsupportedEncodingException { - Locale.setDefault(Locale.forLanguageTag("ca")); - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); - mockServletContext = new MockServletContext(); - } - - @Test - public void dayOfWeekTest() throws IOException, JspException { - assertEquals("dl. dl. dl. dilluns 1 01 dl. dilluns", format( - DayOfWeek.MONDAY, "E EE EEE EEEE e ee eee eeee", null)); - assertEquals("dt. dt. dt. dimarts 2 02 dt. dimarts", format( - DayOfWeek.TUESDAY, "E EE EEE EEEE e ee eee eeee", null)); - } - - @Test - public void instantTest() throws JspException, IOException { - Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); - assertEquals("06/11/2015", format(instant, null, null)); - assertEquals("06/11/15", format(instant, null, "S-")); - assertEquals("06/11/2015", format(instant, null, "M-")); - assertEquals("6 / de novembre / 2015", format(instant, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", - format(instant, null, "F-")); - assertEquals("10:45", format(instant, null, "-S")); - assertEquals("10:45:33", format(instant, null, "-M")); - assertEquals("10:45:33 CET", format(instant, null, "-L")); - assertEquals("10:45:33 CET", format(instant, null, "-F")); - } - - @Test - public void localDateTest() throws IOException, JspException { - LocalDate localDate = LocalDate.parse("2015-11-06"); - assertEquals("06/11/2015", format(localDate, null, null)); - assertEquals("06/11/2015", format(localDate, "dd/MM/yyyy", null)); - assertEquals("06/11/15", format(localDate, null, "S-")); - assertEquals("06/11/2015", format(localDate, null, "M-")); - assertEquals("6 / de novembre / 2015", format(localDate, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", - format(localDate, null, "F-")); - } - - @Test - public void localTimeTest() throws IOException, JspException { - LocalTime localTime = LocalTime.parse("10:53:55.913"); - assertEquals("10:53:55", format(localTime, "HH:mm:ss", null)); - assertEquals("10:53", format(localTime, null, "-S")); - assertEquals("10:53:55", format(localTime, null, "-M")); - assertEquals("10:53:55 CET", format(localTime, null, "-L")); - assertEquals("10:53:55 CET", format(localTime, null, "-F")); - } - - @Test - public void localDateTimeTest() throws IOException, JspException { - LocalDateTime localDateTime = - LocalDateTime.parse("2015-11-06T10:55:53.456"); - assertEquals("06/11/2015 10:55:53", - format(localDateTime, "dd/MM/yyyy HH:mm:ss", null)); - assertEquals("06/11/2015", format(localDateTime, null, null)); - assertEquals("06/11/15", format(localDateTime, null, "S-")); - assertEquals("06/11/2015", format(localDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", format(localDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", - format(localDateTime, null, "F-")); - assertEquals("10:55", format(localDateTime, null, "-S")); - assertEquals("10:55:53", format(localDateTime, null, "-M")); - assertEquals("10:55:53 CET", format(localDateTime, null, "-L")); - assertEquals("10:55:53 CET", format(localDateTime, null, "-F")); - } - - @Test - public void monthTest() throws IOException, JspException { - assertEquals("4 04 d’abr. d’abril 4 04 abr. abril", - format(Month.APRIL, "M MM MMM MMMM L LL LLL LLLL", null)); - } - - @Test - public void monthDayTest() throws IOException, JspException { - MonthDay monthDay = MonthDay.parse("--11-06"); - assertEquals("11 6", format(monthDay, "M d", null)); - } - - @Test - public void offsetDateTimeTest() throws IOException, JspException { - OffsetDateTime offsetDateTime = - OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); - assertEquals("06/11/2015", format(offsetDateTime, null, null)); - assertEquals("06/11/15", format(offsetDateTime, null, "S-")); - assertEquals("06/11/2015", format(offsetDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", - format(offsetDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", - format(offsetDateTime, null, "F-")); - assertEquals("10:58", format(offsetDateTime, null, "-S")); - assertEquals("10:58:21", format(offsetDateTime, null, "-M")); - assertEquals("10:58:21 CET", format(offsetDateTime, null, "-L")); - assertEquals("10:58:21 CET", format(offsetDateTime, null, "-F")); - } - - @Test - public void offsetTimeTest() throws IOException, JspException { - OffsetTime offsetTime = OffsetTime.parse("11:01:39.810+01:00"); - assertEquals("11:01:39", format(offsetTime, "HH:mm:ss", null)); - assertEquals("11:01", format(offsetTime, null, "-S")); - assertEquals("11:01:39", format(offsetTime, null, "-M")); - assertEquals("11:01:39 CET", format(offsetTime, null, "-L")); - assertEquals("11:01:39 CET", format(offsetTime, null, "-F")); - } - - @Test - public void yearTest() throws IOException, JspException { - Year year = Year.parse("2015"); - assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC AD", format( - year, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG", null)); - } - - @Test - public void yearMonthTest() throws IOException, JspException { - YearMonth yearMonth = YearMonth.parse("2015-11"); - assertEquals("dC 2015 2015 11 11 4 04 4T 4t trimestre", format( - yearMonth, "G u y M L Q QQ QQQ QQQQ", null)); - } - - @Test - public void zonedDateTime() throws IOException, JspException { - ZonedDateTime zonedDateTime = - ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); - assertEquals("06/11/2015", format(zonedDateTime, null, null)); - assertEquals("06/11/15", format(zonedDateTime, null, "S-")); - assertEquals("06/11/2015", format(zonedDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", format(zonedDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", - format(zonedDateTime, null, "F-")); - assertEquals("11:04", format(zonedDateTime, null, "-S")); - assertEquals("11:04:47", format(zonedDateTime, null, "-M")); - assertEquals("11:04:47 CET", format(zonedDateTime, null, "-L")); - assertEquals("11:04:47 CET", format(zonedDateTime, null, "-F")); - - ZonedDateTime pstZonedDateTime = - zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); - System.out.println(pstZonedDateTime); - assertEquals("06/11/2015", format(pstZonedDateTime, null, null)); - assertEquals("06/11/15", format(pstZonedDateTime, null, "S-")); - assertEquals("06/11/2015", format(pstZonedDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", - format(pstZonedDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", - format(pstZonedDateTime, null, "F-")); - assertEquals("02:04", format(pstZonedDateTime, null, "-S")); - assertEquals("02:04:47", format(pstZonedDateTime, null, "-M")); - assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-L")); - assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-F")); - } - - private String format(Object o, String pattern, String style) - throws JspException, IOException - { - MockPageContext mockPageContext = new MockPageContext( - mockServletContext); - mockPageContext.getRequest().setCharacterEncoding("UTF-8"); - mockPageContext.getResponse().setCharacterEncoding("UTF-8"); - FormatTag formatTag = new FormatTag(); - formatTag.setPageContext(mockPageContext); - - formatTag.setPattern(pattern); - formatTag.setStyle(style); - formatTag.setValue(o); - formatTag.doEndTag(); - return mockPageContext.getContentAsString(); - } + private MockServletContext mockServletContext; + + @Before + public void setup() throws UnsupportedEncodingException { + Locale.setDefault(Locale.forLanguageTag("ca")); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); + mockServletContext = new MockServletContext(); + } + + @Test + public void dayOfWeekTest() throws IOException, JspException { + assertEquals("dl. dl. dl. dilluns 1 01 dl. dilluns", + format(DayOfWeek.MONDAY, "E EE EEE EEEE e ee eee eeee", null)); + assertEquals("dt. dt. dt. dimarts 2 02 dt. dimarts", + format(DayOfWeek.TUESDAY, "E EE EEE EEEE e ee eee eeee", null)); + } + + @Test + public void instantTest() throws JspException, IOException { + Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); + assertEquals("06/11/2015", format(instant, null, null)); + assertEquals("06/11/15", format(instant, null, "S-")); + assertEquals("06/11/2015", format(instant, null, "M-")); + assertEquals("6 / de novembre / 2015", format(instant, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", format(instant, null, "F-")); + assertEquals("10:45", format(instant, null, "-S")); + assertEquals("10:45:33", format(instant, null, "-M")); + assertEquals("10:45:33 CET", format(instant, null, "-L")); + assertEquals("10:45:33 CET", format(instant, null, "-F")); + } + + @Test + public void localDateTest() throws IOException, JspException { + LocalDate localDate = LocalDate.parse("2015-11-06"); + assertEquals("06/11/2015", format(localDate, null, null)); + assertEquals("06/11/2015", format(localDate, "dd/MM/yyyy", null)); + assertEquals("06/11/15", format(localDate, null, "S-")); + assertEquals("06/11/2015", format(localDate, null, "M-")); + assertEquals("6 / de novembre / 2015", format(localDate, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", format(localDate, null, "F-")); + } + + @Test + public void localTimeTest() throws IOException, JspException { + LocalTime localTime = LocalTime.parse("10:53:55.913"); + assertEquals("10:53:55", format(localTime, "HH:mm:ss", null)); + assertEquals("10:53", format(localTime, null, "-S")); + assertEquals("10:53:55", format(localTime, null, "-M")); + assertEquals("10:53:55 CET", format(localTime, null, "-L")); + assertEquals("10:53:55 CET", format(localTime, null, "-F")); + } + + @Test + public void localDateTimeTest() throws IOException, JspException { + LocalDateTime localDateTime = LocalDateTime.parse("2015-11-06T10:55:53.456"); + assertEquals("06/11/2015 10:55:53", format(localDateTime, "dd/MM/yyyy HH:mm:ss", null)); + assertEquals("06/11/2015", format(localDateTime, null, null)); + assertEquals("06/11/15", format(localDateTime, null, "S-")); + assertEquals("06/11/2015", format(localDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", format(localDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", format(localDateTime, null, "F-")); + assertEquals("10:55", format(localDateTime, null, "-S")); + assertEquals("10:55:53", format(localDateTime, null, "-M")); + assertEquals("10:55:53 CET", format(localDateTime, null, "-L")); + assertEquals("10:55:53 CET", format(localDateTime, null, "-F")); + } + + @Test + public void monthTest() throws IOException, JspException { + assertEquals("4 04 d’abr. d’abril 4 04 abr. abril", format(Month.APRIL, "M MM MMM MMMM L LL LLL LLLL", null)); + } + + @Test + public void monthDayTest() throws IOException, JspException { + MonthDay monthDay = MonthDay.parse("--11-06"); + assertEquals("11 6", format(monthDay, "M d", null)); + } + + @Test + public void offsetDateTimeTest() throws IOException, JspException { + OffsetDateTime offsetDateTime = OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); + assertEquals("06/11/2015", format(offsetDateTime, null, null)); + assertEquals("06/11/15", format(offsetDateTime, null, "S-")); + assertEquals("06/11/2015", format(offsetDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", format(offsetDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", format(offsetDateTime, null, "F-")); + assertEquals("10:58", format(offsetDateTime, null, "-S")); + assertEquals("10:58:21", format(offsetDateTime, null, "-M")); + assertEquals("10:58:21 CET", format(offsetDateTime, null, "-L")); + assertEquals("10:58:21 CET", format(offsetDateTime, null, "-F")); + } + + @Test + public void offsetTimeTest() throws IOException, JspException { + OffsetTime offsetTime = OffsetTime.parse("11:01:39.810+01:00"); + assertEquals("11:01:39", format(offsetTime, "HH:mm:ss", null)); + assertEquals("11:01", format(offsetTime, null, "-S")); + assertEquals("11:01:39", format(offsetTime, null, "-M")); + assertEquals("11:01:39 CET", format(offsetTime, null, "-L")); + assertEquals("11:01:39 CET", format(offsetTime, null, "-F")); + } + + @Test + public void yearTest() throws IOException, JspException { + Year year = Year.parse("2015"); + assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC AD", + format(year, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG", null)); + } + + @Test + public void yearMonthTest() throws IOException, JspException { + YearMonth yearMonth = YearMonth.parse("2015-11"); + assertEquals("dC 2015 2015 11 11 4 04 4T 4t trimestre", format(yearMonth, "G u y M L Q QQ QQQ QQQQ", null)); + } + + @Test + public void zonedDateTime() throws IOException, JspException { + ZonedDateTime zonedDateTime = ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); + assertEquals("06/11/2015", format(zonedDateTime, null, null)); + assertEquals("06/11/15", format(zonedDateTime, null, "S-")); + assertEquals("06/11/2015", format(zonedDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", format(zonedDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", format(zonedDateTime, null, "F-")); + assertEquals("11:04", format(zonedDateTime, null, "-S")); + assertEquals("11:04:47", format(zonedDateTime, null, "-M")); + assertEquals("11:04:47 CET", format(zonedDateTime, null, "-L")); + assertEquals("11:04:47 CET", format(zonedDateTime, null, "-F")); + + ZonedDateTime pstZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); + System.out.println(pstZonedDateTime); + assertEquals("06/11/2015", format(pstZonedDateTime, null, null)); + assertEquals("06/11/15", format(pstZonedDateTime, null, "S-")); + assertEquals("06/11/2015", format(pstZonedDateTime, null, "M-")); + assertEquals("6 / de novembre / 2015", format(pstZonedDateTime, null, "L-")); + assertEquals("divendres, 6 / de novembre / 2015", format(pstZonedDateTime, null, "F-")); + assertEquals("02:04", format(pstZonedDateTime, null, "-S")); + assertEquals("02:04:47", format(pstZonedDateTime, null, "-M")); + assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-L")); + assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-F")); + } + + private String format(Object o, String pattern, String style) throws JspException, IOException { + MockPageContext mockPageContext = new MockPageContext(mockServletContext); + mockPageContext.getRequest().setCharacterEncoding("UTF-8"); + mockPageContext.getResponse().setCharacterEncoding("UTF-8"); + FormatTag formatTag = new FormatTag(); + formatTag.setPageContext(mockPageContext); + + formatTag.setPattern(pattern); + formatTag.setStyle(style); + formatTag.setValue(o); + formatTag.doEndTag(); + return mockPageContext.getContentAsString(); + } } diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index 80d7118..7772c70 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -2,14 +2,13 @@ import java.time.LocalDate; import java.util.Locale; -import javax.servlet.jsp.JspException; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; +import jakarta.servlet.jsp.JspException; import net.sargue.time.jsptags.ParseLocalDateTag; /** From 23876c00eeb159655f204579b42a44f535abac80 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 08:53:42 -0600 Subject: [PATCH 20/48] Gradle 7.3.3 --- gradle/wrapper/gradle-wrapper.jar | Bin 54208 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 286 ++++++++++++++--------- gradlew.bat | 173 +++++++------- 4 files changed, 264 insertions(+), 198 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 740092e660c7c8c4f466792e06a3c2af15f87163..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 54208 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 X0vxc9`e`Br7zqIekz&CA1p@kiHgUFe diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fafa075..2e6e589 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jan 21 21:48:13 CST 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/gradlew b/gradlew index 4453cce..1b6c787 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed 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 +# +# https://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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save ( ) { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,89 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From b11d4654572d9f106d80c65c491ee7283eefe9c3 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 08:56:34 -0600 Subject: [PATCH 21/48] Move test classes into package --- src/test/java/{ => net/sargue/time/jsptags}/FormatTagTest.java | 3 ++- .../{ => net/sargue/time/jsptags}/ParseLocalDateTagTest.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) rename src/test/java/{ => net/sargue/time/jsptags}/FormatTagTest.java (99%) rename src/test/java/{ => net/sargue/time/jsptags}/ParseLocalDateTagTest.java (97%) diff --git a/src/test/java/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java similarity index 99% rename from src/test/java/FormatTagTest.java rename to src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 72524e8..494d503 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -1,3 +1,5 @@ +package net.sargue.time.jsptags; + import static org.junit.Assert.assertEquals; import java.io.IOException; @@ -24,7 +26,6 @@ import org.springframework.mock.web.MockServletContext; import jakarta.servlet.jsp.JspException; -import net.sargue.time.jsptags.FormatTag; /** * Basic format tests. diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/net/sargue/time/jsptags/ParseLocalDateTagTest.java similarity index 97% rename from src/test/java/ParseLocalDateTagTest.java rename to src/test/java/net/sargue/time/jsptags/ParseLocalDateTagTest.java index 7772c70..77d3abb 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/ParseLocalDateTagTest.java @@ -1,3 +1,5 @@ +package net.sargue.time.jsptags; + import java.io.UnsupportedEncodingException; import java.time.LocalDate; import java.util.Locale; @@ -9,7 +11,6 @@ import org.springframework.mock.web.MockServletContext; import jakarta.servlet.jsp.JspException; -import net.sargue.time.jsptags.ParseLocalDateTag; /** * Basic parse tests. From e3a9df26271c5e585d9512f4330aa4f9c1f95c05 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 09:02:24 -0600 Subject: [PATCH 22/48] Fix localTimeTest When specifying a full time, the timezone is now written in words. --- src/test/java/net/sargue/time/jsptags/FormatTagTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 494d503..40b925e 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -84,7 +84,7 @@ public void localTimeTest() throws IOException, JspException { assertEquals("10:53", format(localTime, null, "-S")); assertEquals("10:53:55", format(localTime, null, "-M")); assertEquals("10:53:55 CET", format(localTime, null, "-L")); - assertEquals("10:53:55 CET", format(localTime, null, "-F")); + assertEquals("10:53:55 (Hora del Centre d’Europa)", format(localTime, null, "-F")); } @Test From 4df667baaf0307ce4927e89aa7816272651339a0 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 09:05:28 -0600 Subject: [PATCH 23/48] Fix year test GGGG maps to the full form, which is using words --- src/test/java/net/sargue/time/jsptags/FormatTagTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 40b925e..60bdb5b 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -140,7 +140,7 @@ public void offsetTimeTest() throws IOException, JspException { @Test public void yearTest() throws IOException, JspException { Year year = Year.parse("2015"); - assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC AD", + assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC després de Crist", format(year, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG", null)); } From b6b454d77b4f9ca19dfcdf2c6fe6b4688ed12adb Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 09:06:34 -0600 Subject: [PATCH 24/48] Fix offsetTimeTest Full form of the timezone is words --- src/test/java/net/sargue/time/jsptags/FormatTagTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 60bdb5b..1e23712 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -134,7 +134,7 @@ public void offsetTimeTest() throws IOException, JspException { assertEquals("11:01", format(offsetTime, null, "-S")); assertEquals("11:01:39", format(offsetTime, null, "-M")); assertEquals("11:01:39 CET", format(offsetTime, null, "-L")); - assertEquals("11:01:39 CET", format(offsetTime, null, "-F")); + assertEquals("11:01:39 (Hora del Centre d’Europa)", format(offsetTime, null, "-F")); } @Test From 8f9c00fbadca927a192dc12920915a2e5c520a4e Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 14:15:50 -0600 Subject: [PATCH 25/48] Fix a number of tests - DateTimeFormatter does not pad fields with zero unless explicitly specified - full format uses words for month and timzone --- .../sargue/time/jsptags/FormatTagTest.java | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 1e23712..50cff12 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -91,15 +91,17 @@ public void localTimeTest() throws IOException, JspException { public void localDateTimeTest() throws IOException, JspException { LocalDateTime localDateTime = LocalDateTime.parse("2015-11-06T10:55:53.456"); assertEquals("06/11/2015 10:55:53", format(localDateTime, "dd/MM/yyyy HH:mm:ss", null)); - assertEquals("06/11/2015", format(localDateTime, null, null)); - assertEquals("06/11/15", format(localDateTime, null, "S-")); - assertEquals("06/11/2015", format(localDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", format(localDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", format(localDateTime, null, "F-")); + assertEquals("6/11/15", format(localDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(localDateTime, null, "M-")); + // check default matches medium + assertEquals(format(localDateTime, null, "M-"), format(localDateTime, null, null)); + + assertEquals("6 de novembre de 2015", format(localDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(localDateTime, null, "F-")); assertEquals("10:55", format(localDateTime, null, "-S")); assertEquals("10:55:53", format(localDateTime, null, "-M")); assertEquals("10:55:53 CET", format(localDateTime, null, "-L")); - assertEquals("10:55:53 CET", format(localDateTime, null, "-F")); + assertEquals("10:55:53 (Hora estàndard del Centre d’Europa)", format(localDateTime, null, "-F")); } @Test @@ -116,15 +118,17 @@ public void monthDayTest() throws IOException, JspException { @Test public void offsetDateTimeTest() throws IOException, JspException { OffsetDateTime offsetDateTime = OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); - assertEquals("06/11/2015", format(offsetDateTime, null, null)); - assertEquals("06/11/15", format(offsetDateTime, null, "S-")); - assertEquals("06/11/2015", format(offsetDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", format(offsetDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", format(offsetDateTime, null, "F-")); + assertEquals("6/11/15", format(offsetDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(offsetDateTime, null, "M-")); + // check that default matches medium + assertEquals(format(offsetDateTime, null, "M-"), format(offsetDateTime, null, null)); + + assertEquals("6 de novembre de 2015", format(offsetDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(offsetDateTime, null, "F-")); assertEquals("10:58", format(offsetDateTime, null, "-S")); assertEquals("10:58:21", format(offsetDateTime, null, "-M")); assertEquals("10:58:21 CET", format(offsetDateTime, null, "-L")); - assertEquals("10:58:21 CET", format(offsetDateTime, null, "-F")); + assertEquals("10:58:21 (Hora estàndard del Centre d’Europa)", format(offsetDateTime, null, "-F")); } @Test @@ -153,27 +157,29 @@ public void yearMonthTest() throws IOException, JspException { @Test public void zonedDateTime() throws IOException, JspException { ZonedDateTime zonedDateTime = ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); - assertEquals("06/11/2015", format(zonedDateTime, null, null)); - assertEquals("06/11/15", format(zonedDateTime, null, "S-")); - assertEquals("06/11/2015", format(zonedDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", format(zonedDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", format(zonedDateTime, null, "F-")); + assertEquals("6 de nov. 2015", format(zonedDateTime, null, null)); + assertEquals("6/11/15", format(zonedDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(zonedDateTime, null, "M-")); + assertEquals("6 de novembre de 2015", format(zonedDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(zonedDateTime, null, "F-")); assertEquals("11:04", format(zonedDateTime, null, "-S")); assertEquals("11:04:47", format(zonedDateTime, null, "-M")); assertEquals("11:04:47 CET", format(zonedDateTime, null, "-L")); - assertEquals("11:04:47 CET", format(zonedDateTime, null, "-F")); + assertEquals("11:04:47 (Hora estàndard del Centre d’Europa)", format(zonedDateTime, null, "-F")); ZonedDateTime pstZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); System.out.println(pstZonedDateTime); - assertEquals("06/11/2015", format(pstZonedDateTime, null, null)); - assertEquals("06/11/15", format(pstZonedDateTime, null, "S-")); - assertEquals("06/11/2015", format(pstZonedDateTime, null, "M-")); - assertEquals("6 / de novembre / 2015", format(pstZonedDateTime, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", format(pstZonedDateTime, null, "F-")); - assertEquals("02:04", format(pstZonedDateTime, null, "-S")); - assertEquals("02:04:47", format(pstZonedDateTime, null, "-M")); - assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-L")); - assertEquals("02:04:47 PST", format(pstZonedDateTime, null, "-F")); + assertEquals("6/11/15", format(pstZonedDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(pstZonedDateTime, null, "M-")); + // check that default matches medium + assertEquals(format(pstZonedDateTime, null, "M-"), format(pstZonedDateTime, null, null)); + + assertEquals("6 de novembre de 2015", format(pstZonedDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(pstZonedDateTime, null, "F-")); + assertEquals("2:04", format(pstZonedDateTime, null, "-S")); + assertEquals("2:04:47", format(pstZonedDateTime, null, "-M")); + assertEquals("2:04:47 PST", format(pstZonedDateTime, null, "-L")); + assertEquals("2:04:47 (Hora estàndard del Pacífic d’Amèrica del Nord)", format(pstZonedDateTime, null, "-F")); } private String format(Object o, String pattern, String style) throws JspException, IOException { From 48413ea1e9e6e4bef91defc97bdc719ac04f2f42 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 14:19:10 -0600 Subject: [PATCH 26/48] Rework how day of week is tested The challenge is that the 'e' format specifier may start on Sunday or Monday depending on the locale. The first day of the week has a value of 1, each day after that increments by 1. --- .../sargue/time/jsptags/FormatTagTest.java | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 50cff12..81aca49 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -17,6 +17,9 @@ import java.time.YearMonth; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.time.temporal.WeekFields; import java.util.Locale; import java.util.TimeZone; @@ -46,35 +49,57 @@ public void setup() throws UnsupportedEncodingException { @Test public void dayOfWeekTest() throws IOException, JspException { - assertEquals("dl. dl. dl. dilluns 1 01 dl. dilluns", + // find the first day of the week for the locale + final Locale l = Locale.getDefault(); + final DayOfWeek firstDayOfWeek = WeekFields.of(l).getFirstDayOfWeek(); + final LocalDate firstDayOfWeekLd = LocalDate.now().with(firstDayOfWeek); + + // Find the Monday after the first day of the week + LocalDate mondayLd = firstDayOfWeekLd.with(DayOfWeek.MONDAY); + if (mondayLd.isBefore(firstDayOfWeekLd)) { + mondayLd = mondayLd.plusDays(7); + } + + // the localized day of week (format specifier 'e') is 1 on the first day of the + // week and 2 on the second day of the week for the locale + final long mondayNumeric = firstDayOfWeekLd.until(mondayLd, ChronoUnit.DAYS) + 1; + assertEquals(String.format("dl. dl. dl. dilluns %1$d %1$02d dl. dilluns", mondayNumeric), format(DayOfWeek.MONDAY, "E EE EEE EEEE e ee eee eeee", null)); - assertEquals("dt. dt. dt. dimarts 2 02 dt. dimarts", + + final LocalDate tuesdayLd = mondayLd.plusDays(1); + final long tuesdayNumeric = firstDayOfWeekLd.until(tuesdayLd, ChronoUnit.DAYS) + 1; + assertEquals(String.format("dt. dt. dt. dimarts %1$d %1$02d dt. dimarts", tuesdayNumeric), format(DayOfWeek.TUESDAY, "E EE EEE EEEE e ee eee eeee", null)); } @Test public void instantTest() throws JspException, IOException { Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); - assertEquals("06/11/2015", format(instant, null, null)); - assertEquals("06/11/15", format(instant, null, "S-")); - assertEquals("06/11/2015", format(instant, null, "M-")); - assertEquals("6 / de novembre / 2015", format(instant, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", format(instant, null, "F-")); + assertEquals("6/11/15", format(instant, null, "S-")); + assertEquals("6 de nov. 2015", format(instant, null, "M-")); + // check default matches medium + assertEquals(format(instant, null, "M-"), format(instant, null, null)); + + assertEquals("6 de novembre de 2015", format(instant, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(instant, null, "F-")); assertEquals("10:45", format(instant, null, "-S")); assertEquals("10:45:33", format(instant, null, "-M")); assertEquals("10:45:33 CET", format(instant, null, "-L")); - assertEquals("10:45:33 CET", format(instant, null, "-F")); + assertEquals("10:45:33 (Hora estàndard del Centre d’Europa)", format(instant, null, "-F")); } @Test public void localDateTest() throws IOException, JspException { LocalDate localDate = LocalDate.parse("2015-11-06"); - assertEquals("06/11/2015", format(localDate, null, null)); assertEquals("06/11/2015", format(localDate, "dd/MM/yyyy", null)); - assertEquals("06/11/15", format(localDate, null, "S-")); - assertEquals("06/11/2015", format(localDate, null, "M-")); - assertEquals("6 / de novembre / 2015", format(localDate, null, "L-")); - assertEquals("divendres, 6 / de novembre / 2015", format(localDate, null, "F-")); + assertEquals("6/11/15", format(localDate, null, "S-")); + assertEquals("6 de nov. 2015", format(localDate, null, "M-")); + + // check that default matches medium + assertEquals(format(localDate, null, "M-"), format(localDate, null, null)); + + assertEquals("6 de novembre de 2015", format(localDate, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(localDate, null, "F-")); } @Test From 1a71d1cada5fe399471b2eda5d225ce60d9089c7 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 14:23:50 -0600 Subject: [PATCH 27/48] Don't need the special testCompile configuration Newer versions of gradle have solved this. --- build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.gradle b/build.gradle index 5c2ca8a..4575628 100644 --- a/build.gradle +++ b/build.gradle @@ -27,10 +27,6 @@ repositories { } } -configurations { - testCompile.extendsFrom compileOnly -} - dependencies { implementation(group: "jakarta.servlet", name: "jakarta.servlet-api", version: "5.0.0") implementation(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") From 836ebe06223fa58c6ce287b62be4e02105f578b8 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 14:24:23 -0600 Subject: [PATCH 28/48] Increment version to 2.0.0 The change to the jakarta package names makes this version incompatible with previous versions. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4575628..eb81514 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { group = 'net.sargue' archivesBaseName = 'java-time-jsptags' -version = '1.1.4' +version = '2.0.0' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' From 5daf5e1dc3b88ce8c0d5ae067832f3414e699186 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 22 Jan 2022 14:58:20 -0600 Subject: [PATCH 29/48] Cleanup - update readme with changelog entry - finalize variables and parameters - ensure that release calls super.release - add missing javadoc --- README.md | 17 +- build.gradle | 3 +- .../sargue/time/jsptags/FormatSupport.java | 253 ++++----- .../net/sargue/time/jsptags/FormatTag.java | 123 ++--- .../jsptags/JavaTimeTagLibraryValidator.java | 506 +++++++++--------- .../sargue/time/jsptags/ParseInstantTag.java | 14 +- .../time/jsptags/ParseLocalDateTag.java | 11 +- .../time/jsptags/ParseLocalDateTimeTag.java | 11 +- .../time/jsptags/ParseLocalTimeTag.java | 5 +- .../net/sargue/time/jsptags/ParseSupport.java | 405 +++++++------- .../net/sargue/time/jsptags/Resources.java | 304 +++++------ .../sargue/time/jsptags/SetZoneIdIdTag.java | 18 +- .../sargue/time/jsptags/SetZoneIdSupport.java | 113 ++-- .../java/net/sargue/time/jsptags/Util.java | 99 ++-- .../sargue/time/jsptags/ZoneIdSupport.java | 201 +++---- .../net/sargue/time/jsptags/ZoneIdTag.java | 14 +- .../sargue/time/jsptags/FormatTagTest.java | 1 - 17 files changed, 1074 insertions(+), 1024 deletions(-) diff --git a/README.md b/README.md index cd0143d..1564ff5 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ library and almost exactly as the tags in the original Joda-Time JSP Tags. Requirements ------------ -* Java 8 -* Servlet 2.4 -* JSP 2.0 -* JSTL 1.1 +* Java 17 +* Servlet 5.0 +* JSP 3.0 +* JSTL 2.0 Usage ----- @@ -40,7 +40,7 @@ Usage Add the dependency to your project: ### Gradle -`compile 'net.sargue:java-time-jsptags:1.1.4'` +`compile 'net.sargue:java-time-jsptags:2.0.0'` ### Maven @@ -48,7 +48,7 @@ Add the dependency to your project: net.sargue java-time-jsptags - 1.1.4 + 2.0.0 ``` @@ -265,6 +265,11 @@ Build is based on gradle. See build.gradle included in the repository. Changelog --------- +### v2.0.0 + +Updated for jakarta package names for J2EE classes. +Requires Java 17 now due to dependency on spring-test 6.0. + ### v1.1.4 Made helper method public [by request](https://github.com/sargue/java-time-jsptags/issues/7). diff --git a/build.gradle b/build.gradle index eb81514..9a1ee59 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,8 @@ plugins { id "com.jfrog.bintray" version "1.7.3" id "java" id "eclipse" - id "com.github.ben-manes.versions" version "0.41.0" // adds dependencyUpdates task + id "com.github.ben-manes.versions" version "0.41.0" // adds dependencyUpdates task + id "maven-publish" } group = 'net.sargue' diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index 37c2ace..a543e40 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -13,7 +13,7 @@ * 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 net.sargue.time.jsptags; import java.io.IOException; @@ -34,8 +34,8 @@ import jakarta.servlet.jsp.tagext.TagSupport; /** - * Support for tag handlers for <formatDate>, the date and time - * formatting tag in JSTL 1.0. + * Support for tag handlers for <formatDate>, the date and time formatting + * tag in JSTL 1.0. * * @author Jan Luehe * @author Jim Newsham @@ -43,125 +43,130 @@ */ public abstract class FormatSupport extends TagSupport { - /** The value attribute. */ - protected Object value; - /** The pattern attribute. */ - protected String pattern; - /** The style attribute. */ - protected String style; - /** The zoneId attribute. */ - protected ZoneId zoneId; - /** The locale attribute. */ - protected Locale locale; - /** The var attribute. */ - private String var; - /** The scope attribute. */ - private int scope; - - /** - * Constructor. - */ - public FormatSupport() { - super(); - init(); - } - - private void init() { - var = null; - value = null; - pattern = null; - style = null; - zoneId = null; - locale = null; - scope = PageContext.PAGE_SCOPE; - } - - @SuppressWarnings("UnusedDeclaration") - public void setVar(String var) { - this.var = var; - } - - @SuppressWarnings("UnusedDeclaration") - public void setScope(String scope) { - this.scope = Util.getScope(scope); - } - - /* - * Formats the given instant or partial. - */ - public int doEndTag() throws JspException { - if (value == null) { - if (var != null) { - pageContext.removeAttribute(var, scope); - } - return EVAL_PAGE; - } - - // Create formatter - DateTimeFormatter formatter; - if (pattern != null) { - formatter = DateTimeFormatter.ofPattern(pattern); - } else if (style != null) { - formatter = Util.createFormatterForStyle(style); - } else { - // use a medium date (no time) style by default; same as jstl - formatter = Util.createFormatterForStyle("M-"); - } - - // set formatter locale - Locale locale = this.locale; - if (locale == null) { - locale = Util.getFormattingLocale(pageContext, true, - DateFormat.getAvailableLocales()); - } - if (locale != null) { - formatter = formatter.withLocale(locale); - } - - // set formatter timezone - ZoneId zoneId = this.zoneId; - if (zoneId == null) { - zoneId = ZoneIdSupport.getZoneId(pageContext, this); - } - if (zoneId != null) { - formatter = formatter.withZone(zoneId); - } else { - if (value instanceof Instant || - value instanceof LocalDateTime || - value instanceof OffsetDateTime || - value instanceof OffsetTime || - value instanceof LocalTime) - // these time objects may need a zone to resolve some patterns - // and/or styles, and as there is no zone we revert to the - // system default zone - formatter = formatter.withZone(ZoneId.systemDefault()); - } - - // format value - String formatted; - if (value instanceof TemporalAccessor) { - formatted = formatter.format((TemporalAccessor) value); - } else { - throw new JspException( - "value attribute of format tag must be a TemporalAccessor," + - " was: " + value.getClass().getName()); - } - - if (var != null) { - pageContext.setAttribute(var, formatted, scope); - } else { - try { - pageContext.getOut().print(formatted); - } catch (IOException ioe) { - throw new JspTagException(ioe.toString(), ioe); - } - } - - return EVAL_PAGE; - } - - // Releases any resources we may have (or inherit) - public void release() { - init(); - } + private static final long serialVersionUID = 1L; + + /** The value attribute. */ + protected Object value; + /** The pattern attribute. */ + protected String pattern; + /** The style attribute. */ + protected String style; + /** The zoneId attribute. */ + protected ZoneId zoneId; + /** The locale attribute. */ + protected Locale locale; + /** The var attribute. */ + private String var; + /** The scope attribute. */ + private int scope; + + /** + * Constructor. + */ + public FormatSupport() { + super(); + init(); + } + + private void init() { + var = null; + value = null; + pattern = null; + style = null; + zoneId = null; + locale = null; + scope = PageContext.PAGE_SCOPE; + } + + /** + * + * @param var the variable to store the result in + */ + public void setVar(final String var) { + this.var = var; + } + + /** + * + * @param scope the scope to put the variable in + * @see #setVar(String) + */ + public void setScope(final String scope) { + this.scope = Util.getScope(scope); + } + + /** + * Formats the given instant or partial. + */ + public int doEndTag() throws JspException { + if (value == null) { + if (var != null) { + pageContext.removeAttribute(var, scope); + } + return EVAL_PAGE; + } + + // Create formatter + DateTimeFormatter formatter; + if (pattern != null) { + formatter = DateTimeFormatter.ofPattern(pattern); + } else if (style != null) { + formatter = Util.createFormatterForStyle(style); + } else { + // use a medium date (no time) style by default; same as jstl + formatter = Util.createFormatterForStyle("M-"); + } + + // set formatter locale + Locale locale = this.locale; + if (locale == null) { + locale = Util.getFormattingLocale(pageContext, true, DateFormat.getAvailableLocales()); + } + if (locale != null) { + formatter = formatter.withLocale(locale); + } + + // set formatter timezone + ZoneId zoneId = this.zoneId; + if (zoneId == null) { + zoneId = ZoneIdSupport.getZoneId(pageContext, this); + } + if (zoneId != null) { + formatter = formatter.withZone(zoneId); + } else { + if (value instanceof Instant || value instanceof LocalDateTime || value instanceof OffsetDateTime + || value instanceof OffsetTime || value instanceof LocalTime) + // these time objects may need a zone to resolve some patterns + // and/or styles, and as there is no zone we revert to the + // system default zone + formatter = formatter.withZone(ZoneId.systemDefault()); + } + + // format value + String formatted = null; + if (value instanceof TemporalAccessor) { + formatted = formatter.format((TemporalAccessor) value); + } else { + throw new JspException("value attribute of format tag must be a TemporalAccessor," + " was: " + + value.getClass().getName()); + } + + if (var != null) { + pageContext.setAttribute(var, formatted, scope); + } else { + try { + pageContext.getOut().print(formatted); + } catch (IOException ioe) { + throw new JspTagException(ioe.toString(), ioe); + } + } + + return EVAL_PAGE; + } + + @Override + public void release() { + init(); + super.release(); + } } diff --git a/src/main/java/net/sargue/time/jsptags/FormatTag.java b/src/main/java/net/sargue/time/jsptags/FormatTag.java index ae01a8c..f1e4fd3 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatTag.java +++ b/src/main/java/net/sargue/time/jsptags/FormatTag.java @@ -30,72 +30,73 @@ * @author Jim Newsham * @author Sergi Baila */ -@SuppressWarnings("UnusedDeclaration") public class FormatTag extends FormatSupport { - /** - * Sets the value attribute. - * - * @param value the value - */ - public void setValue(Object value) { - this.value = value; - } + private static final long serialVersionUID = 1L; - /** - * Sets the style attribute. - * - * @param style the style - */ - public void setStyle(String style) { - this.style = style; - } + /** + * Sets the value attribute. + * + * @param value the value + */ + public void setValue(final Object value) { + this.value = value; + } - /** - * Sets the pattern attribute. - * - * @param pattern the pattern - */ - public void setPattern(String pattern) { - this.pattern = pattern; - } + /** + * Sets the style attribute. + * + * @param style the style + */ + public void setStyle(final String style) { + this.style = style; + } - /** - * Sets the zone attribute. - * - * @param dtz the zone - * @throws JspTagException incorrect zone or dtz parameter - */ - public void setZoneId(Object dtz) throws JspTagException { - if (dtz == null || (dtz instanceof String && ((String) dtz).isEmpty())) { - this.zoneId = null; - } else if (dtz instanceof ZoneId) { - this.zoneId = (ZoneId) dtz; - } else if (dtz instanceof String) { - try { - this.zoneId = ZoneId.of((String) dtz); - } catch (IllegalArgumentException iae) { - throw new JspTagException("Incorrect Zone: " + dtz); - } - } else - throw new JspTagException("Can only accept ZoneId or String objects."); - } + /** + * Sets the pattern attribute. + * + * @param pattern the pattern + */ + public void setPattern(final String pattern) { + this.pattern = pattern; + } - /** - * Sets the style attribute. - * - * @param loc the locale - * @throws JspTagException parameter not a Locale or String - */ - public void setLocale(Object loc) throws JspTagException { - if (loc == null) { - this.locale = null; - } else if (loc instanceof Locale) { - this.locale = (Locale) loc; - } else if (loc instanceof String) { - this.locale = Util.parseLocale((String) loc); - } else - throw new JspTagException("Can only accept Locale or String objects."); - } + /** + * Sets the zone attribute. + * + * @param dtz the zone + * @throws JspTagException incorrect zone or dtz parameter + */ + public void setZoneId(final Object dtz) throws JspTagException { + if (dtz == null || (dtz instanceof String && ((String) dtz).isEmpty())) { + this.zoneId = null; + } else if (dtz instanceof ZoneId) { + this.zoneId = (ZoneId) dtz; + } else if (dtz instanceof String) { + try { + this.zoneId = ZoneId.of((String) dtz); + } catch (final IllegalArgumentException iae) { + throw new JspTagException("Incorrect Zone: " + dtz); + } + } else + throw new JspTagException("Can only accept ZoneId or String objects."); + } + + /** + * Sets the style attribute. + * + * @param loc the locale + * @throws JspTagException parameter not a Locale or String + */ + public void setLocale(final Object loc) throws JspTagException { + if (loc == null) { + this.locale = null; + } else if (loc instanceof Locale) { + this.locale = (Locale) loc; + } else if (loc instanceof String) { + this.locale = Util.parseLocale((String) loc); + } else + throw new JspTagException("Can only accept Locale or String objects."); + } } diff --git a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java index 2d52452..149cc39 100644 --- a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java +++ b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java @@ -34,8 +34,8 @@ /** *

- * A SAX-based TagLibraryValidator for the java.time tags. Currently implements the - * following checks: + * A SAX-based TagLibraryValidator for the java.time tags. Currently implements + * the following checks: *

* *
    @@ -53,292 +53,298 @@ */ public class JavaTimeTagLibraryValidator extends TagLibraryValidator { - /* - * Expression syntax validation has been disabled since when I ported this - * code over from Jakarta Taglib, I wanted to reduce dependencies. As I - * understand it, JSP 2.0 containers take over the responsibility of - * handling EL code (both in attribute tags, and externally), so this - * shouldn't be a problem unless you're using something old. If you want to - * restore this validation, you must uncomment the various lines in this - * source, include the Jakarta Taglib's standard.jar library at build and - * runtime, and (I believe, but don't know specifically) make a legacy-style - * tld which describes which attributes should be validated. Have a look at - * fmt.tld, fmt-1.0.tld, fmt-1.0-rt.tld in standard.jar for an example of - * this. - */ + /* + * Expression syntax validation has been disabled since when I ported this code + * over from Jakarta Taglib, I wanted to reduce dependencies. As I understand + * it, JSP 2.0 containers take over the responsibility of handling EL code (both + * in attribute tags, and externally), so this shouldn't be a problem unless + * you're using something old. If you want to restore this validation, you must + * uncomment the various lines in this source, include the Jakarta Taglib's + * standard.jar library at build and runtime, and (I believe, but don't know + * specifically) make a legacy-style tld which describes which attributes should + * be validated. Have a look at fmt.tld, fmt-1.0.tld, fmt-1.0-rt.tld in + * standard.jar for an example of this. + */ - // ********************************************************************* - // Implementation Overview - /* - * We essentially just run the page through a SAX parser, handling the - * callbacks that interest us. We collapse elements into the text - * they contain, since this simplifies processing somewhat. Even a quick - * glance at the implementation shows its necessary, tree-oriented nature: - * multiple Stacks, an understanding of 'depth', and so on all are important - * as we recover necessary state upon each callback. This TLV demonstrates - * various techniques, from the general "how do I use a SAX parser for a - * TLV?" to "how do I read my init parameters and then validate?" But also, - * the specific SAX methodology was kept as general as possible to allow for - * experimentation and flexibility. - */ + // ********************************************************************* + // Implementation Overview + /* + * We essentially just run the page through a SAX parser, handling the callbacks + * that interest us. We collapse elements into the text they contain, + * since this simplifies processing somewhat. Even a quick glance at the + * implementation shows its necessary, tree-oriented nature: multiple Stacks, an + * understanding of 'depth', and so on all are important as we recover necessary + * state upon each callback. This TLV demonstrates various techniques, from the + * general "how do I use a SAX parser for a TLV?" to "how do I read my init + * parameters and then validate?" But also, the specific SAX methodology was + * kept as general as possible to allow for experimentation and flexibility. + */ - // ********************************************************************* - // Constants - // tag names - private static final String SET_ZONEID = "setZoneId"; + // ********************************************************************* + // Constants + // tag names + private static final String SET_ZONEID = "setZoneId"; - private static final String PARSE_INSTANT = "parseInstant"; + private static final String PARSE_INSTANT = "parseInstant"; - private static final String JSP_TEXT = "jsp:text"; + private static final String JSP_TEXT = "jsp:text"; - // attribute names - private static final String VALUE = "value"; + // attribute names + private static final String VALUE = "value"; - // parameter names - // private final String EXP_ATT_PARAM = "expressionAttributes"; + // parameter names + // private final String EXP_ATT_PARAM = "expressionAttributes"; - // attributes - private static final String VAR = "var"; + // attributes + private static final String VAR = "var"; - private static final String SCOPE = "scope"; + private static final String SCOPE = "scope"; - // scopes - private static final String PAGE_SCOPE = "page"; + // scopes + private static final String PAGE_SCOPE = "page"; - private static final String REQUEST_SCOPE = "request"; + private static final String REQUEST_SCOPE = "request"; - private static final String SESSION_SCOPE = "session"; + private static final String SESSION_SCOPE = "session"; - private static final String APPLICATION_SCOPE = "application"; + private static final String APPLICATION_SCOPE = "application"; - // ********************************************************************* - // Validation and configuration state (protected) + // ********************************************************************* + // Validation and configuration state (protected) - private String uri; // our taglib's uri (as passed by JSP container on XML - // View) + private String uri; // our taglib's uri (as passed by JSP container on XML + // View) - private String prefix; // our taglib's prefix + private String prefix; // our taglib's prefix - private List validationMessages; // temporary error messages + private List validationMessages; // temporary error messages // private Map config; // configuration (Map of Sets) // // private boolean failed; // have we failed >0 times? - private String lastElementId; // the last element we've seen + private String lastElementId; // the last element we've seen - // ********************************************************************* - // Constructor and lifecycle management + // ********************************************************************* + // Constructor and lifecycle management - public JavaTimeTagLibraryValidator() { - init(); - } + /** + * Constructor. + */ + public JavaTimeTagLibraryValidator() { + init(); + } - private void init() { - validationMessages = null; - prefix = null; + private void init() { + validationMessages = null; + prefix = null; // config = null; - } - - public void release() { - super.release(); - init(); - } - - public synchronized ValidationMessage[] validate(String prefix, String uri, - PageData page) { - try { - this.uri = uri; - // initialize - validationMessages = new ArrayList<>(); - - // save the prefix - this.prefix = prefix; - - DefaultHandler h = new Handler(); - - // parse the page - SAXParserFactory f = SAXParserFactory.newInstance(); - f.setValidating(false); - f.setNamespaceAware(true); - SAXParser p = f.newSAXParser(); - p.parse(page.getInputStream(), h); - - if (validationMessages.size() == 0) { - return null; - } else { - return validationMessages.toArray(new ValidationMessage[validationMessages.size()]); - } - } catch (SAXException ex) { - return vmFromString(ex.toString()); - } catch (ParserConfigurationException ex) { - return vmFromString(ex.toString()); - } catch (IOException ex) { - return vmFromString(ex.toString()); - } - } - - // utility methods to help us match elements in our tagset - private boolean isTag(String tagUri, String tagLn, String matchUri, - String matchLn) { - if (tagUri == null || tagLn == null || matchUri == null - || matchLn == null) { - return false; - } - // match beginning of URI since some suffix *_rt tags can - // be nested in EL enabled tags as defined by the spec - if (tagUri.length() > matchUri.length()) { - return (tagUri.startsWith(matchUri) && tagLn.equals(matchLn)); - } else { - return (matchUri.startsWith(tagUri) && tagLn.equals(matchLn)); - } - } + } + + public void release() { + super.release(); + init(); + } + + public synchronized ValidationMessage[] validate(final String prefix, final String uri, final PageData page) { + try { + this.uri = uri; + // initialize + validationMessages = new ArrayList<>(); + + // save the prefix + this.prefix = prefix; + + DefaultHandler h = new Handler(); + + // parse the page + SAXParserFactory f = SAXParserFactory.newInstance(); + f.setValidating(false); + f.setNamespaceAware(true); + SAXParser p = f.newSAXParser(); + p.parse(page.getInputStream(), h); + + if (validationMessages.size() == 0) { + return null; + } else { + return validationMessages.toArray(new ValidationMessage[validationMessages.size()]); + } + } catch (SAXException ex) { + return vmFromString(ex.toString()); + } catch (ParserConfigurationException ex) { + return vmFromString(ex.toString()); + } catch (IOException ex) { + return vmFromString(ex.toString()); + } + } + + // utility methods to help us match elements in our tagset + private boolean isTag(final String tagUri, final String tagLn, final String matchUri, final String matchLn) { + if (tagUri == null || tagLn == null || matchUri == null || matchLn == null) { + return false; + } + // match beginning of URI since some suffix *_rt tags can + // be nested in EL enabled tags as defined by the spec + if (tagUri.length() > matchUri.length()) { + return (tagUri.startsWith(matchUri) && tagLn.equals(matchLn)); + } else { + return (matchUri.startsWith(tagUri) && tagLn.equals(matchLn)); + } + } // private boolean isJspTag(String tagUri, String tagLn, String target) { // return isTag(tagUri, tagLn, JSP, target); // } - private boolean isJavaTimeTag(String tagUri, String tagLn, String target) { - return isTag(tagUri, tagLn, this.uri, target); - } + private boolean isJavaTimeTag(final String tagUri, final String tagLn, final String target) { + return isTag(tagUri, tagLn, this.uri, target); + } - // utility method to determine if an attribute exists - private boolean hasAttribute(Attributes a, String att) { - return (a.getValue(att) != null); - } + // utility method to determine if an attribute exists + private boolean hasAttribute(final Attributes a, final String att) { + return (a.getValue(att) != null); + } - /* - * method to assist with failure [ as if it's not easy enough already :-) ] - */ - private void fail(String message) { + /* + * method to assist with failure [ as if it's not easy enough already :-) ] + */ + private void fail(final String message) { // failed = true; - validationMessages.add(new ValidationMessage(lastElementId, message)); - } + validationMessages.add(new ValidationMessage(lastElementId, message)); + } // // returns true if the given attribute name is specified, false otherwise // private boolean isSpecified(TagData data, String attributeName) { // return (data.getAttribute(attributeName) != null); // } - // returns true if the 'scope' attribute is valid - protected boolean hasNoInvalidScope(Attributes a) { - String scope = a.getValue(SCOPE); - return !((scope != null) && !scope.equals(PAGE_SCOPE) - && !scope.equals(REQUEST_SCOPE) && !scope.equals(SESSION_SCOPE) - && !scope.equals(APPLICATION_SCOPE)); - } - - // returns true if the 'var' attribute is empty - protected boolean hasEmptyVar(Attributes a) { - return "".equals(a.getValue(VAR)); - } - - // returns true if the 'scope' attribute is present without 'var' - protected boolean hasDanglingScope(Attributes a) { - return (a.getValue(SCOPE) != null && a.getValue(VAR) == null); - } - - // retrieves the local part of a QName - protected String getLocalPart(String qname) { - int colon = qname.indexOf(":"); - return (colon == -1) ? qname : qname.substring(colon + 1); - } - - // constructs a ValidationMessage[] from a single String and no ID - private static ValidationMessage[] vmFromString(String message) { - return new ValidationMessage[] { new ValidationMessage(null, message) }; - } - - /** - * SAX event handler. - */ - private class Handler extends DefaultHandler { - - private String lastElementName = null; - - private boolean bodyNecessary = false; - - private boolean bodyIllegal = false; - - // process under the existing context (state), then modify it - public void startElement(String ns, String ln, String qn, Attributes a) { - // substitute our own parsed 'ln' if it's not provided - if (ln == null) { - ln = getLocalPart(qn); - } - - // for simplicity, we can ignore for our purposes - // (don't bother distinguishing between it and its characters) - if (qn.equals(JSP_TEXT)) { - return; - } - - // check body-related constraint - if (bodyIllegal) { - fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); - } - - // validate attributes - if (qn.startsWith(prefix + ":") && !hasNoInvalidScope(a)) { - fail(Resources.getMessage("TLV_INVALID_ATTRIBUTE", SCOPE, qn, a - .getValue(SCOPE))); - } - if (qn.startsWith(prefix + ":") && hasEmptyVar(a)) { - fail(Resources.getMessage("TLV_EMPTY_VAR", qn)); - } - if (qn.startsWith(prefix + ":") - && !isJavaTimeTag(ns, ln, SET_ZONEID) - && hasDanglingScope(a)) { - fail(Resources.getMessage("TLV_DANGLING_SCOPE", qn)); - } - - // now, modify state - - // set up a check against illegal attribute/body combinations - bodyIllegal = false; - bodyNecessary = false; - if (isJavaTimeTag(ns, ln, PARSE_INSTANT)) { - if (hasAttribute(a, VALUE)) { - bodyIllegal = true; - } else { - bodyNecessary = true; - } - } - - // record the most recent tag (for error reporting) - lastElementName = qn; - lastElementId = a.getValue("http://java.sun.com/JSP/Page", "id"); - - // we're a new element, so increase depth - } - - public void characters(char[] ch, int start, int length) { - bodyNecessary = false; // body is no longer necessary! - - // ignore strings that are just whitespace - String s = new String(ch, start, length).trim(); - if (s.equals("")) { - return; - } - - // check and update body-related constraints - if (bodyIllegal) { - fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); - } - } - - public void endElement(String ns, String ln, String qn) { - // consistently, we ignore JSP_TEXT - if (qn.equals(JSP_TEXT)) { - return; - } - - // handle body-related invariant - if (bodyNecessary) { - fail(Resources.getMessage("TLV_MISSING_BODY", lastElementName)); - } - bodyIllegal = false; // reset: we've left the tag - } - } + /** + * @param a used to get the 'scope' attribute + * @return true if the 'scope' attribute is valid + */ + protected boolean hasNoInvalidScope(final Attributes a) { + final String scope = a.getValue(SCOPE); + return !((scope != null) && !scope.equals(PAGE_SCOPE) && !scope.equals(REQUEST_SCOPE) + && !scope.equals(SESSION_SCOPE) && !scope.equals(APPLICATION_SCOPE)); + } + + /** + * @param a used to get the 'var' attribute + * @return true if the 'var' attribute is empty + */ + protected boolean hasEmptyVar(final Attributes a) { + return "".equals(a.getValue(VAR)); + } + + /** + * @param a used to get the 'scope' attribute + * @return true if the 'scope' attribute is present without 'var' + */ + protected boolean hasDanglingScope(final Attributes a) { + return (a.getValue(SCOPE) != null && a.getValue(VAR) == null); + } + + /** + * @param qname the QName to get the local part from + * @return local part of a QName + */ + protected static String getLocalPart(final String qname) { + final int colon = qname.indexOf(":"); + return (colon == -1) ? qname : qname.substring(colon + 1); + } + + // constructs a ValidationMessage[] from a single String and no ID + private static ValidationMessage[] vmFromString(final String message) { + return new ValidationMessage[] { new ValidationMessage(null, message) }; + } + + /** + * SAX event handler. + */ + private class Handler extends DefaultHandler { + + private String lastElementName = null; + + private boolean bodyNecessary = false; + + private boolean bodyIllegal = false; + + // process under the existing context (state), then modify it + public void startElement(final String ns, String ln, final String qn, final Attributes a) { + // substitute our own parsed 'ln' if it's not provided + if (ln == null) { + ln = getLocalPart(qn); + } + + // for simplicity, we can ignore for our purposes + // (don't bother distinguishing between it and its characters) + if (qn.equals(JSP_TEXT)) { + return; + } + + // check body-related constraint + if (bodyIllegal) { + fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); + } + + // validate attributes + if (qn.startsWith(prefix + ":") && !hasNoInvalidScope(a)) { + fail(Resources.getMessage("TLV_INVALID_ATTRIBUTE", SCOPE, qn, a.getValue(SCOPE))); + } + if (qn.startsWith(prefix + ":") && hasEmptyVar(a)) { + fail(Resources.getMessage("TLV_EMPTY_VAR", qn)); + } + if (qn.startsWith(prefix + ":") && !isJavaTimeTag(ns, ln, SET_ZONEID) && hasDanglingScope(a)) { + fail(Resources.getMessage("TLV_DANGLING_SCOPE", qn)); + } + + // now, modify state + + // set up a check against illegal attribute/body combinations + bodyIllegal = false; + bodyNecessary = false; + if (isJavaTimeTag(ns, ln, PARSE_INSTANT)) { + if (hasAttribute(a, VALUE)) { + bodyIllegal = true; + } else { + bodyNecessary = true; + } + } + + // record the most recent tag (for error reporting) + lastElementName = qn; + lastElementId = a.getValue("http://java.sun.com/JSP/Page", "id"); + + // we're a new element, so increase depth + } + + public void characters(final char[] ch, final int start, final int length) { + bodyNecessary = false; // body is no longer necessary! + + // ignore strings that are just whitespace + final String s = new String(ch, start, length).trim(); + if (s.equals("")) { + return; + } + + // check and update body-related constraints + if (bodyIllegal) { + fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); + } + } + + public void endElement(final String ns, final String ln, final String qn) { + // consistently, we ignore JSP_TEXT + if (qn.equals(JSP_TEXT)) { + return; + } + + // handle body-related invariant + if (bodyNecessary) { + fail(Resources.getMessage("TLV_MISSING_BODY", lastElementName)); + } + bodyIllegal = false; // reset: we've left the tag + } + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java b/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java index 5903617..6b8d829 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java @@ -22,7 +22,8 @@ /** *

    - * A handler for <parseInstant> that supports rtexprvalue-based attributes. + * A handler for <parseInstant> that supports rtexprvalue-based + * attributes. *

    * * @author Jan Luehe @@ -31,8 +32,11 @@ */ public class ParseInstantTag extends ParseSupport { - @Override - protected TemporalQuery temporalQuery() { - return Instant::from; - } + + private static final long serialVersionUID = 1L; + + @Override + protected TemporalQuery temporalQuery() { + return Instant::from; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java index 32fbdaa..d1807cd 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java @@ -32,8 +32,11 @@ */ public class ParseLocalDateTag extends ParseSupport { - @Override - protected TemporalQuery temporalQuery() { - return LocalDate::from; - } + + private static final long serialVersionUID = 1L; + + @Override + protected TemporalQuery temporalQuery() { + return LocalDate::from; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java index 0852d6b..280b32d 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java @@ -32,8 +32,11 @@ */ public class ParseLocalDateTimeTag extends ParseSupport { - @Override - protected TemporalQuery temporalQuery() { - return LocalDateTime::from; - } + + private static final long serialVersionUID = 1L; + + @Override + protected TemporalQuery temporalQuery() { + return LocalDateTime::from; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java index 1c7befd..bf6e855 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java @@ -32,7 +32,10 @@ */ public class ParseLocalTimeTag extends ParseSupport { - @Override + + private static final long serialVersionUID = 1L; + + @Override protected TemporalQuery temporalQuery() { return LocalTime::from; } diff --git a/src/main/java/net/sargue/time/jsptags/ParseSupport.java b/src/main/java/net/sargue/time/jsptags/ParseSupport.java index 9faf2b3..ac4e344 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ParseSupport.java @@ -39,204 +39,211 @@ */ public abstract class ParseSupport extends BodyTagSupport { - /** The value attribute. */ - protected String value; - /** Status of the value. */ - protected boolean valueSpecified; - /** The pattern attribute. */ - protected String pattern; - /** The style attribute. */ - protected String style; - /** The zone attribute. */ - protected ZoneId zoneId; - /** The locale attribute. */ - protected Locale locale; - /** The var attribute. */ - private String var; - /** The scope attribute. */ - private int scope; - - /** - * Constructor. - */ - public ParseSupport() { - super(); - init(); - } - - private void init() { - value = null; - valueSpecified = false; - pattern = null; - style = null; - zoneId = null; - locale = null; - scope = PageContext.PAGE_SCOPE; - } - - @SuppressWarnings("UnusedDeclaration") - public void setVar(String var) { - this.var = var; - } - - @SuppressWarnings("UnusedDeclaration") - public void setScope(String scope) { - this.scope = Util.getScope(scope); - } - - /** - * Sets the value attribute. - * - * @param value the value - */ - public void setValue(String value) { - this.value = value; - this.valueSpecified = true; - } - - /** - * Sets the style attribute. - * - * @param style the style - */ - @SuppressWarnings("UnusedDeclaration") - public void setStyle(String style) { - this.style = style; - } - - /** - * Sets the pattern attribute. - * - * @param pattern the pattern - */ - public void setPattern(String pattern) { - this.pattern = pattern; - } - - /** - * Sets the zone attribute. - * - * @param dtz the zone - * @throws JspTagException incorrect zone or zone parameter - */ - @SuppressWarnings("UnusedDeclaration") - public void setZoneId(Object dtz) throws JspTagException { - if (dtz == null) - this.zoneId = null; - else if (dtz instanceof ZoneId) - this.zoneId = (ZoneId) dtz; - else if (dtz instanceof String) - try { - String sZone = (String) dtz; - this.zoneId = sZone.isEmpty() ? null : ZoneId.of(sZone); - } catch (IllegalArgumentException iae) { - throw new JspTagException("Incorrect Zone: " + dtz); - } - else - throw new JspTagException("Can only accept ZoneId or String objects."); - } - - /** - * Sets the style attribute. - * - * @param loc the locale - * @throws JspTagException parameter not a Locale or String - */ - @SuppressWarnings("UnusedDeclaration") - public void setLocale(Object loc) throws JspTagException { - if (loc == null) { - this.locale = null; - } else if (loc instanceof Locale) { - this.locale = (Locale) loc; - } else if (loc instanceof String) { - locale = Util.parseLocale((String) loc); - } else - throw new JspTagException("Can only accept Locale or String objects."); - } - - public int doEndTag() throws JspException { - String input = null; - - // determine the input by... - if (valueSpecified) { - // ... reading 'value' attribute - input = value; - } else { - // ... retrieving and trimming our body - if (bodyContent != null && bodyContent.getString() != null) { - input = bodyContent.getString().trim(); - } - } - - if ((input == null) || input.equals("")) { - if (var != null) { - pageContext.removeAttribute(var, scope); - } - return EVAL_PAGE; - } - - // Create formatter - DateTimeFormatter formatter; - if (pattern != null) { - formatter = DateTimeFormatter.ofPattern(pattern); - } else if (style != null) { - formatter = Util.createFormatterForStyle(style); - } else { - formatter = Util.createFormatterForStyle("FF"); - } - - // set formatter locale - Locale locale = this.locale; - if (locale == null) { - locale = Util.getFormattingLocale(pageContext, true, - DateFormat.getAvailableLocales()); - } - if (locale != null) { - formatter = formatter.withLocale(locale); - } - - // set formatter timezone - ZoneId tz = this.zoneId; - if (tz == null) { - tz = ZoneIdSupport.getZoneId(pageContext, this); - } - if (tz != null) { - formatter = formatter.withZone(tz); - } - - // Parse date - TemporalAccessor parsed; - try { - parsed = formatter.parse(input, temporalQuery()); - } catch (DateTimeParseException e) { - throw new JspException(Resources.getMessage( - "PARSE_DATE_PARSE_ERROR", input), e); - } - - if (var != null) { - pageContext.setAttribute(var, parsed, scope); - } else { - try { - pageContext.getOut().print(parsed); - } catch (IOException ioe) { - throw new JspTagException(ioe.toString(), ioe); - } - } - - return EVAL_PAGE; - } - - /** - * Abstract method to define the query used to format the input with - * each specific tag. - * - * @return the temporal query used to parse the input - */ - protected abstract TemporalQuery temporalQuery(); - - // Releases any resources we may have (or inherit) - public void release() { - init(); - } + private static final long serialVersionUID = 1L; + + /** The value attribute. */ + protected String value; + /** Status of the value. */ + protected boolean valueSpecified; + /** The pattern attribute. */ + protected String pattern; + /** The style attribute. */ + protected String style; + /** The zone attribute. */ + protected ZoneId zoneId; + /** The locale attribute. */ + protected Locale locale; + /** The var attribute. */ + private String var; + /** The scope attribute. */ + private int scope; + + /** + * Constructor. + */ + public ParseSupport() { + super(); + init(); + } + + private void init() { + value = null; + valueSpecified = false; + pattern = null; + style = null; + zoneId = null; + locale = null; + scope = PageContext.PAGE_SCOPE; + } + + /** + * + * @param var the variable to store the result in + */ + public void setVar(final String var) { + this.var = var; + } + + /** + * @param scope the scope to store the variable in + * @see #setVar(String) + */ + public void setScope(final String scope) { + this.scope = Util.getScope(scope); + } + + /** + * Sets the value attribute. + * + * @param value the value + */ + public void setValue(final String value) { + this.value = value; + this.valueSpecified = true; + } + + /** + * Sets the style attribute. + * + * @param style the style + */ + public void setStyle(final String style) { + this.style = style; + } + + /** + * Sets the pattern attribute. + * + * @param pattern the pattern + */ + public void setPattern(final String pattern) { + this.pattern = pattern; + } + + /** + * Sets the zone attribute. + * + * @param dtz the zone + * @throws JspTagException incorrect zone or zone parameter + */ + public void setZoneId(final Object dtz) throws JspTagException { + if (dtz == null) { + this.zoneId = null; + } else if (dtz instanceof ZoneId) { + this.zoneId = (ZoneId) dtz; + } else if (dtz instanceof String) { + try { + final String sZone = (String) dtz; + this.zoneId = sZone.isEmpty() ? null : ZoneId.of(sZone); + } catch (final IllegalArgumentException iae) { + throw new JspTagException("Incorrect Zone: " + dtz); + } + } else { + throw new JspTagException("Can only accept ZoneId or String objects."); + } + } + + /** + * Sets the style attribute. + * + * @param loc the locale + * @throws JspTagException parameter not a Locale or String + */ + public void setLocale(final Object loc) throws JspTagException { + if (loc == null) { + this.locale = null; + } else if (loc instanceof Locale) { + this.locale = (Locale) loc; + } else if (loc instanceof String) { + locale = Util.parseLocale((String) loc); + } else { + throw new JspTagException("Can only accept Locale or String objects."); + } + } + + @Override + public int doEndTag() throws JspException { + String input = null; + + // determine the input by... + if (valueSpecified) { + // ... reading 'value' attribute + input = value; + } else { + // ... retrieving and trimming our body + if (bodyContent != null && bodyContent.getString() != null) { + input = bodyContent.getString().trim(); + } + } + + if ((input == null) || input.equals("")) { + if (var != null) { + pageContext.removeAttribute(var, scope); + } + return EVAL_PAGE; + } + + // Create formatter + DateTimeFormatter formatter; + if (pattern != null) { + formatter = DateTimeFormatter.ofPattern(pattern); + } else if (style != null) { + formatter = Util.createFormatterForStyle(style); + } else { + formatter = Util.createFormatterForStyle("FF"); + } + + // set formatter locale + Locale locale = this.locale; + if (locale == null) { + locale = Util.getFormattingLocale(pageContext, true, DateFormat.getAvailableLocales()); + } + if (locale != null) { + formatter = formatter.withLocale(locale); + } + + // set formatter timezone + ZoneId tz = this.zoneId; + if (tz == null) { + tz = ZoneIdSupport.getZoneId(pageContext, this); + } + if (tz != null) { + formatter = formatter.withZone(tz); + } + + // Parse date + TemporalAccessor parsed = null; + try { + parsed = formatter.parse(input, temporalQuery()); + } catch (final DateTimeParseException e) { + throw new JspException(Resources.getMessage("PARSE_DATE_PARSE_ERROR", input), e); + } + + if (var != null) { + pageContext.setAttribute(var, parsed, scope); + } else { + try { + pageContext.getOut().print(parsed); + } catch (final IOException ioe) { + throw new JspTagException(ioe.toString(), ioe); + } + } + + return EVAL_PAGE; + } + + /** + * Abstract method to define the query used to format the input with each + * specific tag. + * + * @return the temporal query used to parse the input + */ + protected abstract TemporalQuery temporalQuery(); + + @Override + public void release() { + init(); + super.release(); + } } diff --git a/src/main/java/net/sargue/time/jsptags/Resources.java b/src/main/java/net/sargue/time/jsptags/Resources.java index 776d5af..4756a76 100644 --- a/src/main/java/net/sargue/time/jsptags/Resources.java +++ b/src/main/java/net/sargue/time/jsptags/Resources.java @@ -13,7 +13,7 @@ * 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 net.sargue.time.jsptags; import java.text.MessageFormat; @@ -21,175 +21,161 @@ import java.util.ResourceBundle; /** - *

    Provides locale-neutral access to string resources. Only the - * documentation and code are in English. :-) + *

    + * Provides locale-neutral access to string resources. Only the documentation + * and code are in English. :-) * - *

    The major goal, aside from globalization, is convenience. - * Access to resources with no parameters is made in the form:

    + *

    + * The major goal, aside from globalization, is convenience. Access to resources + * with no parameters is made in the form: + *

    + * *
    - *     Resources.getMessage(MESSAGE_NAME);
    + * Resources.getMessage(MESSAGE_NAME);
      * 
    * - *

    Access to resources with one parameter works like

    + *

    + * Access to resources with one parameter works like + *

    + * *
    - *     Resources.getMessage(MESSAGE_NAME, arg1);
    + * Resources.getMessage(MESSAGE_NAME, arg1);
      * 
    * - *

    ... and so on.

    + *

    + * ... and so on. + *

    * * @author Shawn Bayern */ -@SuppressWarnings("UnusedDeclaration") public class Resources { - //********************************************************************* - // Static data - - /** The location of our resources. */ - private static final String RESOURCE_LOCATION = "net.sargue.time.jsptags.Resources"; - - /** Our class-wide ResourceBundle. */ - private static ResourceBundle rb = ResourceBundle.getBundle(RESOURCE_LOCATION); - - - //********************************************************************* - // Public static methods - - /** - * Retrieves a message with no arguments. - * - * @param name the resource name - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name) - throws MissingResourceException { - return rb.getString(name); - } - - /** - * Retrieves a message with arbitrarily many arguments. - * - * @param name the resource name - * @param a the a - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, Object[] a) - throws MissingResourceException { - String res = rb.getString(name); - return MessageFormat.format(res, a); - } - - /** - * Retrieves a message with one argument. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, Object a1) - throws MissingResourceException { - return getMessage(name, new Object[] { a1 }); - } - - /** - * Retrieves a message with two arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, Object a1, Object a2) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2 }); - } - - /** - * Retrieves a message with three arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, - Object a1, - Object a2, - Object a3) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3 }); - } - - /** - * Retrieves a message with four arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @param a4 the parameter number 4 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, - Object a1, - Object a2, - Object a3, - Object a4) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3, a4 }); - } - - /** - * Retrieves a message with five arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @param a4 the parameter number 4 - * @param a5 the parameter number 5 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, - Object a1, - Object a2, - Object a3, - Object a4, - Object a5) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3, a4, a5 }); - } - - /** - * Retrieves a message with six arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @param a4 the parameter number 4 - * @param a5 the parameter number 5 - * @param a6 the parameter number 6 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(String name, - Object a1, - Object a2, - Object a3, - Object a4, - Object a5, - Object a6) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3, a4, a5, a6 }); - } + // ********************************************************************* + // Static data + + /** The location of our resources. */ + private static final String RESOURCE_LOCATION = "net.sargue.time.jsptags.Resources"; + + /** Our class-wide ResourceBundle. */ + private static ResourceBundle rb = ResourceBundle.getBundle(RESOURCE_LOCATION); + + // ********************************************************************* + // Public static methods + + /** + * Retrieves a message with no arguments. + * + * @param name the resource name + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name) throws MissingResourceException { + return rb.getString(name); + } + + /** + * Retrieves a message with arbitrarily many arguments. + * + * @param name the resource name + * @param a the a + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object[] a) throws MissingResourceException { + final String res = rb.getString(name); + return MessageFormat.format(res, a); + } + + /** + * Retrieves a message with one argument. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object a1) throws MissingResourceException { + return getMessage(name, new Object[] { a1 }); + } + + /** + * Retrieves a message with two arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object a1, final Object a2) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2 }); + } + + /** + * Retrieves a message with three arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object a1, final Object a2, final Object a3) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3 }); + } + + /** + * Retrieves a message with four arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @param a4 the parameter number 4 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object a1, final Object a2, final Object a3, + final Object a4) throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3, a4 }); + } + + /** + * Retrieves a message with five arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @param a4 the parameter number 4 + * @param a5 the parameter number 5 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object a1, final Object a2, final Object a3, + final Object a4, final Object a5) throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3, a4, a5 }); + } + + /** + * Retrieves a message with six arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @param a4 the parameter number 4 + * @param a5 the parameter number 5 + * @param a6 the parameter number 6 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(final String name, final Object a1, final Object a2, final Object a3, + final Object a4, final Object a5, final Object a6) throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3, a4, a5, a6 }); + } } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java index cbfc46f..96a169a 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java @@ -27,13 +27,15 @@ */ public class SetZoneIdIdTag extends SetZoneIdSupport { - /** - * Sets the value attribute. - * - * @param value the value - */ - public void setValue(Object value) { - this.value = value; - } + private static final long serialVersionUID = 1L; + + /** + * Sets the value attribute. + * + * @param value the value + */ + public void setValue(final Object value) { + this.value = value; + } } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java index 0f072fd..107af3a 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java @@ -33,65 +33,74 @@ */ public abstract class SetZoneIdSupport extends TagSupport { - /** The value attribute. */ - protected Object value; - /** The scope attribute. */ - private int scope; - /** The var attribute. */ - private String var; + private static final long serialVersionUID = 1L; - /** - * Constructor. - */ - public SetZoneIdSupport() { - super(); - init(); - } + /** The value attribute. */ + protected Object value; + /** The scope attribute. */ + private int scope; + /** The var attribute. */ + private String var; - // resets local state - private void init() { - value = null; - var = null; - scope = PageContext.PAGE_SCOPE; - } + /** + * Constructor. + */ + public SetZoneIdSupport() { + super(); + init(); + } - @SuppressWarnings("UnusedDeclaration") - public void setScope(String scope) { - this.scope = Util.getScope(scope); - } + // resets local state + private void init() { + value = null; + var = null; + scope = PageContext.PAGE_SCOPE; + } - @SuppressWarnings("UnusedDeclaration") - public void setVar(String var) { - this.var = var; - } + /** + * + * @param scope the scope to store the variable in + * @see #setVar(String) + */ + public void setScope(final String scope) { + this.scope = Util.getScope(scope); + } - public int doEndTag() throws JspException { - ZoneId dateTimeZone; - if (value == null) { - dateTimeZone = ZoneOffset.UTC; - } else if (value instanceof String) { - try { - dateTimeZone = ZoneId.of((String) value); - } catch (IllegalArgumentException iae) { - dateTimeZone = ZoneOffset.UTC; - } - } else { - dateTimeZone = (ZoneId) value; - } + /** + * + * @param var the variable to store the result in + */ + public void setVar(final String var) { + this.var = var; + } - if (var != null) { - pageContext.setAttribute(var, dateTimeZone, scope); - } else { - Config.set(pageContext, ZoneIdSupport.FMT_TIME_ZONE, - dateTimeZone, scope); - } + public int doEndTag() throws JspException { + ZoneId dateTimeZone; + if (value == null) { + dateTimeZone = ZoneOffset.UTC; + } else if (value instanceof String) { + try { + dateTimeZone = ZoneId.of((String) value); + } catch (IllegalArgumentException iae) { + dateTimeZone = ZoneOffset.UTC; + } + } else { + dateTimeZone = (ZoneId) value; + } - return EVAL_PAGE; - } + if (var != null) { + pageContext.setAttribute(var, dateTimeZone, scope); + } else { + Config.set(pageContext, ZoneIdSupport.FMT_TIME_ZONE, dateTimeZone, scope); + } - // Releases any resources we may have (or inherit) - public void release() { - init(); - } + return EVAL_PAGE; + } + + @Override + public void release() { + init(); + super.release(); + } } diff --git a/src/main/java/net/sargue/time/jsptags/Util.java b/src/main/java/net/sargue/time/jsptags/Util.java index 542ac40..0125fbc 100644 --- a/src/main/java/net/sargue/time/jsptags/Util.java +++ b/src/main/java/net/sargue/time/jsptags/Util.java @@ -27,6 +27,7 @@ import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Locale; @@ -77,8 +78,8 @@ public class Util { * * @return PageContext constant corresponding to given scope description */ - public static int getScope(String scope) { - int ret = PageContext.PAGE_SCOPE; // default + public static int getScope(final String scope) { + final int ret; if (REQUEST.equalsIgnoreCase(scope)) { ret = PageContext.REQUEST_SCOPE; @@ -86,6 +87,9 @@ public static int getScope(String scope) { ret = PageContext.SESSION_SCOPE; } else if (APPLICATION.equalsIgnoreCase(scope)) { ret = PageContext.APPLICATION_SCOPE; + } else { + // default; + ret = PageContext.PAGE_SCOPE; } return ret; } @@ -101,8 +105,8 @@ public static int getScope(String scope) { * @return the locales from the request or an empty enumeration if no preferred * locale has been specified */ - public static Enumeration getRequestLocales(HttpServletRequest request) { - Enumeration values = request.getHeaders("accept-language"); + public static Enumeration getRequestLocales(final HttpServletRequest request) { + final Enumeration values = request.getHeaders("accept-language"); if (values.hasMoreElements()) { // At least one "accept-language". Simply return // the enumeration returned by request.getLocales(). @@ -112,7 +116,7 @@ public static Enumeration getRequestLocales(HttpServletRequest request) { // No header for "accept-language". Simply return // the empty enumeration. // System.out.println("No accept-language"); - return values; + return Collections.emptyEnumeration(); } } @@ -123,7 +127,7 @@ public static Enumeration getRequestLocales(HttpServletRequest request) { * @return {@link java.util.Locale} object corresponding to the given locale * string, or the null if the locale string is null or empty */ - public static Locale parseLocale(String locale) { + public static Locale parseLocale(final String locale) { return parseLocale(locale, null); } @@ -142,8 +146,8 @@ public static Locale parseLocale(String locale) { * @throws IllegalArgumentException if the given locale does not have a language * component or has an empty country component */ - public static Locale parseLocale(String locale, String variant) { - Locale ret; + public static Locale parseLocale(final String locale, final String variant) { + final Locale ret; String language = locale; String country = null; int index; @@ -193,9 +197,9 @@ public static Locale parseLocale(String locale, String variant) { * locale * @param locale the response locale */ - static void setResponseLocale(PageContext pc, Locale locale) { + static void setResponseLocale(final PageContext pc, final Locale locale) { // set response locale - ServletResponse response = pc.getResponse(); + final ServletResponse response = pc.getResponse(); response.setLocale(locale); // get response character encoding and store it in session attribute @@ -223,7 +227,7 @@ static void setResponseLocale(PageContext pc, Locale locale) { * * @return the formatting locale to use */ - static Locale getFormattingLocale(PageContext pc, boolean format, Locale[] avail) { + static Locale getFormattingLocale(final PageContext pc, final boolean format, final Locale[] avail) { LocalizationContext locCtxt; @@ -271,12 +275,14 @@ static Locale getFormattingLocale(PageContext pc, boolean format, Locale[] avail */ static Locale[] availableFormattingLocales; static { - Locale[] dateLocales = DateFormat.getAvailableLocales(); - Set numberLocales = new HashSet<>(Arrays.asList(NumberFormat.getAvailableLocales())); - ArrayList locales = new ArrayList<>(); - for (Locale dateLocale : dateLocales) - if (numberLocales.contains(dateLocale)) + final Locale[] dateLocales = DateFormat.getAvailableLocales(); + final Set numberLocales = new HashSet<>(Arrays.asList(NumberFormat.getAvailableLocales())); + final ArrayList locales = new ArrayList<>(); + for (Locale dateLocale : dateLocales) { + if (numberLocales.contains(dateLocale)) { locales.add(dateLocale); + } + } availableFormattingLocales = new Locale[locales.size()]; availableFormattingLocales = locales.toArray(availableFormattingLocales); } @@ -299,16 +305,18 @@ static Locale getFormattingLocale(PageContext pc, boolean format, Locale[] avail * configuration parameter, or {@code null} if no scoped attribute or * configuration parameter with the given name exists */ - static Locale getLocale(PageContext pageContext, String name) { - Locale loc = null; + static Locale getLocale(final PageContext pageContext, final String name) { + final Locale loc; - Object obj = Config.find(pageContext, name); + final Object obj = Config.find(pageContext, name); if (obj != null) { if (obj instanceof Locale) { loc = (Locale) obj; } else { loc = parseLocale((String) obj); } + } else { + loc = null; } return loc; @@ -327,11 +335,11 @@ static Locale getLocale(PageContext pageContext, String name) { * * @return Best matching locale, or {@code null} if no match was found */ - private static Locale findFormattingMatch(PageContext pageContext, Locale[] avail) { + private static Locale findFormattingMatch(final PageContext pageContext, final Locale[] avail) { Locale match = null; - for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ + for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ .hasMoreElements();) { - Locale locale = (Locale) enum_.nextElement(); + final Locale locale = enum_.nextElement(); match = findFormattingMatch(locale, avail); if (match != null) { break; @@ -358,7 +366,7 @@ private static Locale findFormattingMatch(PageContext pageContext, Locale[] avai * @return Available locale that best matches the given preferred locale, or * {@code null} if no match exists */ - private static Locale findFormattingMatch(Locale pref, Locale[] avail) { + private static Locale findFormattingMatch(final Locale pref, final Locale[] avail) { Locale match = null; boolean langAndCountryMatch = false; for (Locale locale : avail) { @@ -389,10 +397,10 @@ private static Locale findFormattingMatch(Locale pref, Locale[] avail) { * @param pc Page in which to look up the default I18N localization context * @return the localization context */ - public static LocalizationContext getLocalizationContext(PageContext pc) { - LocalizationContext locCtxt; + public static LocalizationContext getLocalizationContext(final PageContext pc) { + final LocalizationContext locCtxt; - Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT); + final Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT); if (obj == null) { return null; } @@ -413,16 +421,14 @@ public static LocalizationContext getLocalizationContext(PageContext pc) { * * Check if a match exists between the ordered set of preferred locales and the * available locales, for the given base name. The set of preferred locales - * consists of a single locale (if the - * {@link Config#FMT_LOCALE} configuration setting is present) - * or is equal to the client's preferred locales determined from the client's - * browser settings. + * consists of a single locale (if the {@link Config#FMT_LOCALE} configuration + * setting is present) or is equal to the client's preferred locales determined + * from the client's browser settings. * *

    * If no match was found in the previous step, check if a match exists between - * the fallback locale (given by the - * {@link Config#FMT_FALLBACK_LOCALE} configuration setting) and - * the available locales, for the given base name. + * the fallback locale (given by the {@link Config#FMT_FALLBACK_LOCALE} + * configuration setting) and the available locales, for the given base name. * * @param pc Page in which the resource bundle with the given base name is * requested @@ -432,7 +438,7 @@ public static LocalizationContext getLocalizationContext(PageContext pc) { * base name and the locale that led to the resource bundle match, or * the empty localization context if no resource bundle match was found */ - public static LocalizationContext getLocalizationContext(PageContext pc, String basename) { + public static LocalizationContext getLocalizationContext(final PageContext pc, final String basename) { LocalizationContext locCtxt = null; ResourceBundle bundle; @@ -503,13 +509,13 @@ public static LocalizationContext getLocalizationContext(PageContext pc, String * given base name and best matching locale, or {@code null} if no * resource bundle match was found */ - private static LocalizationContext findMatch(PageContext pageContext, String basename) { + private static LocalizationContext findMatch(final PageContext pageContext, final String basename) { LocalizationContext locCtxt = null; // Determine locale from client's browser settings. - for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ + for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ .hasMoreElements();) { - Locale pref = (Locale) enum_.nextElement(); + Locale pref = enum_.nextElement(); ResourceBundle match = findMatch(basename, pref); if (match != null) { locCtxt = new LocalizationContext(match, pref); @@ -535,7 +541,7 @@ private static LocalizationContext findMatch(PageContext pageContext, String bas * language-match between the preferred locale and the locale of the * bundle returned by java.util.ResourceBundle.getBundle(). */ - private static ResourceBundle findMatch(String basename, Locale pref) { + private static ResourceBundle findMatch(final String basename, final Locale pref) { ResourceBundle match = null; try { @@ -594,7 +600,7 @@ private static ResourceBundle findMatch(String basename, Locale pref) { * @throws JspException if the style is invalid * @return a formatter for the specified style */ - public static DateTimeFormatter createFormatterForStyle(String style) throws JspException { + public static DateTimeFormatter createFormatterForStyle(final String style) throws JspException { if (style == null || style.length() != 2) { throw new JspException("Invalid style specification: " + style); } @@ -613,16 +619,17 @@ public static DateTimeFormatter createFormatterForStyle(String style) throws Jsp * @param timeStyle the time style * @return the formatter */ - private static DateTimeFormatter createFormatterForStyleIndex(FormatStyle dateStyle, FormatStyle timeStyle) - throws JspException { - if (dateStyle == null && timeStyle == null) + private static DateTimeFormatter createFormatterForStyleIndex(final FormatStyle dateStyle, + final FormatStyle timeStyle) throws JspException { + if (dateStyle == null && timeStyle == null) { throw new JspException("Both styles cannot be null."); - else if (dateStyle != null && timeStyle != null) + } else if (dateStyle != null && timeStyle != null) { return DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); - else if (dateStyle == null) + } else if (dateStyle == null) { return DateTimeFormatter.ofLocalizedTime(timeStyle); - else + } else { return DateTimeFormatter.ofLocalizedDate(dateStyle); + } } /** @@ -631,7 +638,7 @@ else if (dateStyle == null) * @param ch the one character style code * @return the FormatStyle */ - private static FormatStyle selectStyle(char ch) throws JspException { + private static FormatStyle selectStyle(final char ch) throws JspException { switch (ch) { case 'S': return SHORT; diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java index b5712d1..2bac5e5 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java @@ -36,102 +36,109 @@ */ public abstract class ZoneIdSupport extends BodyTagSupport { - /** The config key for the time zone. */ - public static final String FMT_TIME_ZONE = "net.sargue.time.zoneId"; - - /** The value attribute. */ - protected Object value; - - /** The zone. */ - private ZoneId zoneId; - - /** - * Constructor. - */ - public ZoneIdSupport() { - super(); - init(); - } - - private void init() { - value = null; - } - - public ZoneId getZoneId() { - return zoneId; - } - - public int doStartTag() throws JspException { - if (value == null) { - zoneId = ZoneOffset.UTC; - } else if (value instanceof String) { - try { - zoneId = ZoneId.of((String) value); - } catch (IllegalArgumentException iae) { - zoneId = ZoneOffset.UTC; - } - } else { - zoneId = (ZoneId) value; - } - return EVAL_BODY_BUFFERED; - } - - public int doEndTag() throws JspException { - try { - pageContext.getOut().print(bodyContent.getString()); - } catch (IOException ioe) { - throw new JspTagException(ioe.toString(), ioe); - } - return EVAL_PAGE; - } - - // Releases any resources we may have (or inherit) - public void release() { - init(); - } - - /** - * Determines and returns the time zone to be used by the given action. - *

    - * If the given action is nested inside a <zoneId> action, - * the time zone is taken from the enclosing <zoneId> action. - *

    - * Otherwise, the time zone configuration setting - * net.sargue.time.jsptags.ZoneIdSupport.FMT_TIME_ZONE is used. - * - * @param pc the page containing the action for which the time zone - * needs to be determined - * @param fromTag the action for which the time zone needs to be determined - * - * @return the time zone, or null if the given action is not - * nested inside a <zoneId> action and no time zone configuration - * setting exists - */ - static ZoneId getZoneId(PageContext pc, Tag fromTag) { - ZoneId tz = null; - - Tag t = findAncestorWithClass(fromTag, ZoneIdSupport.class); - if (t != null) { - // use time zone from parent tag - ZoneIdSupport parent = (ZoneIdSupport) t; - tz = parent.getZoneId(); - } else { - // get time zone from configuration setting - Object obj = Config.find(pc, FMT_TIME_ZONE); - if (obj != null) { - if (obj instanceof ZoneId) { - tz = (ZoneId) obj; - } else { - try { - tz = ZoneId.of((String) obj); - } catch (IllegalArgumentException iae) { - tz = ZoneOffset.UTC; - } - } - } - } - - return tz; - } + private static final long serialVersionUID = 1L; + + /** The config key for the time zone. */ + public static final String FMT_TIME_ZONE = "net.sargue.time.zoneId"; + + /** The value attribute. */ + protected Object value; + + /** The zone. */ + private ZoneId zoneId; + + /** + * Constructor. + */ + public ZoneIdSupport() { + super(); + init(); + } + + private void init() { + value = null; + } + + /** + * + * @return the zone + */ + public ZoneId getZoneId() { + return zoneId; + } + + public int doStartTag() throws JspException { + if (value == null) { + zoneId = ZoneOffset.UTC; + } else if (value instanceof String) { + try { + zoneId = ZoneId.of((String) value); + } catch (IllegalArgumentException iae) { + zoneId = ZoneOffset.UTC; + } + } else { + zoneId = (ZoneId) value; + } + return EVAL_BODY_BUFFERED; + } + + public int doEndTag() throws JspException { + try { + pageContext.getOut().print(bodyContent.getString()); + } catch (IOException ioe) { + throw new JspTagException(ioe.toString(), ioe); + } + return EVAL_PAGE; + } + + @Override + public void release() { + init(); + super.release(); + } + + /** + * Determines and returns the time zone to be used by the given action. + *

    + * If the given action is nested inside a <zoneId> action, the time zone + * is taken from the enclosing <zoneId> action. + *

    + * Otherwise, the time zone configuration setting + * net.sargue.time.jsptags.ZoneIdSupport.FMT_TIME_ZONE is used. + * + * @param pc the page containing the action for which the time zone needs + * to be determined + * @param fromTag the action for which the time zone needs to be determined + * + * @return the time zone, or null if the given action is not nested + * inside a <zoneId> action and no time zone configuration setting + * exists + */ + static ZoneId getZoneId(final PageContext pc, final Tag fromTag) { + ZoneId tz = null; + + final Tag t = findAncestorWithClass(fromTag, ZoneIdSupport.class); + if (t != null) { + // use time zone from parent tag + final ZoneIdSupport parent = (ZoneIdSupport) t; + tz = parent.getZoneId(); + } else { + // get time zone from configuration setting + final Object obj = Config.find(pc, FMT_TIME_ZONE); + if (obj != null) { + if (obj instanceof ZoneId) { + tz = (ZoneId) obj; + } else { + try { + tz = ZoneId.of((String) obj); + } catch (IllegalArgumentException iae) { + tz = ZoneOffset.UTC; + } + } + } + } + + return tz; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java index 03e8404..96ebb12 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java @@ -16,8 +16,6 @@ */ package net.sargue.time.jsptags; -import jakarta.servlet.jsp.JspTagException; - /** * A handler for <zoneId>. * @@ -26,9 +24,13 @@ */ public class ZoneIdTag extends ZoneIdSupport { - // for tag attribute - public void setValue(Object value) throws JspTagException { - this.value = value; - } + private static final long serialVersionUID = 1L; + + /** + * @param value for tag attribute + */ + public void setValue(final Object value) { + this.value = value; + } } diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java index 81aca49..28f46f3 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/net/sargue/time/jsptags/FormatTagTest.java @@ -18,7 +18,6 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalUnit; import java.time.temporal.WeekFields; import java.util.Locale; import java.util.TimeZone; From 224701aa7a58d5492b1fe960f84279e623d8b2b0 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Tue, 8 Feb 2022 20:01:50 -0600 Subject: [PATCH 30/48] Remove IDE files --- .gitignore | 1 + .project | 17 ----------------- 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 .project diff --git a/.gitignore b/.gitignore index 5a959fb..efb840a 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ crashlytics-build.properties # eclipse bin/ .classpath +.project \ No newline at end of file diff --git a/.project b/.project deleted file mode 100644 index 5ca4635..0000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - java-time-jsptags - - - - org.eclipse.jdt.core.javanature - - - - org.eclipse.jdt.core.javabuilder - - - - - - From df014ec8537a8f4bcacc911ff459b5be11a754ba Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Tue, 8 Feb 2022 20:11:30 -0600 Subject: [PATCH 31/48] Remove eclipse integration This also removes the reference to ben-manes.versions. --- build.gradle | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/build.gradle b/build.gradle index 9a1ee59..759adae 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,6 @@ plugins { id "com.jfrog.bintray" version "1.7.3" id "java" - id "eclipse" - id "com.github.ben-manes.versions" version "0.41.0" // adds dependencyUpdates task id "maven-publish" } @@ -110,15 +108,3 @@ bintray { } } } - -eclipse { - classpath { - file { - // remove entries added due to issue with testsets - // https://github.com/unbroken-dome/gradle-testsets-plugin/issues/77 - whenMerged { - entries.removeAll{it.kind == "lib" && (it.path.endsWith("build/classes/java/test") || it.path.endsWith("build/resources/test"))} - } - } - } -} \ No newline at end of file From c69251b7ee08f95d7717f517e80072746d6a02b4 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 9 Feb 2022 08:34:22 +0100 Subject: [PATCH 32/48] add .gitattributes file to set line endings for whole project --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto From 101a91358b63a330e00fb8b3beab923246f20d5b Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 18 Feb 2022 19:09:08 -0600 Subject: [PATCH 33/48] Revert formatting changes --- .../sargue/time/jsptags/FormatSupport.java | 45 +- .../net/sargue/time/jsptags/FormatTag.java | 19 +- .../jsptags/JavaTimeTagLibraryValidator.java | 519 ++++--- .../sargue/time/jsptags/ParseInstantTag.java | 3 +- .../time/jsptags/ParseLocalDateTimeTag.java | 1 + .../time/jsptags/ParseLocalTimeTag.java | 1 + .../net/sargue/time/jsptags/ParseSupport.java | 417 +++--- .../net/sargue/time/jsptags/Resources.java | 275 ++-- .../sargue/time/jsptags/SetZoneIdIdTag.java | 2 +- .../sargue/time/jsptags/SetZoneIdSupport.java | 17 +- .../java/net/sargue/time/jsptags/Util.java | 1242 +++++++++-------- .../sargue/time/jsptags/ZoneIdSupport.java | 209 ++- .../net/sargue/time/jsptags/ZoneIdTag.java | 6 +- .../time/jsptags => }/FormatTagTest.java | 30 +- .../jsptags => }/ParseLocalDateTagTest.java | 12 +- 15 files changed, 1392 insertions(+), 1406 deletions(-) rename src/test/java/{net/sargue/time/jsptags => }/FormatTagTest.java (95%) rename src/test/java/{net/sargue/time/jsptags => }/ParseLocalDateTagTest.java (93%) diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index a543e40..e9a7c21 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -18,12 +18,7 @@ import java.io.IOException; import java.text.DateFormat; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.ZoneId; +import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.Locale; @@ -34,8 +29,8 @@ import jakarta.servlet.jsp.tagext.TagSupport; /** - * Support for tag handlers for <formatDate>, the date and time formatting - * tag in JSTL 1.0. + * Support for tag handlers for <formatDate>, the date and time + * formatting tag in JSTL 1.0. * * @author Jan Luehe * @author Jim Newsham @@ -78,20 +73,13 @@ private void init() { scope = PageContext.PAGE_SCOPE; } - /** - * - * @param var the variable to store the result in - */ - public void setVar(final String var) { + @SuppressWarnings("UnusedDeclaration") + public void setVar(String var) { this.var = var; } - /** - * - * @param scope the scope to put the variable in - * @see #setVar(String) - */ - public void setScope(final String scope) { + @SuppressWarnings("UnusedDeclaration") + public void setScope(String scope) { this.scope = Util.getScope(scope); } @@ -120,7 +108,8 @@ public int doEndTag() throws JspException { // set formatter locale Locale locale = this.locale; if (locale == null) { - locale = Util.getFormattingLocale(pageContext, true, DateFormat.getAvailableLocales()); + locale = Util.getFormattingLocale(pageContext, true, + DateFormat.getAvailableLocales()); } if (locale != null) { formatter = formatter.withLocale(locale); @@ -134,8 +123,11 @@ public int doEndTag() throws JspException { if (zoneId != null) { formatter = formatter.withZone(zoneId); } else { - if (value instanceof Instant || value instanceof LocalDateTime || value instanceof OffsetDateTime - || value instanceof OffsetTime || value instanceof LocalTime) + if (value instanceof Instant || + value instanceof LocalDateTime || + value instanceof OffsetDateTime || + value instanceof OffsetTime || + value instanceof LocalTime) // these time objects may need a zone to resolve some patterns // and/or styles, and as there is no zone we revert to the // system default zone @@ -143,12 +135,13 @@ public int doEndTag() throws JspException { } // format value - String formatted = null; + String formatted; if (value instanceof TemporalAccessor) { formatted = formatter.format((TemporalAccessor) value); } else { - throw new JspException("value attribute of format tag must be a TemporalAccessor," + " was: " - + value.getClass().getName()); + throw new JspException( + "value attribute of format tag must be a TemporalAccessor," + + " was: " + value.getClass().getName()); } if (var != null) { @@ -164,7 +157,7 @@ public int doEndTag() throws JspException { return EVAL_PAGE; } - @Override + // Releases any resources we may have (or inherit) public void release() { init(); super.release(); diff --git a/src/main/java/net/sargue/time/jsptags/FormatTag.java b/src/main/java/net/sargue/time/jsptags/FormatTag.java index f1e4fd3..7b83a74 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatTag.java +++ b/src/main/java/net/sargue/time/jsptags/FormatTag.java @@ -16,11 +16,11 @@ */ package net.sargue.time.jsptags; +import jakarta.servlet.jsp.JspTagException; + import java.time.ZoneId; import java.util.Locale; -import jakarta.servlet.jsp.JspTagException; - /** *

    * A handler for <format> that supports rtexprvalue-based attributes. @@ -30,7 +30,8 @@ * @author Jim Newsham * @author Sergi Baila */ -public class FormatTag extends FormatSupport { + @SuppressWarnings("UnusedDeclaration") + public class FormatTag extends FormatSupport { private static final long serialVersionUID = 1L; @@ -39,7 +40,7 @@ public class FormatTag extends FormatSupport { * * @param value the value */ - public void setValue(final Object value) { + public void setValue(Object value) { this.value = value; } @@ -48,7 +49,7 @@ public void setValue(final Object value) { * * @param style the style */ - public void setStyle(final String style) { + public void setStyle(String style) { this.style = style; } @@ -57,7 +58,7 @@ public void setStyle(final String style) { * * @param pattern the pattern */ - public void setPattern(final String pattern) { + public void setPattern(String pattern) { this.pattern = pattern; } @@ -67,7 +68,7 @@ public void setPattern(final String pattern) { * @param dtz the zone * @throws JspTagException incorrect zone or dtz parameter */ - public void setZoneId(final Object dtz) throws JspTagException { + public void setZoneId(Object dtz) throws JspTagException { if (dtz == null || (dtz instanceof String && ((String) dtz).isEmpty())) { this.zoneId = null; } else if (dtz instanceof ZoneId) { @@ -75,7 +76,7 @@ public void setZoneId(final Object dtz) throws JspTagException { } else if (dtz instanceof String) { try { this.zoneId = ZoneId.of((String) dtz); - } catch (final IllegalArgumentException iae) { + } catch (IllegalArgumentException iae) { throw new JspTagException("Incorrect Zone: " + dtz); } } else @@ -88,7 +89,7 @@ public void setZoneId(final Object dtz) throws JspTagException { * @param loc the locale * @throws JspTagException parameter not a Locale or String */ - public void setLocale(final Object loc) throws JspTagException { + public void setLocale(Object loc) throws JspTagException { if (loc == null) { this.locale = null; } else if (loc instanceof Locale) { diff --git a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java index 149cc39..314ff2e 100644 --- a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java +++ b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java @@ -16,14 +16,6 @@ */ package net.sargue.time.jsptags; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; @@ -31,11 +23,16 @@ import jakarta.servlet.jsp.tagext.PageData; import jakarta.servlet.jsp.tagext.TagLibraryValidator; import jakarta.servlet.jsp.tagext.ValidationMessage; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.IOException; +import java.util.*; /** *

    - * A SAX-based TagLibraryValidator for the java.time tags. Currently implements - * the following checks: + * A SAX-based TagLibraryValidator for the java.time tags. Currently implements the + * following checks: *

    * *
      @@ -53,298 +50,292 @@ */ public class JavaTimeTagLibraryValidator extends TagLibraryValidator { - /* - * Expression syntax validation has been disabled since when I ported this code - * over from Jakarta Taglib, I wanted to reduce dependencies. As I understand - * it, JSP 2.0 containers take over the responsibility of handling EL code (both - * in attribute tags, and externally), so this shouldn't be a problem unless - * you're using something old. If you want to restore this validation, you must - * uncomment the various lines in this source, include the Jakarta Taglib's - * standard.jar library at build and runtime, and (I believe, but don't know - * specifically) make a legacy-style tld which describes which attributes should - * be validated. Have a look at fmt.tld, fmt-1.0.tld, fmt-1.0-rt.tld in - * standard.jar for an example of this. - */ + /* + * Expression syntax validation has been disabled since when I ported this + * code over from Jakarta Taglib, I wanted to reduce dependencies. As I + * understand it, JSP 2.0 containers take over the responsibility + * of handling EL code (both in attribute tags, and externally), so this + * shouldn't be a problem unless you're using something old. If you want to + * restore this validation, you must uncomment the various lines in this + * source, include the Jakarta Taglib's standard.jar library at build and + * runtime, and (I believe, but don't know specifically) make a legacy-style + * tld which describes which attributes should be validated. Have a look at + * fmt.tld, fmt-1.0.tld, fmt-1.0-rt.tld in standard.jar for an example of + * this. + */ - // ********************************************************************* - // Implementation Overview - /* - * We essentially just run the page through a SAX parser, handling the callbacks - * that interest us. We collapse elements into the text they contain, - * since this simplifies processing somewhat. Even a quick glance at the - * implementation shows its necessary, tree-oriented nature: multiple Stacks, an - * understanding of 'depth', and so on all are important as we recover necessary - * state upon each callback. This TLV demonstrates various techniques, from the - * general "how do I use a SAX parser for a TLV?" to "how do I read my init - * parameters and then validate?" But also, the specific SAX methodology was - * kept as general as possible to allow for experimentation and flexibility. - */ + // ********************************************************************* + // Implementation Overview + /* + * We essentially just run the page through a SAX parser, handling the + * callbacks that interest us. We collapse elements into the text + * they contain, since this simplifies processing somewhat. Even a quick + * glance at the implementation shows its necessary, tree-oriented nature: + * multiple Stacks, an understanding of 'depth', and so on all are important + * as we recover necessary state upon each callback. This TLV demonstrates + * various techniques, from the general "how do I use a SAX parser for a + * various techniques, from the parameters and then validate?" But also, + * the specific SAX methodology was kept as general as possible to allow for + * experimentation and flexibility. + */ - // ********************************************************************* - // Constants - // tag names - private static final String SET_ZONEID = "setZoneId"; + // ********************************************************************* + // Constants + // tag names + private static final String SET_ZONEID = "setZoneId"; - private static final String PARSE_INSTANT = "parseInstant"; + private static final String PARSE_INSTANT = "parseInstant"; - private static final String JSP_TEXT = "jsp:text"; + private static final String JSP_TEXT = "jsp:text"; - // attribute names - private static final String VALUE = "value"; + // attribute names + private static final String VALUE = "value"; - // parameter names - // private final String EXP_ATT_PARAM = "expressionAttributes"; + // parameter names + // private final String EXP_ATT_PARAM = "expressionAttributes"; - // attributes - private static final String VAR = "var"; + // attributes + private static final String VAR = "var"; - private static final String SCOPE = "scope"; + private static final String SCOPE = "scope"; - // scopes - private static final String PAGE_SCOPE = "page"; + // scopes + private static final String PAGE_SCOPE = "page"; - private static final String REQUEST_SCOPE = "request"; + private static final String REQUEST_SCOPE = "request"; - private static final String SESSION_SCOPE = "session"; + private static final String SESSION_SCOPE = "session"; - private static final String APPLICATION_SCOPE = "application"; + private static final String APPLICATION_SCOPE = "application"; - // ********************************************************************* - // Validation and configuration state (protected) + // ********************************************************************* + // Validation and configuration state (protected) - private String uri; // our taglib's uri (as passed by JSP container on XML - // View) + private String uri; // our taglib's uri (as passed by JSP container on XML + // View) - private String prefix; // our taglib's prefix + private String prefix; // our taglib's prefix - private List validationMessages; // temporary error messages + private List validationMessages; // temporary error messages // private Map config; // configuration (Map of Sets) // // private boolean failed; // have we failed >0 times? - private String lastElementId; // the last element we've seen + private String lastElementId; // the last element we've seen - // ********************************************************************* - // Constructor and lifecycle management + // ********************************************************************* + // Constructor and lifecycle management - /** - * Constructor. - */ - public JavaTimeTagLibraryValidator() { - init(); - } + public JavaTimeTagLibraryValidator() { + init(); + } - private void init() { - validationMessages = null; - prefix = null; + private void init() { + validationMessages = null; + prefix = null; // config = null; - } - - public void release() { - super.release(); - init(); - } - - public synchronized ValidationMessage[] validate(final String prefix, final String uri, final PageData page) { - try { - this.uri = uri; - // initialize - validationMessages = new ArrayList<>(); - - // save the prefix - this.prefix = prefix; - - DefaultHandler h = new Handler(); - - // parse the page - SAXParserFactory f = SAXParserFactory.newInstance(); - f.setValidating(false); - f.setNamespaceAware(true); - SAXParser p = f.newSAXParser(); - p.parse(page.getInputStream(), h); - - if (validationMessages.size() == 0) { - return null; - } else { - return validationMessages.toArray(new ValidationMessage[validationMessages.size()]); - } - } catch (SAXException ex) { - return vmFromString(ex.toString()); - } catch (ParserConfigurationException ex) { - return vmFromString(ex.toString()); - } catch (IOException ex) { - return vmFromString(ex.toString()); - } - } - - // utility methods to help us match elements in our tagset - private boolean isTag(final String tagUri, final String tagLn, final String matchUri, final String matchLn) { - if (tagUri == null || tagLn == null || matchUri == null || matchLn == null) { - return false; - } - // match beginning of URI since some suffix *_rt tags can - // be nested in EL enabled tags as defined by the spec - if (tagUri.length() > matchUri.length()) { - return (tagUri.startsWith(matchUri) && tagLn.equals(matchLn)); - } else { - return (matchUri.startsWith(tagUri) && tagLn.equals(matchLn)); - } - } + } + + public void release() { + super.release(); + init(); + } + + public synchronized ValidationMessage[] validate(String prefix, String uri, + PageData page) { + try { + this.uri = uri; + // initialize + validationMessages = new ArrayList<>(); + + // save the prefix + this.prefix = prefix; + + DefaultHandler h = new Handler(); + + // parse the page + SAXParserFactory f = SAXParserFactory.newInstance(); + f.setValidating(false); + f.setNamespaceAware(true); + SAXParser p = f.newSAXParser(); + p.parse(page.getInputStream(), h); + + if (validationMessages.size() == 0) { + return null; + } else { + return validationMessages.toArray(new ValidationMessage[validationMessages.size()]); + } + } catch (SAXException ex) { + return vmFromString(ex.toString()); + } catch (ParserConfigurationException ex) { + return vmFromString(ex.toString()); + } catch (IOException ex) { + return vmFromString(ex.toString()); + } + } + + // utility methods to help us match elements in our tagset + private boolean isTag(String tagUri, String tagLn, String matchUri, + String matchLn) { + if (tagUri == null || tagLn == null || matchUri == null + || matchLn == null) { + return false; + } + // match beginning of URI since some suffix *_rt tags can + // be nested in EL enabled tags as defined by the spec + if (tagUri.length() > matchUri.length()) { + return (tagUri.startsWith(matchUri) && tagLn.equals(matchLn)); + } else { + return (matchUri.startsWith(tagUri) && tagLn.equals(matchLn)); + } + } // private boolean isJspTag(String tagUri, String tagLn, String target) { // return isTag(tagUri, tagLn, JSP, target); // } - private boolean isJavaTimeTag(final String tagUri, final String tagLn, final String target) { - return isTag(tagUri, tagLn, this.uri, target); - } + private boolean isJavaTimeTag(String tagUri, String tagLn, String target) { + return isTag(tagUri, tagLn, this.uri, target); + } - // utility method to determine if an attribute exists - private boolean hasAttribute(final Attributes a, final String att) { - return (a.getValue(att) != null); - } + // utility method to determine if an attribute exists + private boolean hasAttribute(Attributes a, String att) { + return (a.getValue(att) != null); + } - /* - * method to assist with failure [ as if it's not easy enough already :-) ] - */ - private void fail(final String message) { + /* + * method to assist with failure [ as if it's not easy enough already :-) ] + */ + private void fail(String message) { // failed = true; - validationMessages.add(new ValidationMessage(lastElementId, message)); - } + validationMessages.add(new ValidationMessage(lastElementId, message)); + } // // returns true if the given attribute name is specified, false otherwise // private boolean isSpecified(TagData data, String attributeName) { // return (data.getAttribute(attributeName) != null); // } - /** - * @param a used to get the 'scope' attribute - * @return true if the 'scope' attribute is valid - */ - protected boolean hasNoInvalidScope(final Attributes a) { - final String scope = a.getValue(SCOPE); - return !((scope != null) && !scope.equals(PAGE_SCOPE) && !scope.equals(REQUEST_SCOPE) - && !scope.equals(SESSION_SCOPE) && !scope.equals(APPLICATION_SCOPE)); - } - - /** - * @param a used to get the 'var' attribute - * @return true if the 'var' attribute is empty - */ - protected boolean hasEmptyVar(final Attributes a) { - return "".equals(a.getValue(VAR)); - } - - /** - * @param a used to get the 'scope' attribute - * @return true if the 'scope' attribute is present without 'var' - */ - protected boolean hasDanglingScope(final Attributes a) { - return (a.getValue(SCOPE) != null && a.getValue(VAR) == null); - } - - /** - * @param qname the QName to get the local part from - * @return local part of a QName - */ - protected static String getLocalPart(final String qname) { - final int colon = qname.indexOf(":"); - return (colon == -1) ? qname : qname.substring(colon + 1); - } - - // constructs a ValidationMessage[] from a single String and no ID - private static ValidationMessage[] vmFromString(final String message) { - return new ValidationMessage[] { new ValidationMessage(null, message) }; - } - - /** - * SAX event handler. - */ - private class Handler extends DefaultHandler { - - private String lastElementName = null; - - private boolean bodyNecessary = false; - - private boolean bodyIllegal = false; - - // process under the existing context (state), then modify it - public void startElement(final String ns, String ln, final String qn, final Attributes a) { - // substitute our own parsed 'ln' if it's not provided - if (ln == null) { - ln = getLocalPart(qn); - } - - // for simplicity, we can ignore for our purposes - // (don't bother distinguishing between it and its characters) - if (qn.equals(JSP_TEXT)) { - return; - } - - // check body-related constraint - if (bodyIllegal) { - fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); - } - - // validate attributes - if (qn.startsWith(prefix + ":") && !hasNoInvalidScope(a)) { - fail(Resources.getMessage("TLV_INVALID_ATTRIBUTE", SCOPE, qn, a.getValue(SCOPE))); - } - if (qn.startsWith(prefix + ":") && hasEmptyVar(a)) { - fail(Resources.getMessage("TLV_EMPTY_VAR", qn)); - } - if (qn.startsWith(prefix + ":") && !isJavaTimeTag(ns, ln, SET_ZONEID) && hasDanglingScope(a)) { - fail(Resources.getMessage("TLV_DANGLING_SCOPE", qn)); - } - - // now, modify state - - // set up a check against illegal attribute/body combinations - bodyIllegal = false; - bodyNecessary = false; - if (isJavaTimeTag(ns, ln, PARSE_INSTANT)) { - if (hasAttribute(a, VALUE)) { - bodyIllegal = true; - } else { - bodyNecessary = true; - } - } - - // record the most recent tag (for error reporting) - lastElementName = qn; - lastElementId = a.getValue("http://java.sun.com/JSP/Page", "id"); - - // we're a new element, so increase depth - } - - public void characters(final char[] ch, final int start, final int length) { - bodyNecessary = false; // body is no longer necessary! - - // ignore strings that are just whitespace - final String s = new String(ch, start, length).trim(); - if (s.equals("")) { - return; - } - - // check and update body-related constraints - if (bodyIllegal) { - fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); - } - } - - public void endElement(final String ns, final String ln, final String qn) { - // consistently, we ignore JSP_TEXT - if (qn.equals(JSP_TEXT)) { - return; - } - - // handle body-related invariant - if (bodyNecessary) { - fail(Resources.getMessage("TLV_MISSING_BODY", lastElementName)); - } - bodyIllegal = false; // reset: we've left the tag - } - } + // returns true if the 'scope' attribute is valid + protected boolean hasNoInvalidScope(Attributes a) { + final String scope = a.getValue(SCOPE); + return !((scope != null) && !scope.equals(PAGE_SCOPE) + && !scope.equals(REQUEST_SCOPE) && !scope.equals(SESSION_SCOPE) + && !scope.equals(APPLICATION_SCOPE)); + } + + // returns true if the 'var' attribute is empty + protected boolean hasEmptyVar(Attributes a) { + return "".equals(a.getValue(VAR)); + } + + // returns true if the 'scope' attribute is present without 'var' + protected boolean hasDanglingScope(Attributes a) { + return (a.getValue(SCOPE) != null && a.getValue(VAR) == null); + } + + // retrieves the local part of a QName + protected static String getLocalPart(String qname) { + int colon = qname.indexOf(":"); + return (colon == -1) ? qname : qname.substring(colon + 1); + } + + // constructs a ValidationMessage[] from a single String and no ID + private static ValidationMessage[] vmFromString(String message) { + return new ValidationMessage[] { new ValidationMessage(null, message) }; + } + + /** + * SAX event handler. + */ + private class Handler extends DefaultHandler { + + private String lastElementName = null; + + private boolean bodyNecessary = false; + + private boolean bodyIllegal = false; + + // process under the existing context (state), then modify it + public void startElement(String ns, String ln, String qn, Attributes a) { + // substitute our own parsed 'ln' if it's not provided + if (ln == null) { + ln = getLocalPart(qn); + } + + // for simplicity, we can ignore for our purposes + // (don't bother distinguishing between it and its characters) + if (qn.equals(JSP_TEXT)) { + return; + } + + // check body-related constraint + if (bodyIllegal) { + fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); + } + + // validate attributes + if (qn.startsWith(prefix + ":") && !hasNoInvalidScope(a)) { + fail(Resources.getMessage("TLV_INVALID_ATTRIBUTE", SCOPE, qn, a + .getValue(SCOPE))); + } + if (qn.startsWith(prefix + ":") && hasEmptyVar(a)) { + fail(Resources.getMessage("TLV_EMPTY_VAR", qn)); + } + if (qn.startsWith(prefix + ":") + && !isJavaTimeTag(ns, ln, SET_ZONEID) + && hasDanglingScope(a)) { + fail(Resources.getMessage("TLV_DANGLING_SCOPE", qn)); + } + + // now, modify state + + // set up a check against illegal attribute/body combinations + bodyIllegal = false; + bodyNecessary = false; + if (isJavaTimeTag(ns, ln, PARSE_INSTANT)) { + if (hasAttribute(a, VALUE)) { + bodyIllegal = true; + } else { + bodyNecessary = true; + } + } + + // record the most recent tag (for error reporting) + lastElementName = qn; + lastElementId = a.getValue("http://java.sun.com/JSP/Page", "id"); + + // we're a new element, so increase depth + } + + public void characters(char[] ch, int start, int length) { + bodyNecessary = false; // body is no longer necessary! + + // ignore strings that are just whitespace + String s = new String(ch, start, length).trim(); + if (s.equals("")) { + return; + } + + // check and update body-related constraints + if (bodyIllegal) { + fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName)); + } + } + + public void endElement(String ns, String ln, String qn) { + // consistently, we ignore JSP_TEXT + if (qn.equals(JSP_TEXT)) { + return; + } + + // handle body-related invariant + if (bodyNecessary) { + fail(Resources.getMessage("TLV_MISSING_BODY", lastElementName)); + } + bodyIllegal = false; // reset: we've left the tag + } + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java b/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java index 6b8d829..8103168 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java @@ -22,8 +22,7 @@ /** *

      - * A handler for <parseInstant> that supports rtexprvalue-based - * attributes. + * A handler for <parseInstant> that supports rtexprvalue-based attributes. *

      * * @author Jan Luehe diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java index 280b32d..dbc9f1c 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java @@ -17,6 +17,7 @@ package net.sargue.time.jsptags; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQuery; diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java index bf6e855..d6c2531 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java @@ -16,6 +16,7 @@ */ package net.sargue.time.jsptags; +import java.time.LocalDate; import java.time.LocalTime; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQuery; diff --git a/src/main/java/net/sargue/time/jsptags/ParseSupport.java b/src/main/java/net/sargue/time/jsptags/ParseSupport.java index ac4e344..78c9727 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ParseSupport.java @@ -16,6 +16,10 @@ */ package net.sargue.time.jsptags; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.BodyTagSupport; import java.io.IOException; import java.text.DateFormat; import java.time.ZoneId; @@ -25,11 +29,6 @@ import java.time.temporal.TemporalQuery; import java.util.Locale; -import jakarta.servlet.jsp.JspException; -import jakarta.servlet.jsp.JspTagException; -import jakarta.servlet.jsp.PageContext; -import jakarta.servlet.jsp.tagext.BodyTagSupport; - /** * Support for tag handlers for the date and time parsing tags. * @@ -39,211 +38,207 @@ */ public abstract class ParseSupport extends BodyTagSupport { - private static final long serialVersionUID = 1L; - - /** The value attribute. */ - protected String value; - /** Status of the value. */ - protected boolean valueSpecified; - /** The pattern attribute. */ - protected String pattern; - /** The style attribute. */ - protected String style; - /** The zone attribute. */ - protected ZoneId zoneId; - /** The locale attribute. */ - protected Locale locale; - /** The var attribute. */ - private String var; - /** The scope attribute. */ - private int scope; - - /** - * Constructor. - */ - public ParseSupport() { - super(); - init(); - } - - private void init() { - value = null; - valueSpecified = false; - pattern = null; - style = null; - zoneId = null; - locale = null; - scope = PageContext.PAGE_SCOPE; - } - - /** - * - * @param var the variable to store the result in - */ - public void setVar(final String var) { - this.var = var; - } - - /** - * @param scope the scope to store the variable in - * @see #setVar(String) - */ - public void setScope(final String scope) { - this.scope = Util.getScope(scope); - } - - /** - * Sets the value attribute. - * - * @param value the value - */ - public void setValue(final String value) { - this.value = value; - this.valueSpecified = true; - } - - /** - * Sets the style attribute. - * - * @param style the style - */ - public void setStyle(final String style) { - this.style = style; - } - - /** - * Sets the pattern attribute. - * - * @param pattern the pattern - */ - public void setPattern(final String pattern) { - this.pattern = pattern; - } - - /** - * Sets the zone attribute. - * - * @param dtz the zone - * @throws JspTagException incorrect zone or zone parameter - */ - public void setZoneId(final Object dtz) throws JspTagException { - if (dtz == null) { - this.zoneId = null; - } else if (dtz instanceof ZoneId) { - this.zoneId = (ZoneId) dtz; - } else if (dtz instanceof String) { - try { - final String sZone = (String) dtz; - this.zoneId = sZone.isEmpty() ? null : ZoneId.of(sZone); - } catch (final IllegalArgumentException iae) { - throw new JspTagException("Incorrect Zone: " + dtz); - } - } else { - throw new JspTagException("Can only accept ZoneId or String objects."); - } - } - - /** - * Sets the style attribute. - * - * @param loc the locale - * @throws JspTagException parameter not a Locale or String - */ - public void setLocale(final Object loc) throws JspTagException { - if (loc == null) { - this.locale = null; - } else if (loc instanceof Locale) { - this.locale = (Locale) loc; - } else if (loc instanceof String) { - locale = Util.parseLocale((String) loc); - } else { - throw new JspTagException("Can only accept Locale or String objects."); - } - } - - @Override - public int doEndTag() throws JspException { - String input = null; - - // determine the input by... - if (valueSpecified) { - // ... reading 'value' attribute - input = value; - } else { - // ... retrieving and trimming our body - if (bodyContent != null && bodyContent.getString() != null) { - input = bodyContent.getString().trim(); - } - } - - if ((input == null) || input.equals("")) { - if (var != null) { - pageContext.removeAttribute(var, scope); - } - return EVAL_PAGE; - } - - // Create formatter - DateTimeFormatter formatter; - if (pattern != null) { - formatter = DateTimeFormatter.ofPattern(pattern); - } else if (style != null) { - formatter = Util.createFormatterForStyle(style); - } else { - formatter = Util.createFormatterForStyle("FF"); - } - - // set formatter locale - Locale locale = this.locale; - if (locale == null) { - locale = Util.getFormattingLocale(pageContext, true, DateFormat.getAvailableLocales()); - } - if (locale != null) { - formatter = formatter.withLocale(locale); - } - - // set formatter timezone - ZoneId tz = this.zoneId; - if (tz == null) { - tz = ZoneIdSupport.getZoneId(pageContext, this); - } - if (tz != null) { - formatter = formatter.withZone(tz); - } - - // Parse date - TemporalAccessor parsed = null; - try { - parsed = formatter.parse(input, temporalQuery()); - } catch (final DateTimeParseException e) { - throw new JspException(Resources.getMessage("PARSE_DATE_PARSE_ERROR", input), e); - } - - if (var != null) { - pageContext.setAttribute(var, parsed, scope); - } else { - try { - pageContext.getOut().print(parsed); - } catch (final IOException ioe) { - throw new JspTagException(ioe.toString(), ioe); - } - } - - return EVAL_PAGE; - } - - /** - * Abstract method to define the query used to format the input with each - * specific tag. - * - * @return the temporal query used to parse the input - */ - protected abstract TemporalQuery temporalQuery(); - - @Override - public void release() { - init(); - super.release(); - } + private static final long serialVersionUID = 1L; + + /** The value attribute. */ + protected String value; + /** Status of the value. */ + protected boolean valueSpecified; + /** The pattern attribute. */ + protected String pattern; + /** The style attribute. */ + protected String style; + /** The zone attribute. */ + protected ZoneId zoneId; + /** The locale attribute. */ + protected Locale locale; + /** The var attribute. */ + private String var; + /** The scope attribute. */ + private int scope; + + /** + * Constructor. + */ + public ParseSupport() { + super(); + init(); + } + + private void init() { + value = null; + valueSpecified = false; + pattern = null; + style = null; + zoneId = null; + locale = null; + scope = PageContext.PAGE_SCOPE; + } + + @SuppressWarnings("UnusedDeclaration") + public void setVar(String var) { + this.var = var; + } + + @SuppressWarnings("UnusedDeclaration") + public void setScope(String scope) { + this.scope = Util.getScope(scope); + } + + /** + * Sets the value attribute. + * + * @param value the value + */ + public void setValue(String value) { + this.value = value; + this.valueSpecified = true; + } + + /** + * Sets the style attribute. + * + * @param style the style + */ + @SuppressWarnings("UnusedDeclaration") + public void setStyle(String style) { + this.style = style; + } + + /** + * Sets the pattern attribute. + * + * @param pattern the pattern + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * Sets the zone attribute. + * + * @param dtz the zone + * @throws JspTagException incorrect zone or zone parameter + */ + @SuppressWarnings("UnusedDeclaration") + public void setZoneId(Object dtz) throws JspTagException { + if (dtz == null) + this.zoneId = null; + else if (dtz instanceof ZoneId) + this.zoneId = (ZoneId) dtz; + else if (dtz instanceof String) + try { + String sZone = (String) dtz; + this.zoneId = sZone.isEmpty() ? null : ZoneId.of(sZone); + } catch (IllegalArgumentException iae) { + throw new JspTagException("Incorrect Zone: " + dtz); + } + else + throw new JspTagException("Can only accept ZoneId or String objects."); + } + + /** + * Sets the style attribute. + * + * @param loc the locale + * @throws JspTagException parameter not a Locale or String + */ + @SuppressWarnings("UnusedDeclaration") + public void setLocale(Object loc) throws JspTagException { + if (loc == null) { + this.locale = null; + } else if (loc instanceof Locale) { + this.locale = (Locale) loc; + } else if (loc instanceof String) { + locale = Util.parseLocale((String) loc); + } else + throw new JspTagException("Can only accept Locale or String objects."); + } + + public int doEndTag() throws JspException { + String input = null; + + // determine the input by... + if (valueSpecified) { + // ... reading 'value' attribute + input = value; + } else { + // ... retrieving and trimming our body + if (bodyContent != null && bodyContent.getString() != null) { + input = bodyContent.getString().trim(); + } + } + + if ((input == null) || input.equals("")) { + if (var != null) { + pageContext.removeAttribute(var, scope); + } + return EVAL_PAGE; + } + + // Create formatter + DateTimeFormatter formatter; + if (pattern != null) { + formatter = DateTimeFormatter.ofPattern(pattern); + } else if (style != null) { + formatter = Util.createFormatterForStyle(style); + } else { + formatter = Util.createFormatterForStyle("FF"); + } + + // set formatter locale + Locale locale = this.locale; + if (locale == null) { + locale = Util.getFormattingLocale(pageContext, true, + DateFormat.getAvailableLocales()); + } + if (locale != null) { + formatter = formatter.withLocale(locale); + } + + // set formatter timezone + ZoneId tz = this.zoneId; + if (tz == null) { + tz = ZoneIdSupport.getZoneId(pageContext, this); + } + if (tz != null) { + formatter = formatter.withZone(tz); + } + + // Parse date + TemporalAccessor parsed; + try { + parsed = formatter.parse(input, temporalQuery()); + } catch (final DateTimeParseException e) { + throw new JspException(Resources.getMessage( + "PARSE_DATE_PARSE_ERROR", input), e); + } + + if (var != null) { + pageContext.setAttribute(var, parsed, scope); + } else { + try { + pageContext.getOut().print(parsed); + } catch (IOException ioe) { + throw new JspTagException(ioe.toString(), ioe); + } + } + + return EVAL_PAGE; + } + + /** + * Abstract method to define the query used to format the input with + * each specific tag. + * + * @return the temporal query used to parse the input + */ + protected abstract TemporalQuery temporalQuery(); + + // Releases any resources we may have (or inherit) + public void release() { + init(); + super.release(); + } } diff --git a/src/main/java/net/sargue/time/jsptags/Resources.java b/src/main/java/net/sargue/time/jsptags/Resources.java index 4756a76..75782a7 100644 --- a/src/main/java/net/sargue/time/jsptags/Resources.java +++ b/src/main/java/net/sargue/time/jsptags/Resources.java @@ -22,160 +22,177 @@ /** *

      - * Provides locale-neutral access to string resources. Only the documentation - * and code are in English. :-) + * Provides locale-neutral access to string resources. Only the + * documentation and code are in English. :-) * - *

      - * The major goal, aside from globalization, is convenience. Access to resources - * with no parameters is made in the form: - *

      + *

      The major goal, aside from globalization, is convenience. + * Access to resources with no parameters is made in the form:

      * *
        * Resources.getMessage(MESSAGE_NAME);
        * 
      * - *

      - * Access to resources with one parameter works like - *

      + *

      Access to resources with one parameter works like

      * *
        * Resources.getMessage(MESSAGE_NAME, arg1);
        * 
      * - *

      - * ... and so on. - *

      + *

      ... and so on.

      * * @author Shawn Bayern */ -public class Resources { + @SuppressWarnings("UnusedDeclaration") + public class Resources { - // ********************************************************************* - // Static data + // ********************************************************************* + // Static data - /** The location of our resources. */ - private static final String RESOURCE_LOCATION = "net.sargue.time.jsptags.Resources"; + /** The location of our resources. */ + private static final String RESOURCE_LOCATION = "net.sargue.time.jsptags.Resources"; - /** Our class-wide ResourceBundle. */ - private static ResourceBundle rb = ResourceBundle.getBundle(RESOURCE_LOCATION); + /** Our class-wide ResourceBundle. */ + private static ResourceBundle rb = ResourceBundle.getBundle(RESOURCE_LOCATION); - // ********************************************************************* - // Public static methods + + // ********************************************************************* + // Public static methods - /** - * Retrieves a message with no arguments. - * - * @param name the resource name - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name) throws MissingResourceException { - return rb.getString(name); - } + /** + * Retrieves a message with no arguments. + * + * @param name the resource name + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name) + throws MissingResourceException { + return rb.getString(name); + } - /** - * Retrieves a message with arbitrarily many arguments. - * - * @param name the resource name - * @param a the a - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object[] a) throws MissingResourceException { - final String res = rb.getString(name); - return MessageFormat.format(res, a); - } + /** + * Retrieves a message with arbitrarily many arguments. + * + * @param name the resource name + * @param a the a + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, Object[] a) + throws MissingResourceException { + String res = rb.getString(name); + return MessageFormat.format(res, a); + } - /** - * Retrieves a message with one argument. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object a1) throws MissingResourceException { - return getMessage(name, new Object[] { a1 }); - } + /** + * Retrieves a message with one argument. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, Object a1) + throws MissingResourceException { + return getMessage(name, new Object[] { a1 }); + } - /** - * Retrieves a message with two arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object a1, final Object a2) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2 }); - } + /** + * Retrieves a message with two arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, Object a1, Object a2) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2 }); + } - /** - * Retrieves a message with three arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object a1, final Object a2, final Object a3) - throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3 }); - } + /** + * Retrieves a message with three arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, + Object a1, + Object a2, + Object a3) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3 }); + } - /** - * Retrieves a message with four arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @param a4 the parameter number 4 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object a1, final Object a2, final Object a3, - final Object a4) throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3, a4 }); - } + /** + * Retrieves a message with four arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @param a4 the parameter number 4 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, + Object a1, + Object a2, + Object a3, + Object a4) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3, a4 }); + } - /** - * Retrieves a message with five arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @param a4 the parameter number 4 - * @param a5 the parameter number 5 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object a1, final Object a2, final Object a3, - final Object a4, final Object a5) throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3, a4, a5 }); - } + /** + * Retrieves a message with five arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @param a4 the parameter number 4 + * @param a5 the parameter number 5 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, + Object a1, + Object a2, + Object a3, + Object a4, + Object a5) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3, a4, a5 }); + } - /** - * Retrieves a message with six arguments. - * - * @param name the resource name - * @param a1 the parameter number 1 - * @param a2 the parameter number 2 - * @param a3 the parameter number 3 - * @param a4 the parameter number 4 - * @param a5 the parameter number 5 - * @param a6 the parameter number 6 - * @return the message - * @throws MissingResourceException the missing resource exception - */ - public static String getMessage(final String name, final Object a1, final Object a2, final Object a3, - final Object a4, final Object a5, final Object a6) throws MissingResourceException { - return getMessage(name, new Object[] { a1, a2, a3, a4, a5, a6 }); - } + /** + * Retrieves a message with six arguments. + * + * @param name the resource name + * @param a1 the parameter number 1 + * @param a2 the parameter number 2 + * @param a3 the parameter number 3 + * @param a4 the parameter number 4 + * @param a5 the parameter number 5 + * @param a6 the parameter number 6 + * @return the message + * @throws MissingResourceException the missing resource exception + */ + public static String getMessage(String name, + Object a1, + Object a2, + Object a3, + Object a4, + Object a5, + Object a6) + throws MissingResourceException { + return getMessage(name, new Object[] { a1, a2, a3, a4, a5, a6 }); + } } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java index 96a169a..c204ab0 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java @@ -34,7 +34,7 @@ public class SetZoneIdIdTag extends SetZoneIdSupport { * * @param value the value */ - public void setValue(final Object value) { + public void setValue(Object value) { this.value = value; } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java index 107af3a..0d2c714 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java @@ -16,14 +16,14 @@ */ package net.sargue.time.jsptags; -import java.time.ZoneId; -import java.time.ZoneOffset; - import jakarta.servlet.jsp.JspException; import jakarta.servlet.jsp.PageContext; import jakarta.servlet.jsp.jstl.core.Config; import jakarta.servlet.jsp.tagext.TagSupport; +import java.time.ZoneId; +import java.time.ZoneOffset; + /** * Support for tag handlers for <setDateTimeZone>. * @@ -62,7 +62,8 @@ private void init() { * @param scope the scope to store the variable in * @see #setVar(String) */ - public void setScope(final String scope) { + @SuppressWarnings("UnusedDeclaration") + public void setScope(String scope) { this.scope = Util.getScope(scope); } @@ -70,7 +71,8 @@ public void setScope(final String scope) { * * @param var the variable to store the result in */ - public void setVar(final String var) { + @SuppressWarnings("UnusedDeclaration") + public void setVar(String var) { this.var = var; } @@ -91,13 +93,14 @@ public int doEndTag() throws JspException { if (var != null) { pageContext.setAttribute(var, dateTimeZone, scope); } else { - Config.set(pageContext, ZoneIdSupport.FMT_TIME_ZONE, dateTimeZone, scope); + Config.set(pageContext, ZoneIdSupport.FMT_TIME_ZONE, + dateTimeZone, scope); } return EVAL_PAGE; } - @Override + // Releases any resources we may have (or inherit) public void release() { init(); super.release(); diff --git a/src/main/java/net/sargue/time/jsptags/Util.java b/src/main/java/net/sargue/time/jsptags/Util.java index 0125fbc..0fa3252 100644 --- a/src/main/java/net/sargue/time/jsptags/Util.java +++ b/src/main/java/net/sargue/time/jsptags/Util.java @@ -16,25 +16,6 @@ */ package net.sargue.time.jsptags; -import static java.time.format.FormatStyle.FULL; -import static java.time.format.FormatStyle.LONG; -import static java.time.format.FormatStyle.MEDIUM; -import static java.time.format.FormatStyle.SHORT; - -import java.text.DateFormat; -import java.text.NumberFormat; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Locale; -import java.util.MissingResourceException; -import java.util.ResourceBundle; -import java.util.Set; - import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.jsp.JspException; @@ -42,6 +23,14 @@ import jakarta.servlet.jsp.jstl.core.Config; import jakarta.servlet.jsp.jstl.fmt.LocalizationContext; +import java.text.DateFormat; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.*; + +import static java.time.format.FormatStyle.*; + /** *

      * Utilities in support of tag-handler classes. @@ -53,605 +42,618 @@ */ public class Util { - private static final String REQUEST = "request"; - - private static final String SESSION = "session"; - - private static final String APPLICATION = "application"; - - private static final char HYPHEN = '-'; - - private static final char UNDERSCORE = '_'; - - private static final Locale EMPTY_LOCALE = new Locale("", ""); - - static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset"; - - /** - * Converts the given string description of a scope to the corresponding - * PageContext constant. - * - * The validity of the given scope has already been checked by the appropriate - * TLV. - * - * @param scope String description of scope - * - * @return PageContext constant corresponding to given scope description - */ - public static int getScope(final String scope) { - final int ret; - - if (REQUEST.equalsIgnoreCase(scope)) { - ret = PageContext.REQUEST_SCOPE; - } else if (SESSION.equalsIgnoreCase(scope)) { - ret = PageContext.SESSION_SCOPE; - } else if (APPLICATION.equalsIgnoreCase(scope)) { - ret = PageContext.APPLICATION_SCOPE; - } else { - // default; - ret = PageContext.PAGE_SCOPE; - } - return ret; - } - - /** - * HttpServletRequest.getLocales() returns the server's default locale if the - * request did not specify a preferred language. We do not want this behavior, - * because it prevents us from using the fallback locale. We therefore need to - * return an empty Enumeration if no preferred locale has been specified. This - * way, the logic for the fallback locale will be able to kick in. - * - * @param request the http request - * @return the locales from the request or an empty enumeration if no preferred - * locale has been specified - */ - public static Enumeration getRequestLocales(final HttpServletRequest request) { - final Enumeration values = request.getHeaders("accept-language"); - if (values.hasMoreElements()) { - // At least one "accept-language". Simply return - // the enumeration returned by request.getLocales(). - // System.out.println("At least one accept-language"); - return request.getLocales(); - } else { - // No header for "accept-language". Simply return - // the empty enumeration. - // System.out.println("No accept-language"); - return Collections.emptyEnumeration(); - } - } - - /** - * See parseLocale(String, String) for details. - * - * @param locale the locale string to parse - * @return {@link java.util.Locale} object corresponding to the given locale - * string, or the null if the locale string is null or empty - */ - public static Locale parseLocale(final String locale) { - return parseLocale(locale, null); - } - - /** - * Parses the given locale string into its language and (optionally) country - * components, and returns the corresponding {@link java.util.Locale} object. - * - * If the given locale string is null or empty, a null value is returned. - * - * @param locale the locale string to parse - * @param variant the variant - * - * @return {@link java.util.Locale} object corresponding to the given locale - * string, or the null if the locale string is null or empty - * - * @throws IllegalArgumentException if the given locale does not have a language - * component or has an empty country component - */ - public static Locale parseLocale(final String locale, final String variant) { - final Locale ret; - String language = locale; - String country = null; - int index; - - if (locale == null || locale.isEmpty()) - return null; - - if (((index = locale.indexOf(HYPHEN)) > -1) || ((index = locale.indexOf(UNDERSCORE)) > -1)) { - language = locale.substring(0, index); - country = locale.substring(index + 1); - } - - if (language.isEmpty()) { - throw new IllegalArgumentException(Resources.getMessage("LOCALE_NO_LANGUAGE")); - } - - if (country == null) { - if (variant != null) { - ret = new Locale(language, "", variant); - } else { - ret = new Locale(language, ""); - } - } else if (country.length() > 0) { - if (variant != null) { - ret = new Locale(language, country, variant); - } else { - ret = new Locale(language, country); - } - } else { - throw new IllegalArgumentException(Resources.getMessage("LOCALE_EMPTY_COUNTRY")); - } - - return ret; - } - - /** - * Stores the given locale in the response object of the given page context, and - * stores the locale's associated charset in the - * javax.servlet.jsp.jstl.fmt.request.charset session attribute, which may be - * used by the action in a page invoked by a form included in - * the response to set the request charset to the same as the response charset - * (this makes it possible for the container to decode the form parameter values - * properly, since browsers typically encode form field values using the - * response's charset). - * - * @param pc the page context whose response object is assigned the given - * locale - * @param locale the response locale - */ - static void setResponseLocale(final PageContext pc, final Locale locale) { - // set response locale - final ServletResponse response = pc.getResponse(); - response.setLocale(locale); - - // get response character encoding and store it in session attribute - if (pc.getSession() != null) { - try { - pc.setAttribute(REQUEST_CHAR_SET, response.getCharacterEncoding(), PageContext.SESSION_SCOPE); - } catch (IllegalStateException ex) { - // invalidated session ignored - } - } - } - - /** - * Returns the formatting locale to use with the given formatting action in the - * given page. - * - * @param pc The page context containing the formatting action @param fromTag - * The formatting action @param format {@code true} if the - * formatting action is of type {@code } (as opposed to - * {@code }), and {@code false} otherwise (if set to - * {@code true}, the formatting locale that is returned by this - * method is used to set the response locale). - * - * @param avail the array of available locales - * - * @return the formatting locale to use - */ - static Locale getFormattingLocale(final PageContext pc, final boolean format, final Locale[] avail) { - - LocalizationContext locCtxt; - - // Use locale from default I18N localization context, unless it is null - if ((locCtxt = getLocalizationContext(pc)) != null) { - if (locCtxt.getLocale() != null) { - if (format) { - setResponseLocale(pc, locCtxt.getLocale()); - } - return locCtxt.getLocale(); - } - } - - /* - * Establish formatting locale by comparing the preferred locales (in order of - * preference) against the available formatting locales, and determining the - * best matching locale. - */ - Locale match; - Locale pref = getLocale(pc, Config.FMT_LOCALE); - if (pref != null) { - // Preferred locale is application-based - match = findFormattingMatch(pref, avail); - } else { - // Preferred locales are browser-based - match = findFormattingMatch(pc, avail); - } - if (match == null) { - // Use fallback locale. - pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); - if (pref != null) { - match = findFormattingMatch(pref, avail); - } - } - if (format && (match != null)) { - setResponseLocale(pc, match); - } - - return match; - } - - /** - * Setup the available formatting locales that will be used by - * getFormattingLocale(PageContext). - */ - static Locale[] availableFormattingLocales; - static { - final Locale[] dateLocales = DateFormat.getAvailableLocales(); - final Set numberLocales = new HashSet<>(Arrays.asList(NumberFormat.getAvailableLocales())); - final ArrayList locales = new ArrayList<>(); - for (Locale dateLocale : dateLocales) { - if (numberLocales.contains(dateLocale)) { - locales.add(dateLocale); - } - } - availableFormattingLocales = new Locale[locales.size()]; - availableFormattingLocales = locales.toArray(availableFormattingLocales); - } - - /** - * Returns the locale specified by the named scoped attribute or context - * configuration parameter. - * - *

      - * The named scoped attribute is searched in the page, request, session (if - * valid), and application scope(s) (in this order). If no such attribute exists - * in any of the scopes, the locale is taken from the named context - * configuration parameter. - * - * @param pageContext the page in which to search for the named scoped attribute - * or context configuration parameter @param name the name of - * the scoped attribute or context configuration parameter - * - * @return the locale specified by the named scoped attribute or context - * configuration parameter, or {@code null} if no scoped attribute or - * configuration parameter with the given name exists - */ - static Locale getLocale(final PageContext pageContext, final String name) { - final Locale loc; - - final Object obj = Config.find(pageContext, name); - if (obj != null) { - if (obj instanceof Locale) { - loc = (Locale) obj; - } else { - loc = parseLocale((String) obj); - } - } else { - loc = null; - } - - return loc; - } - - // ********************************************************************* - // Private utility methods - - /** - * Determines the client's preferred locales from the request, and compares each - * of the locales (in order of preference) against the available locales in - * order to determine the best matching locale. - * - * @param pageContext Page containing the formatting action @param avail - * Available formatting locales - * - * @return Best matching locale, or {@code null} if no match was found - */ - private static Locale findFormattingMatch(final PageContext pageContext, final Locale[] avail) { - Locale match = null; - for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ - .hasMoreElements();) { - final Locale locale = enum_.nextElement(); - match = findFormattingMatch(locale, avail); - if (match != null) { - break; - } - } - - return match; - } - - /** - * Returns the best match between the given preferred locale and the given - * available locales. - * - * The best match is given as the first available locale that exactly matches - * the given preferred locale ("exact match"). If no exact match exists, the - * best match is given to an available locale that meets the following criteria - * (in order of priority): - available locale's variant is empty and exact match - * for both language and country - available locale's variant and country are - * empty, and exact match for language. - * - * @param pref the preferred locale @param avail the available formatting - * locales - * - * @return Available locale that best matches the given preferred locale, or - * {@code null} if no match exists - */ - private static Locale findFormattingMatch(final Locale pref, final Locale[] avail) { - Locale match = null; - boolean langAndCountryMatch = false; - for (Locale locale : avail) { - if (pref.equals(locale)) { - // Exact match - match = locale; - break; - } else if (!"".equals(pref.getVariant()) && "".equals(locale.getVariant()) - && pref.getLanguage().equals(locale.getLanguage()) - && pref.getCountry().equals(locale.getCountry())) { - // Language and country match; different variant - match = locale; - langAndCountryMatch = true; - } else if (!langAndCountryMatch && pref.getLanguage().equals(locale.getLanguage()) - && ("".equals(locale.getCountry()))) { - // Language match - if (match == null) { - match = locale; - } - } - } - return match; - } - - /** - * Gets the default I18N localization context. - * - * @param pc Page in which to look up the default I18N localization context - * @return the localization context - */ - public static LocalizationContext getLocalizationContext(final PageContext pc) { - final LocalizationContext locCtxt; - - final Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT); - if (obj == null) { - return null; - } - - if (obj instanceof LocalizationContext) { - locCtxt = (LocalizationContext) obj; - } else { - // localization context is a bundle basename - locCtxt = getLocalizationContext(pc, (String) obj); - } - - return locCtxt; - } - - /** - * Gets the resource bundle with the given base name, whose locale is determined - * as follows: - * - * Check if a match exists between the ordered set of preferred locales and the - * available locales, for the given base name. The set of preferred locales - * consists of a single locale (if the {@link Config#FMT_LOCALE} configuration - * setting is present) or is equal to the client's preferred locales determined - * from the client's browser settings. - * - *

      - * If no match was found in the previous step, check if a match exists between - * the fallback locale (given by the {@link Config#FMT_FALLBACK_LOCALE} - * configuration setting) and the available locales, for the given base name. - * - * @param pc Page in which the resource bundle with the given base name is - * requested - * @param basename Resource bundle base name - * - * @return Localization context containing the resource bundle with the given - * base name and the locale that led to the resource bundle match, or - * the empty localization context if no resource bundle match was found - */ - public static LocalizationContext getLocalizationContext(final PageContext pc, final String basename) { - LocalizationContext locCtxt = null; - ResourceBundle bundle; - - if ((basename == null) || basename.equals("")) { - return new LocalizationContext(); - } - - // Try preferred locales - Locale pref = getLocale(pc, Config.FMT_LOCALE); - if (pref != null) { - // Preferred locale is application-based - bundle = findMatch(basename, pref); - if (bundle != null) { - locCtxt = new LocalizationContext(bundle, pref); - } - } else { - // Preferred locales are browser-based - locCtxt = findMatch(pc, basename); - } - - if (locCtxt == null) { - // No match found with preferred locales, try using fallback locale - pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); - if (pref != null) { - bundle = findMatch(basename, pref); - if (bundle != null) { - locCtxt = new LocalizationContext(bundle, pref); - } - } - } - - if (locCtxt == null) { - // try using the root resource bundle with the given basename - try { - bundle = ResourceBundle.getBundle(basename, EMPTY_LOCALE, - Thread.currentThread().getContextClassLoader()); - if (bundle != null) { - locCtxt = new LocalizationContext(bundle, null); - } - } catch (MissingResourceException mre) { - // do nothing - } - } - - if (locCtxt != null) { - // set response locale - if (locCtxt.getLocale() != null) { - setResponseLocale(pc, locCtxt.getLocale()); - } - } else { - // create empty localization context - locCtxt = new LocalizationContext(); - } - - return locCtxt; - } - - /** - * Determines the client's preferred locales from the request, and compares each - * of the locales (in order of preference) against the available locales in - * order to determine the best matching locale. - * - * @param pageContext the page in which the resource bundle with the given base - * name is requested @param basename the resource bundle's - * base name - * - * @return the localization context containing the resource bundle with the - * given base name and best matching locale, or {@code null} if no - * resource bundle match was found - */ - private static LocalizationContext findMatch(final PageContext pageContext, final String basename) { - LocalizationContext locCtxt = null; - - // Determine locale from client's browser settings. - for (Enumeration enum_ = Util.getRequestLocales((HttpServletRequest) pageContext.getRequest()); enum_ - .hasMoreElements();) { - Locale pref = enum_.nextElement(); - ResourceBundle match = findMatch(basename, pref); - if (match != null) { - locCtxt = new LocalizationContext(match, pref); - break; - } - } - - return locCtxt; - } - - /** - * Gets the resource bundle with the given base name and preferred locale. - * - * This method calls java.util.ResourceBundle.getBundle(), but ignores its - * return value unless its locale represents an exact or language match with the - * given preferred locale. - * - * @param basename the resource bundle base name @param pref the preferred - * locale - * - * @return the requested resource bundle, or {@code null} if no resource bundle - * with the given base name exists or if there is no exact- or - * language-match between the preferred locale and the locale of the - * bundle returned by java.util.ResourceBundle.getBundle(). - */ - private static ResourceBundle findMatch(final String basename, final Locale pref) { - ResourceBundle match = null; - - try { - ResourceBundle bundle = ResourceBundle.getBundle(basename, pref, - Thread.currentThread().getContextClassLoader()); - Locale avail = bundle.getLocale(); - if (pref.equals(avail)) { - // Exact match - match = bundle; - } else { - /* - * We have to make sure that the match we got is for the specified locale. The - * way ResourceBundle.getBundle() works, if a match is not found with (1) the - * specified locale, it tries to match with (2) the current default locale as - * returned by Locale.getDefault() or (3) the root resource bundle (basename). - * We must ignore any match that could have worked with (2) or (3). So if an - * exact match is not found, we make the following extra tests: - avail locale - * must be equal to preferred locale - avail country must be empty or equal to - * preferred country (the equality match might have failed on the variant) - */ - if (pref.getLanguage().equals(avail.getLanguage()) - && ("".equals(avail.getCountry()) || pref.getCountry().equals(avail.getCountry()))) { - /* - * Language match. By making sure the available locale does not have a country - * and matches the preferred locale's language, we rule out "matches" based on - * the container's default locale. For example, if the preferred locale is - * "en-US", the container's default locale is "en-UK", and there is a resource - * bundle (with the requested base name) available for "en-UK", - * ResourceBundle.getBundle() will return it, but even though its language - * matches that of the preferred locale, we must ignore it, because matches - * based on the container's default locale are not portable across different - * containers with different default locales. - */ - match = bundle; - } - } - } catch (MissingResourceException mre) { - throw new IllegalStateException("Shouldn't happen?"); - } - - return match; - } - - /* - * This section is based on joda-time DateTimeFormat to handle the two character - * style pattern missing in Java Time. - */ - - /** - * Creates a formatter from a two character style pattern. The first character - * is the date style, and the second character is the time style. Specify a - * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for - * full. A date or time may be ommitted by specifying a style character '-'. - * - * @param style two characters from the set {"S", "M", "L", "F", "-"} - * @throws JspException if the style is invalid - * @return a formatter for the specified style - */ - public static DateTimeFormatter createFormatterForStyle(final String style) throws JspException { - if (style == null || style.length() != 2) { - throw new JspException("Invalid style specification: " + style); - } - FormatStyle dateStyle = selectStyle(style.charAt(0)); - FormatStyle timeStyle = selectStyle(style.charAt(1)); - if (dateStyle == null && timeStyle == null) { - throw new JspException("Style '--' is invalid"); - } - return createFormatterForStyleIndex(dateStyle, timeStyle); - } - - /** - * Gets the formatter for the specified style. - * - * @param dateStyle the date style - * @param timeStyle the time style - * @return the formatter - */ - private static DateTimeFormatter createFormatterForStyleIndex(final FormatStyle dateStyle, - final FormatStyle timeStyle) throws JspException { - if (dateStyle == null && timeStyle == null) { - throw new JspException("Both styles cannot be null."); - } else if (dateStyle != null && timeStyle != null) { - return DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); - } else if (dateStyle == null) { - return DateTimeFormatter.ofLocalizedTime(timeStyle); - } else { - return DateTimeFormatter.ofLocalizedDate(dateStyle); - } - } - - /** - * Gets the FormatStyle style code from first character. - * - * @param ch the one character style code - * @return the FormatStyle - */ - private static FormatStyle selectStyle(final char ch) throws JspException { - switch (ch) { - case 'S': - return SHORT; - case 'M': - return MEDIUM; - case 'L': - return LONG; - case 'F': - return FULL; - case '-': - return null; - default: - throw new JspException("Invalid style character: " + ch); - } - } + private static final String REQUEST = "request"; + + private static final String SESSION = "session"; + + private static final String APPLICATION = "application"; + + private static final char HYPHEN = '-'; + + private static final char UNDERSCORE = '_'; + + private static final Locale EMPTY_LOCALE = new Locale("", ""); + + static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset"; + + /** + * Converts the given string description of a scope to the corresponding + * PageContext constant. + * + * The validity of the given scope has already been checked by the + * appropriate TLV. + * + * @param scope String description of scope + * + * @return PageContext constant corresponding to given scope description + */ + public static int getScope(String scope) { + int ret = PageContext.PAGE_SCOPE; // default + + if (REQUEST.equalsIgnoreCase(scope)) { + ret = PageContext.REQUEST_SCOPE; + } else if (SESSION.equalsIgnoreCase(scope)) { + ret = PageContext.SESSION_SCOPE; + } else if (APPLICATION.equalsIgnoreCase(scope)) { + ret = PageContext.APPLICATION_SCOPE; + } + return ret; + } + + /** + * HttpServletRequest.getLocales() returns the server's default locale if + * the request did not specify a preferred language. We do not want this + * behavior, because it prevents us from using the fallback locale. We + * therefore need to return an empty Enumeration if no preferred locale has + * been specified. This way, the logic for the fallback locale will be able + * to kick in. + * + * @param request the http request + * @return the locales from the request or an empty enumeration if no + * preferred locale has been specified + */ + public static Enumeration getRequestLocales(HttpServletRequest request) { + Enumeration values = request.getHeaders("accept-language"); + if (values.hasMoreElements()) { + // At least one "accept-language". Simply return + // the enumeration returned by request.getLocales(). + // System.out.println("At least one accept-language"); + return request.getLocales(); + } else { + // No header for "accept-language". Simply return + // the empty enumeration. + // System.out.println("No accept-language"); + return values; + } + } + + /** + * See parseLocale(String, String) for details. + * + * @param locale the locale string to parse + * @return java.util.Locale object corresponding to the given + * locale string, or the null if the locale string is null or empty + */ + public static Locale parseLocale(String locale) { + return parseLocale(locale, null); + } + + /** + * Parses the given locale string into its language and (optionally) country + * components, and returns the corresponding java.util.Locale + * object. + * + * If the given locale string is null or empty, a null value is returned. + * + * @param locale the locale string to parse + * @param variant the variant + * + * @return java.util.Locale object corresponding to the given + * locale string, or the null if the locale string is null or empty + * + * @throws IllegalArgumentException if the given locale does not have a + * language component or has an empty country component + */ + public static Locale parseLocale(String locale, String variant) { + Locale ret; + String language = locale; + String country = null; + int index; + + if (locale == null || locale.isEmpty()) + return null; + + if (((index = locale.indexOf(HYPHEN)) > -1) + || ((index = locale.indexOf(UNDERSCORE)) > -1)) { + language = locale.substring(0, index); + country = locale.substring(index + 1); + } + + if (language.isEmpty()) { + throw new IllegalArgumentException(Resources + .getMessage("LOCALE_NO_LANGUAGE")); + } + + if (country == null) { + if (variant != null) { + ret = new Locale(language, "", variant); + } else { + ret = new Locale(language, ""); + } + } else if (country.length() > 0) { + if (variant != null) { + ret = new Locale(language, country, variant); + } else { + ret = new Locale(language, country); + } + } else { + throw new IllegalArgumentException(Resources + .getMessage("LOCALE_EMPTY_COUNTRY")); + } + + return ret; + } + + /** + * Stores the given locale in the response object of the given page context, + * and stores the locale's associated charset in the + * javax.servlet.jsp.jstl.fmt.request.charset session attribute, which may + * be used by the action in a page invoked by a form + * included in the response to set the request charset to the same as the + * response charset (this makes it possible for the container to decode the + * form parameter values properly, since browsers typically encode form + * field values using the response's charset). + * + * @param pc the page context whose response object is assigned the + * given locale + * @param locale the response locale + */ + static void setResponseLocale(PageContext pc, Locale locale) { + // set response locale + ServletResponse response = pc.getResponse(); + response.setLocale(locale); + + // get response character encoding and store it in session attribute + if (pc.getSession() != null) { + try { + pc.setAttribute(REQUEST_CHAR_SET, response + .getCharacterEncoding(), PageContext.SESSION_SCOPE); + } catch (IllegalStateException ex) { + // invalidated session ignored + } + } + } + + /** + * Returns the formatting locale to use with the given formatting action in + * the given page. + * + * @param pc The page context containing the formatting action @param + * fromTag The formatting action @param format true if the + * formatting action is of type (as opposed to ), and + * false otherwise (if set to true, the formatting + * locale that is returned by this method is used to set the response + * locale). + * + * @param avail the array of available locales + * + * @return the formatting locale to use + */ + static Locale getFormattingLocale(PageContext pc, boolean format, Locale[] avail) { + + LocalizationContext locCtxt; + + // Use locale from default I18N localization context, unless it is null + if ((locCtxt = getLocalizationContext(pc)) != null) { + if (locCtxt.getLocale() != null) { + if (format) { + setResponseLocale(pc, locCtxt.getLocale()); + } + return locCtxt.getLocale(); + } + } + + /* + * Establish formatting locale by comparing the preferred locales (in + * order of preference) against the available formatting locales, and + * determining the best matching locale. + */ + Locale match; + Locale pref = getLocale(pc, Config.FMT_LOCALE); + if (pref != null) { + // Preferred locale is application-based + match = findFormattingMatch(pref, avail); + } else { + // Preferred locales are browser-based + match = findFormattingMatch(pc, avail); + } + if (match == null) { + // Use fallback locale. + pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); + if (pref != null) { + match = findFormattingMatch(pref, avail); + } + } + if (format && (match != null)) { + setResponseLocale(pc, match); + } + + return match; + } + + /** + * Setup the available formatting locales that will be used by + * getFormattingLocale(PageContext). + */ + static Locale[] availableFormattingLocales; + static { + Locale[] dateLocales = DateFormat.getAvailableLocales(); + Set numberLocales = new HashSet<>(Arrays.asList(NumberFormat.getAvailableLocales())); + ArrayList locales = new ArrayList<>(); + for (Locale dateLocale : dateLocales) + if (numberLocales.contains(dateLocale)) + locales.add(dateLocale); + availableFormattingLocales = new Locale[locales.size()]; + availableFormattingLocales = locales.toArray(availableFormattingLocales); + } + + /** + * Returns the locale specified by the named scoped attribute or context + * configuration parameter. + * + *

      The named scoped attribute is searched in the page, request, session + * (if valid), and application scope(s) (in this order). If no such + * attribute exists in any of the scopes, the locale is taken from the named + * context configuration parameter. + * + * @param pageContext the page in which to search for the named scoped + * attribute or context configuration parameter @param name the name of the + * scoped attribute or context configuration parameter + * + * @return the locale specified by the named scoped attribute or context + * configuration parameter, or null if no scoped attribute or + * configuration parameter with the given name exists + */ + static Locale getLocale(PageContext pageContext, String name) { + Locale loc = null; + + Object obj = Config.find(pageContext, name); + if (obj != null) { + if (obj instanceof Locale) { + loc = (Locale) obj; + } else { + loc = parseLocale((String) obj); + } + } + + return loc; + } + + // ********************************************************************* + // Private utility methods + + /** + * Determines the client's preferred locales from the request, and compares + * each of the locales (in order of preference) against the available + * locales in order to determine the best matching locale. + * + * @param pageContext Page containing the formatting action @param avail + * Available formatting locales + * + * @return Best matching locale, or null if no match was found + */ + private static Locale findFormattingMatch(PageContext pageContext, + Locale[] avail) { + Locale match = null; + for (Enumeration enum_ = Util + .getRequestLocales((HttpServletRequest) pageContext + .getRequest()); enum_.hasMoreElements();) { + Locale locale = (Locale) enum_.nextElement(); + match = findFormattingMatch(locale, avail); + if (match != null) { + break; + } + } + + return match; + } + + /** + * Returns the best match between the given preferred locale and the given + * available locales. + * + * The best match is given as the first available locale that exactly + * matches the given preferred locale ("exact match"). If no exact match + * exists, the best match is given to an available locale that meets the + * following criteria (in order of priority): - available locale's variant + * is empty and exact match for both language and country - available + * locale's variant and country are empty, and exact match for language. + * + * @param pref the preferred locale @param avail the available formatting + * locales + * + * @return Available locale that best matches the given preferred locale, or + * null if no match exists + */ + private static Locale findFormattingMatch(Locale pref, Locale[] avail) { + Locale match = null; + boolean langAndCountryMatch = false; + for (Locale locale : avail) { + if (pref.equals(locale)) { + // Exact match + match = locale; + break; + } else if (!"".equals(pref.getVariant()) + && "".equals(locale.getVariant()) + && pref.getLanguage().equals(locale.getLanguage()) + && pref.getCountry().equals(locale.getCountry())) { + // Language and country match; different variant + match = locale; + langAndCountryMatch = true; + } else if (!langAndCountryMatch + && pref.getLanguage().equals(locale.getLanguage()) + && ("".equals(locale.getCountry()))) { + // Language match + if (match == null) { + match = locale; + } + } + } + return match; + } + + /** + * Gets the default I18N localization context. + * + * @param pc Page in which to look up the default I18N localization context + * @return the localization context + */ + public static LocalizationContext getLocalizationContext(PageContext pc) { + LocalizationContext locCtxt; + + Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT); + if (obj == null) { + return null; + } + + if (obj instanceof LocalizationContext) { + locCtxt = (LocalizationContext) obj; + } else { + // localization context is a bundle basename + locCtxt = getLocalizationContext(pc, (String) obj); + } + + return locCtxt; + } + + /** + * Gets the resource bundle with the given base name, whose locale is + * determined as follows: + * + * Check if a match exists between the ordered set of preferred locales and + * the available locales, for the given base name. The set of preferred + * locales consists of a single locale (if the + * javax.servlet.jsp.jstl.fmt.locale configuration setting is + * present) or is equal to the client's preferred locales determined from + * the client's browser settings. + * + *

      + * If no match was found in the previous step, check if a match exists + * between the fallback locale (given by the + * javax.servlet.jsp.jstl.fmt.fallbackLocale configuration + * setting) and the available locales, for the given base name. + * + * @param pc Page in which the resource bundle with the given base + * name is requested + * @param basename Resource bundle base name + * + * @return Localization context containing the resource bundle with the + * given base name and the locale that led to the resource bundle match, or + * the empty localization context if no resource bundle match was found + */ + public static LocalizationContext getLocalizationContext(PageContext pc, + String basename) { + LocalizationContext locCtxt = null; + ResourceBundle bundle; + + if ((basename == null) || basename.equals("")) { + return new LocalizationContext(); + } + + // Try preferred locales + Locale pref = getLocale(pc, Config.FMT_LOCALE); + if (pref != null) { + // Preferred locale is application-based + bundle = findMatch(basename, pref); + if (bundle != null) { + locCtxt = new LocalizationContext(bundle, pref); + } + } else { + // Preferred locales are browser-based + locCtxt = findMatch(pc, basename); + } + + if (locCtxt == null) { + // No match found with preferred locales, try using fallback locale + pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE); + if (pref != null) { + bundle = findMatch(basename, pref); + if (bundle != null) { + locCtxt = new LocalizationContext(bundle, pref); + } + } + } + + if (locCtxt == null) { + // try using the root resource bundle with the given basename + try { + bundle = ResourceBundle.getBundle(basename, EMPTY_LOCALE, + Thread.currentThread().getContextClassLoader()); + if (bundle != null) { + locCtxt = new LocalizationContext(bundle, null); + } + } catch (MissingResourceException mre) { + // do nothing + } + } + + if (locCtxt != null) { + // set response locale + if (locCtxt.getLocale() != null) { + setResponseLocale(pc, locCtxt.getLocale()); + } + } else { + // create empty localization context + locCtxt = new LocalizationContext(); + } + + return locCtxt; + } + + /** + * Determines the client's preferred locales from the request, and compares + * each of the locales (in order of preference) against the available + * locales in order to determine the best matching locale. + * + * @param pageContext the page in which the resource bundle with the given + * base name is requested @param basename the resource bundle's base name + * + * @return the localization context containing the resource bundle with the + * given base name and best matching locale, or null if no + * resource bundle match was found + */ + private static LocalizationContext findMatch(PageContext pageContext, + String basename) { + LocalizationContext locCtxt = null; + + // Determine locale from client's browser settings. + for (Enumeration enum_ = Util + .getRequestLocales((HttpServletRequest) pageContext + .getRequest()); enum_.hasMoreElements();) { + Locale pref = (Locale) enum_.nextElement(); + ResourceBundle match = findMatch(basename, pref); + if (match != null) { + locCtxt = new LocalizationContext(match, pref); + break; + } + } + + return locCtxt; + } + + /** + * Gets the resource bundle with the given base name and preferred locale. + * + * This method calls java.util.ResourceBundle.getBundle(), but ignores its + * return value unless its locale represents an exact or language match with + * the given preferred locale. + * + * @param basename the resource bundle base name @param pref the preferred + * locale + * + * @return the requested resource bundle, or null if no resource + * bundle with the given base name exists or if there is no exact- or + * language-match between the preferred locale and the locale of the bundle + * returned by java.util.ResourceBundle.getBundle(). + */ + private static ResourceBundle findMatch(String basename, Locale pref) { + ResourceBundle match = null; + + try { + ResourceBundle bundle = ResourceBundle.getBundle(basename, pref, + Thread.currentThread().getContextClassLoader()); + Locale avail = bundle.getLocale(); + if (pref.equals(avail)) { + // Exact match + match = bundle; + } else { + /* + * We have to make sure that the match we got is for the + * specified locale. The way ResourceBundle.getBundle() works, + * if a match is not found with (1) the specified locale, it + * tries to match with (2) the current default locale as + * returned by Locale.getDefault() or (3) the root resource + * bundle (basename). We must ignore any match that could have + * worked with (2) or (3). So if an exact match is not found, we + * make the following extra tests: - avail locale must be equal + * to preferred locale - avail country must be empty or equal to + * preferred country (the equality match might have failed on + * the variant) + */ + if (pref.getLanguage().equals(avail.getLanguage()) + && ("".equals(avail.getCountry()) || pref.getCountry() + .equals(avail.getCountry()))) { + /* + * Language match. By making sure the available locale does + * not have a country and matches the preferred locale's + * language, we rule out "matches" based on the container's + * default locale. For example, if the preferred locale is + * "en-US", the container's default locale is "en-UK", and + * there is a resource bundle (with the requested base name) + * available for "en-UK", ResourceBundle.getBundle() will + * return it, but even though its language matches that of + * the preferred locale, we must ignore it, because matches + * based on the container's default locale are not portable + * across different containers with different default + * locales. + */ + match = bundle; + } + } + } catch (MissingResourceException mre) { + throw new IllegalStateException("Shouldn't happen?"); + } + + return match; + } + + /* + This section is based on joda-time DateTimeFormat to handle the two character style pattern missing in Java Time. + */ + + /** + * Creates a formatter from a two character style pattern. The first character + * is the date style, and the second character is the time style. Specify a + * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' + * for full. A date or time may be ommitted by specifying a style character '-'. + * + * @param style two characters from the set {"S", "M", "L", "F", "-"} + * @throws JspException if the style is invalid + * @return a formatter for the specified style + */ + public static DateTimeFormatter createFormatterForStyle(String style) + throws JspException + { + if (style == null || style.length() != 2) { + throw new JspException("Invalid style specification: " + style); + } + FormatStyle dateStyle = selectStyle(style.charAt(0)); + FormatStyle timeStyle = selectStyle(style.charAt(1)); + if (dateStyle == null && timeStyle == null) { + throw new JspException("Style '--' is invalid"); + } + return createFormatterForStyleIndex(dateStyle, timeStyle); + } + + /** + * Gets the formatter for the specified style. + * + * @param dateStyle the date style + * @param timeStyle the time style + * @return the formatter + */ + private static DateTimeFormatter createFormatterForStyleIndex(FormatStyle dateStyle, FormatStyle timeStyle) + throws JspException { + if (dateStyle == null && timeStyle == null) + throw new JspException("Both styles cannot be null."); + else if (dateStyle != null && timeStyle != null) + return DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); + else if (dateStyle == null) + return DateTimeFormatter.ofLocalizedTime(timeStyle); + else + return DateTimeFormatter.ofLocalizedDate(dateStyle); + } + + /** + * Gets the FormatStyle style code from first character. + * + * @param ch the one character style code + * @return the FormatStyle + */ + private static FormatStyle selectStyle(char ch) throws JspException { + switch (ch) { + case 'S': + return SHORT; + case 'M': + return MEDIUM; + case 'L': + return LONG; + case 'F': + return FULL; + case '-': + return null; + default: + throw new JspException("Invalid style character: " + ch); + } + } } diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java index 2bac5e5..328fd9d 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdSupport.java @@ -16,10 +16,6 @@ */ package net.sargue.time.jsptags; -import java.io.IOException; -import java.time.ZoneId; -import java.time.ZoneOffset; - import jakarta.servlet.jsp.JspException; import jakarta.servlet.jsp.JspTagException; import jakarta.servlet.jsp.PageContext; @@ -27,6 +23,10 @@ import jakarta.servlet.jsp.tagext.BodyTagSupport; import jakarta.servlet.jsp.tagext.Tag; +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZoneOffset; + /** * Support for tag handlers for <timeZone>. * @@ -36,109 +36,102 @@ */ public abstract class ZoneIdSupport extends BodyTagSupport { - private static final long serialVersionUID = 1L; - - /** The config key for the time zone. */ - public static final String FMT_TIME_ZONE = "net.sargue.time.zoneId"; - - /** The value attribute. */ - protected Object value; - - /** The zone. */ - private ZoneId zoneId; - - /** - * Constructor. - */ - public ZoneIdSupport() { - super(); - init(); - } - - private void init() { - value = null; - } - - /** - * - * @return the zone - */ - public ZoneId getZoneId() { - return zoneId; - } - - public int doStartTag() throws JspException { - if (value == null) { - zoneId = ZoneOffset.UTC; - } else if (value instanceof String) { - try { - zoneId = ZoneId.of((String) value); - } catch (IllegalArgumentException iae) { - zoneId = ZoneOffset.UTC; - } - } else { - zoneId = (ZoneId) value; - } - return EVAL_BODY_BUFFERED; - } - - public int doEndTag() throws JspException { - try { - pageContext.getOut().print(bodyContent.getString()); - } catch (IOException ioe) { - throw new JspTagException(ioe.toString(), ioe); - } - return EVAL_PAGE; - } - - @Override - public void release() { - init(); - super.release(); - } - - /** - * Determines and returns the time zone to be used by the given action. - *

      - * If the given action is nested inside a <zoneId> action, the time zone - * is taken from the enclosing <zoneId> action. - *

      - * Otherwise, the time zone configuration setting - * net.sargue.time.jsptags.ZoneIdSupport.FMT_TIME_ZONE is used. - * - * @param pc the page containing the action for which the time zone needs - * to be determined - * @param fromTag the action for which the time zone needs to be determined - * - * @return the time zone, or null if the given action is not nested - * inside a <zoneId> action and no time zone configuration setting - * exists - */ - static ZoneId getZoneId(final PageContext pc, final Tag fromTag) { - ZoneId tz = null; - - final Tag t = findAncestorWithClass(fromTag, ZoneIdSupport.class); - if (t != null) { - // use time zone from parent tag - final ZoneIdSupport parent = (ZoneIdSupport) t; - tz = parent.getZoneId(); - } else { - // get time zone from configuration setting - final Object obj = Config.find(pc, FMT_TIME_ZONE); - if (obj != null) { - if (obj instanceof ZoneId) { - tz = (ZoneId) obj; - } else { - try { - tz = ZoneId.of((String) obj); - } catch (IllegalArgumentException iae) { - tz = ZoneOffset.UTC; - } - } - } - } - - return tz; - } + /** The config key for the time zone. */ + public static final String FMT_TIME_ZONE = "net.sargue.time.zoneId"; + + /** The value attribute. */ + protected Object value; + + /** The zone. */ + private ZoneId zoneId; + + /** + * Constructor. + */ + public ZoneIdSupport() { + super(); + init(); + } + + private void init() { + value = null; + } + + public ZoneId getZoneId() { + return zoneId; + } + + public int doStartTag() throws JspException { + if (value == null) { + zoneId = ZoneOffset.UTC; + } else if (value instanceof String) { + try { + zoneId = ZoneId.of((String) value); + } catch (IllegalArgumentException iae) { + zoneId = ZoneOffset.UTC; + } + } else { + zoneId = (ZoneId) value; + } + return EVAL_BODY_BUFFERED; + } + + public int doEndTag() throws JspException { + try { + pageContext.getOut().print(bodyContent.getString()); + } catch (IOException ioe) { + throw new JspTagException(ioe.toString(), ioe); + } + return EVAL_PAGE; + } + + // Releases any resources we may have (or inherit) + public void release() { + init(); + } + + /** + * Determines and returns the time zone to be used by the given action. + *

      + * If the given action is nested inside a <zoneId> action, + * the time zone is taken from the enclosing <zoneId> action. + *

      + * Otherwise, the time zone configuration setting + * net.sargue.time.jsptags.ZoneIdSupport.FMT_TIME_ZONE is used. + * + * @param pc the page containing the action for which the time zone + * needs to be determined + * @param fromTag the action for which the time zone needs to be determined + * + * @return the time zone, or null if the given action is not + * nested inside a <zoneId> action and no time zone configuration + * setting exists + */ + static ZoneId getZoneId(PageContext pc, Tag fromTag) { + ZoneId tz = null; + + Tag t = findAncestorWithClass(fromTag, ZoneIdSupport.class); + if (t != null) { + // use time zone from parent tag + ZoneIdSupport parent = (ZoneIdSupport) t; + tz = parent.getZoneId(); + } else { + // get time zone from configuration setting + Object obj = Config.find(pc, FMT_TIME_ZONE); + if (obj != null) { + if (obj instanceof ZoneId) { + tz = (ZoneId) obj; + } else { + try { + tz = ZoneId.of((String) obj); + } catch (IllegalArgumentException iae) { + tz = ZoneOffset.UTC; + } + } + } + } + + return tz; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java index 96ebb12..5c41261 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java @@ -26,10 +26,8 @@ public class ZoneIdTag extends ZoneIdSupport { private static final long serialVersionUID = 1L; - /** - * @param value for tag attribute - */ - public void setValue(final Object value) { + // for tag attribute + public void setValue(Object value) { this.value = value; } diff --git a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java b/src/test/java/FormatTagTest.java similarity index 95% rename from src/test/java/net/sargue/time/jsptags/FormatTagTest.java rename to src/test/java/FormatTagTest.java index 28f46f3..4c19651 100644 --- a/src/test/java/net/sargue/time/jsptags/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -1,33 +1,23 @@ -package net.sargue.time.jsptags; - import static org.junit.Assert.assertEquals; + +import net.sargue.time.jsptags.FormatTag; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockPageContext; +import org.springframework.mock.web.MockServletContext; + +import jakarta.servlet.jsp.JspException; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.time.DayOfWeek; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Month; -import java.time.MonthDay; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.Year; -import java.time.YearMonth; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.time.temporal.ChronoUnit; import java.time.temporal.WeekFields; import java.util.Locale; import java.util.TimeZone; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockPageContext; -import org.springframework.mock.web.MockServletContext; +import static org.junit.Assert.assertEquals; -import jakarta.servlet.jsp.JspException; /** * Basic format tests. diff --git a/src/test/java/net/sargue/time/jsptags/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java similarity index 93% rename from src/test/java/net/sargue/time/jsptags/ParseLocalDateTagTest.java rename to src/test/java/ParseLocalDateTagTest.java index 77d3abb..5c587c8 100644 --- a/src/test/java/net/sargue/time/jsptags/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -1,8 +1,6 @@ -package net.sargue.time.jsptags; - -import java.io.UnsupportedEncodingException; -import java.time.LocalDate; -import java.util.Locale; +import net.sargue.time.jsptags.ParseInstantTag; +import net.sargue.time.jsptags.ParseLocalDateTag; +import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -11,6 +9,10 @@ import org.springframework.mock.web.MockServletContext; import jakarta.servlet.jsp.JspException; +import java.io.UnsupportedEncodingException; +import java.time.LocalDate; +import java.util.Locale; + /** * Basic parse tests. From 1e0faa55d541adac5346610178207d233ab4755b Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 18 Feb 2022 19:16:18 -0600 Subject: [PATCH 34/48] Reverted more formatting --- .../java/net/sargue/time/jsptags/FormatSupport.java | 12 ++++++------ .../time/jsptags/JavaTimeTagLibraryValidator.java | 10 +++++----- .../java/net/sargue/time/jsptags/ParseSupport.java | 2 +- src/main/java/net/sargue/time/jsptags/Resources.java | 4 +--- .../net/sargue/time/jsptags/SetZoneIdSupport.java | 9 --------- src/main/java/net/sargue/time/jsptags/ZoneIdTag.java | 4 +++- src/test/java/FormatTagTest.java | 3 --- src/test/java/ParseLocalDateTagTest.java | 1 + 8 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index e9a7c21..0f0acda 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -16,6 +16,11 @@ */ package net.sargue.time.jsptags; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.TagSupport; + import java.io.IOException; import java.text.DateFormat; import java.time.*; @@ -23,11 +28,6 @@ import java.time.temporal.TemporalAccessor; import java.util.Locale; -import jakarta.servlet.jsp.JspException; -import jakarta.servlet.jsp.JspTagException; -import jakarta.servlet.jsp.PageContext; -import jakarta.servlet.jsp.tagext.TagSupport; - /** * Support for tag handlers for <formatDate>, the date and time * formatting tag in JSTL 1.0. @@ -83,7 +83,7 @@ public void setScope(String scope) { this.scope = Util.getScope(scope); } - /** + /* * Formats the given instant or partial. */ public int doEndTag() throws JspException { diff --git a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java index 314ff2e..3b2d56f 100644 --- a/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java +++ b/src/main/java/net/sargue/time/jsptags/JavaTimeTagLibraryValidator.java @@ -53,8 +53,8 @@ public class JavaTimeTagLibraryValidator extends TagLibraryValidator { /* * Expression syntax validation has been disabled since when I ported this * code over from Jakarta Taglib, I wanted to reduce dependencies. As I - * understand it, JSP 2.0 containers take over the responsibility - * of handling EL code (both in attribute tags, and externally), so this + * understand it, JSP 2.0 containers take over the responsibility of + * handling EL code (both in attribute tags, and externally), so this * shouldn't be a problem unless you're using something old. If you want to * restore this validation, you must uncomment the various lines in this * source, include the Jakarta Taglib's standard.jar library at build and @@ -74,7 +74,7 @@ public class JavaTimeTagLibraryValidator extends TagLibraryValidator { * multiple Stacks, an understanding of 'depth', and so on all are important * as we recover necessary state upon each callback. This TLV demonstrates * various techniques, from the general "how do I use a SAX parser for a - * various techniques, from the parameters and then validate?" But also, + * TLV?" to "how do I read my init parameters and then validate?" But also, * the specific SAX methodology was kept as general as possible to allow for * experimentation and flexibility. */ @@ -219,7 +219,7 @@ private void fail(String message) { // returns true if the 'scope' attribute is valid protected boolean hasNoInvalidScope(Attributes a) { - final String scope = a.getValue(SCOPE); + String scope = a.getValue(SCOPE); return !((scope != null) && !scope.equals(PAGE_SCOPE) && !scope.equals(REQUEST_SCOPE) && !scope.equals(SESSION_SCOPE) && !scope.equals(APPLICATION_SCOPE)); @@ -236,7 +236,7 @@ protected boolean hasDanglingScope(Attributes a) { } // retrieves the local part of a QName - protected static String getLocalPart(String qname) { + protected String getLocalPart(String qname) { int colon = qname.indexOf(":"); return (colon == -1) ? qname : qname.substring(colon + 1); } diff --git a/src/main/java/net/sargue/time/jsptags/ParseSupport.java b/src/main/java/net/sargue/time/jsptags/ParseSupport.java index 78c9727..cd52769 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ParseSupport.java @@ -209,7 +209,7 @@ public int doEndTag() throws JspException { TemporalAccessor parsed; try { parsed = formatter.parse(input, temporalQuery()); - } catch (final DateTimeParseException e) { + } catch (DateTimeParseException e) { throw new JspException(Resources.getMessage( "PARSE_DATE_PARSE_ERROR", input), e); } diff --git a/src/main/java/net/sargue/time/jsptags/Resources.java b/src/main/java/net/sargue/time/jsptags/Resources.java index 75782a7..6058868 100644 --- a/src/main/java/net/sargue/time/jsptags/Resources.java +++ b/src/main/java/net/sargue/time/jsptags/Resources.java @@ -21,8 +21,7 @@ import java.util.ResourceBundle; /** - *

      - * Provides locale-neutral access to string resources. Only the + *

      Provides locale-neutral access to string resources. Only the * documentation and code are in English. :-) * *

      The major goal, aside from globalization, is convenience. @@ -33,7 +32,6 @@ * * *

      Access to resources with one parameter works like

      - * *
        * Resources.getMessage(MESSAGE_NAME, arg1);
        * 
      diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java index 0d2c714..3dd2ea3 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java @@ -57,20 +57,11 @@ private void init() { scope = PageContext.PAGE_SCOPE; } - /** - * - * @param scope the scope to store the variable in - * @see #setVar(String) - */ @SuppressWarnings("UnusedDeclaration") public void setScope(String scope) { this.scope = Util.getScope(scope); } - /** - * - * @param var the variable to store the result in - */ @SuppressWarnings("UnusedDeclaration") public void setVar(String var) { this.var = var; diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java index 5c41261..ce1053b 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java @@ -16,6 +16,8 @@ */ package net.sargue.time.jsptags; +import jakarta.servlet.jsp.JspTagException; + /** * A handler for <zoneId>. * @@ -27,7 +29,7 @@ public class ZoneIdTag extends ZoneIdSupport { private static final long serialVersionUID = 1L; // for tag attribute - public void setValue(Object value) { + public void setValue(Object value) throws JspTagException { this.value = value; } diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index 4c19651..e214d94 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -1,6 +1,3 @@ -import static org.junit.Assert.assertEquals; - - import net.sargue.time.jsptags.FormatTag; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index 5c587c8..ce14a61 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -5,6 +5,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; From 19f24242ec7e2b4db9c1ac67ba23f0fa0cc51b56 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 18 Feb 2022 19:23:13 -0600 Subject: [PATCH 35/48] Add missing newline at EOF --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index efb840a..f05a518 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,4 @@ crashlytics-build.properties # eclipse bin/ .classpath -.project \ No newline at end of file +.project From f85d1ae94bfcbbdca7605dd3c196a7a26139b479 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 18 Feb 2022 19:24:56 -0600 Subject: [PATCH 36/48] More reverting of formatting. --- src/main/java/net/sargue/time/jsptags/FormatSupport.java | 1 - src/main/java/net/sargue/time/jsptags/Resources.java | 1 - src/test/java/ParseLocalDateTagTest.java | 2 -- 3 files changed, 4 deletions(-) diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index 0f0acda..489c486 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -20,7 +20,6 @@ import jakarta.servlet.jsp.JspTagException; import jakarta.servlet.jsp.PageContext; import jakarta.servlet.jsp.tagext.TagSupport; - import java.io.IOException; import java.text.DateFormat; import java.time.*; diff --git a/src/main/java/net/sargue/time/jsptags/Resources.java b/src/main/java/net/sargue/time/jsptags/Resources.java index 6058868..9c9a537 100644 --- a/src/main/java/net/sargue/time/jsptags/Resources.java +++ b/src/main/java/net/sargue/time/jsptags/Resources.java @@ -26,7 +26,6 @@ * *

      The major goal, aside from globalization, is convenience. * Access to resources with no parameters is made in the form:

      - * *
        * Resources.getMessage(MESSAGE_NAME);
        * 
      diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index ce14a61..15b0fba 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -1,7 +1,6 @@ import net.sargue.time.jsptags.ParseInstantTag; import net.sargue.time.jsptags.ParseLocalDateTag; import org.junit.After; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -14,7 +13,6 @@ import java.time.LocalDate; import java.util.Locale; - /** * Basic parse tests. * From a6fb1126c3eabf875819f4d2311228e95a324e1c Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 19 Feb 2022 06:03:55 -0600 Subject: [PATCH 37/48] Remove local maven repository --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 759adae..07b06a7 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ java { } repositories { - mavenLocal() mavenCentral() // for snapshot release of spring that includes jakarta classes maven { From f189aa74fe62454d8e1027e2427370a2c3dc8a0b Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Sat, 19 Feb 2022 06:06:34 -0600 Subject: [PATCH 38/48] Revert changes to calling super.release The code was working before without these, so keep the PR as minimal as possible. --- src/main/java/net/sargue/time/jsptags/FormatSupport.java | 1 - src/main/java/net/sargue/time/jsptags/ParseSupport.java | 1 - src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index 489c486..001292d 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -159,6 +159,5 @@ public int doEndTag() throws JspException { // Releases any resources we may have (or inherit) public void release() { init(); - super.release(); } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseSupport.java b/src/main/java/net/sargue/time/jsptags/ParseSupport.java index cd52769..9bd390b 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseSupport.java +++ b/src/main/java/net/sargue/time/jsptags/ParseSupport.java @@ -238,7 +238,6 @@ public int doEndTag() throws JspException { // Releases any resources we may have (or inherit) public void release() { init(); - super.release(); } } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java index 3dd2ea3..f6519ca 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java @@ -94,7 +94,6 @@ public int doEndTag() throws JspException { // Releases any resources we may have (or inherit) public void release() { init(); - super.release(); } } From f80290aa07637232e15e9c04b8643872cc0305d0 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Sat, 19 Feb 2022 18:51:14 +0100 Subject: [PATCH 39/48] Update README.md --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1564ff5..420622f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,18 @@ The library is pretty stable right now. Should you have any problem please file an issue. There is no planned development for this library, just bugfix maintenance. +Regarding v2.x and the Java EE to Jakarta EE package name migration +---------------------------------------------------------------------- + +**TL;DR Use only v2.x of the library if you are migrating your project to the new +Jakarta EE 9 or higher with the new package names.** + +Version 2.0.0 switches to the new Jakarta EE package naming. No other changes +are introduced but a major version is used as it will break compilation +of existing code. Thanks to [Jon Schewe](https://github.com/jpschewe) for [the +contribution](https://github.com/sargue/java-time-jsptags/pull/11) that made this possible. + + About ----- @@ -260,7 +272,7 @@ a `zoneId` attribute and is not nested within a `` tag. Build ===== -Build is based on gradle. See build.gradle included in the repository. +Build is based on gradle. See `build.gradle` included in the repository. Changelog --------- @@ -268,6 +280,7 @@ Changelog ### v2.0.0 Updated for jakarta package names for J2EE classes. + Requires Java 17 now due to dependency on spring-test 6.0. ### v1.1.4 From 85f41b7f3ca05a494860bb6f74203af69c982940 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Sun, 20 Feb 2022 09:08:37 +0100 Subject: [PATCH 40/48] javadoc fixes for gradle build --- src/main/java/net/sargue/time/jsptags/Util.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/sargue/time/jsptags/Util.java b/src/main/java/net/sargue/time/jsptags/Util.java index 0fa3252..2334fb9 100644 --- a/src/main/java/net/sargue/time/jsptags/Util.java +++ b/src/main/java/net/sargue/time/jsptags/Util.java @@ -111,7 +111,7 @@ public static Enumeration getRequestLocales(HttpServletRequest request) { * See parseLocale(String, String) for details. * * @param locale the locale string to parse - * @return java.util.Locale object corresponding to the given + * @return {@link java.util.Locale} object corresponding to the given * locale string, or the null if the locale string is null or empty */ public static Locale parseLocale(String locale) { @@ -120,7 +120,7 @@ public static Locale parseLocale(String locale) { /** * Parses the given locale string into its language and (optionally) country - * components, and returns the corresponding java.util.Locale + * components, and returns the corresponding {@link java.util.Locale} * object. * * If the given locale string is null or empty, a null value is returned. @@ -128,7 +128,7 @@ public static Locale parseLocale(String locale) { * @param locale the locale string to parse * @param variant the variant * - * @return java.util.Locale object corresponding to the given + * @return {@link java.util.Locale} object corresponding to the given * locale string, or the null if the locale string is null or empty * * @throws IllegalArgumentException if the given locale does not have a @@ -413,14 +413,14 @@ public static LocalizationContext getLocalizationContext(PageContext pc) { * Check if a match exists between the ordered set of preferred locales and * the available locales, for the given base name. The set of preferred * locales consists of a single locale (if the - * javax.servlet.jsp.jstl.fmt.locale configuration setting is + * {@link Config#FMT_LOCALE} configuration setting is * present) or is equal to the client's preferred locales determined from * the client's browser settings. * *

      * If no match was found in the previous step, check if a match exists * between the fallback locale (given by the - * javax.servlet.jsp.jstl.fmt.fallbackLocale configuration + * {@link Config#FMT_FALLBACK_LOCALE} configuration * setting) and the available locales, for the given base name. * * @param pc Page in which the resource bundle with the given base From 0f63b9adde3150f42402c7165f44931a99b52956 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Sun, 20 Feb 2022 09:11:01 +0100 Subject: [PATCH 41/48] remove warnings from test --- src/test/java/FormatTagTest.java | 20 +++++++++++++++----- src/test/java/ParseLocalDateTagTest.java | 5 +---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index e214d94..2964b30 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -1,13 +1,24 @@ +import jakarta.servlet.jsp.JspException; import net.sargue.time.jsptags.FormatTag; import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; -import jakarta.servlet.jsp.JspException; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.time.*; +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.time.temporal.WeekFields; import java.util.Locale; @@ -27,7 +38,7 @@ public class FormatTagTest { private MockServletContext mockServletContext; @Before - public void setup() throws UnsupportedEncodingException { + public void setup() { Locale.setDefault(Locale.forLanguageTag("ca")); TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); mockServletContext = new MockServletContext(); @@ -179,7 +190,6 @@ public void zonedDateTime() throws IOException, JspException { assertEquals("11:04:47 (Hora estàndard del Centre d’Europa)", format(zonedDateTime, null, "-F")); ZonedDateTime pstZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); - System.out.println(pstZonedDateTime); assertEquals("6/11/15", format(pstZonedDateTime, null, "S-")); assertEquals("6 de nov. 2015", format(pstZonedDateTime, null, "M-")); // check that default matches medium diff --git a/src/test/java/ParseLocalDateTagTest.java b/src/test/java/ParseLocalDateTagTest.java index 15b0fba..66127da 100644 --- a/src/test/java/ParseLocalDateTagTest.java +++ b/src/test/java/ParseLocalDateTagTest.java @@ -1,14 +1,11 @@ -import net.sargue.time.jsptags.ParseInstantTag; +import jakarta.servlet.jsp.JspException; import net.sargue.time.jsptags.ParseLocalDateTag; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockPageContext; import org.springframework.mock.web.MockServletContext; -import jakarta.servlet.jsp.JspException; import java.io.UnsupportedEncodingException; import java.time.LocalDate; import java.util.Locale; From 654a9fbe7f748b669c3d34ed27ec2141f3db9c85 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 23 Feb 2022 16:06:47 +0100 Subject: [PATCH 42/48] update gradle build --- build.gradle | 119 +++++++++++++++++----------------------------- gradle.properties | 1 + 2 files changed, 44 insertions(+), 76 deletions(-) create mode 100644 gradle.properties diff --git a/build.gradle b/build.gradle index 07b06a7..34bc093 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,18 @@ plugins { - id "com.jfrog.bintray" version "1.7.3" - id "java" + id "java-library" id "maven-publish" + id 'signing' } group = 'net.sargue' -archivesBaseName = 'java-time-jsptags' version = '2.0.0' -compileJava.options.encoding = 'UTF-8' -compileTestJava.options.encoding = 'UTF-8' - java { - // set version of Java that the source confirms to. - // The bytecode will be for this version of Java as well, unless targetCompatibility is specified. - sourceCompatibility = JavaVersion.VERSION_17 + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } + withJavadocJar() + withSourcesJar() } repositories { @@ -26,84 +24,53 @@ repositories { } dependencies { - implementation(group: "jakarta.servlet", name: "jakarta.servlet-api", version: "5.0.0") - implementation(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") - implementation(group: "jakarta.servlet.jsp.jstl", name: "jakarta.servlet.jsp.jstl-api", version: "2.0.0") + api(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") + api(group: "jakarta.servlet.jsp.jstl", name: "jakarta.servlet.jsp.jstl-api", version: "2.0.0") - testImplementation(group: "junit", name: "junit", version: "4.12") + testImplementation(group: "junit", name: "junit", version: "4.13.2") testImplementation(group: "org.springframework", name: "spring-test", version: "6.0.0-M2") } jar { manifest { - attributes 'Implementation-Title': 'Java 8 java.time JSP tags', - 'Implementation-Version': version + attributes('Implementation-Title': 'Java 8 java.time JSP tags', + 'Implementation-Version': archiveVersion) } } -task javadocJar(type: Jar) { - classifier = 'javadoc' - from javadoc -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource -} - -artifacts { - archives javadocJar, sourcesJar -} - -// install { -// repositories.mavenInstaller { -// pom.project { -// name 'Java 8 java.time JSP tags' -// description 'JSP tag support for Java 8 java.time (JSR-310)' -// url 'https://github.com/sargue/java-time-jsptags' -// -// scm { -// connection 'scm:git:git@github.com:sargue/java-time-jsptags.git' -// developerConnection 'scm:git:git@github.com:sargue/java-time-jsptags.git' -// url 'git@github.com:sargue/java-time-jsptags.git' -// } -// -// licenses { -// license { -// name 'The Apache License, Version 2.0' -// url 'http://www.apache.org/licenses/LICENSE-2.0.txt' -// } -// } -// -// developers { -// developer { -// id 'sargue' -// name 'Sergi Baila' -// email 'sargue@gmail.com' -// } -// } -// } -// } -// } - -bintray { - user = project.hasProperty('BINTRAY_USER') ? BINTRAY_USER : '' - key = project.hasProperty('BINTRAY_KEY') ? BINTRAY_KEY : '' - configurations = ['archives'] - pkg { - repo = 'maven' - name = 'net.sargue:java-time-jsptags' - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/sargue/java-time-jsptags' - version { - name = project.version - desc = 'JSP tag support for Java 8 java.time (JSR-310)' - - gpg { - sign = true - passphrase = project.hasProperty('BINTRAY_GPG') ? BINTRAY_GPG : '' +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + pom { + name = 'Java 8 java.time JSP tags' + description = 'JSP tag support for Java 8 java.time (JSR-310)' + url = 'https://github.com/sargue/java-time-jsptags' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'sargue' + name = 'Sergi Baila' + email = 'sergibaila@protonmail.com' + } + } + scm { + connection = 'scm:git:git@github.com:sargue/java-time-jsptags.git' + developerConnection = 'scm:git:git@github.com:sargue/java-time-jsptags.git' + url = 'git@github.com:sargue/java-time-jsptags.git' + } } } } } + +signing { + useGpgCmd() + sign publishing.publications.mavenJava +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..656f2e6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Dfile.encoding=UTF-8 \ No newline at end of file From 929107fc66ab7475d7fd530bceb1763c23b35364 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 23 Feb 2022 16:16:45 +0100 Subject: [PATCH 43/48] add github action to perform a gradle build and test --- .github/workflows/gradle-build.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/gradle-build.yml diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml new file mode 100644 index 0000000..b23d61f --- /dev/null +++ b/.github/workflows/gradle-build.yml @@ -0,0 +1,19 @@ +name: Java CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'temurin' + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Execute Gradle build + run: ./gradlew build \ No newline at end of file From 8b50c12b558fb9212ea63712a993520a4183dc2d Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 23 Feb 2022 16:18:24 +0100 Subject: [PATCH 44/48] remove travis control file --- .github/workflows/gradle-build.yml | 2 +- .travis.yml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml index b23d61f..6ccaf82 100644 --- a/.github/workflows/gradle-build.yml +++ b/.github/workflows/gradle-build.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK uses: actions/setup-java@v2 with: java-version: '17' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b5300d0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: java -install: /bin/true -jdk: -- oraclejdk8 From 1c7646223faa730b88cf4bae9f0b196b194a015f Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 23 Feb 2022 16:33:45 +0100 Subject: [PATCH 45/48] tabs to spaces --- .../sargue/time/jsptags/FormatSupport.java | 224 +++++------ .../net/sargue/time/jsptags/FormatTag.java | 122 +++--- .../sargue/time/jsptags/ParseInstantTag.java | 10 +- .../time/jsptags/ParseLocalDateTag.java | 10 +- .../time/jsptags/ParseLocalDateTimeTag.java | 10 +- .../time/jsptags/ParseLocalTimeTag.java | 4 +- .../sargue/time/jsptags/SetZoneIdIdTag.java | 18 +- .../sargue/time/jsptags/SetZoneIdSupport.java | 100 ++--- .../net/sargue/time/jsptags/ZoneIdTag.java | 8 +- src/test/java/FormatTagTest.java | 362 +++++++++--------- 10 files changed, 434 insertions(+), 434 deletions(-) diff --git a/src/main/java/net/sargue/time/jsptags/FormatSupport.java b/src/main/java/net/sargue/time/jsptags/FormatSupport.java index 001292d..c7270b4 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatSupport.java +++ b/src/main/java/net/sargue/time/jsptags/FormatSupport.java @@ -37,127 +37,127 @@ */ public abstract class FormatSupport extends TagSupport { - private static final long serialVersionUID = 1L; - - /** The value attribute. */ - protected Object value; - /** The pattern attribute. */ - protected String pattern; - /** The style attribute. */ - protected String style; - /** The zoneId attribute. */ - protected ZoneId zoneId; - /** The locale attribute. */ - protected Locale locale; - /** The var attribute. */ - private String var; - /** The scope attribute. */ - private int scope; - - /** - * Constructor. - */ - public FormatSupport() { - super(); - init(); - } - - private void init() { - var = null; - value = null; - pattern = null; - style = null; - zoneId = null; - locale = null; - scope = PageContext.PAGE_SCOPE; - } + private static final long serialVersionUID = 1L; + + /** The value attribute. */ + protected Object value; + /** The pattern attribute. */ + protected String pattern; + /** The style attribute. */ + protected String style; + /** The zoneId attribute. */ + protected ZoneId zoneId; + /** The locale attribute. */ + protected Locale locale; + /** The var attribute. */ + private String var; + /** The scope attribute. */ + private int scope; + + /** + * Constructor. + */ + public FormatSupport() { + super(); + init(); + } + + private void init() { + var = null; + value = null; + pattern = null; + style = null; + zoneId = null; + locale = null; + scope = PageContext.PAGE_SCOPE; + } @SuppressWarnings("UnusedDeclaration") - public void setVar(String var) { - this.var = var; - } + public void setVar(String var) { + this.var = var; + } @SuppressWarnings("UnusedDeclaration") - public void setScope(String scope) { - this.scope = Util.getScope(scope); - } - - /* - * Formats the given instant or partial. - */ - public int doEndTag() throws JspException { - if (value == null) { - if (var != null) { - pageContext.removeAttribute(var, scope); - } - return EVAL_PAGE; - } - - // Create formatter - DateTimeFormatter formatter; - if (pattern != null) { - formatter = DateTimeFormatter.ofPattern(pattern); - } else if (style != null) { - formatter = Util.createFormatterForStyle(style); - } else { - // use a medium date (no time) style by default; same as jstl - formatter = Util.createFormatterForStyle("M-"); - } - - // set formatter locale - Locale locale = this.locale; - if (locale == null) { - locale = Util.getFormattingLocale(pageContext, true, + public void setScope(String scope) { + this.scope = Util.getScope(scope); + } + + /* + * Formats the given instant or partial. + */ + public int doEndTag() throws JspException { + if (value == null) { + if (var != null) { + pageContext.removeAttribute(var, scope); + } + return EVAL_PAGE; + } + + // Create formatter + DateTimeFormatter formatter; + if (pattern != null) { + formatter = DateTimeFormatter.ofPattern(pattern); + } else if (style != null) { + formatter = Util.createFormatterForStyle(style); + } else { + // use a medium date (no time) style by default; same as jstl + formatter = Util.createFormatterForStyle("M-"); + } + + // set formatter locale + Locale locale = this.locale; + if (locale == null) { + locale = Util.getFormattingLocale(pageContext, true, DateFormat.getAvailableLocales()); - } - if (locale != null) { - formatter = formatter.withLocale(locale); - } - - // set formatter timezone - ZoneId zoneId = this.zoneId; - if (zoneId == null) { - zoneId = ZoneIdSupport.getZoneId(pageContext, this); - } - if (zoneId != null) { - formatter = formatter.withZone(zoneId); - } else { - if (value instanceof Instant || + } + if (locale != null) { + formatter = formatter.withLocale(locale); + } + + // set formatter timezone + ZoneId zoneId = this.zoneId; + if (zoneId == null) { + zoneId = ZoneIdSupport.getZoneId(pageContext, this); + } + if (zoneId != null) { + formatter = formatter.withZone(zoneId); + } else { + if (value instanceof Instant || value instanceof LocalDateTime || value instanceof OffsetDateTime || value instanceof OffsetTime || value instanceof LocalTime) - // these time objects may need a zone to resolve some patterns - // and/or styles, and as there is no zone we revert to the - // system default zone - formatter = formatter.withZone(ZoneId.systemDefault()); - } - - // format value - String formatted; - if (value instanceof TemporalAccessor) { - formatted = formatter.format((TemporalAccessor) value); - } else { - throw new JspException( + // these time objects may need a zone to resolve some patterns + // and/or styles, and as there is no zone we revert to the + // system default zone + formatter = formatter.withZone(ZoneId.systemDefault()); + } + + // format value + String formatted; + if (value instanceof TemporalAccessor) { + formatted = formatter.format((TemporalAccessor) value); + } else { + throw new JspException( "value attribute of format tag must be a TemporalAccessor," + " was: " + value.getClass().getName()); - } - - if (var != null) { - pageContext.setAttribute(var, formatted, scope); - } else { - try { - pageContext.getOut().print(formatted); - } catch (IOException ioe) { - throw new JspTagException(ioe.toString(), ioe); - } - } - - return EVAL_PAGE; - } - - // Releases any resources we may have (or inherit) - public void release() { - init(); - } + } + + if (var != null) { + pageContext.setAttribute(var, formatted, scope); + } else { + try { + pageContext.getOut().print(formatted); + } catch (IOException ioe) { + throw new JspTagException(ioe.toString(), ioe); + } + } + + return EVAL_PAGE; + } + + // Releases any resources we may have (or inherit) + public void release() { + init(); + } } diff --git a/src/main/java/net/sargue/time/jsptags/FormatTag.java b/src/main/java/net/sargue/time/jsptags/FormatTag.java index 7b83a74..ae353d6 100644 --- a/src/main/java/net/sargue/time/jsptags/FormatTag.java +++ b/src/main/java/net/sargue/time/jsptags/FormatTag.java @@ -33,71 +33,71 @@ @SuppressWarnings("UnusedDeclaration") public class FormatTag extends FormatSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** - * Sets the value attribute. - * - * @param value the value - */ - public void setValue(Object value) { - this.value = value; - } + /** + * Sets the value attribute. + * + * @param value the value + */ + public void setValue(Object value) { + this.value = value; + } - /** - * Sets the style attribute. - * - * @param style the style - */ - public void setStyle(String style) { - this.style = style; - } + /** + * Sets the style attribute. + * + * @param style the style + */ + public void setStyle(String style) { + this.style = style; + } - /** - * Sets the pattern attribute. - * - * @param pattern the pattern - */ - public void setPattern(String pattern) { - this.pattern = pattern; - } + /** + * Sets the pattern attribute. + * + * @param pattern the pattern + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } - /** - * Sets the zone attribute. - * - * @param dtz the zone - * @throws JspTagException incorrect zone or dtz parameter - */ - public void setZoneId(Object dtz) throws JspTagException { - if (dtz == null || (dtz instanceof String && ((String) dtz).isEmpty())) { - this.zoneId = null; - } else if (dtz instanceof ZoneId) { - this.zoneId = (ZoneId) dtz; - } else if (dtz instanceof String) { - try { - this.zoneId = ZoneId.of((String) dtz); - } catch (IllegalArgumentException iae) { - throw new JspTagException("Incorrect Zone: " + dtz); - } - } else - throw new JspTagException("Can only accept ZoneId or String objects."); - } + /** + * Sets the zone attribute. + * + * @param dtz the zone + * @throws JspTagException incorrect zone or dtz parameter + */ + public void setZoneId(Object dtz) throws JspTagException { + if (dtz == null || (dtz instanceof String && ((String) dtz).isEmpty())) { + this.zoneId = null; + } else if (dtz instanceof ZoneId) { + this.zoneId = (ZoneId) dtz; + } else if (dtz instanceof String) { + try { + this.zoneId = ZoneId.of((String) dtz); + } catch (IllegalArgumentException iae) { + throw new JspTagException("Incorrect Zone: " + dtz); + } + } else + throw new JspTagException("Can only accept ZoneId or String objects."); + } - /** - * Sets the style attribute. - * - * @param loc the locale - * @throws JspTagException parameter not a Locale or String - */ - public void setLocale(Object loc) throws JspTagException { - if (loc == null) { - this.locale = null; - } else if (loc instanceof Locale) { - this.locale = (Locale) loc; - } else if (loc instanceof String) { - this.locale = Util.parseLocale((String) loc); - } else - throw new JspTagException("Can only accept Locale or String objects."); - } + /** + * Sets the style attribute. + * + * @param loc the locale + * @throws JspTagException parameter not a Locale or String + */ + public void setLocale(Object loc) throws JspTagException { + if (loc == null) { + this.locale = null; + } else if (loc instanceof Locale) { + this.locale = (Locale) loc; + } else if (loc instanceof String) { + this.locale = Util.parseLocale((String) loc); + } else + throw new JspTagException("Can only accept Locale or String objects."); + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java b/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java index 8103168..e11e2f7 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseInstantTag.java @@ -32,10 +32,10 @@ public class ParseInstantTag extends ParseSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Override - protected TemporalQuery temporalQuery() { - return Instant::from; - } + @Override + protected TemporalQuery temporalQuery() { + return Instant::from; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java index d1807cd..ab4962b 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTag.java @@ -33,10 +33,10 @@ public class ParseLocalDateTag extends ParseSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Override - protected TemporalQuery temporalQuery() { - return LocalDate::from; - } + @Override + protected TemporalQuery temporalQuery() { + return LocalDate::from; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java index dbc9f1c..e9d360d 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalDateTimeTag.java @@ -34,10 +34,10 @@ public class ParseLocalDateTimeTag extends ParseSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Override - protected TemporalQuery temporalQuery() { - return LocalDateTime::from; - } + @Override + protected TemporalQuery temporalQuery() { + return LocalDateTime::from; + } } diff --git a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java index d6c2531..7d8417a 100644 --- a/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java +++ b/src/main/java/net/sargue/time/jsptags/ParseLocalTimeTag.java @@ -34,9 +34,9 @@ public class ParseLocalTimeTag extends ParseSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Override + @Override protected TemporalQuery temporalQuery() { return LocalTime::from; } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java index c204ab0..6c47ee1 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdIdTag.java @@ -27,15 +27,15 @@ */ public class SetZoneIdIdTag extends SetZoneIdSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** - * Sets the value attribute. - * - * @param value the value - */ - public void setValue(Object value) { - this.value = value; - } + /** + * Sets the value attribute. + * + * @param value the value + */ + public void setValue(Object value) { + this.value = value; + } } diff --git a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java index f6519ca..93a327e 100644 --- a/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java +++ b/src/main/java/net/sargue/time/jsptags/SetZoneIdSupport.java @@ -33,67 +33,67 @@ */ public abstract class SetZoneIdSupport extends TagSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** The value attribute. */ - protected Object value; - /** The scope attribute. */ - private int scope; - /** The var attribute. */ - private String var; + /** The value attribute. */ + protected Object value; + /** The scope attribute. */ + private int scope; + /** The var attribute. */ + private String var; - /** - * Constructor. - */ - public SetZoneIdSupport() { - super(); - init(); - } + /** + * Constructor. + */ + public SetZoneIdSupport() { + super(); + init(); + } - // resets local state - private void init() { - value = null; - var = null; - scope = PageContext.PAGE_SCOPE; - } + // resets local state + private void init() { + value = null; + var = null; + scope = PageContext.PAGE_SCOPE; + } @SuppressWarnings("UnusedDeclaration") - public void setScope(String scope) { - this.scope = Util.getScope(scope); - } + public void setScope(String scope) { + this.scope = Util.getScope(scope); + } @SuppressWarnings("UnusedDeclaration") - public void setVar(String var) { - this.var = var; - } + public void setVar(String var) { + this.var = var; + } - public int doEndTag() throws JspException { - ZoneId dateTimeZone; - if (value == null) { - dateTimeZone = ZoneOffset.UTC; - } else if (value instanceof String) { - try { - dateTimeZone = ZoneId.of((String) value); - } catch (IllegalArgumentException iae) { - dateTimeZone = ZoneOffset.UTC; - } - } else { - dateTimeZone = (ZoneId) value; - } + public int doEndTag() throws JspException { + ZoneId dateTimeZone; + if (value == null) { + dateTimeZone = ZoneOffset.UTC; + } else if (value instanceof String) { + try { + dateTimeZone = ZoneId.of((String) value); + } catch (IllegalArgumentException iae) { + dateTimeZone = ZoneOffset.UTC; + } + } else { + dateTimeZone = (ZoneId) value; + } - if (var != null) { - pageContext.setAttribute(var, dateTimeZone, scope); - } else { - Config.set(pageContext, ZoneIdSupport.FMT_TIME_ZONE, + if (var != null) { + pageContext.setAttribute(var, dateTimeZone, scope); + } else { + Config.set(pageContext, ZoneIdSupport.FMT_TIME_ZONE, dateTimeZone, scope); - } + } - return EVAL_PAGE; - } + return EVAL_PAGE; + } - // Releases any resources we may have (or inherit) - public void release() { - init(); - } + // Releases any resources we may have (or inherit) + public void release() { + init(); + } } diff --git a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java index ce1053b..c105655 100644 --- a/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java +++ b/src/main/java/net/sargue/time/jsptags/ZoneIdTag.java @@ -26,11 +26,11 @@ */ public class ZoneIdTag extends ZoneIdSupport { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; // for tag attribute - public void setValue(Object value) throws JspTagException { - this.value = value; - } + public void setValue(Object value) throws JspTagException { + this.value = value; + } } diff --git a/src/test/java/FormatTagTest.java b/src/test/java/FormatTagTest.java index 2964b30..98f604f 100644 --- a/src/test/java/FormatTagTest.java +++ b/src/test/java/FormatTagTest.java @@ -35,185 +35,185 @@ */ public class FormatTagTest { - private MockServletContext mockServletContext; - - @Before - public void setup() { - Locale.setDefault(Locale.forLanguageTag("ca")); - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); - mockServletContext = new MockServletContext(); - } - - @Test - public void dayOfWeekTest() throws IOException, JspException { - // find the first day of the week for the locale - final Locale l = Locale.getDefault(); - final DayOfWeek firstDayOfWeek = WeekFields.of(l).getFirstDayOfWeek(); - final LocalDate firstDayOfWeekLd = LocalDate.now().with(firstDayOfWeek); - - // Find the Monday after the first day of the week - LocalDate mondayLd = firstDayOfWeekLd.with(DayOfWeek.MONDAY); - if (mondayLd.isBefore(firstDayOfWeekLd)) { - mondayLd = mondayLd.plusDays(7); - } - - // the localized day of week (format specifier 'e') is 1 on the first day of the - // week and 2 on the second day of the week for the locale - final long mondayNumeric = firstDayOfWeekLd.until(mondayLd, ChronoUnit.DAYS) + 1; - assertEquals(String.format("dl. dl. dl. dilluns %1$d %1$02d dl. dilluns", mondayNumeric), - format(DayOfWeek.MONDAY, "E EE EEE EEEE e ee eee eeee", null)); - - final LocalDate tuesdayLd = mondayLd.plusDays(1); - final long tuesdayNumeric = firstDayOfWeekLd.until(tuesdayLd, ChronoUnit.DAYS) + 1; - assertEquals(String.format("dt. dt. dt. dimarts %1$d %1$02d dt. dimarts", tuesdayNumeric), - format(DayOfWeek.TUESDAY, "E EE EEE EEEE e ee eee eeee", null)); - } - - @Test - public void instantTest() throws JspException, IOException { - Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); - assertEquals("6/11/15", format(instant, null, "S-")); - assertEquals("6 de nov. 2015", format(instant, null, "M-")); - // check default matches medium - assertEquals(format(instant, null, "M-"), format(instant, null, null)); - - assertEquals("6 de novembre de 2015", format(instant, null, "L-")); - assertEquals("divendres, 6 de novembre de 2015", format(instant, null, "F-")); - assertEquals("10:45", format(instant, null, "-S")); - assertEquals("10:45:33", format(instant, null, "-M")); - assertEquals("10:45:33 CET", format(instant, null, "-L")); - assertEquals("10:45:33 (Hora estàndard del Centre d’Europa)", format(instant, null, "-F")); - } - - @Test - public void localDateTest() throws IOException, JspException { - LocalDate localDate = LocalDate.parse("2015-11-06"); - assertEquals("06/11/2015", format(localDate, "dd/MM/yyyy", null)); - assertEquals("6/11/15", format(localDate, null, "S-")); - assertEquals("6 de nov. 2015", format(localDate, null, "M-")); - - // check that default matches medium - assertEquals(format(localDate, null, "M-"), format(localDate, null, null)); - - assertEquals("6 de novembre de 2015", format(localDate, null, "L-")); - assertEquals("divendres, 6 de novembre de 2015", format(localDate, null, "F-")); - } - - @Test - public void localTimeTest() throws IOException, JspException { - LocalTime localTime = LocalTime.parse("10:53:55.913"); - assertEquals("10:53:55", format(localTime, "HH:mm:ss", null)); - assertEquals("10:53", format(localTime, null, "-S")); - assertEquals("10:53:55", format(localTime, null, "-M")); - assertEquals("10:53:55 CET", format(localTime, null, "-L")); - assertEquals("10:53:55 (Hora del Centre d’Europa)", format(localTime, null, "-F")); - } - - @Test - public void localDateTimeTest() throws IOException, JspException { - LocalDateTime localDateTime = LocalDateTime.parse("2015-11-06T10:55:53.456"); - assertEquals("06/11/2015 10:55:53", format(localDateTime, "dd/MM/yyyy HH:mm:ss", null)); - assertEquals("6/11/15", format(localDateTime, null, "S-")); - assertEquals("6 de nov. 2015", format(localDateTime, null, "M-")); - // check default matches medium - assertEquals(format(localDateTime, null, "M-"), format(localDateTime, null, null)); - - assertEquals("6 de novembre de 2015", format(localDateTime, null, "L-")); - assertEquals("divendres, 6 de novembre de 2015", format(localDateTime, null, "F-")); - assertEquals("10:55", format(localDateTime, null, "-S")); - assertEquals("10:55:53", format(localDateTime, null, "-M")); - assertEquals("10:55:53 CET", format(localDateTime, null, "-L")); - assertEquals("10:55:53 (Hora estàndard del Centre d’Europa)", format(localDateTime, null, "-F")); - } - - @Test - public void monthTest() throws IOException, JspException { - assertEquals("4 04 d’abr. d’abril 4 04 abr. abril", format(Month.APRIL, "M MM MMM MMMM L LL LLL LLLL", null)); - } - - @Test - public void monthDayTest() throws IOException, JspException { - MonthDay monthDay = MonthDay.parse("--11-06"); - assertEquals("11 6", format(monthDay, "M d", null)); - } - - @Test - public void offsetDateTimeTest() throws IOException, JspException { - OffsetDateTime offsetDateTime = OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); - assertEquals("6/11/15", format(offsetDateTime, null, "S-")); - assertEquals("6 de nov. 2015", format(offsetDateTime, null, "M-")); - // check that default matches medium - assertEquals(format(offsetDateTime, null, "M-"), format(offsetDateTime, null, null)); - - assertEquals("6 de novembre de 2015", format(offsetDateTime, null, "L-")); - assertEquals("divendres, 6 de novembre de 2015", format(offsetDateTime, null, "F-")); - assertEquals("10:58", format(offsetDateTime, null, "-S")); - assertEquals("10:58:21", format(offsetDateTime, null, "-M")); - assertEquals("10:58:21 CET", format(offsetDateTime, null, "-L")); - assertEquals("10:58:21 (Hora estàndard del Centre d’Europa)", format(offsetDateTime, null, "-F")); - } - - @Test - public void offsetTimeTest() throws IOException, JspException { - OffsetTime offsetTime = OffsetTime.parse("11:01:39.810+01:00"); - assertEquals("11:01:39", format(offsetTime, "HH:mm:ss", null)); - assertEquals("11:01", format(offsetTime, null, "-S")); - assertEquals("11:01:39", format(offsetTime, null, "-M")); - assertEquals("11:01:39 CET", format(offsetTime, null, "-L")); - assertEquals("11:01:39 (Hora del Centre d’Europa)", format(offsetTime, null, "-F")); - } - - @Test - public void yearTest() throws IOException, JspException { - Year year = Year.parse("2015"); - assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC després de Crist", - format(year, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG", null)); - } - - @Test - public void yearMonthTest() throws IOException, JspException { - YearMonth yearMonth = YearMonth.parse("2015-11"); - assertEquals("dC 2015 2015 11 11 4 04 4T 4t trimestre", format(yearMonth, "G u y M L Q QQ QQQ QQQQ", null)); - } - - @Test - public void zonedDateTime() throws IOException, JspException { - ZonedDateTime zonedDateTime = ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); - assertEquals("6 de nov. 2015", format(zonedDateTime, null, null)); - assertEquals("6/11/15", format(zonedDateTime, null, "S-")); - assertEquals("6 de nov. 2015", format(zonedDateTime, null, "M-")); - assertEquals("6 de novembre de 2015", format(zonedDateTime, null, "L-")); - assertEquals("divendres, 6 de novembre de 2015", format(zonedDateTime, null, "F-")); - assertEquals("11:04", format(zonedDateTime, null, "-S")); - assertEquals("11:04:47", format(zonedDateTime, null, "-M")); - assertEquals("11:04:47 CET", format(zonedDateTime, null, "-L")); - assertEquals("11:04:47 (Hora estàndard del Centre d’Europa)", format(zonedDateTime, null, "-F")); - - ZonedDateTime pstZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); - assertEquals("6/11/15", format(pstZonedDateTime, null, "S-")); - assertEquals("6 de nov. 2015", format(pstZonedDateTime, null, "M-")); - // check that default matches medium - assertEquals(format(pstZonedDateTime, null, "M-"), format(pstZonedDateTime, null, null)); - - assertEquals("6 de novembre de 2015", format(pstZonedDateTime, null, "L-")); - assertEquals("divendres, 6 de novembre de 2015", format(pstZonedDateTime, null, "F-")); - assertEquals("2:04", format(pstZonedDateTime, null, "-S")); - assertEquals("2:04:47", format(pstZonedDateTime, null, "-M")); - assertEquals("2:04:47 PST", format(pstZonedDateTime, null, "-L")); - assertEquals("2:04:47 (Hora estàndard del Pacífic d’Amèrica del Nord)", format(pstZonedDateTime, null, "-F")); - } - - private String format(Object o, String pattern, String style) throws JspException, IOException { - MockPageContext mockPageContext = new MockPageContext(mockServletContext); - mockPageContext.getRequest().setCharacterEncoding("UTF-8"); - mockPageContext.getResponse().setCharacterEncoding("UTF-8"); - FormatTag formatTag = new FormatTag(); - formatTag.setPageContext(mockPageContext); - - formatTag.setPattern(pattern); - formatTag.setStyle(style); - formatTag.setValue(o); - formatTag.doEndTag(); - return mockPageContext.getContentAsString(); - } + private MockServletContext mockServletContext; + + @Before + public void setup() { + Locale.setDefault(Locale.forLanguageTag("ca")); + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); + mockServletContext = new MockServletContext(); + } + + @Test + public void dayOfWeekTest() throws IOException, JspException { + // find the first day of the week for the locale + final Locale l = Locale.getDefault(); + final DayOfWeek firstDayOfWeek = WeekFields.of(l).getFirstDayOfWeek(); + final LocalDate firstDayOfWeekLd = LocalDate.now().with(firstDayOfWeek); + + // Find the Monday after the first day of the week + LocalDate mondayLd = firstDayOfWeekLd.with(DayOfWeek.MONDAY); + if (mondayLd.isBefore(firstDayOfWeekLd)) { + mondayLd = mondayLd.plusDays(7); + } + + // the localized day of week (format specifier 'e') is 1 on the first day of the + // week and 2 on the second day of the week for the locale + final long mondayNumeric = firstDayOfWeekLd.until(mondayLd, ChronoUnit.DAYS) + 1; + assertEquals(String.format("dl. dl. dl. dilluns %1$d %1$02d dl. dilluns", mondayNumeric), + format(DayOfWeek.MONDAY, "E EE EEE EEEE e ee eee eeee", null)); + + final LocalDate tuesdayLd = mondayLd.plusDays(1); + final long tuesdayNumeric = firstDayOfWeekLd.until(tuesdayLd, ChronoUnit.DAYS) + 1; + assertEquals(String.format("dt. dt. dt. dimarts %1$d %1$02d dt. dimarts", tuesdayNumeric), + format(DayOfWeek.TUESDAY, "E EE EEE EEEE e ee eee eeee", null)); + } + + @Test + public void instantTest() throws JspException, IOException { + Instant instant = Instant.parse("2015-11-06T09:45:33.652Z"); + assertEquals("6/11/15", format(instant, null, "S-")); + assertEquals("6 de nov. 2015", format(instant, null, "M-")); + // check default matches medium + assertEquals(format(instant, null, "M-"), format(instant, null, null)); + + assertEquals("6 de novembre de 2015", format(instant, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(instant, null, "F-")); + assertEquals("10:45", format(instant, null, "-S")); + assertEquals("10:45:33", format(instant, null, "-M")); + assertEquals("10:45:33 CET", format(instant, null, "-L")); + assertEquals("10:45:33 (Hora estàndard del Centre d’Europa)", format(instant, null, "-F")); + } + + @Test + public void localDateTest() throws IOException, JspException { + LocalDate localDate = LocalDate.parse("2015-11-06"); + assertEquals("06/11/2015", format(localDate, "dd/MM/yyyy", null)); + assertEquals("6/11/15", format(localDate, null, "S-")); + assertEquals("6 de nov. 2015", format(localDate, null, "M-")); + + // check that default matches medium + assertEquals(format(localDate, null, "M-"), format(localDate, null, null)); + + assertEquals("6 de novembre de 2015", format(localDate, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(localDate, null, "F-")); + } + + @Test + public void localTimeTest() throws IOException, JspException { + LocalTime localTime = LocalTime.parse("10:53:55.913"); + assertEquals("10:53:55", format(localTime, "HH:mm:ss", null)); + assertEquals("10:53", format(localTime, null, "-S")); + assertEquals("10:53:55", format(localTime, null, "-M")); + assertEquals("10:53:55 CET", format(localTime, null, "-L")); + assertEquals("10:53:55 (Hora del Centre d’Europa)", format(localTime, null, "-F")); + } + + @Test + public void localDateTimeTest() throws IOException, JspException { + LocalDateTime localDateTime = LocalDateTime.parse("2015-11-06T10:55:53.456"); + assertEquals("06/11/2015 10:55:53", format(localDateTime, "dd/MM/yyyy HH:mm:ss", null)); + assertEquals("6/11/15", format(localDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(localDateTime, null, "M-")); + // check default matches medium + assertEquals(format(localDateTime, null, "M-"), format(localDateTime, null, null)); + + assertEquals("6 de novembre de 2015", format(localDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(localDateTime, null, "F-")); + assertEquals("10:55", format(localDateTime, null, "-S")); + assertEquals("10:55:53", format(localDateTime, null, "-M")); + assertEquals("10:55:53 CET", format(localDateTime, null, "-L")); + assertEquals("10:55:53 (Hora estàndard del Centre d’Europa)", format(localDateTime, null, "-F")); + } + + @Test + public void monthTest() throws IOException, JspException { + assertEquals("4 04 d’abr. d’abril 4 04 abr. abril", format(Month.APRIL, "M MM MMM MMMM L LL LLL LLLL", null)); + } + + @Test + public void monthDayTest() throws IOException, JspException { + MonthDay monthDay = MonthDay.parse("--11-06"); + assertEquals("11 6", format(monthDay, "M d", null)); + } + + @Test + public void offsetDateTimeTest() throws IOException, JspException { + OffsetDateTime offsetDateTime = OffsetDateTime.parse("2015-11-06T10:58:21.207+01:00"); + assertEquals("6/11/15", format(offsetDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(offsetDateTime, null, "M-")); + // check that default matches medium + assertEquals(format(offsetDateTime, null, "M-"), format(offsetDateTime, null, null)); + + assertEquals("6 de novembre de 2015", format(offsetDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(offsetDateTime, null, "F-")); + assertEquals("10:58", format(offsetDateTime, null, "-S")); + assertEquals("10:58:21", format(offsetDateTime, null, "-M")); + assertEquals("10:58:21 CET", format(offsetDateTime, null, "-L")); + assertEquals("10:58:21 (Hora estàndard del Centre d’Europa)", format(offsetDateTime, null, "-F")); + } + + @Test + public void offsetTimeTest() throws IOException, JspException { + OffsetTime offsetTime = OffsetTime.parse("11:01:39.810+01:00"); + assertEquals("11:01:39", format(offsetTime, "HH:mm:ss", null)); + assertEquals("11:01", format(offsetTime, null, "-S")); + assertEquals("11:01:39", format(offsetTime, null, "-M")); + assertEquals("11:01:39 CET", format(offsetTime, null, "-L")); + assertEquals("11:01:39 (Hora del Centre d’Europa)", format(offsetTime, null, "-F")); + } + + @Test + public void yearTest() throws IOException, JspException { + Year year = Year.parse("2015"); + assertEquals("2015 15 2015 2015 2015 15 2015 2015 dC dC dC després de Crist", + format(year, "u uu uuu uuuu y yy yyyy yyyy G GG GGG GGGG", null)); + } + + @Test + public void yearMonthTest() throws IOException, JspException { + YearMonth yearMonth = YearMonth.parse("2015-11"); + assertEquals("dC 2015 2015 11 11 4 04 4T 4t trimestre", format(yearMonth, "G u y M L Q QQ QQQ QQQQ", null)); + } + + @Test + public void zonedDateTime() throws IOException, JspException { + ZonedDateTime zonedDateTime = ZonedDateTime.parse("2015-11-06T11:04:47.409+01:00[Europe/Paris]"); + assertEquals("6 de nov. 2015", format(zonedDateTime, null, null)); + assertEquals("6/11/15", format(zonedDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(zonedDateTime, null, "M-")); + assertEquals("6 de novembre de 2015", format(zonedDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(zonedDateTime, null, "F-")); + assertEquals("11:04", format(zonedDateTime, null, "-S")); + assertEquals("11:04:47", format(zonedDateTime, null, "-M")); + assertEquals("11:04:47 CET", format(zonedDateTime, null, "-L")); + assertEquals("11:04:47 (Hora estàndard del Centre d’Europa)", format(zonedDateTime, null, "-F")); + + ZonedDateTime pstZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles")); + assertEquals("6/11/15", format(pstZonedDateTime, null, "S-")); + assertEquals("6 de nov. 2015", format(pstZonedDateTime, null, "M-")); + // check that default matches medium + assertEquals(format(pstZonedDateTime, null, "M-"), format(pstZonedDateTime, null, null)); + + assertEquals("6 de novembre de 2015", format(pstZonedDateTime, null, "L-")); + assertEquals("divendres, 6 de novembre de 2015", format(pstZonedDateTime, null, "F-")); + assertEquals("2:04", format(pstZonedDateTime, null, "-S")); + assertEquals("2:04:47", format(pstZonedDateTime, null, "-M")); + assertEquals("2:04:47 PST", format(pstZonedDateTime, null, "-L")); + assertEquals("2:04:47 (Hora estàndard del Pacífic d’Amèrica del Nord)", format(pstZonedDateTime, null, "-F")); + } + + private String format(Object o, String pattern, String style) throws JspException, IOException { + MockPageContext mockPageContext = new MockPageContext(mockServletContext); + mockPageContext.getRequest().setCharacterEncoding("UTF-8"); + mockPageContext.getResponse().setCharacterEncoding("UTF-8"); + FormatTag formatTag = new FormatTag(); + formatTag.setPageContext(mockPageContext); + + formatTag.setPattern(pattern); + formatTag.setStyle(style); + formatTag.setValue(o); + formatTag.doEndTag(); + return mockPageContext.getContentAsString(); + } } From 99521b7f91e84955d1291335ba94482f824771e5 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 23 Feb 2022 16:51:13 +0100 Subject: [PATCH 46/48] remove javadoc warnings --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index 34bc093..f352bc7 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,12 @@ java { withSourcesJar() } +allprojects { + tasks.withType(Javadoc) { + options.addBooleanOption('Xdoclint:none', true) + } +} + repositories { mavenCentral() // for snapshot release of spring that includes jakarta classes From fd04804874e2f4cdb9e50bd44f67bb2af3ed4bec Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Wed, 23 Feb 2022 17:52:22 +0100 Subject: [PATCH 47/48] gradle section to publish to maven central --- build.gradle | 11 +++++++++++ gradle.properties | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f352bc7..5e4087b 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,17 @@ publishing { } } } + repositories { + maven { + def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials { + username sonatypeUsername + password sonatypePassword + } + } + } } signing { diff --git a/gradle.properties b/gradle.properties index 656f2e6..c04959f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,3 @@ -org.gradle.jvmargs=-Dfile.encoding=UTF-8 \ No newline at end of file +org.gradle.jvmargs=-Dfile.encoding=UTF-8 +sonatypeUsername= +sonatypePassword= \ No newline at end of file From 0751516ac39c563fe116b4c203daef2a7a4f3836 Mon Sep 17 00:00:00 2001 From: Sergi Baila Date: Tue, 26 Mar 2024 08:59:10 +0100 Subject: [PATCH 48/48] changed dependency types to "compileOnly" --- README.md | 4 ++++ build.gradle | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 420622f..49a47e1 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,10 @@ Build is based on gradle. See `build.gradle` included in the repository. Changelog --------- +### v2.0.2 + +Changed dependency types to "compileOnly" so this library is not leaking specific JSP/JSTL libraries. + ### v2.0.0 Updated for jakarta package names for J2EE classes. diff --git a/build.gradle b/build.gradle index 5e4087b..eb7f9db 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'net.sargue' -version = '2.0.0' +version = '2.0.2' java { toolchain { @@ -30,10 +30,11 @@ repositories { } dependencies { - api(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") - api(group: "jakarta.servlet.jsp.jstl", name: "jakarta.servlet.jsp.jstl-api", version: "2.0.0") - + compileOnly(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") + compileOnly(group: "jakarta.servlet.jsp.jstl", name: "jakarta.servlet.jsp.jstl-api", version: "2.0.0") + testImplementation(group: "jakarta.servlet.jsp", name: "jakarta.servlet.jsp-api", version: "3.0.0") + testImplementation(group: "jakarta.servlet.jsp.jstl", name: "jakarta.servlet.jsp.jstl-api", version: "2.0.0") testImplementation(group: "junit", name: "junit", version: "4.13.2") testImplementation(group: "org.springframework", name: "spring-test", version: "6.0.0-M2") }