You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2022/03/05 22:38:57 UTC
[freemarker] branch FREEMARKER-35 updated: [FREEMARKER-35] Added MissingTimeZoneParserPolicy to parse method, and implemented it for JavaTemplateTemporalFormat and ISOLikeTemplateTemporalTemporalFormat. Added more parsing tests. Changed TemporalUtils to internal class (now called _TemporalUtils).
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 192364c [FREEMARKER-35] Added MissingTimeZoneParserPolicy to parse method, and implemented it for JavaTemplateTemporalFormat and ISOLikeTemplateTemporalTemporalFormat. Added more parsing tests. Changed TemporalUtils to internal class (now called _TemporalUtils).
192364c is described below
commit 192364cd85548acc87bdda28a988ab530a19e8d4
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Mar 5 14:17:06 2022 +0100
[FREEMARKER-35] Added MissingTimeZoneParserPolicy to parse method, and implemented it for JavaTemplateTemporalFormat and ISOLikeTemplateTemporalTemporalFormat. Added more parsing tests. Changed TemporalUtils to internal class (now called _TemporalUtils).
---
src/main/java/freemarker/core/Configurable.java | 3 +-
.../DateTimeFormatBasedTemplateTemporalFormat.java | 143 +++++++
src/main/java/freemarker/core/Environment.java | 3 +-
.../ISOLikeTemplateTemporalTemporalFormat.java | 99 +----
.../core/ISOTemplateTemporalFormatFactory.java | 4 +-
.../core/JavaTemplateTemporalFormat.java | 106 ++----
.../core/MissingTimeZoneParserPolicy.java | 54 +++
.../freemarker/core/TemplateTemporalFormat.java | 5 +-
.../core/XSTemplateTemporalFormatFactory.java | 4 +-
src/main/java/freemarker/core/_MessageUtil.java | 5 +
.../_TemporalUtils.java} | 41 +-
.../core/AbstractTemporalFormatTest.java | 130 ++++++-
....java => CustomTemplateTemporalFormatTest.java} | 4 +-
...pochMillisDivTemplateTemporalFormatFactory.java | 2 +-
.../EpochMillisTemplateTemporalFormatFactory.java | 2 +-
.../core/HTMLISOTemplateTemporalFormatFactory.java | 2 +-
...java => ISOLikeTemplateTemporalFormatTest.java} | 412 ++++++++++++++++++---
...st.java => JavaTemplateTemporalFormatTest.java} | 145 +++++---
...ndTZSensitiveTemplateTemporalFormatFactory.java | 2 +-
.../_TemporalUtilsTest.java} | 29 +-
.../utility/DateUtilsPatternParsingTest.java | 11 +-
21 files changed, 894 insertions(+), 312 deletions(-)
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 17fca68..525c25f 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -82,7 +82,6 @@ import freemarker.template.Version;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.StringUtil;
-import freemarker.template.utility.TemporalUtils;
/**
* This is a common superclass of {@link freemarker.template.Configuration},
@@ -1458,7 +1457,7 @@ public class Configurable {
} else {
// Handle the unlikely situation that in some future Java version we can have subclasses.
Class<? extends Temporal> normTemporalClass =
- TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
+ _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
if (normTemporalClass == temporalClass) {
throw new IllegalArgumentException("There's no temporal format setting for this class: "
+ temporalClass.getName());
diff --git a/src/main/java/freemarker/core/DateTimeFormatBasedTemplateTemporalFormat.java b/src/main/java/freemarker/core/DateTimeFormatBasedTemplateTemporalFormat.java
new file mode 100644
index 0000000..eb8ee95
--- /dev/null
+++ b/src/main/java/freemarker/core/DateTimeFormatBasedTemplateTemporalFormat.java
@@ -0,0 +1,143 @@
+/*
+ * 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.core._TemporalUtils.*;
+import static freemarker.template.utility.StringUtil.*;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
+
+/**
+ * Was created ad-hoc to contain whatever happens to be common between some of our {@link TemplateTemporalFormat}-s.
+ */
+abstract class DateTimeFormatBasedTemplateTemporalFormat extends TemplateTemporalFormat {
+ protected final Class<? extends Temporal> temporalClass;
+ protected final boolean isLocalTemporalClass;
+ protected final ZoneId zoneId;
+
+ public DateTimeFormatBasedTemplateTemporalFormat(
+ Class<? extends Temporal> temporalClass, ZoneId zoneId) {
+ temporalClass = _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
+ this.temporalClass = temporalClass;
+ this.isLocalTemporalClass = isLocalTemporalClass(temporalClass);
+ this.zoneId = zoneId;
+ }
+
+ protected Temporal parse(
+ String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy,
+ DateTimeFormatter parserDateTimeFormatter) throws UnparsableValueException {
+ try {
+ TemporalAccessor parseResult = parserDateTimeFormatter.parse(s);
+
+ if (isLocalTemporalClass
+ || parseResult.isSupported(ChronoField.OFFSET_SECONDS)
+ || parseResult.isSupported(ChronoField.INSTANT_SECONDS)) {
+ 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);
+ }
+ }
+
+ Temporal resultTemporal = parseResult.query(
+ _TemporalUtils.getTemporalQuery(localFallbackTemporalClass));
+ if (fallbackToLocal) {
+ return resultTemporal;
+ }
+ ZonedDateTime zonedDateTime = ((LocalDateTime) resultTemporal).atZone(zoneId);
+ if (temporalClass == ZonedDateTime.class) {
+ return zonedDateTime;
+ } else if (temporalClass == OffsetDateTime.class) {
+ return zonedDateTime.toOffsetDateTime();
+ } 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();
+ }
+ } catch (DateTimeException e) {
+ throw newUnparsableValueException(s, parserDateTimeFormatter, e.getMessage(), e);
+ }
+ }
+
+ protected UnparsableValueException newUnparsableValueException(
+ String s, DateTimeFormatter dateTimeFormatter,
+ String cause, DateTimeException e) {
+ StringBuilder message = new StringBuilder();
+
+ message.append("Failed to parse value ").append(jQuote(s))
+ .append(" with format ").append(jQuote(getDescription()))
+ .append(", and target class ").append(temporalClass.getSimpleName());
+ if (dateTimeFormatter != null) {
+ message.append(", ").append("locale ").append(jQuote(dateTimeFormatter.getLocale()));
+ }
+ if (zoneId != null) {
+ message.append(", ").append("zoneId ").append(jQuote(zoneId));
+ }
+ message.append(".");
+ if (dateTimeFormatter != null) {
+ message.append("\n(DateTimeFormatter used: ").append(jQuote(dateTimeFormatter)).append(")");
+ }
+ message.append("\nCause: ").append(cause);
+
+ return new UnparsableValueException(
+ message.toString(),
+ e);
+ }
+
+}
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 374bd98..b7153d2 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -82,7 +82,6 @@ import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
import freemarker.template.utility.NullWriter;
import freemarker.template.utility.StringUtil;
import freemarker.template.utility.TemplateModelUtils;
-import freemarker.template.utility.TemporalUtils;
import freemarker.template.utility.UndeclaredThrowableException;
/**
@@ -2343,7 +2342,7 @@ public final class Environment extends Configurable {
String settingName;
String settingValue;
try {
- settingName = TemporalUtils.temporalClassToFormatSettingName(
+ settingName = _TemporalUtils.temporalClassToFormatSettingName(
temporalClass,
blamedTemporalSourceExp != null
? blamedTemporalSourceExp.getTemplate().getActualNamingConvention()
diff --git a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
index 2923940..07d6b8e 100644
--- a/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
+++ b/src/main/java/freemarker/core/ISOLikeTemplateTemporalTemporalFormat.java
@@ -19,8 +19,7 @@
package freemarker.core;
-import static freemarker.template.utility.StringUtil.*;
-import static freemarker.template.utility.TemporalUtils.*;
+import static freemarker.core._TemporalUtils.*;
import java.time.DateTimeException;
import java.time.Instant;
@@ -29,19 +28,14 @@ import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
-import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
-import java.time.temporal.TemporalAccessor;
-import java.time.temporal.TemporalQuery;
import java.util.TimeZone;
import java.util.regex.Pattern;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateTemporalModel;
-import freemarker.template.utility.TemporalUtils;
// TODO [FREEMARKER-35] These should support parameters similar to {@link ISOTemplateDateFormat},
@@ -50,31 +44,24 @@ import freemarker.template.utility.TemporalUtils;
*
* @since 2.3.32
*/
-final class ISOLikeTemplateTemporalTemporalFormat extends TemplateTemporalFormat {
+final class ISOLikeTemplateTemporalTemporalFormat extends DateTimeFormatBasedTemplateTemporalFormat {
private final DateTimeFormatter dateTimeFormatter;
private final boolean instantConversion;
- private final ZoneId zoneId;
private final String description;
- private final TemporalQuery<? extends Temporal> temporalQuery;
- private final Class<? extends Temporal> temporalClass;
private final DateTimeFormatter parserExtendedDateTimeFormatter;
private final DateTimeFormatter parserBasicDateTimeFormatter;
- private final boolean localTemporalClass;
ISOLikeTemplateTemporalTemporalFormat(
DateTimeFormatter dateTimeFormatter,
DateTimeFormatter parserExtendedDateTimeFormatter,
DateTimeFormatter parserBasicDateTimeFormatter,
Class<? extends Temporal> temporalClass, TimeZone zone, String formatString) {
+ super(temporalClass, zone.toZoneId());
temporalClass = normalizeSupportedTemporalClass(temporalClass);
this.dateTimeFormatter = dateTimeFormatter;
this.parserExtendedDateTimeFormatter = parserExtendedDateTimeFormatter;
this.parserBasicDateTimeFormatter = parserBasicDateTimeFormatter;
- this.temporalQuery = TemporalUtils.getTemporalQuery(temporalClass);
this.instantConversion = temporalClass == Instant.class;
- this.temporalClass = temporalClass;
- this.localTemporalClass = isLocalTemporalClass(temporalClass);
- this.zoneId = temporalClass == Instant.class ? zone.toZoneId() : null;
this.description = formatString;
}
@@ -97,7 +84,9 @@ final class ISOLikeTemplateTemporalTemporalFormat extends TemplateTemporalFormat
}
@Override
- public Object parse(String s) throws TemplateValueFormatException {
+ public Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws TemplateValueFormatException {
+ // TODO [FREEMARKER-35] Implement missingTimeZoneParserPolicy
+
final boolean extendedFormat;
final boolean add1Day;
if (temporalClass == LocalDate.class || temporalClass == YearMonth.class) {
@@ -116,10 +105,9 @@ final class ISOLikeTemplateTemporalTemporalFormat extends TemplateTemporalFormat
} else {
int tIndex = s.indexOf('T');
if (tIndex < 1) {
- throw new UnparsableValueException(
- "Failed to parse value " + jQuote(s) + " with format " + jQuote(description)
- + ", and target class " + temporalClass.getSimpleName() + ": "
- + "Character \"T\" must be used to separate the date and time part.");
+ throw newUnparsableValueException(
+ s, null,
+ "Character \"T\" must be used to separate the date and time part.", null);
}
if (s.indexOf(":", tIndex + 1) != -1) {
extendedFormat = true;
@@ -138,35 +126,11 @@ final class ISOLikeTemplateTemporalTemporalFormat extends TemplateTemporalFormat
DateTimeFormatter parserDateTimeFormatter = parserBasicDateTimeFormatter == null || extendedFormat
? parserExtendedDateTimeFormatter : parserBasicDateTimeFormatter;
- try {
- TemporalAccessor parseResult = parserDateTimeFormatter.parse(s);
- if (!localTemporalClass && !parseResult.isSupported(ChronoField.OFFSET_SECONDS)) {
- // Unlike for the Java format, for ISO we require the string to contain the offset for a non-local
- // target type. We could use the default time zone, but that's really just guessing, also DST creates
- // ambiguous cases. For the Java formatter we are lenient, as the shared date-time format typically
- // misses the offset, and because we don't want a format-and-then-parse cycle to fail. But in ISO
- // format, the offset is always shown for a non-local temporal.
- throw new UnparsableValueException(
- "Failed to parse value " + jQuote(s) + " with format " + jQuote(description)
- + ", and target class " + temporalClass.getSimpleName() + ": "
- + "The string must contain the time zone offset for this target class. "
- + "(Defaulting to the current time zone is not allowed for ISO-style formats.)");
-
- }
- Temporal resultTemporal = parseResult.query(temporalQuery);
- if (add1Day) {
- resultTemporal = resultTemporal.plus(1, ChronoUnit.DAYS);
- }
- return resultTemporal;
- } catch (DateTimeException e) {
- throw new UnparsableValueException(
- "Failed to parse value " + jQuote(s) + " with format " + jQuote(description)
- + ", and target class " + temporalClass.getSimpleName() + ", "
- + "zoneId " + jQuote(zoneId) + ".\n"
- + "(Used this DateTimeFormatter: " + parserDateTimeFormatter + ")\n"
- + "(Root cause message: " + e.getMessage() + ")",
- e);
+ Temporal resultTemporal = parse(s, missingTimeZoneParserPolicy, parserDateTimeFormatter);
+ if (add1Day) {
+ resultTemporal = resultTemporal.plus(1, ChronoUnit.DAYS);
}
+ return resultTemporal;
}
private final static Pattern ZERO_TIME_AFTER_HH = Pattern.compile("(?::?+00(?::?+00(?:.?+0+)?)?)?");
@@ -197,40 +161,6 @@ final class ISOLikeTemplateTemporalTemporalFormat extends TemplateTemporalFormat
return ZERO_TIME_AFTER_HH.matcher(timeAfterHH).matches();
}
- //!!T
- public static void main(String[] args) {
- for (String original : new String[] {"24", "24:00", "24:00:00", "24:00:00.0"}) {
- for (boolean basic : new boolean[] {false, true}) {
- for (String prefix : new String[] {"", "T"}) {
- for (String suffix : new String[] {"", "Z", "-01", "+01"}) {
- String s = prefix + (basic ? original.replace(":", "") : original)+ suffix;
-
- int startIndex = s.indexOf("24");
- if (!isStartOf240000(s, startIndex)) {
- throw new AssertionError("Couldn't find end of time part in: " + s);
- }
- }
- }
- }
- }
-
- for (String original : new String[] {
- "24:", "24:01", "24:00:01", "24:00:00.1", "24:0", "24:00:x",
- "2401", "240001", "240000.1", "240"}) {
- for (String prefix : new String[] {"", "T"}) {
- for (String suffix : new String[] {"", "Z", "-01", "+01"}) {
- String s = prefix + original + suffix;
-
- int startIndex = s.indexOf("24");
- if (isStartOf240000(s, startIndex)) {
- throw new AssertionError("Shouldn't match: " + s);
- }
- }
- }
- }
-
- }
-
@Override
public boolean isLocaleBound() {
return false;
@@ -238,7 +168,8 @@ final class ISOLikeTemplateTemporalTemporalFormat extends TemplateTemporalFormat
@Override
public boolean isTimeZoneBound() {
- return zoneId != null;
+ // TODO [FREEMARKER-35] Even for local temporals?
+ return true;
}
@Override
diff --git a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
index 48b48a5..9a6ad0c 100644
--- a/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/ISOTemplateTemporalFormatFactory.java
@@ -40,8 +40,6 @@ import java.time.temporal.Temporal;
import java.util.Locale;
import java.util.TimeZone;
-import freemarker.template.utility.TemporalUtils;
-
/**
* Format factory related to {@link someJava8Temporal?string.iso}, {@link someJava8Temporal?string.iso_...}, etc.
*/
@@ -212,7 +210,7 @@ class ISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
final DateTimeFormatter parserExtendedDateTimeFormatter;
final DateTimeFormatter parserBasicDateTimeFormatter;
final String description;
- temporalClass = TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
+ temporalClass = _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
if (temporalClass == LocalTime.class || temporalClass == OffsetTime.class) {
dateTimeFormatter = ISO8601_TIME_FORMAT;
parserExtendedDateTimeFormatter = PARSER_ISO8601_EXTENDED_TIME_FORMAT;
diff --git a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
index 85311f6..718f70b 100644
--- a/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateTemporalFormat.java
@@ -31,10 +31,8 @@ import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
import java.time.temporal.Temporal;
-import java.time.temporal.TemporalQuery;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
@@ -43,23 +41,19 @@ import java.util.regex.Pattern;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateTemporalModel;
import freemarker.template.utility.ClassUtil;
-import freemarker.template.utility.TemporalUtils;
/**
* See {@link JavaTemplateTemporalFormatFactory}.
*
* @since 2.3.32
*/
-class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
+class JavaTemplateTemporalFormat extends DateTimeFormatBasedTemplateTemporalFormat {
enum PreFormatValueConversion {
INSTANT_TO_ZONED_DATE_TIME,
+ AS_LOCAL_IN_CURRENT_ZONE,
SET_ZONE_FROM_OFFSET,
- OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION
- }
-
- enum SpecialParsing {
- OFFSET_TIME_DST_ERROR
+ OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION,
}
static final String SHORT = "short";
@@ -77,24 +71,19 @@ class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
"(?:" + ANY_FORMAT_STYLE + "(?:_" + ANY_FORMAT_STYLE + ")?)?");
private final DateTimeFormatter dateTimeFormatter;
- private final TemporalQuery<? extends Temporal> temporalQuery;
private final ZoneId zoneId;
private final String formatString;
- private final Class<? extends Temporal> temporalClass;
private final PreFormatValueConversion preFormatValueConversion;
- private final SpecialParsing specialParsing;
- JavaTemplateTemporalFormat(String formatString, Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone)
+ JavaTemplateTemporalFormat(String formatString, Class<? extends Temporal> temporalClass, Locale locale,
+ TimeZone timeZone)
throws InvalidFormatParametersException {
- this.temporalClass = TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
-
- temporalQuery = TemporalUtils.getTemporalQuery(temporalClass);
+ super(temporalClass, timeZone.toZoneId());
final Matcher formatStylePatternMatcher = FORMAT_STYLE_PATTERN.matcher(formatString);
final boolean isFormatStyleString = formatStylePatternMatcher.matches();
FormatStyle timePartFormatStyle; // Maybe changes later for re-attempts
final FormatStyle datePartFormatStyle;
- boolean formatWithZone;
DateTimeFormatter dateTimeFormatter; // Maybe changes later for re-attempts
if (isFormatStyleString) {
@@ -120,29 +109,28 @@ class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
} else {
throw new InvalidFormatParametersException(
"Format styles (like " + jQuote(formatString) + ") is not supported for "
- + temporalClass.getName() + " values.");
+ + temporalClass.getName() + " values.");
}
} else {
datePartFormatStyle = null;
timePartFormatStyle = null;
try {
- dateTimeFormatter = TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern(formatString, locale);
+ dateTimeFormatter = _TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern(formatString, locale);
} catch (IllegalArgumentException e) {
throw new InvalidFormatParametersException(e.getMessage(), e);
}
}
// Handling of time zone related edge cases
- if (TemporalUtils.isLocalTemporalClass(temporalClass)) {
+ if (isLocalTemporalClass) {
this.preFormatValueConversion = null;
- this.specialParsing = null;
- formatWithZone = false;
if (isFormatStyleString && (temporalClass == LocalTime.class || temporalClass == LocalDateTime.class)) {
// The localized pattern possibly contains the time zone (for most locales, LONG and FULL does), so they
// fail with local temporals that have a time part. To work this issue around, we decrease the verbosity
// of the time style until formatting succeeds. (See also: JDK-8085887)
- localFormatAttempt: while (true) {
+ localFormatAttempt:
+ while (true) {
try {
dateTimeFormatter.format(LOCAL_DATE_TIME_SAMPLE); // We only care if it throws exception or not
break localFormatAttempt; // It worked
@@ -168,50 +156,39 @@ class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
}
} else { // is non-local temporal
PreFormatValueConversion preFormatValueConversion;
- SpecialParsing specialParsing;
- if (showsZone(dateTimeFormatter)) {
+ if (showsOffsetOrZone(dateTimeFormatter)) {
if (temporalClass == Instant.class) {
preFormatValueConversion = PreFormatValueConversion.INSTANT_TO_ZONED_DATE_TIME;
- specialParsing = null;
} else if (isFormatStyleString &&
(temporalClass == OffsetDateTime.class || temporalClass == OffsetTime.class)) {
preFormatValueConversion = PreFormatValueConversion.SET_ZONE_FROM_OFFSET;
- specialParsing = null;
} else {
preFormatValueConversion = null;
- specialParsing = null;
}
- formatWithZone = false;
} else { // Doesn't show zone
if (temporalClass == OffsetTime.class) {
// We give up, but delay the exception until the format is actually used, just in case
// format creation is triggered without actually using it.
- preFormatValueConversion = PreFormatValueConversion.OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION;
- specialParsing = SpecialParsing.OFFSET_TIME_DST_ERROR;
- formatWithZone = false;
+ preFormatValueConversion =
+ PreFormatValueConversion.OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION;
} else {
// As no zone is shown, but our temporal class is not local, we tell the formatter convert to
// the current time zone. Also, when parsing, that same time zone will be assumed.
- preFormatValueConversion = null;
- specialParsing = null;
- formatWithZone = true;
+ preFormatValueConversion = PreFormatValueConversion.AS_LOCAL_IN_CURRENT_ZONE;
}
}
this.preFormatValueConversion = preFormatValueConversion;
- this.specialParsing = specialParsing;
}
dateTimeFormatter = dateTimeFormatter.withLocale(locale);
- if (formatWithZone) {
- dateTimeFormatter = dateTimeFormatter.withZone(timeZone.toZoneId());
- }
this.dateTimeFormatter = dateTimeFormatter;
this.formatString = formatString;
this.zoneId = timeZone.toZoneId();
}
@Override
- public String formatToPlainText(TemplateTemporalModel tm) throws TemplateValueFormatException, TemplateModelException {
+ public String formatToPlainText(TemplateTemporalModel tm) throws TemplateValueFormatException,
+ TemplateModelException {
DateTimeFormatter dateTimeFormatter = this.dateTimeFormatter;
Temporal temporal = TemplateFormatUtil.getNonNullTemporal(tm);
@@ -237,6 +214,19 @@ class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
+ ClassUtil.getShortClassNameOfObject(temporal));
}
break;
+ case AS_LOCAL_IN_CURRENT_ZONE:
+ // We could use dateTimeFormatter.withZone(zoneId) for these, but it's not obvious if that will
+ // always behave as a straightforward conversion to the local temporal type.
+ if (temporal instanceof OffsetDateTime) {
+ temporal = ((OffsetDateTime) temporal).atZoneSameInstant(zoneId).toLocalDateTime();
+ } else if (temporal instanceof ZonedDateTime) {
+ temporal = ((ZonedDateTime) temporal).withZoneSameInstant(zoneId).toLocalDateTime();
+ } else if (temporal instanceof Instant) {
+ temporal = ((Instant) temporal).atZone(zoneId).toLocalDateTime();
+ } else {
+ throw new AssertionError("Unhandled case: " + temporal.getClass());
+ }
+ break;
case OFFSET_TIME_WITHOUT_OFFSET_ON_THE_FORMAT_EXCEPTION:
throw newOffsetTimeWithoutOffsetOnTheFormatException();
default:
@@ -259,28 +249,9 @@ class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
}
@Override
- public Object parse(String s) throws TemplateValueFormatException {
- if (specialParsing != null) {
- switch (specialParsing) {
- case OFFSET_TIME_DST_ERROR:
- throw newOffsetTimeWithoutOffsetOnTheFormatException();
- default:
- throw new BugException();
- }
- }
-
- try {
- return dateTimeFormatter.parse(s, temporalQuery);
- } catch (DateTimeParseException e) {
- throw new UnparsableValueException(
- "Failed to parse value " + jQuote(s) + " with format " + jQuote(formatString)
- + ", and target class " + temporalClass.getSimpleName() + ", "
- + "locale " + jQuote(dateTimeFormatter.getLocale()) + ", "
- + "zoneId " + jQuote(zoneId) + ".\n"
- + "(Used this DateTimeFormatter: " + dateTimeFormatter + ")\n"
- + "(Root cause message: " + e.getMessage() + ")",
- e);
- }
+ public Temporal parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws
+ TemplateValueFormatException {
+ return parse(s, missingTimeZoneParserPolicy, dateTimeFormatter);
}
@Override
@@ -301,17 +272,18 @@ class JavaTemplateTemporalFormat extends TemplateTemporalFormat {
*/
@Override
public boolean isTimeZoneBound() {
+ // TODO [FREEMARKER-35] Even for local temporals?
return true;
}
- private static final ZonedDateTime SHOWS_ZONE_SAMPLE_TEMPORAL_1 = ZonedDateTime.of(
+ private static final ZonedDateTime SHOWS_OFFSET_OR_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(
+ private static final ZonedDateTime SHOWS_OFFSET_OR_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 boolean showsOffsetOrZone(DateTimeFormatter dateTimeFormatter) {
+ return !dateTimeFormatter.format(SHOWS_OFFSET_OR_ZONE_SAMPLE_TEMPORAL_1)
+ .equals(dateTimeFormatter.format(SHOWS_OFFSET_OR_ZONE_SAMPLE_TEMPORAL_2));
}
private static FormatStyle getMoreVerboseStyle(FormatStyle style) {
diff --git a/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java b/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java
new file mode 100644
index 0000000..c4d7b68
--- /dev/null
+++ b/src/main/java/freemarker/core/MissingTimeZoneParserPolicy.java
@@ -0,0 +1,54 @@
+/*
+ * 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.OffsetDateTime;
+
+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}).
+ *
+ * <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},
+ * {@code ?offset_date_time} uses {@link #ASSUME_CURRENT_TIME_ZONE}, {@code ?offset_or_local_date_time} uses {@link
+ * #FALL_BACK_TO_LOCAL_TEMPORAL}, and {@code ?unambiguous_offset_date_time} uses {@link #FAIL}.
+ *
+ * <p>This is not used when parsing to {@link java.util.Date}, and the policy there is always effectively
+ * {@link #ASSUME_CURRENT_TIME_ZONE}.
+ *
+ * @since 2.3.32
+ */
+public enum MissingTimeZoneParserPolicy {
+ /**
+ * Use {@link Environment#getTimeZone()}.
+ */
+ ASSUME_CURRENT_TIME_ZONE,
+ /**
+ * Return a local temporal instead of the requested type.
+ */
+ FALL_BACK_TO_LOCAL_TEMPORAL,
+ /**
+ * Give up with error.
+ */
+ FAIL
+}
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java b/src/main/java/freemarker/core/TemplateTemporalFormat.java
index 75eede4..2c1dc26 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java
@@ -81,7 +81,10 @@ public abstract class TemplateTemporalFormat extends TemplateValueFormat {
* 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}.
+ *
+ * @throws ParsingNotSupportedException If this format doesn't implement parsing.
*/
- public abstract Object parse(String s) throws TemplateValueFormatException;
+ public abstract Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy)
+ throws TemplateValueFormatException;
}
diff --git a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
index ab39c97..3243572 100644
--- a/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/XSTemplateTemporalFormatFactory.java
@@ -35,8 +35,6 @@ import java.time.temporal.Temporal;
import java.util.Locale;
import java.util.TimeZone;
-import freemarker.template.utility.TemporalUtils;
-
/**
* Format factory related to {@link someJava8Temporal?string.xs}, {@link someJava8Temporal?string.xs_...}, etc.
*/
@@ -64,7 +62,7 @@ class XSTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
final DateTimeFormatter dateTimeFormatter;
final DateTimeFormatter parserDateTimeFormatter;
final String description;
- temporalClass = TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
+ temporalClass = _TemporalUtils.normalizeSupportedTemporalClass(temporalClass);
if (temporalClass == LocalTime.class || temporalClass == OffsetTime.class) {
dateTimeFormatter = ISO8601_TIME_FORMAT;
parserDateTimeFormatter = PARSER_ISO8601_EXTENDED_TIME_FORMAT;
diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java
index 0d5093d..112399f 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -54,6 +54,11 @@ public class _MessageUtil {
+ "to specify which fields to display. "
};
+ 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()
+ + "." + MissingTimeZoneParserPolicy.FAIL + ").";
+
static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n";
static final String EMBEDDED_MESSAGE_END = "\n---end-message---";
diff --git a/src/main/java/freemarker/template/utility/TemporalUtils.java b/src/main/java/freemarker/core/_TemporalUtils.java
similarity index 94%
rename from src/main/java/freemarker/template/utility/TemporalUtils.java
rename to src/main/java/freemarker/core/_TemporalUtils.java
index 3498ff5..8aa38f8 100644
--- a/src/main/java/freemarker/template/utility/TemporalUtils.java
+++ b/src/main/java/freemarker/core/_TemporalUtils.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package freemarker.template.utility;
+package freemarker.core;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
@@ -49,15 +49,16 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
-import freemarker.core._JavaTimeBugUtils;
import freemarker.template.Configuration;
+import freemarker.template.utility.StringUtil;
/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
* Static utilities related to {@link Temporal}-s, and other {@code java.time} classes.
*
* @since 2.3.32
*/
-public final class TemporalUtils {
+public final class _TemporalUtils {
private static final Map<Class<? extends Temporal>, TemporalQuery<? extends Temporal>> TEMPORAL_CLASS_TO_QUERY_MAP;
static {
TEMPORAL_CLASS_TO_QUERY_MAP = new IdentityHashMap<>();
@@ -65,7 +66,7 @@ public final class TemporalUtils {
TEMPORAL_CLASS_TO_QUERY_MAP.put(LocalDate.class, LocalDate::from);
TEMPORAL_CLASS_TO_QUERY_MAP.put(LocalDateTime.class, LocalDateTime::from);
TEMPORAL_CLASS_TO_QUERY_MAP.put(LocalTime.class, LocalTime::from);
- TEMPORAL_CLASS_TO_QUERY_MAP.put(OffsetDateTime.class, TemporalUtils::offsetDateTimeFrom);
+ TEMPORAL_CLASS_TO_QUERY_MAP.put(OffsetDateTime.class, _TemporalUtils::offsetDateTimeFrom);
TEMPORAL_CLASS_TO_QUERY_MAP.put(OffsetTime.class, OffsetTime::from);
TEMPORAL_CLASS_TO_QUERY_MAP.put(ZonedDateTime.class, ZonedDateTime::from);
TEMPORAL_CLASS_TO_QUERY_MAP.put(Year.class, Year::from);
@@ -75,6 +76,7 @@ public final class TemporalUtils {
/**
* {@link Temporal} subclasses directly suppoerted by FreeMarker.
*/
+ // Not private because of tests
static final List<Class<? extends Temporal>> SUPPORTED_TEMPORAL_CLASSES = Arrays.asList(
Instant.class,
LocalDate.class,
@@ -86,10 +88,11 @@ public final class TemporalUtils {
Year.class,
YearMonth.class);
+ // 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);
- private TemporalUtils() {
+ private _TemporalUtils() {
throw new AssertionError();
}
@@ -100,8 +103,7 @@ public final class TemporalUtils {
public static TemporalQuery<? extends Temporal> getTemporalQuery(Class<? extends Temporal> temporalClass) {
TemporalQuery<? extends Temporal> temporalQuery = TEMPORAL_CLASS_TO_QUERY_MAP.get(temporalClass);
if (temporalQuery == null) {
- Class<? extends Temporal> normalizedTemporalClass = normalizeSupportedTemporalClass(
- temporalClass);
+ Class<? extends Temporal> normalizedTemporalClass = normalizeSupportedTemporalClass(temporalClass);
if (temporalClass != normalizedTemporalClass) {
temporalQuery = TEMPORAL_CLASS_TO_QUERY_MAP.get(normalizedTemporalClass);
}
@@ -432,8 +434,6 @@ public final class TemporalUtils {
* a future Java version, use this method before using {@code ==}.
*
* @throws IllegalArgumentException If the temporal class is not currently supported by FreeMarker.
- *
- * @since 2.3.32
*/
public static Class<? extends Temporal> normalizeSupportedTemporalClass(Class<? extends Temporal> temporalClass) {
if (SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL) {
@@ -467,8 +467,6 @@ public final class TemporalUtils {
* 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.
- *
- * @since 2.3.32
*/
public static boolean isLocalTemporalClass(Class<? extends Temporal> temporalClass) {
temporalClass = normalizeSupportedTemporalClass(temporalClass);
@@ -482,6 +480,26 @@ public final class TemporalUtils {
}
/**
+ * Returns the local variation of a non-local class, or {@code null} if no local pair is known, or the class is not
+ * recognized .
+ *
+ * @throws IllegalArgumentException If the temporal class is not currently supported by FreeMarker.
+ */
+ public static Class<? extends Temporal> getLocalTemporalClassForNonLocal(Class<? extends Temporal> temporalClass) {
+ temporalClass = normalizeSupportedTemporalClass(temporalClass);
+ if (temporalClass == OffsetDateTime.class) {
+ return LocalDateTime.class;
+ }
+ if (temporalClass == ZonedDateTime.class) {
+ return LocalDateTime.class;
+ }
+ if (temporalClass == OffsetTime.class) {
+ return LocalTime.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.
@@ -515,4 +533,5 @@ public final class TemporalUtils {
throw new IllegalArgumentException("Unsupported temporal class: " + temporalClass.getName());
}
}
+
}
diff --git a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
index 4fcb177..75518fa 100644
--- a/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
+++ b/src/test/java/freemarker/core/AbstractTemporalFormatTest.java
@@ -20,6 +20,8 @@
package freemarker.core;
import static freemarker.template.utility.StringUtil.*;
+import static freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.io.IOException;
@@ -65,9 +67,44 @@ public abstract class AbstractTemporalFormatTest {
return sb.toString();
}
+ /**
+ * Same as {@link #assertParsingResults(Consumer, MissingTimeZoneParserPolicy, Object...)} with 2nd argument {@link
+ * MissingTimeZoneParserPolicy#ASSUME_CURRENT_TIME_ZONE}.
+ */
static protected void assertParsingResults(
Consumer<Configurable> configurator,
- Object... stringsAndExpectedResults) throws TemplateException, TemplateValueFormatException {
+ Object... inputsAndExpectedResults) throws TemplateException, TemplateValueFormatException {
+ assertParsingResults(
+ configurator,
+ MissingTimeZoneParserPolicy.ASSUME_CURRENT_TIME_ZONE,
+ inputsAndExpectedResults);
+ }
+
+ /**
+ * Same as {@link #assertParsingResults(Consumer, MissingTimeZoneParserPolicy, Object...)}, but 2nd argument going
+ * through all {@link MissingTimeZoneParserPolicy} values.
+ */
+ static protected void assertParsingResultsWithAllMissingTimeZonePolicies(
+ Consumer<Configurable> configurator,
+ Object... inputsAndExpectedResults) throws TemplateException, TemplateValueFormatException {
+ for (MissingTimeZoneParserPolicy missingTimeZoneParserPolicy : MissingTimeZoneParserPolicy.values()) {
+ assertParsingResults(
+ configurator,
+ MissingTimeZoneParserPolicy.ASSUME_CURRENT_TIME_ZONE,
+ inputsAndExpectedResults);
+ }
+ }
+
+ /**
+ * @param inputsAndExpectedResults
+ * Repeats this pattern: Parsed string ({@link String}), optional target temporal class ({@link Class}),
+ * expected result ({@link Temporal}). If the target temporal class is left out, it will detect it from the
+ * type of the expected result. ,
+ */
+ static protected void assertParsingResults(
+ Consumer<Configurable> configurator,
+ MissingTimeZoneParserPolicy missingTimeZoneParserPolicy,
+ Object... inputsAndExpectedResults) throws TemplateException, TemplateValueFormatException {
Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
conf.setTimeZone(DateUtil.UTC);
conf.setLocale(Locale.US);
@@ -81,45 +118,76 @@ public abstract class AbstractTemporalFormatTest {
throw new UncheckedIOException(e);
}
- if (stringsAndExpectedResults.length % 2 != 0) {
- throw new IllegalArgumentException(
- "stringsAndExpectedResults.length must be even, but was " + stringsAndExpectedResults.length + ".");
+ boolean hasTargetClasses = inputsAndExpectedResults.length >= 2 && inputsAndExpectedResults[1] instanceof Class;
+
+ if (inputsAndExpectedResults.length == 0) {
+ throw new IllegalArgumentException("inputsAndExpectedResults can't be empty.");
}
- for (int i = 0; i < stringsAndExpectedResults.length; i += 2) {
- Object value = stringsAndExpectedResults[i];
+ int i = 0;
+ while (i < inputsAndExpectedResults.length) {
+ Object value = inputsAndExpectedResults[i];
+
if (!(value instanceof String)) {
- throw new IllegalArgumentException("stringsAndExpectedResults[" + i + "] should be a String");
+ throw new IllegalArgumentException("inputsAndExpectedResults[" + i + "] should be a "
+ + "String, but it's this " + ClassUtil.getShortClassNameOfObject(value) + ": " + value);
}
- String string = (String) value;
-
- value = stringsAndExpectedResults[i + 1];
- if (!(value instanceof Temporal)) {
- throw new IllegalArgumentException("stringsAndExpectedResults[" + (i + 1) + "] should be a Temporal");
+ String stringToParse = (String) value;
+ i++;
+
+ final Class<? extends Temporal> temporalClass;
+ final Temporal expectedResult;
+ if (i == inputsAndExpectedResults.length) {
+ throw new IllegalArgumentException("inputsAndExpectedResults[" + i + "] is out of array bounds; "
+ + "expecting a Temporal or Class<? extends Temporal> there.");
+ }
+ value = inputsAndExpectedResults[i];
+ if (value instanceof Temporal) {
+ expectedResult = (Temporal) value;
+ temporalClass = expectedResult.getClass();
+ i++;
+ } else if (value instanceof Class) {
+ temporalClass = (Class<? extends Temporal>) value;
+ i++;
+
+ if (i == inputsAndExpectedResults.length) {
+ throw new IllegalArgumentException("inputsAndExpectedResults[" + i + "] is out of array bounds; "
+ + "expecting a Temporal there.");
+ }
+ value = inputsAndExpectedResults[i];
+ if (!(value instanceof Temporal)) {
+ throw new IllegalArgumentException("inputsAndExpectedResults[" + i + "] should be a "
+ + "Temporal, but it's this " + ClassUtil.getShortClassNameOfObject(value) + ": " + value);
+ }
+ expectedResult = (Temporal) value;
+ i++;
+ } else {
+ throw new IllegalArgumentException("inputsAndExpectedResults[" + i + "] should be a "
+ + "Temporal or Class<? extends Temporal>, but it's this "
+ + ClassUtil.getShortClassNameOfObject(value) + ": " + value);
}
- Temporal expectedResult = (Temporal) value;
- Class<? extends Temporal> temporalClass = expectedResult.getClass();
TemplateTemporalFormat templateTemporalFormat = env.getTemplateTemporalFormat(temporalClass);
Temporal actualResult;
{
- Object actualResultObject = templateTemporalFormat.parse(string);
+ Object actualResultObject = templateTemporalFormat.parse(stringToParse, missingTimeZoneParserPolicy);
if (actualResultObject instanceof Temporal) {
actualResult = (Temporal) actualResultObject;
} else if (actualResultObject instanceof TemplateTemporalModel) {
actualResult = ((TemplateTemporalModel) actualResultObject).getAsTemporal();
} else {
throw new AssertionError(
- "Parsing result of " + jQuote(string) + " is not of an expected type: "
+ "Parsing result of " + jQuote(stringToParse) + " is not of an expected type: "
+ ClassUtil.getShortClassNameOfObject(actualResultObject));
}
}
if (!expectedResult.equals(actualResult)) {
throw new AssertionError(
- "Parsing result of " + jQuote(string) + " "
+ "Parsing result of " + jQuote(stringToParse) + " "
+ "(with temporalFormat[" + temporalClass.getSimpleName() + "]="
+ jQuote(env.getTemporalFormat(temporalClass)) + ", "
+ + "missingTimeZoneParserPolicy=" + missingTimeZoneParserPolicy + ", "
+ "timeZone=" + jQuote(env.getTimeZone().toZoneId()) + ", "
+ "locale=" + jQuote(env.getLocale()) + ") "
+ "differs from expected.\n"
@@ -131,10 +199,23 @@ public abstract class AbstractTemporalFormatTest {
static protected void assertParsingFails(
Consumer<Configurable> configurator,
- String parsed,
+ String stringToParse,
Class<? extends Temporal> temporalClass,
Consumer<TemplateValueFormatException> exceptionAssertions) throws TemplateException,
TemplateValueFormatException {
+ assertParsingFails(
+ configurator,
+ MissingTimeZoneParserPolicy.ASSUME_CURRENT_TIME_ZONE,
+ stringToParse, temporalClass,
+ exceptionAssertions);
+ }
+
+ static protected void assertParsingFails(
+ Consumer<Configurable> configurator,
+ MissingTimeZoneParserPolicy missingTimeZoneParserPolicy,
+ String stringToParse, Class<? extends Temporal> temporalClass,
+ Consumer<TemplateValueFormatException> exceptionAssertions) throws TemplateException,
+ TemplateValueFormatException {
Configuration conf = new Configuration(Configuration.VERSION_2_3_32);
conf.setTimeZone(DateUtil.UTC);
conf.setLocale(Locale.US);
@@ -151,11 +232,20 @@ public abstract class AbstractTemporalFormatTest {
TemplateTemporalFormat templateTemporalFormat = env.getTemplateTemporalFormat(temporalClass);
try {
- templateTemporalFormat.parse(parsed);
- fail("Parsing " + jQuote(parsed) + " with " + templateTemporalFormat + " should have failed.");
+ templateTemporalFormat.parse(stringToParse, missingTimeZoneParserPolicy);
+ fail("Parsing " + jQuote(stringToParse) + " with " + templateTemporalFormat + " should have failed.");
} catch (TemplateValueFormatException e) {
exceptionAssertions.accept(e);
}
}
+ protected static void assertMissingTimeZoneFailPolicyTriggered(TemplateValueFormatException e) {
+ assertThat(
+ e.getMessage(),
+ allOf(
+ containsStringIgnoringCase("doesn't contain time zone or offset"),
+ containsString(MissingTimeZoneParserPolicy.class.getName() + "."
+ + MissingTimeZoneParserPolicy.FAIL)));
+ }
+
}
diff --git a/src/test/java/freemarker/core/TemporalFormatWithCustomFormatTest.java b/src/test/java/freemarker/core/CustomTemplateTemporalFormatTest.java
similarity index 93%
rename from src/test/java/freemarker/core/TemporalFormatWithCustomFormatTest.java
rename to src/test/java/freemarker/core/CustomTemplateTemporalFormatTest.java
index 92b2a4a..2eb887b 100644
--- a/src/test/java/freemarker/core/TemporalFormatWithCustomFormatTest.java
+++ b/src/test/java/freemarker/core/CustomTemplateTemporalFormatTest.java
@@ -33,9 +33,9 @@ import freemarker.template.Configuration;
import freemarker.test.TemplateTest;
/**
- * Like {@link TemporalFormatWithJavaFormatTest}, but this one contains the tests that utilize {@link TemplateTest}.
+ * Like {@link JavaTemplateTemporalFormatTest}, but this one contains the tests that utilize {@link TemplateTest}.
*/
-public class TemporalFormatWithCustomFormatTest extends TemplateTest {
+public class CustomTemplateTemporalFormatTest extends TemplateTest {
@Before
public void setup() {
diff --git a/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
index b3d6c5a..4c5174d 100644
--- a/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
@@ -78,7 +78,7 @@ public class EpochMillisDivTemplateTemporalFormatFactory extends TemplateTempora
}
@Override
- public Object parse(String s) throws TemplateValueFormatException {
+ public Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws TemplateValueFormatException {
throw new ParsingNotSupportedException("Parsing is not implement for this test class");
}
diff --git a/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
index a88c905..4eaf410 100644
--- a/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
@@ -64,7 +64,7 @@ public class EpochMillisTemplateTemporalFormatFactory extends TemplateTemporalFo
}
@Override
- public Object parse(String s) throws TemplateValueFormatException {
+ public Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws TemplateValueFormatException {
throw new ParsingNotSupportedException("Parsing is not implement for this test class");
}
diff --git a/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
index cb9a954..36c3c7e 100644
--- a/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
@@ -60,7 +60,7 @@ public class HTMLISOTemplateTemporalFormatFactory extends TemplateTemporalFormat
}
@Override
- public Object parse(String s) throws TemplateValueFormatException {
+ public Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws TemplateValueFormatException {
throw new ParsingNotSupportedException("Parsing is not implement for this test class");
}
diff --git a/src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java b/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java
similarity index 55%
rename from src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java
rename to src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java
index 27a4bf3..7590d62 100644
--- a/src/test/java/freemarker/core/TemporalFormatWithIsoFormatTest.java
+++ b/src/test/java/freemarker/core/ISOLikeTemplateTemporalFormatTest.java
@@ -20,6 +20,7 @@
package freemarker.core;
import static freemarker.template.utility.StringUtil.*;
+import static freemarker.test.hamcerst.Matchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@@ -46,11 +47,17 @@ import org.junit.Test;
import freemarker.template.TemplateException;
import freemarker.template.utility.DateUtil;
-public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest {
+public class ISOLikeTemplateTemporalFormatTest extends AbstractTemporalFormatTest {
+
+ private final static TimeZone TIME_ZONE = TimeZone.getTimeZone("America/New_York");
private static final Consumer<Configurable> ISO_DATE_CONFIGURATOR = conf -> conf.setDateFormat("iso");
private static final Consumer<Configurable> ISO_TIME_CONFIGURATOR = conf -> conf.setTimeFormat("iso");
- private static final Consumer<Configurable> ISO_DATE_TIME_CONFIGURATOR = conf -> conf.setDateTimeFormat("iso");
+ private static final Consumer<Configurable> ISO_DATE_TIME_CONFIGURATOR = conf -> {
+ conf.setDateTimeFormat("iso");
+ conf.setTimeZone(TIME_ZONE);
+ conf.setLocale(Locale.GERMANY); // So if the decimal separator has a problem, we will notice
+ };
private static Consumer<Configurable> isoDateTimeConfigurator(TimeZone timeZone) {
return conf -> { ISO_DATE_TIME_CONFIGURATOR.accept(conf); conf.setTimeZone(timeZone); };
@@ -112,7 +119,7 @@ public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest
"2021-12-11T13:01:02",
formatTemporal(
ISO_DATE_TIME_CONFIGURATOR,
- LocalDateTime.of(2021, 12, 11, 13, 1, 2, 0)));
+ LocalDateTime.of(2021, 12, 11, 13, 1, 2)));
assertEquals(
"2021-12-11T13:01:02.0123",
formatTemporal(
@@ -262,44 +269,46 @@ public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest
@Test
public void testParseOffsetDateTime() throws TemplateException, TemplateValueFormatException {
- testParseOffsetDateTimeAndInstant(OffsetDateTime.class);
+ testParseNonLocalDateTimeAndInstant(OffsetDateTime.class);
}
@Test
public void testParseInstant() throws TemplateException, TemplateValueFormatException {
- testParseOffsetDateTimeAndInstant(Instant.class);
+ testParseNonLocalDateTimeAndInstant(Instant.class);
}
@Test
public void testParseZonedDateTime() throws TemplateException, TemplateValueFormatException {
- testParseOffsetDateTimeAndInstant(ZonedDateTime.class);
+ testParseNonLocalDateTimeAndInstant(ZonedDateTime.class);
}
- private Temporal convertToClass(OffsetDateTime offsetDateTime, Class<? extends Temporal> temporalClass) {
+ private Temporal convertToClass(ZonedDateTime zonedDateTime, Class<? extends Temporal> temporalClass) {
+ if (temporalClass == ZonedDateTime.class) {
+ return zonedDateTime;
+ }
if (temporalClass == OffsetDateTime.class) {
- return offsetDateTime;
+ return zonedDateTime.toOffsetDateTime();
}
if (temporalClass == Instant.class) {
- return offsetDateTime.toInstant();
- }
- if (temporalClass == ZonedDateTime.class) {
- return offsetDateTime.toZonedDateTime();
+ return zonedDateTime.toInstant();
}
throw new IllegalArgumentException();
}
- private void testParseOffsetDateTimeAndInstant(Class<? extends Temporal> temporalClass)
+ private void testParseNonLocalDateTimeAndInstant(Class<? extends Temporal> temporalClass)
throws TemplateException, TemplateValueFormatException {
// ISO extended and ISO basic format:
- for (String parsedString : new String[]{"2021-12-11T13:01:02.0123Z", "20211211T130102.0123Z"}) {
+ for (String stringToParse : new String[]{"2021-12-11T13:01:02.0123Z", "20211211T130102.0123Z"}) {
assertParsingResults(
ISO_DATE_TIME_CONFIGURATOR,
- parsedString,
- OffsetDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, ZoneOffset.UTC)) ;
+ stringToParse,
+ convertToClass(
+ ZonedDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000, ZoneOffset.UTC),
+ temporalClass));
}
// Optional parts:
- for (String parsedString : new String[] {
+ for (String stringToParse : new String[] {
"2021-12-11T13:00:00.0+02:00",
"2021-12-11T13:00:00+02:00",
"2021-12-11T13:00+02",
@@ -311,74 +320,89 @@ public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest
}) {
assertParsingResults(
ISO_DATE_TIME_CONFIGURATOR,
- parsedString,
+ stringToParse,
convertToClass(
- OffsetDateTime.of(2021, 12, 11, 13, 0, 0, 0, ZoneOffset.ofHours(2)),
+ ZonedDateTime.of(2021, 12, 11, 13, 0, 0, 0, ZoneOffset.ofHours(2)),
temporalClass));
}
// Negative year:
- for (String parsedString : new String[] {
+ for (String stringToParse : new String[] {
"-1000-02-03T04Z",
"-10000203T04Z"
}) {
assertParsingResults(
ISO_DATE_TIME_CONFIGURATOR,
- parsedString,
+ stringToParse,
convertToClass(
- OffsetDateTime.of(-1000, 2, 3, 4, 0, 0, 0, ZoneOffset.UTC),
+ ZonedDateTime.of(-1000, 2, 3, 4, 0, 0, 0, ZoneOffset.UTC),
temporalClass));
}
// Hour 24:
- for (String parsedString : new String[] {
+ for (String stringToParse : new String[] {
"2020-01-02T24Z",
"2020-01-02T24:00Z",
"2020-01-02T24:00:00Z",
"2020-01-02T24:00:00.0Z",
"2020-01-02T24:00:00.0+00",
- // For local temporals only: "2020-01-02T24:00:00",
"20200102T24Z",
"20200102T2400Z",
"20200102T240000Z",
"20200102T240000.0Z",
"20200102T240000.0+00",
- // For local temporals only: "20200102T240000"
}) {
assertParsingResults(
ISO_DATE_TIME_CONFIGURATOR,
- parsedString,
+ stringToParse,
convertToClass(
- OffsetDateTime.of(2020, 1, 3, 0, 0, 0, 0, ZoneOffset.UTC),
+ ZonedDateTime.of(2020, 1, 3, 0, 0, 0, 0, ZoneOffset.UTC),
temporalClass));
}
- // Unlike for the Java format, for ISO we require the string to contain the offset for a non-local target type.
- for (String parsedString : new String[] {
+ // MissingTimeZoneParserPolicy-es:
+ String[] localStringsToParse = {
"2020-01-02T03", "2020-01-02T03:00", "2020-01-02T03:00:00",
- "20200102T03", "20200102T0300", "20200102T030000"}) {
- assertParsingFails(
+ "20200102T03", "20200102T0300", "20200102T030000"};
+ for (String stringToParse : localStringsToParse) {
+ assertParsingResults(
ISO_DATE_TIME_CONFIGURATOR,
- parsedString,
+ stringToParse,
+ convertToClass(
+ ZonedDateTime.of(2020, 1, 2, 3, 0, 0, 0, TIME_ZONE.toZoneId()),
+ temporalClass));
+ }
+ for (String stringToParse : localStringsToParse) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR, MissingTimeZoneParserPolicy.FALL_BACK_TO_LOCAL_TEMPORAL,
+ stringToParse,
+ LocalDateTime.of(2020, 1, 2, 3, 0, 0));
+ }
+ for (String stringToParse : localStringsToParse) {
+ assertParsingFails(
+ ISO_DATE_TIME_CONFIGURATOR, MissingTimeZoneParserPolicy.FAIL,
+ stringToParse,
temporalClass,
e -> assertThat(e.getMessage(), allOf(
- containsString(jQuote(parsedString)),
- containsString("time zone offset"),
- containsString(temporalClass.getSimpleName()))));
+ containsString(jQuote(stringToParse)),
+ containsString("time zone or offset"),
+ containsString(temporalClass.getSimpleName()))));
}
- for (String parsedString : new String[] {
+ // Invalid strings:
+ for (String stringToParse : new String[] {
"2021-12-11", "20211211", "2021-12-11T", "2021-12-11T0Z",
+ "2021-12-11T0102Z", "20211211T01:02Z",
"2021-12-11T25Z", "2022-02-29T23Z", "2021-13-11T23Z"}) {
assertParsingFails(
ISO_DATE_TIME_CONFIGURATOR,
- parsedString,
+ stringToParse,
temporalClass,
e -> {
assertThat(e.getMessage(), allOf(
- containsString(jQuote(parsedString)),
+ containsString(jQuote(stringToParse)),
containsString(temporalClass.getSimpleName())));
- if (!parsedString.contains("T")) {
+ if (!stringToParse.contains("T")) {
assertThat(e.getMessage(), containsString("\"T\""));
}
});
@@ -387,22 +411,302 @@ public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest
@Test
public void testParseOffsetTime() throws TemplateException, TemplateValueFormatException {
- // TODO [FREEMARKER-35]
+ // ISO extended and ISO basic format:
+ for (String stringToParse : new String[]{"13:01:02.0123Z", "130102.0123Z"}) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ OffsetTime.of(13, 1, 2, 12_300_000, ZoneOffset.UTC)) ;
+ }
+
+ // Optional parts:
+ for (String stringToParse : new String[] {
+ "13:00:00.0+02:00",
+ "13:00:00+02:00",
+ "13:00+02",
+ "13+02",
+ "130000.0+0200",
+ "130000+0200",
+ "1300+02",
+ "13+02",
+ }) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ OffsetTime.of(13, 0, 0, 0, ZoneOffset.ofHours(2)));
+ }
+
+ // Hour 24:
+ for (String stringToParse : new String[] {
+ "24Z",
+ "24:00Z",
+ "24:00:00Z",
+ "24:00:00.0Z",
+ "24:00:00.0+00",
+ "24Z",
+ "2400Z",
+ "240000Z",
+ "240000.0Z",
+ "240000.0+00",
+ }) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC));
+ }
+
+ // MissingTimeZoneParserPolicy-es:
+ String[] localStringsToParse = {
+ "03", "03:00", "03:00:00", "03:00:00.0",
+ "0300", "030000", "030000.0"};
+ for (String stringToParse : localStringsToParse) {
+ assertParsingFails(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ OffsetTime.class,
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsStringIgnoringCase("daylight saving"),
+ containsString(OffsetTime.class.getSimpleName()))));
+ }
+ for (String stringToParse : localStringsToParse) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR, MissingTimeZoneParserPolicy.FALL_BACK_TO_LOCAL_TEMPORAL,
+ stringToParse,
+ LocalTime.of(3, 0, 0));
+ }
+ for (String stringToParse : localStringsToParse) {
+ assertParsingFails(
+ ISO_TIME_CONFIGURATOR, MissingTimeZoneParserPolicy.FAIL,
+ stringToParse,
+ OffsetTime.class,
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsString("time zone or offset"),
+ containsString(OffsetTime.class.getSimpleName()))));
+ }
+
+ // Invalid strings:
+ for (String stringToParse : new String[] {"Z", "1Z", "T01Z", "25Z", "1161Z", "01:02:03:00Z", "20210101T01Z"}) {
+ assertParsingFails(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ OffsetTime.class,
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsString(OffsetTime.class.getSimpleName()))));
+ }
}
@Test
public void testParseLocalDateTime() throws TemplateException, TemplateValueFormatException {
- // TODO [FREEMARKER-35]
+ // ISO extended and ISO basic format:
+ for (String stringToParse : new String[]{"2021-12-11T13:01:02.0123", "20211211T130102.0123"}) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalDateTime.of(2021, 12, 11, 13, 1, 2, 12_300_000)) ;
+ }
+
+ // Optional parts:
+ for (String stringToParse : new String[] {
+ "2021-12-11T13:00:00.0",
+ "2021-12-11T13:00:00",
+ "2021-12-11T13:00",
+ "2021-12-11T13",
+ "20211211T130000.0",
+ "20211211T130000",
+ "20211211T1300",
+ "20211211T13",
+ }) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalDateTime.of(2021, 12, 11, 13, 0, 0));
+ }
+
+ // Negative year:
+ for (String stringToParse : new String[] {
+ "-1000-02-03T04Z",
+ "-10000203T04Z"
+ }) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalDateTime.of(-1000, 2, 3, 4, 0, 0));
+ }
+
+ // Hour 24:
+ for (String stringToParse : new String[] {
+ "2020-01-02T24",
+ "2020-01-02T24:00",
+ "2020-01-02T24:00:00",
+ "2020-01-02T24:00:00.0",
+ "20200102T24",
+ "20200102T2400",
+ "20200102T240000",
+ "20200102T240000.0",
+ }) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalDateTime.of(2020, 1, 3, 0, 0, 0));
+ }
+
+ // MissingTimeZoneParserPolicy-es:
+ String[] localStringsToParse = {
+ "2020-01-02T03", "2020-01-02T03:00", "2020-01-02T03:00:00",
+ "20200102T03", "20200102T0300", "20200102T030000"};
+ for (MissingTimeZoneParserPolicy missingTimeZoneParserPolicy : MissingTimeZoneParserPolicy.values()) {
+ for (String stringToParse : localStringsToParse) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR, missingTimeZoneParserPolicy,
+ stringToParse,
+ LocalDateTime.of(2020, 1, 2, 3, 0));
+ }
+ }
+
+ // Offset is ignored:
+ for (String stringToParse : new String[] {
+ "2021-12-11T03:04:05Z", "2021-12-11T03:04:05+01", "2021-12-11T03:04:05-01:30"}) {
+ assertParsingResults(
+ ISO_DATE_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalDateTime.of(2021, 12, 11, 3, 4, 5));
+ }
+
+ // Invalid strings:
+ for (String stringToParse : new String[] {
+ "2021-12-11", "20211211", "2021-12-11T", "2021-12-11T0",
+ "2021-12-11T0102", "20211211T01:02",
+ "2021-12-11T25", "2022-02-29T23", "2021-13-11T23"}) {
+ assertParsingFails(
+ ISO_DATE_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalDateTime.class,
+ e -> {
+ assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsString(LocalDateTime.class.getSimpleName())));
+ if (!stringToParse.contains("T")) {
+ assertThat(e.getMessage(), containsString("\"T\""));
+ }
+ });
+ }
}
@Test
public void testParseLocalDate() throws TemplateException, TemplateValueFormatException {
- // TODO [FREEMARKER-35]
+ // ISO extended and ISO basic format:
+ for (String stringToParse : new String[]{"2021-12-11", "20211211"}) {
+ assertParsingResults(
+ ISO_DATE_CONFIGURATOR,
+ stringToParse,
+ LocalDate.of(2021, 12, 11)) ;
+ }
+
+ // Negative year:
+ for (String stringToParse : new String[] {
+ "-1000-02-03",
+ "-10000203"
+ }) {
+ assertParsingResults(
+ ISO_DATE_CONFIGURATOR,
+ stringToParse,
+ LocalDate.of(-1000, 2, 3));
+ }
+
+ // MissingTimeZoneParserPolicy-es:
+ for (MissingTimeZoneParserPolicy missingTimeZoneParserPolicy : MissingTimeZoneParserPolicy.values()) {
+ assertParsingResults(
+ ISO_DATE_CONFIGURATOR, missingTimeZoneParserPolicy,
+ "20200102",
+ LocalDate.of(2020, 1, 2));
+ }
+
+ // Invalid strings:
+ for (String stringToParse : new String[] {
+ "2021-12-11Z", "2021-12-11T", "2021-1211",
+ "2022-02-29", "2021-13-11"}) {
+ assertParsingFails(
+ ISO_DATE_CONFIGURATOR,
+ stringToParse,
+ LocalDate.class,
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsString(LocalDate.class.getSimpleName()))));
+ }
}
@Test
public void testParseLocalTime() throws TemplateException, TemplateValueFormatException {
- // TODO [FREEMARKER-35]
+ // ISO extended and ISO basic format:
+ for (String stringToParse : new String[]{"13:01:02.0123", "130102.0123"}) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalTime.of(13, 1, 2, 12_300_000));
+ }
+
+ // Optional parts:
+ for (String stringToParse : new String[] {
+ "13:00:00.0",
+ "13:00:00",
+ "13:00",
+ "13",
+ "130000.0",
+ "130000",
+ "1300",
+ }) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalTime.of(13, 0, 0));
+ }
+
+ // Hour 24:
+ for (String stringToParse : new String[] {
+ "24",
+ "24:00",
+ "24:00:00",
+ "24:00:00.0",
+ "24:00:00.0",
+ "2400",
+ "240000",
+ "240000.0"
+ }) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalTime.of(0, 0, 0));
+ }
+
+ // MissingTimeZoneParserPolicy-es:
+ for (MissingTimeZoneParserPolicy missingTimeZoneParserPolicy : MissingTimeZoneParserPolicy.values()) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR, missingTimeZoneParserPolicy,
+ "03:04:05",
+ LocalTime.of(3, 4, 5));
+ }
+
+ // Offset is ignored:
+ for (String stringToParse : new String[] {"03:04:05Z", "03:04:05+01", "03:04:05-01:30"}) {
+ assertParsingResults(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalTime.of(3, 4, 5));
+ }
+
+ // Invalid strings:
+ for (String stringToParse : new String[] {"", "1", "T01", "25", "1161", "01:02:03:00", "2021-01-01T01"}) {
+ assertParsingFails(
+ ISO_TIME_CONFIGURATOR,
+ stringToParse,
+ LocalTime.class,
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsString(LocalTime.class.getSimpleName()))));
+ }
}
@Test
@@ -416,12 +720,9 @@ public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest
ISO_DATE_TIME_CONFIGURATOR,
"2021-01",
Year.class,
- e -> {
- assertThat(e.getMessage(), allOf(
- containsString(jQuote("2021-01")),
- containsString("Year")));
- }
- );
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote("2021-01")),
+ containsString("Year"))));
}
@Test
@@ -435,17 +736,14 @@ public class TemporalFormatWithIsoFormatTest extends AbstractTemporalFormatTest
assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "-1000-01", YearMonth.of(-1000, 1));
assertParsingResults(ISO_YEAR_MONTH_CONFIGURATOR, "-100001", YearMonth.of(-1000, 1));
- for (String parsedString : new String[] {"2021", "2021-12-11", "2021-13", "202113"}) {
+ for (String stringToParse : new String[] {"2021", "2021-12-11", "2021-13", "202113"}) {
assertParsingFails(
ISO_YEAR_MONTH_CONFIGURATOR,
- parsedString,
+ stringToParse,
YearMonth.class,
- e -> {
- assertThat(e.getMessage(), allOf(
- containsString(jQuote(parsedString)),
- containsString("YearMonth")));
- }
- );
+ e -> assertThat(e.getMessage(), allOf(
+ containsString(jQuote(stringToParse)),
+ containsString("YearMonth"))));
}
}
diff --git a/src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java b/src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java
similarity index 72%
rename from src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java
rename to src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java
index 56fe2ab..a81841f 100644
--- a/src/test/java/freemarker/core/TemporalFormatWithJavaFormatTest.java
+++ b/src/test/java/freemarker/core/JavaTemplateTemporalFormatTest.java
@@ -24,6 +24,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.io.IOException;
+import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@@ -34,6 +35,7 @@ import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
+import java.time.temporal.Temporal;
import java.util.Locale;
import java.util.TimeZone;
import java.util.function.Consumer;
@@ -44,7 +46,7 @@ import freemarker.template.TemplateException;
import freemarker.template.utility.DateUtil;
import freemarker.test.hamcerst.Matchers;
-public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest {
+public class JavaTemplateTemporalFormatTest extends AbstractTemporalFormatTest {
@Test
public void testFormatOffsetTimeAndZones() throws TemplateException, IOException {
@@ -95,31 +97,33 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
LocalDateTime winterLocalDateTime = LocalDateTime.of(winterLocalDate, localTime);
OffsetDateTime winterOffsetDateTime = OffsetDateTime.of(winterLocalDateTime, ZoneOffset.ofHours(2));
ZonedDateTime winterZonedDateTime = ZonedDateTime.of(winterLocalDateTime, nyZone.toZoneId());
+ Instant winterInstant = winterZonedDateTime.toInstant();
LocalDateTime summerLocalDateTime = LocalDateTime.of(summerLocalDate, localTime);
OffsetDateTime summerOffsetDateTime = OffsetDateTime.of(summerLocalDateTime, ZoneOffset.ofHours(2));
ZonedDateTime summerZonedDateTime = ZonedDateTime.of(summerLocalDateTime, nyZone.toZoneId());
+ Instant summerInstant = summerZonedDateTime.toInstant();
// 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",
+ "2021-06-30 10:30, 2021-06-30 09:30, 2021-06-30 15:30, 2021-06-30 15:30, "
+ + "2021-12-30 10:30, 2021-12-30 08:30, 2021-12-30 15:30, 2021-12-30 15:30",
formatTemporal(
conf -> {
conf.setDateTimeFormat("yyyy-MM-dd HH:mm");
conf.setTimeZone(gbZone);
},
- summerLocalDateTime, summerOffsetDateTime, summerZonedDateTime,
- winterLocalDateTime, winterOffsetDateTime, winterZonedDateTime));
+ summerLocalDateTime, summerOffsetDateTime, summerZonedDateTime, summerInstant,
+ winterLocalDateTime, winterOffsetDateTime, winterZonedDateTime, winterInstant));
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",
+ "2021-06-30 10:30, 2021-06-30 08:30, 2021-06-30 14:30, 2021-06-30 14:30, "
+ + "2021-12-30 10:30, 2021-12-30 08:30, 2021-12-30 15:30, 2021-12-30 15:30",
formatTemporal(
conf -> {
conf.setDateTimeFormat("yyyy-MM-dd HH:mm");
conf.setTimeZone(DateUtil.UTC);
},
- summerLocalDateTime, summerOffsetDateTime, summerZonedDateTime,
- winterLocalDateTime, winterOffsetDateTime, winterZonedDateTime));
+ summerLocalDateTime, summerOffsetDateTime, summerZonedDateTime, summerInstant,
+ winterLocalDateTime, winterOffsetDateTime, winterZonedDateTime, winterInstant));
// If the time zone (or offset) is shown, the value is not converted from its original time zone:
assertEquals(
@@ -138,9 +142,7 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
public void testFormatCanNotFormatLocalIfTimeZoneIsShown() {
try {
formatTemporal(
- conf -> {
- conf.setDateTimeFormat("yyyy-MM-dd HH:mmX");
- },
+ conf -> conf.setDateTimeFormat("yyyy-MM-dd HH:mmX"),
LocalDateTime.of(2021, 10, 30, 1, 2));
fail();
} catch (TemplateException e) {
@@ -157,9 +159,7 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
public void testFormatStylesAreNotSupportedForYear() {
try {
formatTemporal(
- conf -> {
- conf.setYearFormat("medium");
- },
+ conf -> conf.setYearFormat("medium"),
Year.of(2021));
fail();
} catch (TemplateException e) {
@@ -194,7 +194,7 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
ZoneId cfgZoneId = ZoneId.of("America/New_York");
TimeZone cfgTimeZone = TimeZone.getTimeZone(cfgZoneId);
- for (boolean winter : new boolean[] {true, false}) {
+ for (boolean winter : new boolean[]{true, false}) {
final String stringToParse;
final LocalDateTime localDateTime;
if (winter) {
@@ -205,18 +205,44 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
localDateTime = LocalDateTime.of(2020, 07, 10, 13, 14);
}
+ Consumer<Configurable> localLikeFormatConfigurator = conf -> {
+ conf.setDateTimeFormat("y-MM-dd HH:mm");
+ conf.setTimeZone(cfgTimeZone);
+ };
{
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, cfgZoneId);
assertParsingResults(
- conf -> {
- conf.setDateTimeFormat("y-MM-dd HH:mm");
- conf.setTimeZone(cfgTimeZone);
- },
+ localLikeFormatConfigurator,
stringToParse, localDateTime,
stringToParse, zonedDateTime,
stringToParse, zonedDateTime.toInstant(),
stringToParse, zonedDateTime.toOffsetDateTime());
}
+ {
+ ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, cfgZoneId);
+ assertParsingResults(
+ localLikeFormatConfigurator,
+ MissingTimeZoneParserPolicy.FALL_BACK_TO_LOCAL_TEMPORAL,
+ stringToParse, localDateTime,
+ stringToParse, ZonedDateTime.class, localDateTime,
+ stringToParse, OffsetDateTime.class, localDateTime,
+ stringToParse, Instant.class, localDateTime);
+ }
+ {
+ ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, cfgZoneId);
+ assertParsingResults(
+ localLikeFormatConfigurator,
+ MissingTimeZoneParserPolicy.FAIL,
+ stringToParse, localDateTime);
+ }
+ for (Class<? extends Temporal> temporalClass
+ : new Class[]{OffsetDateTime.class, ZonedDateTime.class, Instant.class}) {
+ assertParsingFails(
+ localLikeFormatConfigurator,
+ MissingTimeZoneParserPolicy.FAIL,
+ stringToParse, temporalClass,
+ JavaTemplateTemporalFormatTest::assertMissingTimeZoneFailPolicyTriggered);
+ }
{
String stringToParseWithOffset = stringToParse + "+02";
@@ -274,34 +300,58 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
@Test
public void testParseDate() throws TemplateException, TemplateValueFormatException {
LocalDate localDate = LocalDate.of(2020, 11, 10);
- assertParsingResults(
+ assertParsingResultsWithAllMissingTimeZonePolicies(
conf -> conf.setDateFormat("y-MM-dd"),
"2020-11-10", localDate);
- assertParsingResults(
+ assertParsingResultsWithAllMissingTimeZonePolicies(
conf -> conf.setDateFormat("yy-MM-dd"),
"20-11-10", localDate);
+
+ assertParsingFails(
+ conf -> conf.setDateFormat("yy-MM-dd"),
+ "20-13-01",
+ LocalDate.class,
+ e -> assertThat(e.getMessage(), containsStringIgnoringCase("month")));
}
@Test
public void testParseLocalTime() throws TemplateException, TemplateValueFormatException {
String stringToParse = "13:14";
- assertParsingResults(
+ assertParsingResultsWithAllMissingTimeZonePolicies(
conf -> conf.setTimeFormat("HH:mm"),
stringToParse, LocalTime.of(13, 14));
- assertParsingResults(
- conf -> {
- conf.setTimeFormat("HH:mmX");
- conf.setTimeZone(TimeZone.getTimeZone("GMT+02"));
- },
- stringToParse + "+02", LocalTime.of(13, 14));
+ for (String offsetSuffix : new String[] {"+02", "-01"}) {
+ assertParsingResultsWithAllMissingTimeZonePolicies(
+ conf -> {
+ conf.setTimeFormat("HH:mmX");
+ conf.setTimeZone(TimeZone.getTimeZone("GMT+02"));
+ },
+ stringToParse + offsetSuffix, LocalTime.of(13, 14));
+ }
+
+ assertParsingResultsWithAllMissingTimeZonePolicies(
+ conf -> conf.setTimeFormat("HH:mm:ss.SSS"),
+ "01:02:03.400", LocalTime.of(1, 2, 3, 400_000_000));
+
+ assertParsingResultsWithAllMissingTimeZonePolicies(
+ conf -> conf.setTimeFormat("hh:mm a"),
+ "01:02 AM", LocalTime.of(1, 2));
+ assertParsingResultsWithAllMissingTimeZonePolicies(
+ conf -> conf.setTimeFormat("hh:mm a"),
+ "01:02 PM", LocalTime.of(13, 2));
+
+ assertParsingFails(
+ conf -> conf.setTimeFormat("hh:mm a"),
+ "25:00", LocalTime.class,
+ e -> assertThat(e.getMessage(), containsStringIgnoringCase("hour")));
}
@Test
public void testParseLocalization() throws TemplateException, TemplateValueFormatException {
LocalDate localDate = LocalDate.of(2020, 11, 10);
- for (Locale locale : new Locale[] {
+ for (Locale locale : new Locale[]{
Locale.CHINA,
Locale.GERMANY,
new Locale("th", "TH"), // Because of the Buddhist calendar
@@ -321,7 +371,7 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
ZoneId zoneId = ZoneId.of("America/New_York");
TimeZone timeZone = TimeZone.getTimeZone(zoneId);
- assertParsingResults(
+ assertParsingResultsWithAllMissingTimeZonePolicies(
conf -> {
conf.setTimeFormat("HH:mmXX");
conf.setTimeZone(timeZone);
@@ -329,18 +379,27 @@ public class TemporalFormatWithJavaFormatTest extends AbstractTemporalFormatTest
"13:14+0130",
OffsetTime.of(LocalTime.of(13, 14), ZoneOffset.ofHoursMinutesSeconds(1, 30, 0)));
- try {
- assertParsingResults(
- conf -> {
- conf.setTimeFormat("HH:mm");
- conf.setTimeZone(timeZone);
- },
- "13:14",
- OffsetTime.now() /* Value doesn't matter, as parsing will fail */);
- fail("OffsetTime parsing should have failed when offset is not specified");
- } catch (InvalidFormatParametersException e) {
- assertThat(e.getMessage(), Matchers.containsStringIgnoringCase("daylight saving"));
- }
+ Consumer<Configurable> localLikeFormatConfigurator = conf -> {
+ conf.setTimeFormat("HH:mm");
+ conf.setTimeZone(timeZone);
+ };
+
+ assertParsingFails(
+ localLikeFormatConfigurator,
+ "13:14", OffsetTime.class,
+ e -> assertThat(e.getMessage(), Matchers.containsStringIgnoringCase("daylight saving")));
+
+ assertParsingResults(
+ localLikeFormatConfigurator,
+ MissingTimeZoneParserPolicy.FALL_BACK_TO_LOCAL_TEMPORAL,
+ "13:14", OffsetTime.class,
+ LocalTime.of(13, 14));
+
+ assertParsingFails(
+ localLikeFormatConfigurator,
+ MissingTimeZoneParserPolicy.FAIL,
+ "13:14", OffsetTime.class,
+ JavaTemplateTemporalFormatTest::assertMissingTimeZoneFailPolicyTriggered);
}
}
diff --git a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java
index f8827e2..b58ecd4 100644
--- a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java
+++ b/src/test/java/freemarker/core/LocAndTZSensitiveTemplateTemporalFormatFactory.java
@@ -69,7 +69,7 @@ public class LocAndTZSensitiveTemplateTemporalFormatFactory extends TemplateTemp
}
@Override
- public Object parse(String s) throws TemplateValueFormatException {
+ public Object parse(String s, MissingTimeZoneParserPolicy missingTimeZoneParserPolicy) throws TemplateValueFormatException {
throw new ParsingNotSupportedException("Parsing is not implement for this test class");
}
diff --git a/src/test/java/freemarker/template/utility/TemporalUtilsTest.java b/src/test/java/freemarker/core/_TemporalUtilsTest.java
similarity index 67%
rename from src/test/java/freemarker/template/utility/TemporalUtilsTest.java
rename to src/test/java/freemarker/core/_TemporalUtilsTest.java
index 2b5c06f..be258b2 100644
--- a/src/test/java/freemarker/template/utility/TemporalUtilsTest.java
+++ b/src/test/java/freemarker/core/_TemporalUtilsTest.java
@@ -17,11 +17,16 @@
* under the License.
*/
-package freemarker.template.utility;
+package freemarker.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.temporal.Temporal;
import java.util.HashSet;
@@ -31,21 +36,21 @@ import org.junit.Test;
import freemarker.template.Configuration;
-public class TemporalUtilsTest {
+public class _TemporalUtilsTest {
@Test
public void testSupportedTemporalClassAreFinal() {
assertTrue(
"FreeMarker was implemented with the assumption that temporal classes are final. While there "
+ "are measures in place to handle if it's not a case, it would be better to review the code.",
- TemporalUtils.SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL);
+ _TemporalUtils.SUPPORTED_TEMPORAL_CLASSES_ARE_FINAL);
}
@Test
public void testGetTemporalFormat() {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
- for (Class<? extends Temporal> supportedTemporalClass : TemporalUtils.SUPPORTED_TEMPORAL_CLASSES) {
+ for (Class<? extends Temporal> supportedTemporalClass : _TemporalUtils.SUPPORTED_TEMPORAL_CLASSES) {
assertNotNull(cfg.getTemporalFormat(supportedTemporalClass));
}
@@ -63,20 +68,28 @@ public class TemporalUtilsTest {
for (boolean camelCase : new boolean[] {false, true}) {
Set<String> uniqueSettingNames = new HashSet<>();
- for (Class<? extends Temporal> supportedTemporalClass : TemporalUtils.SUPPORTED_TEMPORAL_CLASSES) {
+ for (Class<? extends Temporal> supportedTemporalClass : _TemporalUtils.SUPPORTED_TEMPORAL_CLASSES) {
uniqueSettingNames.add(
- TemporalUtils.temporalClassToFormatSettingName(supportedTemporalClass, camelCase));
+ _TemporalUtils.temporalClassToFormatSettingName(supportedTemporalClass, camelCase));
}
- assertThat(uniqueSettingNames.size(), equalTo(TemporalUtils.SUPPORTED_TEMPORAL_CLASSES.size() - 4));
+ assertThat(uniqueSettingNames.size(), equalTo(_TemporalUtils.SUPPORTED_TEMPORAL_CLASSES.size() - 4));
assertTrue(uniqueSettingNames.stream().allMatch(it -> cfg.getSettingNames(camelCase).contains(it)));
}
try {
- TemporalUtils.temporalClassToFormatSettingName(ChronoLocalDate.class, false);
+ _TemporalUtils.temporalClassToFormatSettingName(ChronoLocalDate.class, false);
fail();
} catch (IllegalArgumentException e) {
// Expected
}
}
+ @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));
+ }
+
}
\ No newline at end of file
diff --git a/src/test/java/freemarker/template/utility/DateUtilsPatternParsingTest.java b/src/test/java/freemarker/template/utility/DateUtilsPatternParsingTest.java
index c11b6ef..df8e740 100644
--- a/src/test/java/freemarker/template/utility/DateUtilsPatternParsingTest.java
+++ b/src/test/java/freemarker/template/utility/DateUtilsPatternParsingTest.java
@@ -40,6 +40,7 @@ import org.hamcrest.Matchers;
import org.junit.Test;
import freemarker.core._JavaVersion;
+import freemarker.core._TemporalUtils;
/**
* Move pattern parsing related tests from {@link DateUtilTest} to here.
@@ -176,14 +177,14 @@ public class DateUtilsPatternParsingTest {
@Test
public void testInvalidPatternExceptions() {
try {
- TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern("y v", SAMPLE_LOCALE);
+ _TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern("y v", SAMPLE_LOCALE);
fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), Matchers.containsString("\"v\""));
}
try {
- TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern("XXXX", SAMPLE_LOCALE);
+ _TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern("XXXX", SAMPLE_LOCALE);
fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), Matchers.containsString("4"));
@@ -195,7 +196,7 @@ public class DateUtilsPatternParsingTest {
assertEquals(
LocalDateTime.of(2021, 12, 23, 1, 2, 3),
LocalDateTime.from(
- TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern("yyyyMMddHHmmss", SAMPLE_LOCALE)
+ _TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern("yyyyMMddHHmmss", SAMPLE_LOCALE)
.parse("20211223010203")));
}
@@ -263,7 +264,7 @@ public class DateUtilsPatternParsingTest {
SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
sdf.setTimeZone(timeZone);
- DateTimeFormatter dtf = TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern(pattern, locale);
+ DateTimeFormatter dtf = _TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern(pattern, locale);
String sdfOutput = sdf.format(date);
String dtfOutput = dtf.format(temporal);
@@ -302,7 +303,7 @@ public class DateUtilsPatternParsingTest {
private LocalDate parseLocalDate(String pattern, String string, Locale locale) {
return LocalDate.from(
- TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern(pattern, locale)
+ _TemporalUtils.dateTimeFormatterFromSimpleDateFormatPattern(pattern, locale)
.parse(string));
}