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 9bd0817 [FREEMARKER-35] - When no time zone (or offset) is shown,
convert temporals to the FreeMarker time zone. - Changed temporal format
defaults to give useful output (at least on Java 8 and English locale) -
Improved error message in case of formatting exception (mostly handy when
people try to format local temporals with a format that wants to show a time
zone or offset) - Improved javadocs
9bd0817 is described below
commit 9bd081750e567dd41be15b88819cdae80afafcd0
Author: ddekany <[email protected]>
AuthorDate: Sun Oct 31 17:47:22 2021 +0100
[FREEMARKER-35]
- When no time zone (or offset) is shown, convert temporals to the
FreeMarker time zone.
- Changed temporal format defaults to give useful output (at least on Java
8 and English locale)
- Improved error message in case of formatting exception (mostly handy when
people try to format local temporals with a format that wants to show a time
zone or offset)
- Improved javadocs
---
.../freemarker/core/BuiltInsForMultipleTypes.java | 2 +-
src/main/java/freemarker/core/Configurable.java | 158 +++++++++++++-
src/main/java/freemarker/core/Environment.java | 28 ++-
src/main/java/freemarker/core/EvalUtil.java | 8 +-
.../core/JavaTemplateTemporalFormat.java | 92 ++++++--
src/main/java/freemarker/core/_MessageUtil.java | 25 ++-
.../freemarker/template/TemplateDateModel.java | 3 +
.../freemarker/template/TemplateTemporalModel.java | 14 ++
.../java/freemarker/core/TemporalFormatTest.java | 243 +++++++++++++++++++++
.../test/templatesuite/templates/temporal.ftl | 42 ++--
src/test/resources/logback-test.xml | 1 +
11 files changed, 548 insertions(+), 68 deletions(-)
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index aebbd57..8f15548 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -649,7 +649,7 @@ class BuiltInsForMultipleTypes {
cachedValue =
EvalUtil.assertFormatResultNotNull(defaultFormat.format(temporalModel));
} catch (TemplateValueFormatException e) {
try {
- throw
_MessageUtil.newCantFormatTemporalException(defaultFormat, target, e, true);
+ throw
_MessageUtil.newCantFormatTemporalException(defaultFormat, temporalModel,
target, e, true);
} catch (TemplateException e2) {
// `e` should always be a TemplateModelException
here, but to be sure:
throw
_CoreAPI.ensureIsTemplateModelException("Failed to format date/time/datetime",
e2);
diff --git a/src/main/java/freemarker/core/Configurable.java
b/src/main/java/freemarker/core/Configurable.java
index c1ffce9..bce76e8 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -33,6 +33,8 @@ import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Collection;
@@ -517,31 +519,31 @@ public class Configurable {
dateTimeFormat = "";
properties.setProperty(DATETIME_FORMAT_KEY, dateTimeFormat);
- instantFormat = "";
+ instantFormat = JavaTemplateTemporalFormat.MEDIUM;
properties.setProperty(INSTANT_FORMAT_KEY, instantFormat);
- localDateFormat = "";
+ localDateFormat = JavaTemplateTemporalFormat.MEDIUM;
properties.setProperty(LOCAL_DATE_FORMAT_KEY, localDateFormat);
- localDateTimeFormat = "";
+ localDateTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
properties.setProperty(LOCAL_DATE_TIME_FORMAT_KEY,
localDateTimeFormat);
- localTimeFormat = "";
+ localTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
properties.setProperty(LOCAL_TIME_FORMAT_KEY, localTimeFormat);
- offsetDateTimeFormat = "";
+ offsetDateTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
properties.setProperty(OFFSET_DATE_TIME_FORMAT_KEY,
offsetDateTimeFormat);
- offsetTimeFormat = "";
+ offsetTimeFormat = JavaTemplateTemporalFormat.LONG;
properties.setProperty(OFFSET_TIME_FORMAT_KEY, offsetTimeFormat);
- zonedDateTimeFormat = "";
+ zonedDateTimeFormat = JavaTemplateTemporalFormat.MEDIUM;
properties.setProperty(ZONED_DATE_TIME_FORMAT_KEY,
zonedDateTimeFormat);
- yearFormat = "";
+ yearFormat = "iso";
properties.setProperty(YEAR_FORMAT_KEY, yearFormat);
- yearMonthFormat = "";
+ yearMonthFormat = "iso";
properties.setProperty(YEAR_MONTH_FORMAT_KEY, yearMonthFormat);
classicCompatible = Integer.valueOf(0);
@@ -1370,9 +1372,15 @@ public class Configurable {
}
/**
- * Sets the format used to convert {@link java.time.Instant}-s to
string-s, also the format that
+ * Sets the format used to convert {@link java.time.Instant}-s to strings,
also the format that
* {@code someString?instant} will use to parse strings.
- * <p>Defaults to TODO [FREEMARKER-35].
+ *
+ * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)};
+ * {@code iso}/{@code xs} will show the time offset.
+ *
* @since 2.3.32
*/
public void setInstantFormat(String instantFormat) {
@@ -1381,6 +1389,7 @@ public class Configurable {
/**
* Getter pair of {@link #setInstantFormat(String)}.
+ *
* @since 2.3.32
*/
public String getInstantFormat() {
@@ -1396,12 +1405,25 @@ public class Configurable {
return instantFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.LocalDate}-s to
strings, also the format that
+ * {@code someString?local_date} will use to parse strings.
+ *
+ * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)};
+ * {@code iso}/{@code xs} will not show the time part.
+ *
+ * @since 2.3.32
+ */
public void setLocalDateFormat(String localDateFormat) {
this.localDateFormat = localDateFormat;
}
/**
* Getter pair of {@link #setLocalDateFormat(String)}.
+ *
* @since 2.3.32
*/
public String getLocalDateFormat() {
@@ -1417,12 +1439,25 @@ public class Configurable {
return localDateFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.LocalDateTime}-s to
strings, also the format that
+ * {@code someString?local_date_time} will use to parse strings.
+ *
+ * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)};
+ * {@code iso}/{@code xs} will not show an offset.
+ *
+ * @since 2.3.32
+ */
public void setLocalDateTimeFormat(String localDateTimeFormat) {
this.localDateTimeFormat = localDateTimeFormat;
}
/**
* Getter pair of {@link #setLocalDateTimeFormat(String)}.
+ *
* @since 2.3.32
*/
public String getLocalDateTimeFormat() {
@@ -1438,12 +1473,25 @@ public class Configurable {
return localDateTimeFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.LocalTime}-s to
strings, also the format that
+ * {@code someString?local_time} will use to parse strings.
+ *
+ * <p>Defaults to {@code "medium"}, which means {@link FormatStyle#MEDIUM}.
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)};
+ * {@code iso}/{@code xs} will not show the time offset.
+ *
+ * @since 2.3.32
+ */
public void setLocalTimeFormat(String localTimeFormat) {
this.localTimeFormat = localTimeFormat;
}
/**
* Getter pair of {@link #setLocalTimeFormat(String)}.
+ *
* @since 2.3.32
*/
public String getLocalTimeFormat() {
@@ -1459,6 +1507,20 @@ public class Configurable {
return localTimeFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.OffsetDateTime}-s to
strings, also the format that
+ * {@code someString?offset_date_time} will use to parse strings.
FreeMarker will detect if the format doesn't
+ * show the offset (as is typically the case for the {@code "medium"}
format), and then it will convert the value to
+ * the time zone specified in the {@link #setTimeZone(TimeZone) timeZone}
setting of FreeMarker.
+ *
+ * <p>Defaults to {@code "medium"}, which means {@link
FormatStyle#MEDIUM}, which usually doesn't show the time
+ * offset; see the parameter JavaDoc for more.
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)}.
+ *
+ * @since 2.3.32
+ */
public void setOffsetDateTimeFormat(String offsetDateTimeFormat) {
this.offsetDateTimeFormat = offsetDateTimeFormat;
}
@@ -1480,6 +1542,23 @@ public class Configurable {
return offsetDateTimeFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.OffsetTime}-s to
strings, also the format that
+ * {@code someString?offset_time} will use to parse strings. The format
<b>should show the offset</b>, unless you
+ * are sure that {@link #setTimeZone(TimeZone) timeZone} setting will be a
zone that has no daylight saving.
+ * This is because if the offset is not shown, FreeMarker has to convert
the value to the time zone specified in the
+ * {@link #setTimeZone(TimeZone) timeZone} setting, but we don't know the
day, so we can't account for daylight
+ * saving changes, and thus we can't do zone conversion reliably if a
daylight saving is possible.
+ *
+ * <p>Defaults to {@code "long"}, which means {@link FormatStyle#LONG},
which usually show the time offset; see the
+ * parameter JavaDoc for more.
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)}, but it <b>must show the offset</b>
+ * (see earlier why).
+ *
+ * @since 2.3.32
+ */
public void setOffsetTimeFormat(String offsetTimeFormat) {
this.offsetTimeFormat = offsetTimeFormat;
}
@@ -1501,6 +1580,34 @@ public class Configurable {
return offsetTimeFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.ZonedDateTime}-s to
strings, also the format that
+ * {@code someString?offset_date_time} will use to parse strings.
FreeMarker will detect if the format doesn't
+ * show the zone or offset (as is typically the case for the {@code
"medium"} format), and then it will convert the
+ * value to the time zone specified in the {@link #setTimeZone(TimeZone)
timeZone} setting of FreeMarker.
+ *
+ * <p>Defaults to {@code "medium"}, which means {@link
FormatStyle#MEDIUM}, which usually doesn't show the time
+ * zone; see the parameter JavaDoc for more.
+ *
+ * @param localDateTimeFormat
+ * One of:
+ * <ul>
+ * <li>{@code "iso"}: ISO-8601 format (like {@code
2021-09-29T13:00:05.2})
+ * <li>{@code "xs"}: XSD format (same as ISO-8601, but parsing is
more restrictive)
+ * <li>{@code "short"}, {@code "medium"}, {@code "long"}, {@code
"full"}, or two of these connected with
+ * an {@code "_"}: Refers to the {@link FormatStyle}
constants. When in a pair, as in
+ * {@code "medium_long"}, the 1st style refers to the date
part, and the 2nd style to the time part.
+ * Java doesn't specify what these styles actually mean.
However, experience with Java 8 shows
+ * that "short" and "medium" will not show the time zone or
time offset (which then triggers the zone
+ * conversion mentioned earlier), and will show months with
numbers, while "long" and "full" will show
+ * the zone and/or offset, and shows months with their names.
(Also "long" and "full" before Java 9
+ * fails for {@link LocalDateTime} and {@link LocalTime},
because of bug JDK-8085887.)
+ * <li>Other: Interpreted as pattern via {@link
DateTimeFormatter#ofPattern}. Example:
+ * {@code "yyyy-MM-dd HH:mm:ss X"}.
+ * </ul>
+ *
+ * @since 2.3.32
+ */
public void setZonedDateTimeFormat(String zonedDateTimeFormat) {
this.zonedDateTimeFormat = zonedDateTimeFormat;
}
@@ -1522,6 +1629,19 @@ public class Configurable {
return zonedDateTimeFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.Year}-s to strings,
also the format that
+ * {@code someString?local_time} will use to parse strings.
+ *
+ * <p>Defaults to {@code "iso"}, which will simply show the year like
{@code "2021"} (without the quotation marks).
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)},
+ * {@code iso}/{@code xs} only the year is shown.
+ * Java (as of version 8) doesn't support "styles" (like "short",
"medium", etc.) for this.
+ *
+ * @since 2.3.32
+ */
public void setYearFormat(String yearFormat) {
this.yearFormat = yearFormat;
}
@@ -1543,12 +1663,26 @@ public class Configurable {
return yearFormat != null;
}
+ /**
+ * Sets the format used to convert {@link java.time.YearMonth}-s to
strings, also the format that
+ * {@code someString?local_time} will use to parse strings.
+ *
+ * <p>Defaults to {@code "iso"}, which will show the value like {@code
"2021-12"} (without the quotation marks).
+ *
+ * @param localDateTimeFormat
+ * See the similar parameter of {@link
#setZonedDateTimeFormat(String)};
+ * {@code iso}/{@code xs} will look like {@code 2021-12}.
+ * Java (as of version 8) doesn't support "styles" (like "short",
"medium", etc.) for this.
+ *
+ * @since 2.3.32
+ */
public void setYearMonthFormat(String yearMonthFormat) {
this.yearMonthFormat = yearMonthFormat;
}
/**
* Getter pair of {@link #setYearMonthFormat(String)}.
+ *
* @since 2.3.32
*/
public String getYearMonthFormat() {
@@ -1571,7 +1705,7 @@ public class Configurable {
* are these: {@link Instant}, {@link LocalDate}, {@link
LocalDateTime}, {@link LocalTime},
* {@link OffsetDateTime}, {@link OffsetTime}, {@link Year}, {@link
YearMonth}, {@link ZonedDateTime}.
*
- * @return Never {@code null}, maybe {@code ""} though.
+ * @return Never {@code null}.
*
* @throws NullPointerException If {@link temporalClass} was {@code null}
* @throws IllegalArgumentException If {@link temporalClass} is not a
supported {@link Temporal} subclass.
diff --git a/src/main/java/freemarker/core/Environment.java
b/src/main/java/freemarker/core/Environment.java
index 283e062..4f0826c 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -2217,7 +2217,8 @@ public final class Environment extends Configurable {
* @param blamedTtmSourceExp
* The blamed expression if an error occurs; only used for
error messages.
*/
- String formatTemporalToPlainText(TemplateTemporalModel ttm, String
formatString,
+ String formatTemporalToPlainText(
+ TemplateTemporalModel ttm, String formatString,
Expression blamedTtmSourceExp, Expression blamedFormatterSourceExp,
boolean useTempModelExc)
throws TemplateException {
@@ -2225,21 +2226,26 @@ public final class Environment extends Configurable {
formatString, ttm,
blamedTtmSourceExp, blamedFormatterSourceExp,
useTempModelExc);
- try {
- return EvalUtil.assertFormatResultNotNull(ttf.format(ttm));
- } catch (TemplateValueFormatException e) {
- throw _MessageUtil.newCantFormatTemporalException(ttf,
blamedTtmSourceExp, e, true);
- }
+ return Environment.this.formatTemporalToPlainText(ttm,
blamedTtmSourceExp, ttf, true);
}
- String formatTemporalToPlainText(TemplateTemporalModel ttm, Expression
blamedTtmSourceExp,
- boolean useTempModelExc) throws TemplateException {
+ String formatTemporalToPlainText(
+ TemplateTemporalModel ttm, Expression blamedTtmSourceExp,
+ boolean useTempModelExc)
+ throws TemplateException {
TemplateTemporalFormat ttf = getTemplateTemporalFormat(
ttm, blamedTtmSourceExp, useTempModelExc);
+ return formatTemporalToPlainText(ttm, blamedTtmSourceExp, ttf, false);
+ }
+
+ String formatTemporalToPlainText(
+ TemplateTemporalModel ttm, Expression blamedTtmSourceExp,
TemplateTemporalFormat ttf,
+ boolean useTempModelExc)
+ throws TemplateException {
try {
return EvalUtil.assertFormatResultNotNull(ttf.format(ttm));
} catch (TemplateValueFormatException e) {
- throw _MessageUtil.newCantFormatTemporalException(ttf,
blamedTtmSourceExp, e, false);
+ throw _MessageUtil.newCantFormatTemporalException(ttf, ttm,
blamedTtmSourceExp, e, useTempModelExc);
}
}
@@ -2275,8 +2281,8 @@ public final class Environment extends Configurable {
}
_ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
- "The value of the \"", settingName,
- "\" FreeMarker configuration setting is a malformed
temporal format string: ",
+ "Problem with using the \"", settingName,
+ "\" FreeMarker configuration setting value, ",
new _DelayedJQuote(settingValue), ". Reason given: ",
e.getMessage());
throw useTempModelExc ? new _TemplateModelException(e, desc) : new
_MiscTemplateException(e, desc);
diff --git a/src/main/java/freemarker/core/EvalUtil.java
b/src/main/java/freemarker/core/EvalUtil.java
index 78d626a..2dbf57e 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -413,7 +413,7 @@ class EvalUtil {
try {
return assertFormatResultNotNull(format.format(ttm));
} catch (TemplateValueFormatException e) {
- throw _MessageUtil.newCantFormatTemporalException(format, exp,
e, false);
+ throw _MessageUtil.newCantFormatTemporalException(format, ttm,
exp, e, false);
}
} else if (tm instanceof TemplateMarkupOutputModel) {
return tm;
@@ -424,10 +424,10 @@ class EvalUtil {
/**
* Like {@link #coerceModelToStringOrMarkup(TemplateModel, Expression,
String, Environment)}, but gives error
- * if the result is markup. This is what you normally used where markup
results can't be used.
+ * if the result is markup. This is what you normally use where markup
results can't be used.
*
* @param seqTip
- * Tip to display if the value type is not coercable, but it's
sequence or collection.
+ * Tip to display if the value type is not coercable, and it's
sequence or collection.
*
* @return Never {@code null}
*/
@@ -456,7 +456,7 @@ class EvalUtil {
try {
return ensureFormatResultString(format.format(ttm), exp, env);
} catch (TemplateValueFormatException e) {
- throw _MessageUtil.newCantFormatTemporalException(format, exp,
e, false);
+ throw _MessageUtil.newCantFormatTemporalException(format, ttm,
exp, e, false);
}
} else {
return coerceModelToTextualCommon(tm, exp, seqTip, false, false,
env);
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index 5482826..fb5f32d 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -25,7 +25,10 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
+import java.time.Year;
+import java.time.YearMonth;
import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
@@ -49,7 +52,8 @@ class JavaTemplateTemporalFormat extends
TemplateTemporalFormat {
enum FormatTimeConversion {
INSTANT_TO_ZONED_DATE_TIME,
- SET_ZONE_FROM_OFFSET
+ SET_ZONE_FROM_OFFSET,
+ CONVERT_TO_CURRENT_ZONE
}
static final String SHORT = "short";
@@ -71,20 +75,13 @@ class JavaTemplateTemporalFormat extends
TemplateTemporalFormat {
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;
- }
+ Matcher formatStylePatternMatcher =
FORMAT_STYLE_PATTERN.matcher(formatString);
+ boolean isFormatStyleString = formatStylePatternMatcher.matches();
DateTimeFormatter dateTimeFormatter;
- if (isLocalizedPattern) {
- FormatStyle datePartFormatStyle =
FormatStyle.valueOf(localizedPatternMatcher.group(1).toUpperCase(Locale.ROOT));
- String group2 = localizedPatternMatcher.group(2);
+ if (isFormatStyleString) {
+ FormatStyle datePartFormatStyle =
FormatStyle.valueOf(formatStylePatternMatcher.group(1).toUpperCase(Locale.ROOT));
+ String group2 = formatStylePatternMatcher.group(2);
FormatStyle timePartFormatStyle = group2 != null
? FormatStyle.valueOf(group2.toUpperCase(Locale.ROOT))
: datePartFormatStyle;
@@ -97,8 +94,8 @@ class JavaTemplateTemporalFormat extends
TemplateTemporalFormat {
dateTimeFormatter =
DateTimeFormatter.ofLocalizedDate(datePartFormatStyle);
} else {
throw new InvalidFormatParametersException(
- "Format " + StringUtil.jQuote(formatString) + " is not
supported for "
- + temporalClass.getName());
+ "Format styles (like " +
StringUtil.jQuote(formatString) + ") is not supported for "
+ + temporalClass.getName() + " values.");
}
} else {
try {
@@ -109,6 +106,30 @@ class JavaTemplateTemporalFormat extends
TemplateTemporalFormat {
}
this.dateTimeFormatter = dateTimeFormatter.withLocale(locale);
+ if (isLocalTemporalClass(temporalClass)) {
+ this.formatTimeConversion = null;
+ } else {
+ if (showsZone(dateTimeFormatter)) {
+ if (temporalClass == Instant.class) {
+ this.formatTimeConversion =
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME;
+ } else if (isFormatStyleString &&
+ (temporalClass == OffsetDateTime.class ||
temporalClass == OffsetTime.class)) {
+ this.formatTimeConversion =
FormatTimeConversion.SET_ZONE_FROM_OFFSET;
+ } else {
+ this.formatTimeConversion = null;
+ }
+ } else {
+ if (temporalClass == OffsetTime.class &&
timeZone.useDaylightTime()) {
+ throw new InvalidFormatParametersException(
+ "The format must show the time offset, as the
current FreeMarker time zone, "
+ + StringUtil.jQuote(timeZone.getID()) + ",
may uses Daylight Saving Time, and thus "
+ + "it's not possible to convert the value
to the local time in that zone, "
+ + "since we don't know the day.");
+ }
+ this.formatTimeConversion =
FormatTimeConversion.CONVERT_TO_CURRENT_ZONE;
+ }
+ }
+
this.zoneId = timeZone.toZoneId();
}
@@ -119,14 +140,33 @@ class JavaTemplateTemporalFormat extends
TemplateTemporalFormat {
if (formatTimeConversion ==
FormatTimeConversion.INSTANT_TO_ZONED_DATE_TIME) {
temporal = ((Instant) temporal).atZone(zoneId);
+ } else if (formatTimeConversion ==
FormatTimeConversion.CONVERT_TO_CURRENT_ZONE) {
+ if (temporal instanceof Instant) {
+ temporal = ((Instant) temporal).atZone(zoneId);
+ } else if (temporal instanceof OffsetDateTime) {
+ temporal = ((OffsetDateTime)
temporal).atZoneSameInstant(zoneId);
+ } else if (temporal instanceof ZonedDateTime) {
+ temporal = ((ZonedDateTime)
temporal).withZoneSameInstant(zoneId);
+ } else if (temporal instanceof OffsetTime) {
+ // Because of logic in the constructor, this is only reached
if the zone never uses Daylight Saving.
+ temporal = ((OffsetTime)
temporal).withOffsetSameInstant(zoneId.getRules().getOffset(Instant.EPOCH));
+ } else {
+ throw new InvalidFormatParametersException(
+ "Don't know how to convert value of type " +
temporal.getClass().getName() + " to the current "
+ + "FreeMarker time zone, " +
StringUtil.jQuote(zoneId.getId()) + ", which is "
+ + "needed to format with " +
StringUtil.jQuote(formatString) + ".");
+ }
} else if (formatTimeConversion ==
FormatTimeConversion.SET_ZONE_FROM_OFFSET) {
+ // Formats like "long" want a time zone field, but oddly, they
don't treat the zoneOffset as such.
if (temporal instanceof OffsetDateTime) {
- dateTimeFormatter =
dateTimeFormatter.withZone(((OffsetDateTime) temporal).getOffset());
+ OffsetDateTime offsetDateTime = (OffsetDateTime) temporal;
+ temporal = ZonedDateTime.of(offsetDateTime.toLocalDateTime(),
offsetDateTime.getOffset());
} else if (temporal instanceof OffsetTime) {
+ // There's no ZonedTime class, so we must manipulate the
format.
dateTimeFormatter = dateTimeFormatter.withZone(((OffsetTime)
temporal).getOffset());
} else {
throw new IllegalArgumentException(
- "Formatter was created for OffsetTime and
OffsetDateTime, but value was a "
+ "Formatter was created for OffsetTime or
OffsetDateTime, but value was a "
+
ClassUtil.getShortClassNameOfObject(temporal));
}
}
@@ -159,4 +199,22 @@ class JavaTemplateTemporalFormat extends
TemplateTemporalFormat {
return true;
}
+ private static final ZonedDateTime SHOWS_ZONE_SAMPLE_TEMPORAL_1 =
ZonedDateTime.of(
+ LocalDateTime.of(2011, 1, 1, 1, 1), ZoneOffset.ofHours(0));
+ private static final ZonedDateTime SHOWS_ZONE_SAMPLE_TEMPORAL_2 =
ZonedDateTime.of(
+ LocalDateTime.of(2011, 1, 1, 1, 1), ZoneOffset.ofHours(1));
+
+ private boolean showsZone(DateTimeFormatter dateTimeFormatter) {
+ return !dateTimeFormatter.format(SHOWS_ZONE_SAMPLE_TEMPORAL_1)
+
.equals(dateTimeFormatter.format(SHOWS_ZONE_SAMPLE_TEMPORAL_2));
+ }
+
+ private static boolean isLocalTemporalClass(Class<? extends Temporal>
normalizedTemporalClass) {
+ return normalizedTemporalClass == LocalDateTime.class
+ || normalizedTemporalClass == LocalTime.class
+ || normalizedTemporalClass == LocalDate.class
+ || normalizedTemporalClass == Year.class
+ || normalizedTemporalClass == YearMonth.class;
+ }
+
}
diff --git a/src/main/java/freemarker/core/_MessageUtil.java
b/src/main/java/freemarker/core/_MessageUtil.java
index 034ec0f..74b4664 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -19,6 +19,7 @@
package freemarker.core;
+import java.time.temporal.Temporal;
import java.util.ArrayList;
import freemarker.template.Template;
@@ -27,6 +28,7 @@ import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateHashModelEx2;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
import freemarker.template.utility.StringUtil;
/**
@@ -322,10 +324,12 @@ public class _MessageUtil {
: new _MiscTemplateException(e, null, desc);
}
- public static TemplateException
newCantFormatTemporalException(TemplateTemporalFormat format, Expression
dataSrcExp,
+ public static TemplateException
newCantFormatTemporalException(TemplateTemporalFormat format,
TemplateTemporalModel ttm, Expression dataSrcExp,
TemplateValueFormatException e, boolean useTempModelExc) {
_ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
- "Failed to format temporal value with format ", new
_DelayedJQuote(format.getDescription()), ": ",
+ "Failed to format temporal value of class ",
safeGetTemporalClass(ttm),
+ ", value ", new _DelayedJQuote(new
_DelayedToString(safeGetTemporalValue(ttm))),
+ ", with format ", new
_DelayedJQuote(format.getDescription()), ": ",
e.getMessage())
.blame(dataSrcExp);
return useTempModelExc
@@ -333,6 +337,23 @@ public class _MessageUtil {
: new _MiscTemplateException(e, null, desc);
}
+ private static String safeGetTemporalClass(TemplateTemporalModel ttm) {
+ try {
+ return ttm.getAsTemporal().getClass().getName();
+ } catch (TemplateModelException e) {
+ return "[failed to get]";
+ }
+ }
+
+ private static Object safeGetTemporalValue(TemplateTemporalModel ttm) {
+ try {
+ Temporal value = ttm.getAsTemporal();
+ return value != null ? value : "null";
+ } catch (TemplateModelException e) {
+ return "[failed to get]";
+ }
+ }
+
public static TemplateException
newCantFormatNumberException(TemplateNumberFormat format, Expression dataSrcExp,
TemplateValueFormatException e, boolean useTempModelExc) {
_ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
diff --git a/src/main/java/freemarker/template/TemplateDateModel.java
b/src/main/java/freemarker/template/TemplateDateModel.java
index a00cbcf..d354fda 100644
--- a/src/main/java/freemarker/template/TemplateDateModel.java
+++ b/src/main/java/freemarker/template/TemplateDateModel.java
@@ -31,6 +31,9 @@ import java.util.List;
* <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>{@link java.time.temporal.Temporal} values (the date/time classes
introduced with Java 8) are handled by
+ * {@link TemplateTemporalModel}.
*/
public interface TemplateDateModel extends TemplateModel {
diff --git a/src/main/java/freemarker/template/TemplateTemporalModel.java
b/src/main/java/freemarker/template/TemplateTemporalModel.java
index c02e3e5..1f2d6d6 100644
--- a/src/main/java/freemarker/template/TemplateTemporalModel.java
+++ b/src/main/java/freemarker/template/TemplateTemporalModel.java
@@ -18,8 +18,22 @@
*/
package freemarker.template;
+import java.time.LocalDateTime;
+import java.time.YearMonth;
import java.time.temporal.Temporal;
+/**
+ * Any {@link Temporal} value that's included in Java; in Java 8 these are:
{@link LocalDateTime}, {@link LocalDate},
+ * {@link LocalTime}, {@link OffsetDateTime}, {@link OffsetTime}, {@link
ZonedDateTime}, {@link ZonedTime},
+ * {@link YearMonth}, {@link Year}.
+ * This does not deal with {@link java.time.Duration}, and {@link
java.time.Period}, because those don't implement the
+ * {@link Temporal} interface.
+ *
+ * <p>{@link java.util.Date} values (the way date/time values were represented
prior Java 8) are handled by
+ * {@link TemplateDateModel}.
+ *
+ * @since 2.3.32
+ */
public interface TemplateTemporalModel extends TemplateModel {
/**
* Returns the date value. The return value must not be {@code null}.
diff --git a/src/test/java/freemarker/core/TemporalFormatTest.java
b/src/test/java/freemarker/core/TemporalFormatTest.java
new file mode 100644
index 0000000..3e0e3ed
--- /dev/null
+++ b/src/test/java/freemarker/core/TemporalFormatTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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 static freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Year;
+import java.time.YearMonth;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.Temporal;
+import java.util.TimeZone;
+import java.util.function.Consumer;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.SimpleTemporal;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.utility.DateUtil;
+
+public class TemporalFormatTest {
+
+ @Test
+ public void testOffsetTimeAndZones() throws TemplateException, IOException
{
+ OffsetTime offsetTime = OffsetTime.of(LocalTime.of(10, 0, 0),
ZoneOffset.ofHours(1));
+
+ TimeZone zoneWithoutDST = TimeZone.getTimeZone("GMT+2");
+ assertFalse(zoneWithoutDST.useDaylightTime());
+
+ TimeZone zoneWithDST = TimeZone.getTimeZone("America/New_York");
+ assertTrue(zoneWithDST.useDaylightTime());
+
+ assertEquals(
+ "11:00",
+ formatTemporal(
+ conf -> {
+ conf.setOffsetTimeFormat("HH:mm");
+ conf.setTimeZone(zoneWithoutDST);
+ },
+ offsetTime));
+
+ try {
+ assertEquals(
+ "11:00",
+ formatTemporal(
+ conf -> {
+ conf.setOffsetTimeFormat("HH:mm");
+ conf.setTimeZone(zoneWithDST);
+ },
+ offsetTime));
+ fail();
+ } catch (TemplateException e) {
+ assertThat(e.getMessage(), containsStringIgnoringCase("daylight
saving"));
+ }
+
+ assertEquals(
+ "10:00+01",
+ formatTemporal(
+ conf -> {
+ conf.setOffsetTimeFormat("HH:mmX");
+ conf.setTimeZone(zoneWithDST);
+ },
+ offsetTime));
+
+ assertEquals(
+ "10:00+01",
+ formatTemporal(
+ conf -> {
+ conf.setOffsetTimeFormat("HH:mmX");
+ conf.setTimeZone(zoneWithoutDST);
+ },
+ offsetTime));
+ }
+
+ @Test
+ public void testZoneConvertedWhenOffsetOrZoneNotShown() throws
TemplateException, IOException {
+ TimeZone gbZone = TimeZone.getTimeZone("GB");
+ assertTrue(gbZone.useDaylightTime());
+ // Summer: GMT+1
+ // Winter: GMT+0
+
+ TimeZone nyZone = TimeZone.getTimeZone("America/New_York");
+ assertTrue(nyZone.useDaylightTime());
+ // Summer: GMT-4
+ // Winter: GMT-5
+
+ LocalTime localTime = LocalTime.of(10, 30, 0);
+ LocalDate winterLocalDate = LocalDate.of(2021, 12, 30);
+ LocalDate summerLocalDate = LocalDate.of(2021, 6, 30);
+ LocalDateTime winterLocalDateTime = LocalDateTime.of(winterLocalDate,
localTime);
+ OffsetDateTime winterOffsetDateTime =
OffsetDateTime.of(winterLocalDateTime, ZoneOffset.ofHours(2));
+ ZonedDateTime winterZonedDateTime =
ZonedDateTime.of(winterLocalDateTime, nyZone.toZoneId());
+ LocalDateTime summerLocalDateTime = LocalDateTime.of(summerLocalDate,
localTime);
+ OffsetDateTime summerOffsetDateTime =
OffsetDateTime.of(summerLocalDateTime, ZoneOffset.ofHours(2));
+ ZonedDateTime summerZonedDateTime =
ZonedDateTime.of(summerLocalDateTime, nyZone.toZoneId());
+
+ // If time zone (or offset) is not shown, the value is converted to
the FreeMarker time zone:
+ assertEquals(
+ "2021-06-30 10:30, 2021-06-30 09:30, 2021-06-30 15:30, "
+ + "2021-12-30 10:30, 2021-12-30 08:30, 2021-12-30
15:30",
+ formatTemporal(
+ conf -> {
+ conf.setLocalDateTimeFormat("yyyy-MM-dd HH:mm");
+ conf.setOffsetDateTimeFormat("yyyy-MM-dd HH:mm");
+ conf.setZonedDateTimeFormat("yyyy-MM-dd HH:mm");
+ conf.setTimeZone(gbZone);
+ },
+ summerLocalDateTime, summerOffsetDateTime,
summerZonedDateTime,
+ winterLocalDateTime, winterOffsetDateTime,
winterZonedDateTime));
+ assertEquals(
+ "2021-06-30 10:30, 2021-06-30 08:30, 2021-06-30 14:30, "
+ + "2021-12-30 10:30, 2021-12-30 08:30, 2021-12-30
15:30",
+ formatTemporal(
+ conf -> {
+ conf.setLocalDateTimeFormat("yyyy-MM-dd HH:mm");
+ conf.setOffsetDateTimeFormat("yyyy-MM-dd HH:mm");
+ conf.setZonedDateTimeFormat("yyyy-MM-dd HH:mm");
+ conf.setTimeZone(DateUtil.UTC);
+ },
+ summerLocalDateTime, summerOffsetDateTime,
summerZonedDateTime,
+ winterLocalDateTime, winterOffsetDateTime,
winterZonedDateTime));
+
+ // If the time zone (or offset) is shown, the value is not converted
from its original time zone:
+ assertEquals(
+ "2021-06-30 10:30+02, 2021-06-30 10:30-04, "
+ + "2021-12-30 10:30+02, 2021-12-30 10:30-05",
+ formatTemporal(
+ conf -> {
+ conf.setOffsetDateTimeFormat("yyyy-MM-dd HH:mmX");
+ conf.setZonedDateTimeFormat("yyyy-MM-dd HH:mmX");
+ conf.setTimeZone(gbZone);
+ },
+ summerOffsetDateTime, summerZonedDateTime,
+ winterOffsetDateTime, winterZonedDateTime));
+ }
+
+ @Test
+ public void testCanNotFormatLocalIfTimeZoneIsShown() {
+ try {
+ formatTemporal(
+ conf -> {
+ conf.setLocalDateTimeFormat("yyyy-MM-dd HH:mmX");
+ },
+ LocalDateTime.of(2021, 10, 30, 1, 2));
+ fail();
+ } catch (TemplateException e) {
+ assertThat(e.getMessage(),
+ allOf(
+ containsString("LocalDateTime"),
+ containsString("2021-10-30T01:02"),
+ containsString("yyyy-MM-dd HH:mmX"),
+ anyOf(containsStringIgnoringCase("offset"),
containsStringIgnoringCase("zone"))));
+ }
+ }
+
+ @Test
+ public void testStylesAreNotSupportedForYear() {
+ try {
+ formatTemporal(
+ conf -> {
+ conf.setYearFormat("medium");
+ },
+ Year.of(2021));
+ fail();
+ } catch (TemplateException e) {
+ assertThat(e.getMessage(),
+ allOf(
+ containsString("\"medium\""),
+ containsString(Year.class.getName()),
+ containsStringIgnoringCase("style")));
+ }
+ }
+
+ @Test
+ public void testStylesAreNotSupportedForYearMonth() {
+ try {
+ formatTemporal(
+ conf -> {
+ conf.setYearMonthFormat("medium");
+ },
+ YearMonth.of(2021, 10));
+ fail();
+ } catch (TemplateException e) {
+ assertThat(e.getMessage(),
+ allOf(
+ containsString("\"medium\""),
+ containsString(YearMonth.class.getName()),
+ containsStringIgnoringCase("style")));
+ }
+ }
+
+ static private String formatTemporal(Consumer<Configurable> configurer,
Temporal... values) throws
+ TemplateException {
+ Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
+
+ configurer.accept(conf);
+
+ Environment env = null;
+ try {
+ env = new Template(null, "",
conf).createProcessingEnvironment(null, null);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (Temporal value : values) {
+ if (sb.length() != 0) {
+ sb.append(", ");
+ }
+ sb.append(env.formatTemporalToPlainText(new SimpleTemporal(value),
null, false));
+ }
+
+ return sb.toString();
+ }
+}
diff --git
a/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
b/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
index 5c42c29..cdadba8 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/temporal.ftl
@@ -16,15 +16,15 @@
specific language governing permissions and limitations
under the License.
-->
-<@assertEquals expected="2003-04-05T07:07:08+01:00[GMT+01:00]"
actual=instant?string />
-<@assertEquals expected="2003-04-05T06:07:08" actual=localDateTime?string />
-<@assertEquals expected="2003-04-05" actual=localDate?string />
-<@assertEquals expected="06:07:08" actual=localTime?string />
-<@assertEquals expected="2003-04-05T06:07:08Z" actual=offsetDateTime?string />
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="Apr 5, 2003 7:07:08 AM" actual=instant?string />
+<@assertEquals expected="Apr 5, 2003 6:07:08 AM" actual=localDateTime?string />
+<@assertEquals expected="Apr 5, 2003" actual=localDate?string />
+<@assertEquals expected="6:07:08 AM" actual=localTime?string />
+<@assertEquals expected="Apr 5, 2003 7:07:08 AM" actual=offsetDateTime?string
/>
+<@assertEquals expected="6:07:08 AM Z" actual=offsetTime?string />
<@assertEquals expected="2003" actual=year?string />
<@assertEquals expected="2003-04" actual=yearMonth?string />
-<@assertEquals expected="2003-04-05T06:07:08Z[UTC]"
actual=zonedDateTime?string />
+<@assertEquals expected="Apr 5, 2003 7:07:08 AM" actual=zonedDateTime?string />
<#setting timeZone="America/New_York">
<@assertEquals expected="2003-04-05T01:07:08-05:00" actual=instant?string.iso
/>
@@ -32,7 +32,7 @@
<@assertEquals expected="2003-04-05" actual=localDate?string.iso />
<@assertEquals expected="06:07:08" actual=localTime?string.iso />
<@assertEquals expected="2003-04-05T06:07:08Z"
actual=offsetDateTime?string.iso />
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="06:07:08Z" actual=offsetTime?string.iso />
<@assertEquals expected="2003" actual=year?string.iso />
<@assertEquals expected="2003-04" actual=yearMonth?string.iso />
<@assertEquals expected="2003-04-05T06:07:08Z" actual=zonedDateTime?string.iso
/>
@@ -43,7 +43,7 @@
<@assertEquals expected="2003-04-05" actual=localDate?string.iso />
<@assertEquals expected="06:07:08" actual=localTime?string.iso />
<@assertEquals expected="2003-04-05T06:07:08Z"
actual=offsetDateTime?string.iso />
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="06:07:08Z" actual=offsetTime?string.iso />
<@assertEquals expected="2003" actual=year?string.iso />
<@assertEquals expected="2003-04" actual=yearMonth?string.iso />
<@assertEquals expected="2003-04-05T06:07:08Z" actual=zonedDateTime?string.iso
/>
@@ -53,7 +53,7 @@
<@assertEquals expected="2003-04-05" actual=localDate?string.xs />
<@assertEquals expected="06:07:08" actual=localTime?string.xs />
<@assertEquals expected="2003-04-05T06:07:08Z" actual=offsetDateTime?string.xs
/>
-<@assertEquals expected="06:07:08Z" actual=offsetTime?string />
+<@assertEquals expected="06:07:08Z" actual=offsetTime?string.xs />
<@assertEquals expected="2003" actual=year?string.xs />
<@assertEquals expected="2003-04" actual=yearMonth?string.xs />
<@assertEquals expected="2003-04-05T06:07:08Z" actual=zonedDateTime?string.xs
/>
@@ -65,14 +65,14 @@
<@assertEquals expected="5 avril 2003 01:07:08 EST" actual=instant?string.long
/>
<@assertEquals expected="samedi 5 avril 2003 01 h 07 EST"
actual=instant?string.full />
-<@assertEquals expected="05/04/03 06:07" actual=offsetDateTime?string.short />
-<@assertEquals expected="5 avr. 2003 06:07:08"
actual=offsetDateTime?string.medium />
+<@assertEquals expected="05/04/03 01:07" actual=offsetDateTime?string.short />
+<@assertEquals expected="5 avr. 2003 01:07:08"
actual=offsetDateTime?string.medium />
<@assertEquals expected="5 avril 2003 06:07:08 Z"
actual=offsetDateTime?string.long />
<@assertEquals expected="samedi 5 avril 2003 06 h 07 Z"
actual=offsetDateTime?string.full />
<@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?
+<#-- These fail on Java 8 because of JDK-8085887
<@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 />
-->
@@ -87,14 +87,14 @@
<@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 />
+<@assertEquals expected="05/04/03 01:07" actual=zonedDateTime?string.short />
+<@assertEquals expected="5 avr. 2003 01:07:08"
actual=zonedDateTime?string.medium />
<@assertEquals expected="5 avril 2003 06:07:08 UTC"
actual=zonedDateTime?string.long />
<@assertEquals expected="samedi 5 avril 2003 06 h 07 UTC"
actual=zonedDateTime?string.full />
<@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?
+<#-- These fail on Java 8 because of JDK-8085887
<@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 />
-->
@@ -103,12 +103,12 @@
<@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?
+<#-- These fail on Java 8 because of JDK-8085887
<@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?
+<#-- These fail on Java 8 because of JDK-8085887
<@assertEquals expected="samedi 5 avril 2003 06 h 07 ET"
actual=localDateTime?string.full_full />
-->
@@ -124,12 +124,12 @@
<#setting localDateFormat="yyyy MMM dd">
<@assertEquals expected="2003 Apr 05" actual=localDate?string />
<#setting localDateTimeFormat="HH:mm:ss">
-<@assertEquals expected="06:07:08" actual=localTime?string />
+<@assertEquals expected="6:07:08 AM" actual=localTime?string />
<#setting offsetDateTimeFormat="yyyy MMM dd HH:mm:ss">
-<@assertEquals expected="2003 Apr 05 06:07:08" actual=offsetDateTime?string />
+<@assertEquals expected="2003 Apr 05 01:07:08" actual=offsetDateTime?string />
<#setting yearFormat="yyyy">
<@assertEquals expected="2003" actual=year?string />
<#setting yearMonthFormat="yyyy MMM">
<@assertEquals expected="2003 Apr" actual=yearMonth?string />
<#setting zonedDateTimeFormat="yyyy MMM dd HH:mm:ss">
-<@assertEquals expected="2003 Apr 05 06:07:08" actual=zonedDateTime?string />
+<@assertEquals expected="2003 Apr 05 01:07:08" actual=zonedDateTime?string />
diff --git a/src/test/resources/logback-test.xml
b/src/test/resources/logback-test.xml
index e3876ec..0aefe85 100644
--- a/src/test/resources/logback-test.xml
+++ b/src/test/resources/logback-test.xml
@@ -26,6 +26,7 @@
</appender>
<logger name="org.eclipse.jetty" level="INFO" />
+ <logger name="freemarker.runtime.attempt" level="INFO" />
<root level="debug">
<appender-ref ref="STDOUT" />