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

Reply via email to