This is an automated email from the ASF dual-hosted git repository. ddekany pushed a commit to branch FREEMARKER-35 in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit 28545e14abab92d7497c73a00761d3e11559061e Author: ddekany <[email protected]> AuthorDate: Sat Jul 16 20:59:36 2022 +0200 [FREEMARKER-35] Code cleanup in Temporal related code --- src/main/java/freemarker/core/Configurable.java | 25 +++--- ...eTimeFormatterBasedTemplateTemporalFormat.java} | 93 ++++++++++++---------- src/main/java/freemarker/core/Environment.java | 32 ++++---- .../ISOLikeTemplateTemporalTemporalFormat.java | 23 ++++-- .../core/ISOTemplateTemporalFormatFactory.java | 10 ++- .../core/JavaTemplateTemporalFormat.java | 7 +- .../core/JavaTemplateTemporalFormatFactory.java | 5 +- .../core/MissingTimeZoneParserPolicy.java | 5 +- .../java/freemarker/core/TemplateDateFormat.java | 44 +++++----- .../java/freemarker/core/TemplateNumberFormat.java | 45 ++++++----- .../freemarker/core/TemplateTemporalFormat.java | 62 +++++++++++---- .../core/TemplateTemporalFormatFactory.java | 2 +- .../java/freemarker/core/TemplateValueFormat.java | 3 +- .../core/XSTemplateTemporalFormatFactory.java | 7 +- src/main/java/freemarker/core/_MessageUtil.java | 4 +- src/main/java/freemarker/core/_TemporalUtils.java | 14 ++-- .../java/freemarker/template/Configuration.java | 4 + .../freemarker/template/TemplateDateModel.java | 9 +-- .../freemarker/template/TemplateTemporalModel.java | 5 +- .../core/AbstractTemporalFormatTest.java | 2 +- .../core/ISOLikeTemplateTemporalFormatTest.java | 4 +- .../java/freemarker/core/_TemporalUtilsTest.java | 12 +-- 22 files changed, 246 insertions(+), 171 deletions(-) diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java index 57becfac..cc3ea3c9 100644 --- a/src/main/java/freemarker/core/Configurable.java +++ b/src/main/java/freemarker/core/Configurable.java @@ -1440,7 +1440,8 @@ public class Configurable { */ public String getTemporalFormat(Class<? extends Temporal> temporalClass) { Objects.requireNonNull(temporalClass); - // The temporal classes are final (for now at least), so we can use == operator instead of instanceof. + // We can use == operator instead of instanceof, as temporal classes are final in Java 8. Just in case that + // changes in some later Java version, we have "else" branch that retries with a normalized class. if (temporalClass == Instant.class || temporalClass == LocalDateTime.class || temporalClass == ZonedDateTime.class @@ -1455,14 +1456,14 @@ public class Configurable { } else if (temporalClass == YearMonth.class) { return getYearMonthFormat(); } else { - // Handle the unlikely situation that in some future Java version we can have subclasses. - Class<? extends Temporal> normTemporalClass = + // Branch to handle the unlikely situation that in some Java version we can have subclasses. + Class<? extends Temporal> normalizedTemporalClass = _TemporalUtils.normalizeSupportedTemporalClass(temporalClass); - if (normTemporalClass == temporalClass) { + if (normalizedTemporalClass == temporalClass) { throw new IllegalArgumentException("There's no temporal format setting for this class: " + temporalClass.getName()); } else { - return getTemporalFormat(normTemporalClass); + return getTemporalFormat(normalizedTemporalClass); } } } @@ -1503,7 +1504,7 @@ public class Configurable { * date_format}, {@link #setDateTimeFormat(String) time_format}, and {@link #setDateTimeFormat(String) * datetime_format} settings with values starting with <code>@<i>name</i></code>. * - * <p>It's important that the formats you set here will be only used when formatting {@link Date}-s, not when + * <p>It's important that the formats you set here will be only visible when formatting {@link Date}-s, not when * formatting {@link Temporal}-s. For the later, use {@link #setCustomTemporalFormats(Map)}. Ideally, you set the * same custom formatter names with both methods. * @@ -1528,7 +1529,8 @@ public class Configurable { } /** - * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}. + * Tells if this setting is set directly in this object, or its value is coming from the {@link #getParent() + * parent}. * * @since 2.3.24 */ @@ -1588,16 +1590,16 @@ public class Configurable { } /** - * Associates names with {@link Temporal} formatter factories, which then can be referred by the + * Associates names with {@link TemplateTemporalFormatFactory}-es, which then can be referred by the * {@link #setDateTimeFormat(String) date_time_format}, {@link #setDateFormat(String) date_format}, and * {@link #setTimeFormat(String) time_format} settings, with values starting with <code>@<i>name</i></code>. * - * <p>It's important that the formats you set here will be only used when formatting {@link Temporal}-s, not when + * <p>It's important that the formats you set here will be only visible when formatting {@link Temporal}-s, not when * formatting {@link Date}-s. For the later, use {@link #setCustomDateFormats(Map)}. Ideally, you set the same * custom formatter names with both methods. * * @param customTemporalFormats - * Can't be {@code null}. The name must start with an UNICODE letter, and can only contain UNICODE + * Can't be {@code null}. The name must start with a UNICODE letter, and can only contain UNICODE * letters and digits. * * @see #setCustomDateFormats(Map) @@ -1611,7 +1613,8 @@ public class Configurable { } /** - * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}. + * Tells if this setting is set directly in this object, or its value is coming from the {@link #getParent() + * parent}. * * @since 2.3.32 */ diff --git a/src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java b/src/main/java/freemarker/core/DateTimeFormatterBasedTemplateTemporalFormat.java similarity index 60% rename from src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java rename to src/main/java/freemarker/core/DateTimeFormatterBasedTemplateTemporalFormat.java index b83ec05d..46c4db0c 100644 --- a/src/main/java/freemarker/core/JavaOrISOLikeTemplateTemporalFormat.java +++ b/src/main/java/freemarker/core/DateTimeFormatterBasedTemplateTemporalFormat.java @@ -19,6 +19,7 @@ package freemarker.core; +import static freemarker.core.MissingTimeZoneParserPolicy.*; import static freemarker.core._TemporalUtils.*; import static freemarker.template.utility.StringUtil.*; @@ -37,15 +38,15 @@ import java.util.Objects; import java.util.TimeZone; /** - * Was created ad-hoc to contain whatever happens to be common between some of our {@link TemplateTemporalFormat}-s. + * Common logic among our {@link TemplateTemporalFormat}-s that are based on {@link TemplateTemporalFormat}. */ -abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalFormat { +abstract class DateTimeFormatterBasedTemplateTemporalFormat extends TemplateTemporalFormat { protected final Class<? extends Temporal> temporalClass; protected final boolean isLocalTemporalClass; protected final TimeZone timeZone; protected final ZoneId zoneId; - public JavaOrISOLikeTemplateTemporalFormat( + public DateTimeFormatterBasedTemplateTemporalFormat( Class<? extends Temporal> temporalClass, TimeZone timeZone) { this.temporalClass = Objects.requireNonNull(_TemporalUtils.normalizeSupportedTemporalClass(temporalClass)); this.isLocalTemporalClass = isLocalTemporalClass(this.temporalClass); @@ -58,6 +59,11 @@ abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalForma } } + /** + * Called from {@link TemplateTemporalFormat#parse(String, MissingTimeZoneParserPolicy)}, when that has figured + * out the {@link DateTimeFormatter} to use, this method will deal with the time zone related matters, and some + * more (like converting parsing exceptions). + */ protected Temporal parse( String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy, DateTimeFormatter parserDateTimeFormatter) throws UnparsableValueException { @@ -70,39 +76,36 @@ abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalForma return parseResult.query(_TemporalUtils.getTemporalQuery(temporalClass)); } - switch (missingTimeZoneParserPolicy) { - case ASSUME_CURRENT_TIME_ZONE: - case FALL_BACK_TO_LOCAL_TEMPORAL: - boolean fallbackToLocal = missingTimeZoneParserPolicy == MissingTimeZoneParserPolicy.FALL_BACK_TO_LOCAL_TEMPORAL; - Class<? extends Temporal> localFallbackTemporalClass; - if (temporalClass == Instant.class) { - localFallbackTemporalClass = LocalDateTime.class; - } else { - localFallbackTemporalClass = getLocalTemporalClassForNonLocal(temporalClass); - if (localFallbackTemporalClass == null) { - throw newUnparsableValueException( - s, parserDateTimeFormatter, - "String contains no zone offset, and no local temporal type " - + "exists for target type " + temporalClass.getName(), - null); - } - if (!fallbackToLocal && temporalClass == OffsetTime.class) { - throw newUnparsableValueException( - s, parserDateTimeFormatter, - "It's not possible to parse the string that contains no zone offset to OffsetTime, " - + "because we don't know the day, and hence can't account for " - + "Daylight Saving Time, and thus we can't apply the current time zone." - + temporalClass.getName(), - null); - } - } + if (missingTimeZoneParserPolicy == ASSUME_CURRENT_TIME_ZONE || + missingTimeZoneParserPolicy == FALL_BACK_TO_LOCAL_TEMPORAL) { + boolean fallbackToLocal = missingTimeZoneParserPolicy == FALL_BACK_TO_LOCAL_TEMPORAL; + Class<? extends Temporal> localFallbackTemporalClass; + localFallbackTemporalClass = tryGetLocalTemporalClassForNonLocal(temporalClass); + if (localFallbackTemporalClass == null) { + throw newUnparsableValueException( + s, parserDateTimeFormatter, + "String contains no zone, nor offset, and no local variant exists for target type " + + temporalClass.getName(), + null); + } + if (!fallbackToLocal && temporalClass == OffsetTime.class) { + throw newUnparsableValueException( + s, parserDateTimeFormatter, + "It's not possible to parse a string that contains no zone, nor offset, to OffsetTime. " + + "We don't know the day, and hence can't account for Daylight Saving Time, " + + "and thus we can't use the current time zone." + + temporalClass.getName(), + null); + } - Temporal resultTemporal = parseResult.query( - _TemporalUtils.getTemporalQuery(localFallbackTemporalClass)); - if (fallbackToLocal) { - return resultTemporal; - } - ZonedDateTime zonedDateTime = ((LocalDateTime) resultTemporal).atZone(zoneId); + Temporal resultLocalTemporal = parseResult.query( + getTemporalQuery(localFallbackTemporalClass)); + if (fallbackToLocal) { + return resultLocalTemporal; + } + + if (resultLocalTemporal instanceof LocalDateTime) { + ZonedDateTime zonedDateTime = ((LocalDateTime) resultLocalTemporal).atZone(zoneId); if (temporalClass == ZonedDateTime.class) { return zonedDateTime; } else if (temporalClass == OffsetDateTime.class) { @@ -110,22 +113,24 @@ abstract class JavaOrISOLikeTemplateTemporalFormat extends TemplateTemporalForma } else if (temporalClass == Instant.class) { return zonedDateTime.toInstant(); } - throw new AssertionError("Unexpected case: " + temporalClass); - case FAIL: - throw newUnparsableValueException( - s, parserDateTimeFormatter, - _MessageUtil.FAIL_MISSING_TIME_ZONE_PARSER_POLICY_ERROR_DETAIL, null); - default: - throw new AssertionError(); + } + throw new BugException("Unexpected case: " + + "temporalClass=" + temporalClass + ", " + + "missingTimeZoneParserPolicy=" + missingTimeZoneParserPolicy); + } else if (missingTimeZoneParserPolicy == FAIL) { + throw newUnparsableValueException( + s, parserDateTimeFormatter, + _MessageUtil.FAIL_MISSING_TIME_ZONE_PARSER_POLICY_ERROR_DETAIL, null); } - } catch (DateTimeException e) { + throw new AssertionError(); + } catch (DateTimeException|ArithmeticException e) { throw newUnparsableValueException(s, parserDateTimeFormatter, e.getMessage(), e); } } protected UnparsableValueException newUnparsableValueException( String s, DateTimeFormatter dateTimeFormatter, - String cause, DateTimeException e) { + String cause, Exception e) { StringBuilder message = new StringBuilder(); message.append("Failed to parse value ").append(jQuote(s)) diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index cd2ce83e..c7baa2dd 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -176,7 +176,7 @@ public final class Environment extends Configurable { private TemplateTemporalFormatCache cachedTemporalFormatCache; private final class TemplateTemporalFormatCache { // Notes: - // - "reusable" fields are set when the current cache field is set + // - "reusable" fields are set together with related non-reusable fields // - non-reusable fields are cleared when any related setting is changed, but reusableXxx fields are only // if the format string changes // - When there's a cache-miss, we check if the "reusable" field has compatible timeZone, and locale, and if @@ -1760,7 +1760,7 @@ public final class Environment extends Configurable { */ private TemplateNumberFormat getTemplateNumberFormatWithoutCache(String formatString, Locale locale) throws TemplateValueFormatException { - int formatStringLen = formatString.length(); + final int formatStringLen = formatString.length(); if (formatStringLen > 1 && formatString.charAt(0) == '@' && (isIcI2324OrLater() || hasCustomFormats()) @@ -1768,7 +1768,7 @@ public final class Environment extends Configurable { final String name; final String params; { - int endIdx = getCustomFormatStringNameEnd(formatString, formatStringLen); + int endIdx = getCustomFormatStringNameEnd(formatString); name = formatString.substring(1, endIdx); params = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; } @@ -2344,7 +2344,7 @@ public final class Environment extends Configurable { && Character.isLetter(formatString.charAt(1))) { final String name; { - int endIdx = getCustomFormatStringNameEnd(formatString, formatStringLen); + int endIdx = getCustomFormatStringNameEnd(formatString); name = formatString.substring(1, endIdx); formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; } @@ -2479,9 +2479,8 @@ public final class Environment extends Configurable { settingName = _TemporalUtils.temporalClassToFormatSettingName( temporalClass, blamedTemporalSourceExp != null - ? blamedTemporalSourceExp.getTemplate().getActualNamingConvention() - == Configuration.CAMEL_CASE_NAMING_CONVENTION - : false); + && blamedTemporalSourceExp.getTemplate().getActualNamingConvention() + == Configuration.CAMEL_CASE_NAMING_CONVENTION); settingValue = getTemporalFormat(temporalClass); } catch (IllegalArgumentException e2) { settingName = "???"; @@ -2730,14 +2729,16 @@ public final class Environment extends Configurable { /** * Returns the {@link TemplateTemporalFormat} for the given parameters without using the {@link Environment}-level - * cache. Of course, the {@link TemplateTemporalFormatFactory} involved might still uses its own cache, which can be - * global (class-loader-level) or {@link Environment}-level. + * cache. The {@link TemplateTemporalFormatFactory} involved might still uses its own internal cache, which can be + * global (class-loader-level), or {@link Environment}-level. * * @param formatString * See the similar parameter of {@link TemplateTemporalFormatFactory#get} - * @param dateType + * @param temporalClass * See the similar parameter of {@link TemplateTemporalFormatFactory#get} - * @param zonelessInput + * @param locale + * See the similar parameter of {@link TemplateTemporalFormatFactory#get} + * @param timeZone * See the similar parameter of {@link TemplateTemporalFormatFactory#get} */ private TemplateTemporalFormat getTemplateTemporalFormat( @@ -2765,7 +2766,7 @@ public final class Environment extends Configurable { && Character.isLetter(formatString.charAt(1))) { final String name; { - int endIdx = getCustomFormatStringNameEnd(formatString, formatStringLen); + int endIdx = getCustomFormatStringNameEnd(formatString); name = formatString.substring(1, endIdx); formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; } @@ -2783,13 +2784,12 @@ public final class Environment extends Configurable { return formatFactory.get(formatParams, temporalClass, locale, timeZone, this); } - private static int getCustomFormatStringNameEnd(String formatString, int formatStringLen) { + private static int getCustomFormatStringNameEnd(String formatString) { int endIdx; - findParamsStart: - for (endIdx = 1; endIdx < formatStringLen; endIdx++) { + for (endIdx = 1; endIdx < formatString.length(); endIdx++) { char c = formatString.charAt(endIdx); if (c == ' ' || c == '_') { - break findParamsStart; + return endIdx; } } return endIdx; diff --git a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java index 680546dd..6f1e1d27 100644 --- a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java +++ b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java @@ -45,7 +45,7 @@ import freemarker.template.TemplateTemporalModel; * * @since 2.3.32 */ -final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat { +final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatterBasedTemplateTemporalFormat { private final DateTimeFormatter dateTimeFormatter; private final boolean instantConversion; private final String description; @@ -56,7 +56,9 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT DateTimeFormatter dateTimeFormatter, DateTimeFormatter parserExtendedDateTimeFormatter, DateTimeFormatter parserBasicDateTimeFormatter, - Class<? extends Temporal> temporalClass, TimeZone timeZone, String formatString) { + Class<? extends Temporal> temporalClass, + TimeZone timeZone, + String formatString) { super(temporalClass, timeZone); temporalClass = normalizeSupportedTemporalClass(temporalClass); this.dateTimeFormatter = dateTimeFormatter; @@ -92,10 +94,10 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT extendedFormat = s.indexOf('-', 1) != -1; add1Day = false; } else if (temporalClass == LocalTime.class || temporalClass == OffsetTime.class) { - extendedFormat = s.indexOf(":") != -1; + extendedFormat = s.contains(":"); add1Day = false; // ISO 8601 allows hour 24 if the rest of the time is 0: - if (isStartOf240000(s, 0)) { + if (isStartOf240000InISOFormat(s, 0)) { s = "00" + s.substring(2); } } else if (temporalClass == Year.class) { @@ -103,19 +105,21 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT add1Day = false; } else { int tIndex = s.indexOf('T'); - if (tIndex < 1) { + if (tIndex < 1) { // tIndex 0 is deliberately not accepted throw newUnparsableValueException( s, null, "Character \"T\" must be used to separate the date and time part.", null); } if (s.indexOf(":", tIndex + 1) != -1) { + // Time part has ":" => extendedFormat extendedFormat = true; } else { + // Date part has "-" => extendedFormat // Note: false for: -5000101T00, as there the last '-' has index 0 extendedFormat = s.lastIndexOf('-', tIndex - 1) > 0; } // ISO 8601 allows hour 24 if the rest of the time is 0: - if (isStartOf240000(s, tIndex + 1)) { + if (isStartOf240000InISOFormat(s, tIndex + 1)) { s = s.substring(0, tIndex + 1) + "00" + s.substring(tIndex + 3); add1Day = true; } else { @@ -136,7 +140,12 @@ final class ISOLikeTemplateTemporalTemporalFormat extends JavaOrISOLikeTemplateT private final static Pattern ZERO_TIME_AFTER_HH = Pattern.compile("(?::?+00(?::?+00(?:.?+0+)?)?)?"); - private static boolean isStartOf240000(String s, int from) { + /** + * Checks if starting from the given index we have {@code "24:00:00"} or equivalent (like {@code "240000"}, + * {@code "24:00:00.000"}, {@code "2400"}, {@code "24"}). This only accepts a format that is valid in ISO 8601, + * like for {@code "24:0"} this returns {@code false}, as ISO requires two 0-s. + */ + private static boolean isStartOf240000InISOFormat(String s, int from) { if (from + 1 >= s.length() || s.charAt(from) != '2' || s.charAt(from + 1) != '4') { return false; } diff --git a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java index 9a6ad0c5..4a532ce2 100644 --- a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java +++ b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java @@ -41,7 +41,7 @@ import java.util.Locale; import java.util.TimeZone; /** - * Format factory related to {@link someJava8Temporal?string.iso}, {@link someJava8Temporal?string.iso_...}, etc. + * Format factory related to {@code someJava8Temporal?string.iso}, {@code someJava8Temporal?string.iso_...}, etc. */ class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory { @@ -195,8 +195,9 @@ class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory { .withResolverStyle(ResolverStyle.STRICT); @Override - public TemplateTemporalFormat get(String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) throws - TemplateValueFormatException { + public TemplateTemporalFormat get( + String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) + throws TemplateValueFormatException { if (!params.isEmpty()) { // TODO [FREEMARKER-35] throw new InvalidFormatParametersException("iso currently doesn't support parameters for Java 8 temporal types"); @@ -205,7 +206,8 @@ class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory { return getISOFormatter(temporalClass, timeZone); } - private static ISOLikeTemplateTemporalTemporalFormat getISOFormatter(Class<? extends Temporal> temporalClass, TimeZone timeZone) { + private static ISOLikeTemplateTemporalTemporalFormat getISOFormatter( + Class<? extends Temporal> temporalClass, TimeZone timeZone) { final DateTimeFormatter dateTimeFormatter; final DateTimeFormatter parserExtendedDateTimeFormatter; final DateTimeFormatter parserBasicDateTimeFormatter; diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java index dc7845af..47208c2f 100644 --- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java +++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java @@ -47,7 +47,7 @@ import freemarker.template.utility.ClassUtil; * * @since 2.3.32 */ -class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat { +class JavaTemplateTemporalFormat extends DateTimeFormatterBasedTemplateTemporalFormat { enum PreFormatValueConversion { IDENTITY, @@ -76,8 +76,8 @@ class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat { private final String formatString; private final PreFormatValueConversion preFormatValueConversion; - JavaTemplateTemporalFormat(String formatString, Class<? extends Temporal> temporalClass, Locale locale, - TimeZone timeZone) + JavaTemplateTemporalFormat( + String formatString, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone) throws InvalidFormatParametersException { super(temporalClass, timeZone); this.locale = Objects.requireNonNull(locale); @@ -139,6 +139,7 @@ class JavaTemplateTemporalFormat extends JavaOrISOLikeTemplateTemporalFormat { } catch (DateTimeException e) { timePartFormatStyle = getLessVerboseStyle(timePartFormatStyle); if (timePartFormatStyle == null) { + // Not even the least verbose style worked throw e; } diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java index b1e049db..ac4228bf 100644 --- a/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java +++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java @@ -31,8 +31,9 @@ class JavaTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory { } @Override - public TemplateTemporalFormat get(String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, - Environment env) throws TemplateValueFormatException { + public TemplateTemporalFormat get( + String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) + throws TemplateValueFormatException { return new JavaTemplateTemporalFormat(params, temporalClass, locale, timeZone); } diff --git a/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java b/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java index c4d7b681..89e4ebf4 100644 --- a/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java +++ b/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java @@ -20,13 +20,14 @@ package freemarker.core; import java.time.OffsetDateTime; +import java.time.temporal.Temporal; import freemarker.template.Configuration; /** * Used as a parameter to {@link TemplateTemporalFormat#parse(String, MissingTimeZoneParserPolicy)}, specifies what to - * do if we have to parse a string that contains no time zone or offset information to a non-local {@code java.time} - * temporal (like to {@link OffsetDateTime}). + * do if we have to parse a string that contains no time zone, nor offset information to a non-local {@link Temporal} + * (like to {@link OffsetDateTime}). * * <p>There's no {@link Configuration} setting for this. Instead, the build-ins that parse to given non-local temporal * type have 3 variants, one for each policy. For example, in the case of parsing a string to {@link OffsetDateTime}, diff --git a/src/main/java/freemarker/core/TemplateDateFormat.java b/src/main/java/freemarker/core/TemplateDateFormat.java index da2cfab3..a4f729f9 100644 --- a/src/main/java/freemarker/core/TemplateDateFormat.java +++ b/src/main/java/freemarker/core/TemplateDateFormat.java @@ -27,9 +27,9 @@ import freemarker.template.TemplateModelException; /** * Represents a date/time/dateTime format; used in templates for formatting and parsing with that format. This is - * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define + * similar to Java's {@link DateFormat}, but made to fit the requirements of FreeMarker. Also, it allows defining * formats that can't be described with Java's existing {@link DateFormat} implementations. - * + * * <p> * Implementations need not be thread-safe if the {@link TemplateDateFormatFactory} doesn't recycle them among * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single @@ -42,12 +42,15 @@ import freemarker.template.TemplateModelException; public abstract class TemplateDateFormat extends TemplateValueFormat { /** + * Formats the value to plain text (string that contains no markup or escaping). + * * @param dateModel - * The date/time/dateTime to format; not {@code null}. Most implementations will just work with the return value of - * {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of - * a custom {@link TemplateDateModel} implementation. - * - * @return The date/time/dateTime as text, with no escaping (like no HTML escaping); can't be {@code null}. + * The date/time/dateTime to format; not {@code null}. Most implementations will just work with the + * return value of {@link TemplateDateModel#getAsDate()}, but some may format differently depending on + * the properties of a custom {@link TemplateDateModel} implementation. + * + * @return The date/time/dateTime value as plain text (not markup), with no escaping (like no HTML escaping); + * can't be {@code null}. * * @throws TemplateValueFormatException * When a problem occurs during the formatting of the value. Notable subclass: @@ -59,14 +62,15 @@ public abstract class TemplateDateFormat extends TemplateValueFormat { throws TemplateValueFormatException, TemplateModelException; /** - * Formats the model to markup instead of to plain text if the result markup will be more than just plain text - * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of - * {@link #formatToPlainText(TemplateDateModel)} escaped, it must return the {@link String} that + * Formats the value to markup instead of to plain text, but only if the result markup will be more than just plain + * text escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of + * {@link #formatToPlainText(TemplateDateModel)} escaped, then instead it must return the {@link String} that * {@link #formatToPlainText(TemplateDateModel)} does. * * <p>The implementation in {@link TemplateDateFormat} simply calls {@link #formatToPlainText(TemplateDateModel)}. - * - * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}. + * + * @return A {@link String} (assumed to be plain text, not markup), or a {@link TemplateMarkupOutputModel}; + * not {@code null}. */ public Object format(TemplateDateModel dateModel) throws TemplateValueFormatException, TemplateModelException { return formatToPlainText(dateModel); @@ -86,14 +90,14 @@ public abstract class TemplateDateFormat extends TemplateValueFormat { * respectively. This parameter rarely if ever {@link TemplateDateModel#UNKNOWN}, but the implementation * that cares about this parameter should be prepared for that. If nothing else, it should throw * {@link UnknownDateTypeParsingUnsupportedException} then. - * - * @return The interpretation of the text either as a {@link Date} or {@link TemplateDateModel}. Typically, a - * {@link Date}. {@link TemplateDateModel} is used if you have to attach some application-specific - * meta-information that's also extracted during {@link #formatToPlainText(TemplateDateModel)} (so if you format - * something and then parse it, you get back an equivalent result). It can't be {@code null}. Known issue - * (at least in FTL 2): {@code ?date}/{@code ?time}/{@code ?datetime}, when not invoked as a method, can't - * return the {@link TemplateDateModel}, only the {@link Date} from inside it, hence the additional - * application-specific meta-info will be lost. + * + * @return The text converted to either {@link Date}, or to {@link TemplateDateModel}; not {@code null}. + * Typically, the result should be a {@link Date}. Converting to {@link TemplateDateModel} should only be + * done if you need to store additional data next to the {@link Date}, which is then also used by + * {@link #formatToPlainText(TemplateDateModel)} (so if you format something and then parse it, you get + * back an equivalent result). Known issue (at least in 2.x): {@code ?date}/{@code ?time}/{@code ?datetime}, + * when not invoked as a method, can't return the {@link TemplateDateModel}, only the {@link Date} from + * inside it, hence the additional application-specific meta-info will be lost. */ public abstract Object parse(String s, int dateType) throws TemplateValueFormatException; diff --git a/src/main/java/freemarker/core/TemplateNumberFormat.java b/src/main/java/freemarker/core/TemplateNumberFormat.java index 23325755..1c89f917 100644 --- a/src/main/java/freemarker/core/TemplateNumberFormat.java +++ b/src/main/java/freemarker/core/TemplateNumberFormat.java @@ -20,57 +20,60 @@ package freemarker.core; import java.text.NumberFormat; -import freemarker.template.TemplateDateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateNumberModel; /** * Represents a number format; used in templates for formatting and parsing with that format. This is similar to Java's - * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define formats that - * can't be represented with Java's existing {@link NumberFormat} implementations. - * + * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it allows defining formats that can't be + * described with Java's existing {@link NumberFormat} implementations. + * * <p> * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single * {@link Environment}, and {@link Environment}-s are thread-local objects. - * + * * @since 2.3.24 */ public abstract class TemplateNumberFormat extends TemplateValueFormat { /** + * Formats the value to plain text (string that contains no markup or escaping). + * * @param numberModel * The number to format; not {@code null}. Most implementations will just work with the return value of - * {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of - * a custom {@link TemplateDateModel} implementation. - * - * @return The number as text, with no escaping (like no HTML escaping); can't be {@code null}. - * + * {@link TemplateNumberModel#getAsNumber()}, but some may format differently depending on the properties + * of a custom {@link TemplateNumberModel} implementation. + * + * @return The {@link Number} as plain text (not markup), with no escaping (like no HTML escaping); + * can't be {@code null}. + * * @throws TemplateValueFormatException * If any problem occurs while parsing/getting the format. Notable subclass: * {@link UnformattableValueException}. * @throws TemplateModelException - * Exception thrown by the {@code dateModel} object when calling its methods. + * Exception thrown by the {@code numberModel} object when calling its methods. */ public abstract String formatToPlainText(TemplateNumberModel numberModel) throws TemplateValueFormatException, TemplateModelException; /** - * Formats the model to markup instead of to plain text if the result markup will be more than just plain text - * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of - * {@link #formatToPlainText(TemplateNumberModel)} escaped, it must return the {@link String} that + * Formats the value to markup instead of to plain text, but only if the result markup will be more than just plain + * text escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of + * {@link #formatToPlainText(TemplateNumberModel)} escaped, then instead it must return the {@link String} that * {@link #formatToPlainText(TemplateNumberModel)} does. - * + * * <p> * The implementation in {@link TemplateNumberFormat} simply calls {@link #formatToPlainText(TemplateNumberModel)}. - * - * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}. + * + * @return A {@link String} (assumed to be plain text, not markup), or a {@link TemplateMarkupOutputModel}; + * not {@code null}. */ public Object format(TemplateNumberModel numberModel) throws TemplateValueFormatException, TemplateModelException { return formatToPlainText(numberModel); } - + /** * Tells if this formatter should be re-created if the locale changes. */ @@ -80,11 +83,11 @@ public abstract class TemplateNumberFormat extends TemplateValueFormat { * This method is reserved for future purposes; currently it always throws {@link ParsingNotSupportedException}. We * don't yet support number parsing with {@link TemplateNumberFormat}-s, because currently FTL parses strings to * number with the {@link ArithmeticEngine} ({@link TemplateNumberFormat} were only introduced in 2.3.24). If it - * will be support, it will be similar to {@link TemplateDateFormat#parse(String, int)}. + * will be supported, it will behave similarly to {@link TemplateDateFormat#parse(String, int)}. */ public final Object parse(String s) throws TemplateValueFormatException { throw new ParsingNotSupportedException("Number formats currenly don't support parsing"); } - - + + } diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java b/src/main/java/freemarker/core/TemplateTemporalFormat.java index a282093d..f4335bb2 100644 --- a/src/main/java/freemarker/core/TemplateTemporalFormat.java +++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java @@ -23,13 +23,14 @@ import java.time.temporal.Temporal; import java.util.Locale; import java.util.TimeZone; +import freemarker.template.TemplateDateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateTemporalModel; /** * Represents a {@link Temporal} format; used in templates for formatting {@link Temporal}-s, and parsing strings to * {@link Temporal}-s. This is similar to Java's {@link DateTimeFormatter}, but made to fit the requirements of - * FreeMarker. Also, it makes it possible to define formats that can't be described with {@link DateTimeFormatter}. + * FreeMarker. Also, it allows defining formats that can't be described with Java's {@link DateTimeFormatter}. * * <p>{@link TemplateTemporalFormat} instances are usually created by a {@link TemplateTemporalFormatFactory}. * @@ -45,44 +46,75 @@ import freemarker.template.TemplateTemporalModel; */ public abstract class TemplateTemporalFormat extends TemplateValueFormat { + /** + * Formats the value to plain text (string that contains no markup or escaping). + * + * @param temporalModel + * The temporal value to format; not {@code null}. Most implementations will just work with the return + * value of {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the + * properties of a custom {@link TemplateDateModel} implementation. + * + * @return The {@link Temporal} value as plain text (not markup), with no escaping (like no HTML escaping); + * can't be {@code null}. + * + * @throws TemplateValueFormatException + * If any problem occurs while parsing/getting the format. Notable subclass: + * {@link UnformattableValueException}. + * @throws TemplateModelException + * Exception thrown by the {@code temporalModel} object when calling its methods. + */ public abstract String formatToPlainText(TemplateTemporalModel temporalModel) throws TemplateValueFormatException, TemplateModelException; /** - * Formats the model to markup instead of to plain text, if the result markup will be more than just plain text - * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of - * {@link #formatToPlainText(TemplateTemporalModel)} escaped, it must return the {@link String} that - * {@link #formatToPlainText(TemplateTemporalModel)} does. + * Formats the value to markup instead of to plain text, but only if the result markup will be more than just plain + * text escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of + * {@link #formatToPlainText(TemplateTemporalModel)} escaped, then it must return the {@link String} that + * {@link #formatToPlainText(TemplateTemporalModel)} would. * - * <p>The implementation in {@link TemplateTemporalFormat} simply calls {@link #formatToPlainText(TemplateTemporalModel)}. + * <p>The implementation in {@link TemplateTemporalFormat} simply calls + * {@link #formatToPlainText(TemplateTemporalModel)}. * - * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}. + * @return A {@link String} (assumed to be plain text, not markup), or a {@link TemplateMarkupOutputModel}; + * not {@code null}. */ public Object format(TemplateTemporalModel temporalModel) throws TemplateValueFormatException, TemplateModelException { return formatToPlainText(temporalModel); } /** - * Tells if this formatter can be used for the given locale. + * Tells if this formatter can be used for the parameter {@link Locale}. Meant to be used for cache entry + * invalidation. + * + * @param locale Not {@code null} */ public abstract boolean canBeUsedForLocale(Locale locale); /** - * Tells if this formatter can be used for the given {@link TimeZone}. + * Tells if this formatter can be used for the parameter {@link TimeZone}. Meant to be used for cache entry + * invalidation. + * + * @param timeZone Not {@code null} */ public abstract boolean canBeUsedForTimeZone(TimeZone timeZone); /** - * Parser a string to a {@link Temporal}, according to this format. Some format implementations may throw - * {@link ParsingNotSupportedException} here. + * Parses a string to a {@link Temporal}, according to this format. This is optional functionality; some + * implementations may throw {@link ParsingNotSupportedException} here. * * @param s * The string to parse + * @param missingTimeZoneParserPolicy + * See {@link MissingTimeZoneParserPolicy}; shouldn't be {@code null}, unless you are sure + * that the target type is a local temporal type, or that the input string contains zone offset, + * time zone, or distance from the UTC epoch. The implementation must accept {@code null} if the + * policy is not actually needed. * - * @return The interpretation of the text either as a {@link Temporal} or {@link TemplateTemporalModel}. Typically, - * a {@link Temporal}. {@link TemplateTemporalModel} is used if you have to attach some application-specific - * meta-information that's also extracted during {@link #formatToPlainText(TemplateTemporalModel)} (so if - * you format something and then parse it, you get back an equivalent result). It can't be {@code null}. + * @return The text converted to either {@link Temporal}, or to {@link TemplateTemporalModel}; not {@code null}. + * Typically, the result should be a {@link Temporal}. Converting to {@link TemplateTemporalModel} should + * only be done if you need to store additional data next to the {@link Temporal}, which is then also used + * by {@link #formatToPlainText(TemplateTemporalModel)} (so if you format something and then parse it, you + * get back an equivalent object). * * @throws ParsingNotSupportedException If this format doesn't implement parsing. */ diff --git a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java index 3e8c8c6d..16f5757a 100644 --- a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java +++ b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java @@ -23,7 +23,7 @@ import java.util.Locale; import java.util.TimeZone; /** - * Factory for a certain kind of {@link Temporal} formatting ({@link TemplateTemporalFormat}). + * Factory for a certain kind of {@link TemplateTemporalFormat}. * See more at {@link TemplateValueFormatFactory}. * * @see Configurable#setCustomTemporalFormats(java.util.Map) diff --git a/src/main/java/freemarker/core/TemplateValueFormat.java b/src/main/java/freemarker/core/TemplateValueFormat.java index 488fc8e6..e8eb82d3 100644 --- a/src/main/java/freemarker/core/TemplateValueFormat.java +++ b/src/main/java/freemarker/core/TemplateValueFormat.java @@ -19,7 +19,8 @@ package freemarker.core; /** - * Superclass of all value format objects; objects that convert values to strings, or parse strings. + * Superclass of all value format objects; objects that convert values to strings in templates, or parse strings + * to an object of the given type in templates. * * @since 2.3.24 */ diff --git a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java index 32435728..31719ccc 100644 --- a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java +++ b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java @@ -36,7 +36,7 @@ import java.util.Locale; import java.util.TimeZone; /** - * Format factory related to {@link someJava8Temporal?string.xs}, {@link someJava8Temporal?string.xs_...}, etc. + * Format factory related to {@code someJava8Temporal?string.xs}, {@code someJava8Temporal?string.xs_...}, etc. */ // TODO [FREEMARKER-35] Historical date handling compared to ISO class XSTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory { @@ -48,8 +48,9 @@ class XSTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory { } @Override - public TemplateTemporalFormat get(String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) throws - TemplateValueFormatException { + public TemplateTemporalFormat get( + String params, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone, Environment env) + throws TemplateValueFormatException { if (!params.isEmpty()) { // TODO [FREEMARKER-35] throw new InvalidFormatParametersException("xs currently doesn't support parameters for Java 8 temporal types"); diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java index 112399f6..7eff6501 100644 --- a/src/main/java/freemarker/core/_MessageUtil.java +++ b/src/main/java/freemarker/core/_MessageUtil.java @@ -55,8 +55,8 @@ public class _MessageUtil { }; static final String FAIL_MISSING_TIME_ZONE_PARSER_POLICY_ERROR_DETAIL - = "The parsed string doesn't contain time zone or offset, and the specified policy is " - + "to fail in that case (see " + MissingTimeZoneParserPolicy.class.getName() + = "The parsed string doesn't contain time zone, nor offset, and that target type is non-local, and the " + + "specified policy is to fail in that case (see " + MissingTimeZoneParserPolicy.class.getName() + "." + MissingTimeZoneParserPolicy.FAIL + ")."; static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n"; diff --git a/src/main/java/freemarker/core/_TemporalUtils.java b/src/main/java/freemarker/core/_TemporalUtils.java index 8aa38f8c..311bf987 100644 --- a/src/main/java/freemarker/core/_TemporalUtils.java +++ b/src/main/java/freemarker/core/_TemporalUtils.java @@ -90,7 +90,7 @@ public final class _TemporalUtils { // Not private because of tests static final boolean SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL = SUPPORTED_TEMPORAL_CLASSES.stream() - .allMatch(cl -> (cl.getModifiers() & Modifier.FINAL) == Modifier.FINAL); + .allMatch(cl -> (cl.getModifiers() & Modifier.FINAL) != 0); private _TemporalUtils() { throw new AssertionError(); @@ -439,6 +439,7 @@ public final class _TemporalUtils { if (SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL) { return temporalClass; } else { + if (true) throw new AssertionError(); //!!T if (Instant.class.isAssignableFrom(temporalClass)) { return Instant.class; } else if (LocalDate.class.isAssignableFrom(temporalClass)) { @@ -458,13 +459,13 @@ public final class _TemporalUtils { } else if (Year.class.isAssignableFrom(temporalClass)) { return Year.class; } else { - throw new IllegalArgumentException("Unsupprted temporal class: " + temporalClass.getName()); + throw new IllegalArgumentException("Unsupported temporal class: " + temporalClass.getName()); } } } /** - * Tells if the temporal class is one that doesn't store, nor have an implied time zone or offset. + * Tells if the temporal class is one that doesn't store, nor have an implied time zone, or offset. * * @throws IllegalArgumentException If the temporal class is not currently supported by FreeMarker. */ @@ -485,7 +486,7 @@ public final class _TemporalUtils { * * @throws IllegalArgumentException If the temporal class is not currently supported by FreeMarker. */ - public static Class<? extends Temporal> getLocalTemporalClassForNonLocal(Class<? extends Temporal> temporalClass) { + public static Class<? extends Temporal> tryGetLocalTemporalClassForNonLocal(Class<? extends Temporal> temporalClass) { temporalClass = normalizeSupportedTemporalClass(temporalClass); if (temporalClass == OffsetDateTime.class) { return LocalDateTime.class; @@ -496,13 +497,16 @@ public final class _TemporalUtils { if (temporalClass == OffsetTime.class) { return LocalTime.class; } + if (temporalClass == Instant.class) { + return LocalDateTime.class; + } return null; } /** * Returns the FreeMarker configuration format setting name for a temporal class. * - * @throws IllegalArgumentException If {@link temporalClass} is not a supported {@link Temporal} subclass. + * @throws IllegalArgumentException If {@code temporalClass} is not a supported {@link Temporal} subclass. */ public static String temporalClassToFormatSettingName(Class<? extends Temporal> temporalClass, boolean camelCase) { temporalClass = normalizeSupportedTemporalClass(temporalClass); diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java index 45f2c78b..678e3ed7 100644 --- a/src/main/java/freemarker/template/Configuration.java +++ b/src/main/java/freemarker/template/Configuration.java @@ -122,10 +122,14 @@ import freemarker.template.utility.XmlEscape; * cfg.set<i>SomeSetting</i>(...); * cfg.set<i>OtherSetting</i>(...); * ... + * // Do not modify the settings later, when you have already started processing templates! * * // Later, whenever the application needs a template (so you may do this a lot, and from multiple threads): * {@link Template Template} myTemplate = cfg.{@link #getTemplate(String) getTemplate}("myTemplate.ftlh"); * myTemplate.{@link Template#process(Object, java.io.Writer) process}(dataModel, out);</pre> + * + * <p><b>Do not modify the {@link Configuration} settings after you started processing templates!</b> Doing so can + * cause to undefined behavior, even if you only have a single thread!</p> * * <p>A couple of settings that you should not leave on its default value are: * <ul> diff --git a/src/main/java/freemarker/template/TemplateDateModel.java b/src/main/java/freemarker/template/TemplateDateModel.java index d354fda3..24a551b8 100644 --- a/src/main/java/freemarker/template/TemplateDateModel.java +++ b/src/main/java/freemarker/template/TemplateDateModel.java @@ -25,12 +25,11 @@ import java.util.Date; import java.util.List; /** - * "date", "time" and "date-time" template language data types: corresponds to {@link java.util.Date}. Contrary to Java, - * FreeMarker distinguishes date (no time part), time and date-time values. + * "date", "time", and "date-time" template language data types: corresponds to {@link java.util.Date}. Contrary to + * Java, FreeMarker distinguishes date (no time part), time (no date part), and date-time values. * - * <p> - * Objects of this type should be immutable, that is, calling {@link #getAsDate()} and {@link #getDateType()} should - * always return the same value as for the first time. + * <p>Objects of this type should be immutable, that is, {@link #getAsDate()}, and {@link #getDateType()} should always + * return the same value as for the first time. * * <p>{@link java.time.temporal.Temporal} values (the date/time classes introduced with Java 8) are handled by * {@link TemplateTemporalModel}. diff --git a/src/main/java/freemarker/template/TemplateTemporalModel.java b/src/main/java/freemarker/template/TemplateTemporalModel.java index 7b642bfe..ea7c2419 100644 --- a/src/main/java/freemarker/template/TemplateTemporalModel.java +++ b/src/main/java/freemarker/template/TemplateTemporalModel.java @@ -35,6 +35,9 @@ import java.time.temporal.Temporal; * This does not deal with {@link java.time.Duration}, and {@link java.time.Period}, because those don't implement the * {@link Temporal} interface. * + * <p>Objects of this type should be immutable, that is, {@link #getAsTemporal()}} should always return the same value + * as for the first time. + * * <p>{@link java.util.Date} values (the way date/time values were represented prior Java 8) are handled by * {@link TemplateDateModel}. * @@ -42,7 +45,7 @@ import java.time.temporal.Temporal; */ public interface TemplateTemporalModel extends TemplateModel { /** - * Returns the date value. The return value must not be {@code null}. + * Returns the temporal value; can't be {@code null}. */ Temporal getAsTemporal() throws TemplateModelException; } diff --git a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java index 75518fa0..d7661e59 100644 --- a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java +++ b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java @@ -243,7 +243,7 @@ public abstract class AbstractTemporalFormatTest { assertThat( e.getMessage(), allOf( - containsStringIgnoringCase("doesn't contain time zone or offset"), + containsStringIgnoringCase("doesn't contain time zone, nor offset"), containsString(MissingTimeZoneParserPolicy.class.getName() + "." + MissingTimeZoneParserPolicy.FAIL))); } diff --git a/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java b/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java index 7590d62e..7850b4bd 100644 --- a/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java +++ b/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java @@ -385,7 +385,7 @@ public class ISOLikeTemplateTemporalFormatTest extends AbstractTemporalFormatTes temporalClass, e -> assertThat(e.getMessage(), allOf( containsString(jQuote(stringToParse)), - containsString("time zone or offset"), + containsString("time zone, nor offset"), containsString(temporalClass.getSimpleName())))); } @@ -482,7 +482,7 @@ public class ISOLikeTemplateTemporalFormatTest extends AbstractTemporalFormatTes OffsetTime.class, e -> assertThat(e.getMessage(), allOf( containsString(jQuote(stringToParse)), - containsString("time zone or offset"), + containsString("time zone, nor offset"), containsString(OffsetTime.class.getSimpleName())))); } diff --git a/src/test/java/freemarker/core/_TemporalUtilsTest.java b/src/test/java/freemarker/core/_TemporalUtilsTest.java index be258b24..bdfd755d 100644 --- a/src/test/java/freemarker/core/_TemporalUtilsTest.java +++ b/src/test/java/freemarker/core/_TemporalUtilsTest.java @@ -22,6 +22,7 @@ package freemarker.core; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.time.Instant; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; @@ -85,11 +86,12 @@ public class _TemporalUtilsTest { } @Test - public void testGetLocalTemporalClassForNonLocal() { - assertThat(_TemporalUtils.getLocalTemporalClassForNonLocal(OffsetDateTime.class), equalTo(LocalDateTime.class)); - assertThat(_TemporalUtils.getLocalTemporalClassForNonLocal(ZonedDateTime.class), equalTo(LocalDateTime.class)); - assertThat(_TemporalUtils.getLocalTemporalClassForNonLocal(OffsetTime.class), equalTo(LocalTime.class)); - assertNull(_TemporalUtils.getLocalTemporalClassForNonLocal(LocalDateTime.class)); + public void testTryGetLocalTemporalClassForNonLocal() { + assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(OffsetDateTime.class), equalTo(LocalDateTime.class)); + assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(ZonedDateTime.class), equalTo(LocalDateTime.class)); + assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(OffsetTime.class), equalTo(LocalTime.class)); + assertThat(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(Instant.class), equalTo(LocalDateTime.class)); + assertNull(_TemporalUtils.tryGetLocalTemporalClassForNonLocal(LocalDateTime.class)); } } \ No newline at end of file
