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


The following commit(s) were added to refs/heads/FREEMARKER-35 by this push:
     new 034534a  [FREEMARKER-35] Continued reworking/cleaning:
034534a is described below

commit 034534a6b0f70e1fd1d2cb646a612b5fcaa06aa3
Author: ddekany <[email protected]>
AuthorDate: Mon Jun 29 01:43:53 2020 +0200

    [FREEMARKER-35] Continued reworking/cleaning:
    
    - Some generic cleanup in the classes added in the last commit
    - Don't ever add offset/zone for localized temporals
    - Nor add zone to offset temporals, except when using localized patterns 
(i.e., short, medium, long, short).
    - Use the proper DateTimeFormatter.ofLocalizedXxx methods to get the 
localized format, instead of trying to edit the pattern.
    - Removed localized pattern modification attempts that try to support 
formatting temporal types for which the Java API doesn't provide localized 
pattern. Reason is, we can't do that reliably. If user introduces a new locale 
to their app, maybe templates start to fail. Aways failing is better then 
failing sometimes. Will revisit this, if we can do something else that's 
predictable enough.
---
 ...seJavaTemplateTemporalFormatTemplateFormat.java |  64 ----------
 src/main/java/freemarker/core/Configurable.java    |   2 +-
 .../core/ISOLikeTemplateTemporalFormat.java        |  47 -------
 .../ISOLikeTemplateTemporalTemporalFormat.java     |  81 ++++++++++++
 .../core/ISOTemplateTemporalFormatFactory.java     |  33 +++--
 .../core/JavaTemplateTemporalFormat.java           | 136 ++++++++++++---------
 .../core/JavaTemplateTemporalFormatFactory.java    |   3 -
 .../freemarker/core/TemplateTemporalFormat.java    |   2 +
 .../core/XSTemplateTemporalFormatFactory.java      |  33 +++--
 .../test/templatesuite/templates/temporal.ftl      |  29 +++--
 10 files changed, 218 insertions(+), 212 deletions(-)

diff --git 
a/src/main/java/freemarker/core/BaseJavaTemplateTemporalFormatTemplateFormat.java
 
b/src/main/java/freemarker/core/BaseJavaTemplateTemporalFormatTemplateFormat.java
deleted file mode 100644
index a7e7412..0000000
--- 
a/src/main/java/freemarker/core/BaseJavaTemplateTemporalFormatTemplateFormat.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.core;
-
-import java.time.DateTimeException;
-import java.time.Instant;
-import java.time.OffsetDateTime;
-import java.time.OffsetTime;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.Temporal;
-
-import freemarker.template.TemplateModelException;
-import freemarker.template.TemplateTemporalModel;
-
-abstract class BaseJavaTemplateTemporalFormatTemplateFormat extends 
TemplateTemporalFormat {
-    private final DateTimeFormatter dateTimeFormatterWithZoneOverride;
-
-    protected BaseJavaTemplateTemporalFormatTemplateFormat(DateTimeFormatter 
dateTimeFormatterWithZoneOverride) {
-        this.dateTimeFormatterWithZoneOverride = 
dateTimeFormatterWithZoneOverride;
-    }
-
-    @Override
-    public String format(TemplateTemporalModel tm)
-            throws TemplateValueFormatException, TemplateModelException {
-        try {
-            DateTimeFormatter dateTimeFormatter = 
this.dateTimeFormatterWithZoneOverride;
-            Temporal temporal = TemplateFormatUtil.getNonNullTemporal(tm);
-
-            // TODO [FREEMARKER-35] Doing these on runtime is wasteful if it's 
know if for which format setting
-            // this object is used for.
-            if (temporal instanceof Instant) {
-                temporal = ((Instant) 
temporal).atZone(dateTimeFormatter.getZone());
-            } else if (temporal instanceof OffsetDateTime) {
-                dateTimeFormatter = 
dateTimeFormatter.withZone(((OffsetDateTime) temporal).getOffset());
-            } else if (temporal instanceof OffsetTime) {
-                dateTimeFormatter = dateTimeFormatter.withZone(((OffsetTime) 
temporal).getOffset());
-            } else if (temporal instanceof ZonedDateTime) {
-                dateTimeFormatter = dateTimeFormatter.withZone(null);
-            }
-
-            return dateTimeFormatter.format(temporal);
-        } catch (DateTimeException e) {
-            throw new UnformattableValueException(e.getMessage(), e);
-        }
-    }
-}
diff --git a/src/main/java/freemarker/core/Configurable.java 
b/src/main/java/freemarker/core/Configurable.java
index d3c8646..adb513d 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -1442,7 +1442,7 @@ public class Configurable {
         } else {
             Class<? extends Temporal> normTemporalClass = 
_CoreTemporalUtils.normalizeSupportedTemporalClass(temporalClass);
             if (normTemporalClass == temporalClass) {
-                throw new IllegalArgumentException("There's no temporal format 
seting for this class: "
+                throw new IllegalArgumentException("There's no temporal format 
setting for this class: "
                         + temporalClass.getName());
             } else {
                 return getTemporalFormat(normTemporalClass);
diff --git a/src/main/java/freemarker/core/ISOLikeTemplateTemporalFormat.java 
b/src/main/java/freemarker/core/ISOLikeTemplateTemporalFormat.java
deleted file mode 100644
index 8a8f8ff..0000000
--- a/src/main/java/freemarker/core/ISOLikeTemplateTemporalFormat.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.core;
-
-import java.time.format.DateTimeFormatter;
-
-// TODO [FREEMARKER-35] These should support parameters similar to {@link 
ISOTemplateDateFormat},
-final class ISOLikeTemplateTemporalFormat extends 
BaseJavaTemplateTemporalFormatTemplateFormat {
-    private final String description;
-
-    public ISOLikeTemplateTemporalFormat(DateTimeFormatter dateTimeFormatter, 
String description) {
-        super(dateTimeFormatter);
-        this.description = description;
-    }
-
-    @Override
-    public boolean isLocaleBound() {
-        return false;
-    }
-
-    @Override
-    public boolean isTimeZoneBound() {
-        return true;
-    }
-
-    @Override
-    public String getDescription() {
-        return description;
-    }
-}
diff --git 
a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java 
b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
new file mode 100644
index 0000000..87eb859
--- /dev/null
+++ b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+import java.util.TimeZone;
+
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
+
+// TODO [FREEMARKER-35] These should support parameters similar to {@link 
ISOTemplateDateFormat},
+final class ISOLikeTemplateTemporalTemporalFormat extends 
TemplateTemporalFormat {
+    private final DateTimeFormatter dateTimeFormatter;
+    private final boolean instantConversion;
+    private final ZoneId zoneId;
+    private final String description;
+
+    public ISOLikeTemplateTemporalTemporalFormat(
+            DateTimeFormatter dateTimeFormatter, Class<? extends Temporal> 
temporalClass, TimeZone zone, String description) {
+        this.dateTimeFormatter = dateTimeFormatter;
+        this.instantConversion = Instant.class.isAssignableFrom(temporalClass);
+        if (instantConversion) {
+            zoneId = zone.toZoneId();
+        } else {
+            zoneId = null;
+        }
+        this.description = description;
+    }
+
+    @Override
+    public String format(TemplateTemporalModel tm) throws 
TemplateValueFormatException,
+            TemplateModelException {
+        Temporal temporal = TemplateFormatUtil.getNonNullTemporal(tm);
+
+        if (instantConversion) {
+            temporal = ((Instant) temporal).atZone(zoneId);
+        }
+
+        try {
+            return dateTimeFormatter.format(temporal);
+        } catch (DateTimeException e) {
+            throw new UnformattableValueException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean isLocaleBound() {
+        return false;
+    }
+
+    @Override
+    public boolean isTimeZoneBound() {
+        return zoneId != null;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+}
diff --git 
a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java 
b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
index 58e47c0..e932d3f 100644
--- a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
@@ -19,11 +19,11 @@
 
 package freemarker.core;
 
+import java.time.LocalDate;
 import java.time.LocalTime;
+import java.time.OffsetTime;
 import java.time.Year;
 import java.time.YearMonth;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
@@ -35,6 +35,15 @@ class ISOTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
 
     static final ISOTemplateTemporalFormatFactory INSTANCE = new 
ISOTemplateTemporalFormatFactory();
 
+    private ISOTemplateTemporalFormatFactory() {
+        // Not meant to be called from outside
+    }
+
+    private static final DateTimeFormatter ISO8601_DATE_FORMAT = new 
DateTimeFormatterBuilder()
+            .append(DateTimeFormatter.ISO_LOCAL_DATE)
+            .toFormatter()
+            .withLocale(Locale.US);
+
     private static final DateTimeFormatter ISO8601_DATE_TIME_FORMAT = new 
DateTimeFormatterBuilder()
             .append(DateTimeFormatter.ISO_LOCAL_DATE)
             .optionalStart()
@@ -50,7 +59,6 @@ class ISOTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             .optionalEnd()
             .optionalEnd()
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
     private static final DateTimeFormatter ISO8601_TIME_FORMAT = new 
DateTimeFormatterBuilder()
@@ -60,8 +68,10 @@ class ISOTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             .appendLiteral(":")
             .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
             .appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true)
+            .optionalStart()
+            .appendOffsetId()
+            .optionalEnd()
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
     private static final DateTimeFormatter ISO8601_YEARMONTH_FORMAT = new 
DateTimeFormatterBuilder()
@@ -69,13 +79,11 @@ class ISOTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             .appendLiteral("-")
             .appendValue(ChronoField.MONTH_OF_YEAR, 2)
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
     static final DateTimeFormatter ISO8601_YEAR_FORMAT = new 
DateTimeFormatterBuilder()
             .appendValue(ChronoField.YEAR)
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
     @Override
@@ -86,13 +94,13 @@ class ISOTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             throw new InvalidFormatParametersException("xs currently doesn't 
support parameters");
         }
 
-        return getXSFormatter(temporalClass, timeZone.toZoneId());
+        return getISOFormatter(temporalClass, timeZone);
     }
 
-    private static ISOLikeTemplateTemporalFormat getXSFormatter(Class<? 
extends Temporal> temporalClass, ZoneId timeZone) {
+    private static ISOLikeTemplateTemporalTemporalFormat 
getISOFormatter(Class<? extends Temporal> temporalClass, TimeZone timeZone) {
         final DateTimeFormatter dateTimeFormatter;
         final String description;
-        if (temporalClass == LocalTime.class) {
+        if (temporalClass == LocalTime.class || temporalClass == 
OffsetTime.class) {
             dateTimeFormatter = ISO8601_TIME_FORMAT;
             description = "ISO 8601 (subset) time";
         } else if (temporalClass == Year.class) {
@@ -101,18 +109,21 @@ class ISOTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
         } else if (temporalClass == YearMonth.class) {
             dateTimeFormatter = ISO8601_YEARMONTH_FORMAT;
             description = "ISO 8601 (subset) year-month";
+        } else if (temporalClass == LocalDate.class) {
+            dateTimeFormatter = ISO8601_DATE_FORMAT;
+            description = "ISO 8601 (subset) date";
         } else {
             Class<? extends Temporal> normTemporalClass =
                     
_CoreTemporalUtils.normalizeSupportedTemporalClass(temporalClass);
             if (normTemporalClass != temporalClass) {
-                return getXSFormatter(normTemporalClass, timeZone);
+                return getISOFormatter(normTemporalClass, timeZone);
             } else {
                 dateTimeFormatter = ISO8601_DATE_TIME_FORMAT;
                 description = "ISO 8601 (subset) date-time";
             }
         }
         // TODO [FREEMARKER-35] What about date-only?
-        return new 
ISOLikeTemplateTemporalFormat(dateTimeFormatter.withZone(timeZone), 
description);
+        return new ISOLikeTemplateTemporalTemporalFormat(dateTimeFormatter, 
temporalClass, timeZone, description);
     }
 
 }
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java 
b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index 001b870..da966fb 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -18,75 +18,113 @@
  */
 package freemarker.core;
 
-import java.time.Year;
-import java.time.YearMonth;
-import java.time.chrono.IsoChronology;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.FormatStyle;
 import java.time.temporal.Temporal;
 import java.util.Locale;
 import java.util.TimeZone;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateTemporalModel;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.StringUtil;
 
-final class JavaTemplateTemporalFormat extends 
BaseJavaTemplateTemporalFormatTemplateFormat {
-    private static final Pattern FORMAT_STYLE_PATTERN = 
Pattern.compile("^(short|medium|long|full)(_(short|medium|long|full))?$");
+class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
 
-    // TODO [FREEMARKER-35] This is not right, but for now we mimic what 
TemporalUtils did
-    private static final DateTimeFormatter SHORT_FORMAT = 
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
-    private static final DateTimeFormatter MEDIUM_FORMAT = 
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
-    private static final DateTimeFormatter LONG_FORMAT = 
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
-    private static final DateTimeFormatter FULL_FORMAT = 
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL);
+    enum FormatTimeConversion {
+        INSTANT_TO_ZONED_DATE_TIME,
+        SET_ZONE_FROM_OFFSET
+    }
+
+    private static final Pattern FORMAT_STYLE_PATTERN = 
Pattern.compile("(short|medium|long|full)(?:_(short|medium|long|full))?");
 
+    private final DateTimeFormatter dateTimeFormatter;
+    private final ZoneId zoneId;
     private final String formatString;
+    private final FormatTimeConversion formatTimeConversion;
 
     JavaTemplateTemporalFormat(String formatString, Class<? extends Temporal> 
temporalClass, Locale locale, TimeZone timeZone)
             throws InvalidFormatParametersException {
-        super(getDateTimeFormat(formatString, temporalClass, locale, 
timeZone));
         this.formatString = formatString;
-    }
 
-    private static DateTimeFormatter getDateTimeFormat(String formatString, 
Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone) 
throws
-            InvalidFormatParametersException {
-        DateTimeFormatter result;
-        if (FORMAT_STYLE_PATTERN.matcher(formatString).matches()) {
-            // TODO [FREEMARKER-35] This is not right, but for now we mimic 
what TemporalUtils did
-            boolean isYear = Year.class.isAssignableFrom(temporalClass);
-            boolean isYearMonth = 
YearMonth.class.isAssignableFrom(temporalClass);
-            String[] formatSplt = formatString.split("_");
-            if (isYear || isYearMonth) {
-                String reducedPattern = 
DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.valueOf(formatSplt[0].toUpperCase()),
 null, IsoChronology.INSTANCE, locale);
-                if (isYear)
-                    result = 
DateTimeFormatter.ofPattern(removeNonYM(reducedPattern, false));
-                else
-                    result = 
DateTimeFormatter.ofPattern(removeNonYM(reducedPattern, true));
-            } else if ("short".equals(formatString))
-                result =  SHORT_FORMAT;
-            else if ("medium".equals(formatString))
-                result =  MEDIUM_FORMAT;
-            else if ("long".equals(formatString))
-                result =  LONG_FORMAT;
-            else if ("full".equals(formatString))
-                result = FULL_FORMAT;
-            else
-                result = 
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.valueOf(formatSplt[0].toUpperCase()),
 FormatStyle.valueOf(formatSplt[1].toUpperCase()));
+        temporalClass = 
_CoreTemporalUtils.normalizeSupportedTemporalClass(temporalClass);
+
+        Matcher localizedPatternMatcher = 
FORMAT_STYLE_PATTERN.matcher(formatString);
+        boolean isLocalizedPattern = localizedPatternMatcher.matches();
+        if (temporalClass == Instant.class) {
+            this.formatTimeConversion = 
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME;
+        } else if (isLocalizedPattern && (temporalClass == 
OffsetDateTime.class || temporalClass == OffsetTime.class)) {
+            this.formatTimeConversion = 
FormatTimeConversion.SET_ZONE_FROM_OFFSET;
+        } else {
+            this.formatTimeConversion = null;
+        }
+
+        DateTimeFormatter dateTimeFormatter;
+        if (isLocalizedPattern) {
+            FormatStyle datePartFormatStyle = 
FormatStyle.valueOf(localizedPatternMatcher.group(1).toUpperCase(Locale.ROOT));
+            String group2 = localizedPatternMatcher.group(2);
+            FormatStyle timePartFormatStyle = group2 != null
+                    ? FormatStyle.valueOf(group2.toUpperCase(Locale.ROOT))
+                    : datePartFormatStyle;
+            if (temporalClass == LocalDateTime.class || temporalClass == 
ZonedDateTime.class
+                    || temporalClass == OffsetDateTime.class || temporalClass 
== Instant.class) {
+                dateTimeFormatter = 
DateTimeFormatter.ofLocalizedDateTime(datePartFormatStyle, timePartFormatStyle);
+            } else if (temporalClass == LocalTime.class || temporalClass == 
OffsetTime.class) {
+                dateTimeFormatter = 
DateTimeFormatter.ofLocalizedTime(timePartFormatStyle);
+            } else if (temporalClass == LocalDate.class) {
+                dateTimeFormatter = 
DateTimeFormatter.ofLocalizedDate(datePartFormatStyle);
+            } else {
+                throw new InvalidFormatParametersException(
+                        "Format " + StringUtil.jQuote(formatString) + " is not 
supported for "
+                        + temporalClass.getName());
+            }
         } else {
             try {
-                result = DateTimeFormatter.ofPattern(formatString);
+                dateTimeFormatter = DateTimeFormatter.ofPattern(formatString);
             } catch (IllegalArgumentException e) {
                 throw new InvalidFormatParametersException(e.getMessage(), e);
             }
         }
-        return result.withLocale(locale).withZone(timeZone.toZoneId());
+        this.dateTimeFormatter = dateTimeFormatter.withLocale(locale);
+
+        this.zoneId = timeZone.toZoneId();
     }
 
-    // TODO [FREEMARKER-35] This override should be unecessary. Move logic 
here into getDateTimeFormat somehow.
     @Override
     public String format(TemplateTemporalModel tm) throws 
TemplateValueFormatException, TemplateModelException {
-        return super.format(tm);
+        DateTimeFormatter dateTimeFormatter = this.dateTimeFormatter;
+        Temporal temporal = TemplateFormatUtil.getNonNullTemporal(tm);
+
+        if (formatTimeConversion == 
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME) {
+            temporal = ((Instant) temporal).atZone(zoneId);
+        } else if (formatTimeConversion == 
FormatTimeConversion.SET_ZONE_FROM_OFFSET) {
+            if (temporal instanceof OffsetDateTime) {
+                dateTimeFormatter = 
dateTimeFormatter.withZone(((OffsetDateTime) temporal).getOffset());
+            } else if (temporal instanceof OffsetTime) {
+                dateTimeFormatter = dateTimeFormatter.withZone(((OffsetTime) 
temporal).getOffset());
+            } else {
+                throw new IllegalArgumentException(
+                        "Formatter was created for OffsetTime and 
OffsetDateTime, but value was a "
+                                + 
ClassUtil.getShortClassNameOfObject(temporal));
+            }
+        }
+
+        try {
+            return dateTimeFormatter.format(temporal);
+        } catch (DateTimeException e) {
+            throw new UnformattableValueException(e.getMessage(), e);
+        }
     }
 
     @Override
@@ -110,20 +148,4 @@ final class JavaTemplateTemporalFormat extends 
BaseJavaTemplateTemporalFormatTem
         return true;
     }
 
-    // TODO [FREEMARKER-35] This is not right, but for now we mimic what 
TemporalUtils did
-    private static String removeNonYM(String pattern, boolean withMonth) {
-        boolean separator = false;
-        boolean copy = true;
-        StringBuilder newPattern = new StringBuilder();
-        for (char c : pattern.toCharArray()) {
-            if (c == '\'')
-                separator = !separator;
-            if (!separator && Character.isAlphabetic(c))
-                copy = c == 'y' || c == 'u' || (withMonth && (c == 'M' || c == 
'L'));
-            if (copy)
-                newPattern.append(c);
-        }
-        return newPattern.toString();
-    }
-
 }
diff --git 
a/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java 
b/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java
index 5e92a89..b1e049d 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormatFactory.java
@@ -19,12 +19,9 @@
 
 package freemarker.core;
 
-import java.time.format.DateTimeFormatter;
-import java.time.format.FormatStyle;
 import java.time.temporal.Temporal;
 import java.util.Locale;
 import java.util.TimeZone;
-import java.util.regex.Pattern;
 
 class JavaTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
     public static final JavaTemplateTemporalFormatFactory INSTANCE = new 
JavaTemplateTemporalFormatFactory();
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java 
b/src/main/java/freemarker/core/TemplateTemporalFormat.java
index 9fa51ef..f41fb0c 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java
@@ -49,4 +49,6 @@ public abstract class TemplateTemporalFormat extends 
TemplateValueFormat {
      */
     public abstract boolean isTimeZoneBound();
 
+    // TODO [FREEMARKER-35] Add parse method
+
 }
diff --git a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java 
b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
index 1f36313..8749c74 100644
--- a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
@@ -21,11 +21,11 @@ package freemarker.core;
 
 import static freemarker.core.ISOTemplateTemporalFormatFactory.*;
 
+import java.time.LocalDate;
 import java.time.LocalTime;
+import java.time.OffsetTime;
 import java.time.Year;
 import java.time.YearMonth;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
@@ -41,9 +41,13 @@ class XSTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
         // Not meant to be called from outside
     }
 
-    private final static DateTimeFormatter XSD_DATE_TIME_FORMAT = new 
DateTimeFormatterBuilder()
+    private static final DateTimeFormatter XSD_DATE_FORMAT = new 
DateTimeFormatterBuilder()
+            .append(DateTimeFormatter.ISO_LOCAL_DATE)
+            .toFormatter()
+            .withLocale(Locale.US);
+
+    private static final DateTimeFormatter XSD_DATE_TIME_FORMAT = new 
DateTimeFormatterBuilder()
             .append(DateTimeFormatter.ISO_LOCAL_DATE)
-            .optionalStart()
             .appendLiteral('T')
             .appendValue(ChronoField.HOUR_OF_DAY, 2)
             .appendLiteral(":")
@@ -51,15 +55,13 @@ class XSTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             .appendLiteral(":")
             .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
             .appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true)
-            .optionalEnd()
             .optionalStart()
             .appendOffsetId()
             .optionalEnd()
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
-    private final static DateTimeFormatter XSD_TIME_FORMAT = new 
DateTimeFormatterBuilder()
+    private static final DateTimeFormatter XSD_TIME_FORMAT = new 
DateTimeFormatterBuilder()
             .appendValue(ChronoField.HOUR_OF_DAY, 2)
             .appendLiteral(":")
             .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
@@ -70,18 +72,13 @@ class XSTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             .appendOffsetId()
             .optionalEnd()
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
     private static final DateTimeFormatter XSD_YEARMONTH_FORMAT = new 
DateTimeFormatterBuilder()
             .appendValue(ChronoField.YEAR)
             .appendLiteral("-")
             .appendValue(ChronoField.MONTH_OF_YEAR, 2)
-            .optionalStart()
-            .appendOffsetId()
-            .optionalEnd()
             .toFormatter()
-            .withZone(ZoneOffset.UTC)
             .withLocale(Locale.US);
 
     @Override
@@ -92,13 +89,13 @@ class XSTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
             throw new InvalidFormatParametersException("xs currently doesn't 
support parameters");
         }
 
-        return getXSFormatter(temporalClass, timeZone.toZoneId());
+        return getXSFormatter(temporalClass, timeZone);
     }
 
-    private static ISOLikeTemplateTemporalFormat getXSFormatter(Class<? 
extends Temporal> temporalClass, ZoneId timeZone) {
+    private static ISOLikeTemplateTemporalTemporalFormat 
getXSFormatter(Class<? extends Temporal> temporalClass, TimeZone timeZone) {
         final DateTimeFormatter dateTimeFormatter;
         final String description;
-        if (temporalClass == LocalTime.class) {
+        if (temporalClass == LocalTime.class || temporalClass == 
OffsetTime.class) {
             dateTimeFormatter = XSD_TIME_FORMAT;
             description = "W3C XML Schema time";
         } else if (temporalClass == Year.class) {
@@ -107,6 +104,9 @@ class XSTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
         } else if (temporalClass == YearMonth.class) {
             dateTimeFormatter = XSD_YEARMONTH_FORMAT;
             description = "W3C XML Schema year-month";
+        } else if (temporalClass == LocalDate.class) {
+            dateTimeFormatter = XSD_DATE_FORMAT;
+            description = "W3C XML Schema date";
         } else {
             Class<? extends Temporal> normTemporalClass =
                     
_CoreTemporalUtils.normalizeSupportedTemporalClass(temporalClass);
@@ -117,8 +117,7 @@ class XSTemplateTemporalFormatFactory extends 
TemplateTemporalFormatFactory {
                 description = "W3C XML Schema date-time";
             }
         }
-        // TODO [FREEMARKER-35] What about date-only?
-        return new 
ISOLikeTemplateTemporalFormat(dateTimeFormatter.withZone(timeZone), 
description);
+        return new ISOLikeTemplateTemporalTemporalFormat(dateTimeFormatter, 
temporalClass, timeZone, description);
     }
 
 }
diff --git 
a/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl 
b/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
index cbc5fd1..5c42c29 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
@@ -72,21 +72,20 @@
 
 <@assertEquals expected="05/04/03 06:07" actual=localDateTime?string.short />
 <@assertEquals expected="5 avr. 2003 06:07:08" 
actual=localDateTime?string.medium />
+<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
 <@assertEquals expected="5 avril 2003 06:07:08 ET" 
actual=localDateTime?string.long />
 <@assertEquals expected="samedi 5 avril 2003 06 h 07 ET" 
actual=localDateTime?string.full />
+-->
 
-<@assertEquals expected="03" actual=year?string.short />
-<@assertEquals expected="2003" actual=year?string.medium />
-<@assertEquals expected="2003" actual=year?string.long />
-<@assertEquals expected="2003" actual=year?string.full />
-
-<@assertEquals expected="04/03" actual=yearMonth?string.short />
-<@assertEquals expected="avr. 2003" actual=yearMonth?string.medium />
-<@assertEquals expected="avril 2003" actual=yearMonth?string.long />
-<@assertEquals expected="avril 2003" actual=yearMonth?string.full />
-<#setting locale="es_ES">
-<@assertEquals expected="abril de 2003" actual=yearMonth?string.full />
-<#setting locale="fr_FR">
+<#-- There combinations are clearly not supported by the "localized pattern" 
API. -->
+<@assertFails message="not supported for 
java.time.Year">${year?string.short}</@>
+<@assertFails message="not supported for 
java.time.Year">${year?string.medium}</@>
+<@assertFails message="not supported for 
java.time.Year">${year?string.long}</@>
+<@assertFails message="not supported for 
java.time.Year">${year?string.full}</@>
+<@assertFails message="not supported for 
java.time.YearMonth">${yearMonth?string.short}</@>
+<@assertFails message="not supported for 
java.time.YearMonth">${yearMonth?string.medium}</@>
+<@assertFails message="not supported for 
java.time.YearMonth">${yearMonth?string.long}</@>
+<@assertFails message="not supported for 
java.time.YearMonth">${yearMonth?string.full}</@>
 
 <@assertEquals expected="05/04/03 06:07" actual=zonedDateTime?string.short />
 <@assertEquals expected="5 avr. 2003 06:07:08" 
actual=zonedDateTime?string.medium />
@@ -95,17 +94,23 @@
 
 <@assertEquals expected="05/04/03 06:07" 
actual=localDateTime?string.short_short />
 <@assertEquals expected="05/04/03 06:07:08" 
actual=localDateTime?string.short_medium />
+<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
 <@assertEquals expected="05/04/03 06:07:08 ET" 
actual=localDateTime?string.short_long />
 <@assertEquals expected="05/04/03 06 h 07 ET" 
actual=localDateTime?string.short_full />
+-->
 
 <@assertEquals expected="5 avr. 2003 06:07:08" 
actual=localDateTime?string.medium_medium />
 <@assertEquals expected="5 avril 2003 06:07:08" 
actual=localDateTime?string.long_medium />
 <@assertEquals expected="samedi 5 avril 2003 06:07:08" 
actual=localDateTime?string.full_medium />
 
+<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
 <@assertEquals expected="5 avril 2003 06:07:08 ET" 
actual=localDateTime?string.long_long />
 <@assertEquals expected="samedi 5 avril 2003 06:07:08 ET" 
actual=localDateTime?string.full_long />
+-->
 
+<#-- TODO [FREEMARKER-35] These combinations are not supported by Java in 
practice. What should FM do?
 <@assertEquals expected="samedi 5 avril 2003 06 h 07 ET" 
actual=localDateTime?string.full_full />
+-->
 
 <@assertEquals expected="2003-04-05" actual=localDateTime?string('yyyy-MM-dd') 
/>
 <@assertEquals expected="2003-04-05 06:07:08" 
actual=localDateTime?string('yyyy-MM-dd HH:mm:ss') />

Reply via email to