From bbb481b8ecbaa3e5778ae6e547f0247881c34822 Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 21 Jan 2022 22:01:28 -0600 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 101a91358b63a330e00fb8b3beab923246f20d5b Mon Sep 17 00:00:00 2001 From: Jon Schewe Date: Fri, 18 Feb 2022 19:09:08 -0600 Subject: [PATCH 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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(); } }