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 2021/11/03 23:43:29 UTC
[freemarker] branch FREEMARKER-35 updated: [FREEMARKER-35] Added
custom format support to temporals. Fixed TemplateTemporalFormat.format
return type.
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 8a9ec95 [FREEMARKER-35] Added custom format support to temporals. Fixed TemplateTemporalFormat.format return type.
8a9ec95 is described below
commit 8a9ec953d64cde5ed17ead3b87180f90ee6be898
Author: ddekany <dd...@apache.org>
AuthorDate: Thu Nov 4 00:42:13 2021 +0100
[FREEMARKER-35] Added custom format support to temporals. Fixed TemplateTemporalFormat.format return type.
---
src/main/java/freemarker/core/Configurable.java | 150 ++++++++++++++++++---
.../freemarker/core/TemplateConfiguration.java | 8 ++
.../java/freemarker/core/TemplateFormatUtil.java | 18 ++-
.../freemarker/core/TemplateTemporalFormat.java | 2 +-
.../core/TemplateTemporalFormatFactory.java | 4 +-
.../core/BaseNTemplateNumberFormatFactory.java | 2 +-
.../EpochMillisDivTemplateDateFormatFactory.java | 2 +-
...ochMillisDivTemplateTemporalFormatFactory.java} | 47 ++++---
.../EpochMillisTemplateTemporalFormatFactory.java | 83 ++++++++++++
.../core/HTMLISOTemplateTemporalFormatFactory.java | 86 ++++++++++++
...LocAndTZSensitiveTemplateDateFormatFactory.java | 2 +-
...=> LocAndTZSensitiveTemporalFormatFactory.java} | 48 ++++---
.../freemarker/core/TemplateConfigurationTest.java | 2 +
.../java/freemarker/core/TemporalFormatTest2.java | 76 +++++++++++
14 files changed, 454 insertions(+), 76 deletions(-)
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index bce76e8..a3d2d64 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -33,7 +33,6 @@ 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;
@@ -151,6 +150,13 @@ public class Configurable {
/** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
public static final String DATETIME_FORMAT_KEY = DATETIME_FORMAT_KEY_SNAKE_CASE;
+ /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.32 */
+ public static final String CUSTOM_TEMPORAL_FORMATS_KEY_SNAKE_CASE = "custom_temporal_formats";
+ /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.32 */
+ public static final String CUSTOM_TEMPORAL_FORMATS_KEY_CAMEL_CASE = "customTemporalFormats";
+ /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+ public static final String CUSTOM_TEMPORAL_FORMATS_KEY = CUSTOM_TEMPORAL_FORMATS_KEY_SNAKE_CASE;
+
public static final String INSTANT_FORMAT_KEY_SNAKE_CASE = "instant_format";
public static final String INSTANT_FORMAT_KEY_CAMEL_CASE = "instantFormat";
public static final String INSTANT_FORMAT_KEY = INSTANT_FORMAT_KEY_SNAKE_CASE;
@@ -357,6 +363,7 @@ public class Configurable {
CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
+ CUSTOM_TEMPORAL_FORMATS_KEY_SNAKE_CASE,
DATE_FORMAT_KEY_SNAKE_CASE,
DATETIME_FORMAT_KEY_SNAKE_CASE,
INSTANT_FORMAT_KEY_SNAKE_CASE,
@@ -399,6 +406,7 @@ public class Configurable {
CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
+ CUSTOM_TEMPORAL_FORMATS_KEY_CAMEL_CASE,
DATE_FORMAT_KEY_CAMEL_CASE,
DATETIME_FORMAT_KEY_CAMEL_CASE,
INSTANT_FORMAT_KEY_CAMEL_CASE,
@@ -470,6 +478,7 @@ public class Configurable {
private Boolean logTemplateExceptions;
private Boolean wrapUncheckedExceptions;
private Map<String, ? extends TemplateDateFormatFactory> customDateFormats;
+ private Map<String, ? extends TemplateTemporalFormatFactory> customTemporalFormats;
private Map<String, ? extends TemplateNumberFormatFactory> customNumberFormats;
private LinkedHashMap<String, String> autoImports;
private ArrayList<String> autoIncludes;
@@ -588,6 +597,7 @@ public class Configurable {
customAttributes = new HashMap();
customDateFormats = Collections.emptyMap();
+ customTemporalFormats = Collections.emptyMap();
customNumberFormats = Collections.emptyMap();
lazyImports = false;
@@ -1023,7 +1033,7 @@ public class Configurable {
throw new IllegalArgumentException("Format name must start with letter: " + name);
}
for (int i = 1; i < name.length(); i++) {
- // Note that we deliberately don't allow "_" here.
+ // We don't allow "_" here, as that's used to separate the parameters from the name at some places.
if (!Character.isLetterOrDigit(name.charAt(i))) {
throw new IllegalArgumentException("Format name can only contain letters and digits: " + name);
}
@@ -1041,7 +1051,11 @@ public class Configurable {
}
/**
- * Gets the custom name format registered for the name.
+ * Gets the custom number format factory registered for the name, or {@code null} if no format with the given name
+ * was registered.
+ *
+ * @name The name of the custom format; do not start it with {@code '@'}!
+ * @return The format factory, or {@code null}
*
* @since 2.3.24
*/
@@ -1064,7 +1078,8 @@ public class Configurable {
public boolean hasCustomFormats() {
return customNumberFormats != null && !customNumberFormats.isEmpty()
|| customDateFormats != null && !customDateFormats.isEmpty()
- || getParent() != null && getParent().hasCustomFormats();
+ || customTemporalFormats != null && !customTemporalFormats.isEmpty()
+ || getParent() != null && getParent().hasCustomFormats();
}
/**
@@ -1602,8 +1617,8 @@ public class Configurable {
* 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"}.
+ * <li>Anything that starts with {@code "@"} followed by a letter is interpreted as a custom temporal
+ * format ({@link #setCustomTemporalFormats(Map)}).
* </ul>
*
* @since 2.3.32
@@ -1749,7 +1764,7 @@ public class Configurable {
* that value, otherwise it returns the value from the parent {@link Configurable}. So beware, the returned value
* doesn't reflect the {@link Map} key granularity fallback logic that FreeMarker actually uses for this setting
* (for that, use {@link #getCustomDateFormat(String)}). The returned value isn't a snapshot; it may or may not
- * shows the changes later made to this setting on this {@link Configurable} level (but usually it's well defined if
+ * show the changes later made to this setting on this {@link Configurable} level (but usually it's well-defined if
* until what point settings are possibly modified).
*
* <p>
@@ -1805,7 +1820,11 @@ public class Configurable {
}
/**
- * Gets the custom name format registered for the name.
+ * Gets the custom date format factory registered for the name, or {@code null} if no format with the given name
+ * was registered.
+ *
+ * @name The name of the custom format; do not start it with {@code '@'}!
+ * @return The format factory, or {@code null}
*
* @since 2.3.24
*/
@@ -1819,15 +1838,83 @@ public class Configurable {
}
return parent != null ? parent.getCustomDateFormat(name) : null;
}
+
+ /**
+ * Getter pair of {@link #setCustomTemporalFormats(Map)}; do not modify the returned {@link Map}! To be consistent with
+ * other setting getters, if this setting was set directly on this {@link Configurable} object, this simply returns
+ * that value, otherwise it returns the value from the parent {@link Configurable}. So beware, the returned value
+ * doesn't reflect the {@link Map} key granularity fallback logic that FreeMarker actually uses for this setting
+ * (for that, use {@link #getCustomTemporalFormat(String)}). The returned value isn't a snapshot; it may or may not
+ * show the changes later made to this setting on this {@link Configurable} level (but usually it's well-defined if
+ * until what point settings are possibly modified).
+ *
+ * <p>
+ * The return value is never {@code null}; called on the {@link Configuration} (top) level, it defaults to an empty
+ * {@link Map}.
+ *
+ * @see #getCustomTemporalFormatsWithoutFallback()
+ *
+ * @since 2.3.32
+ */
+ public Map<String, ? extends TemplateTemporalFormatFactory> getCustomTemporalFormats() {
+ return customTemporalFormats == null ? parent.getCustomTemporalFormats() : customTemporalFormats;
+ }
/**
- * Gets the custom name format registered for the name.
+ * Like {@link #getCustomTemporalFormats()}, but doesn't fall back to the parent {@link Configurable}, nor does it
+ * provide a non-{@code null} default when called as the method of a {@link Configuration}.
*
- * @since 2.3.31
+ * @since 2.3.32
+ */
+ public Map<String, ? extends TemplateTemporalFormatFactory> getCustomTemporalFormatsWithoutFallback() {
+ return customTemporalFormats;
+ }
+
+ /**
+ * Associates names with formatter factories, which then can be referred by the various temporal format settings
+ * (like {@link #setLocalDateTimeFormat(String) local_date_time_format},
+ * {@link #setLocalDateFormat(String) local_date_format}, {@link #setLocalTimeFormat(String) local_time_format},
+ * and so on) a value starting with <code>@<i>name</i></code>.
+ *
+ * @param customTemporalFormats
+ * Can't be {@code null}. The name must start with an UNICODE letter, and can only contain UNICODE
+ * letters and digits.
+ *
+ * @since 2.3.32
+ */
+ public void setCustomTemporalFormats(Map<String, ? extends TemplateTemporalFormatFactory> customTemporalFormats) {
+ NullArgumentException.check("customTemporalFormats", customTemporalFormats);
+ validateFormatNames(customTemporalFormats.keySet());
+ this.customTemporalFormats = customTemporalFormats;
+ }
+
+ /**
+ * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+ *
+ * @since 2.3.32
+ */
+ public boolean isCustomTemporalFormatsSet() {
+ return this.customTemporalFormats != null;
+ }
+
+ /**
+ * Gets the custom temporal format factory registered for the name, or {@code null} if no format with the given name
+ * was registered.
+ *
+ * @name The name of the custom format; do not start it with {@code '@'}!
+ * @return The format factory or, {@code null}
+ *
+ * @since 2.3.32
*/
public TemplateTemporalFormatFactory getCustomTemporalFormat(String name) {
- // TODO [FREEMARKER-35]
- return null;
+ TemplateTemporalFormatFactory r;
+ if (customTemporalFormats != null) {
+ r = customTemporalFormats.get(name);
+ if (r != null) {
+ return r;
+ }
+ }
+ return parent != null ? parent.getCustomTemporalFormat(name) : null;
}
/**
@@ -2677,7 +2764,12 @@ public class Configurable {
* <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>.
* <br>Example: <code>{ "trade": com.example.TradeTemplateDateFormatFactory,
* "log": com.example.LogTemplateDateFormatFactory }</code>
- *
+ *
+ * <li><p>{@code "custom_temporal_formats"}: See {@link #setCustomTemporalFormats(Map)}.
+ * <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>.
+ * <br>Example: <code>{ "trade": com.example.TradeTemporalFormatFactory,
+ * "log": com.example.LogTemporalFormatFactory }</code>
+ *
* <li><p>{@code "template_exception_handler"}:
* See {@link #setTemplateExceptionHandler(TemplateExceptionHandler)}.
* <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
@@ -2705,7 +2797,7 @@ public class Configurable {
* If the value does not contain dot,
* then it must be one of these special values (case insensitive):
* {@code "bigdecimal"}, {@code "conservative"}.
- *
+ *
* <li><p>{@code "object_wrapper"}:
* See {@link #setObjectWrapper(ObjectWrapper)}.
* <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
@@ -2724,12 +2816,27 @@ public class Configurable {
* {@code "jython"} (means {@link freemarker.ext.jython.JythonWrapper#DEFAULT_WRAPPER})
*
* <li><p>{@code "number_format"}: See {@link #setNumberFormat(String)}.
- *
+ *
* <li><p>{@code "boolean_format"}: See {@link #setBooleanFormat(String)} .
- *
+ *
* <li><p>{@code "date_format", "time_format", "datetime_format"}:
- * See {@link #setDateFormat(String)}, {@link #setTimeFormat(String)}, {@link #setDateTimeFormat(String)}.
- *
+ * See {@link #setDateFormat(String)}, {@link #setTimeFormat(String)}, {@link #setDateTimeFormat(String)}.
+ *
+ * <li><p>{@code "local_date_format", "local_time_format", "local_datetime_format"}:
+ * See {@link #setLocalDateFormat(String)}, {@link #setLocalTimeFormat(String)}, {@link #setLocalDateTimeFormat(String)}.
+ *
+ * <li><p>{@code "offset_time_format", "offset_datetime_format"}:
+ * See {@link #setOffsetTimeFormat(String)}, {@link #setOffsetDateTimeFormat(String)}.
+ *
+ * <li><p>{@code "zoned_date_time_format"}:
+ * See {@link #setZonedDateTimeFormat(String)}.
+ *
+ * <li><p>{@code "year_format"}:
+ * See {@link #setYearFormat(String)}.
+ *
+ * <li><p>{@code "year_month_format"}:
+ * See {@link #setYearMonthFormat(String)}.
+ *
* <li><p>{@code "time_zone"}:
* See {@link #setTimeZone(TimeZone)}.
* <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, since 2.3.21
@@ -3154,6 +3261,13 @@ public class Configurable {
_CoreAPI.checkSettingValueItemsType("Map keys", String.class, map.keySet());
_CoreAPI.checkSettingValueItemsType("Map values", TemplateDateFormatFactory.class, map.values());
setCustomDateFormats(map);
+ } else if (CUSTOM_TEMPORAL_FORMATS_KEY_SNAKE_CASE.equals(name)
+ || CUSTOM_TEMPORAL_FORMATS_KEY_CAMEL_CASE.equals(name)) {
+ Map map = (Map) _ObjectBuilderSettingEvaluator.eval(
+ value, Map.class, false, _SettingEvaluationEnvironment.getCurrent());
+ _CoreAPI.checkSettingValueItemsType("Map keys", String.class, map.keySet());
+ _CoreAPI.checkSettingValueItemsType("Map values", TemplateTemporalFormatFactory.class, map.values());
+ setCustomTemporalFormats(map);
} else if (TIME_ZONE_KEY_SNAKE_CASE.equals(name) || TIME_ZONE_KEY_CAMEL_CASE.equals(name)) {
setTimeZone(parseTimeZoneSettingValue(value));
} else if (SQL_DATE_AND_TIME_TIME_ZONE_KEY_SNAKE_CASE.equals(name)
diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java
index cd8410f..7e41d7f 100644
--- a/src/main/java/freemarker/core/TemplateConfiguration.java
+++ b/src/main/java/freemarker/core/TemplateConfiguration.java
@@ -181,6 +181,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
if (tc.isCustomDateFormatsSet()) {
setCustomDateFormats(mergeMaps(getCustomDateFormats(), tc.getCustomDateFormats(), false));
}
+ if (tc.isCustomTemporalFormatsSet()) {
+ setCustomTemporalFormats(mergeMaps(getCustomTemporalFormats(), tc.getCustomTemporalFormats(), false));
+ }
if (tc.isCustomNumberFormatsSet()) {
setCustomNumberFormats(mergeMaps(getCustomNumberFormats(), tc.getCustomNumberFormats(), false));
}
@@ -349,6 +352,10 @@ public final class TemplateConfiguration extends Configurable implements ParserC
template.setCustomDateFormats(
mergeMaps(getCustomDateFormats(), template.getCustomDateFormatsWithoutFallback(), false));
}
+ if (isCustomTemporalFormatsSet()) {
+ template.setCustomTemporalFormats(
+ mergeMaps(getCustomTemporalFormats(), template.getCustomTemporalFormatsWithoutFallback(), false));
+ }
if (isCustomNumberFormatsSet()) {
template.setCustomNumberFormats(
mergeMaps(getCustomNumberFormats(), template.getCustomNumberFormatsWithoutFallback(), false));
@@ -730,6 +737,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
|| isBooleanFormatSet()
|| isClassicCompatibleSet()
|| isCustomDateFormatsSet()
+ || isCustomTemporalFormatsSet()
|| isCustomNumberFormatsSet()
|| isDateFormatSet()
|| isDateTimeFormatSet()
diff --git a/src/main/java/freemarker/core/TemplateFormatUtil.java b/src/main/java/freemarker/core/TemplateFormatUtil.java
index 76acfb0..4dd8ab9 100644
--- a/src/main/java/freemarker/core/TemplateFormatUtil.java
+++ b/src/main/java/freemarker/core/TemplateFormatUtil.java
@@ -65,7 +65,7 @@ public final class TemplateFormatUtil {
* Utility method to extract the {@link Date} from an {@link TemplateDateModel}, and throw
* {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateDateModel}
* that store {@code null} are in principle not allowed, and so are considered to be bugs in the
- * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+ * {@link ObjectWrapper} or {@link TemplateDateModel} implementation.
*/
public static Date getNonNullDate(TemplateDateModel dateModel) throws TemplateModelException {
Date date = dateModel.getAsDate();
@@ -75,12 +75,20 @@ public final class TemplateFormatUtil {
return date;
}
+ /**
+ * Utility method to extract the {@link Temporal} from an {@link TemplateTemporalModel}, and throw
+ * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateTemporalModel}
+ * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+ * {@link ObjectWrapper} or {@link TemplateTemporalModel} implementation.
+ *
+ * @since 2.3.32
+ */
public static Temporal getNonNullTemporal(TemplateTemporalModel temporalModel) throws TemplateModelException {
- Temporal date = temporalModel.getAsTemporal();
- if (date == null) {
- throw EvalUtil.newModelHasStoredNullException(Date.class, temporalModel, null);
+ Temporal temporal = temporalModel.getAsTemporal();
+ if (temporal == null) {
+ throw EvalUtil.newModelHasStoredNullException(Temporal.class, temporalModel, null);
}
- return date;
+ return temporal;
}
}
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormat.java b/src/main/java/freemarker/core/TemplateTemporalFormat.java
index 7b43265..bf0602f 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormat.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormat.java
@@ -50,7 +50,7 @@ public abstract class TemplateTemporalFormat extends TemplateValueFormat {
*
* @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
*/
- public String format(TemplateTemporalModel temporalModel) throws TemplateValueFormatException, TemplateModelException {
+ public Object format(TemplateTemporalModel temporalModel) throws TemplateValueFormatException, TemplateModelException {
return formatToPlainText(temporalModel);
}
diff --git a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
index 885ea7b..5ab995b 100644
--- a/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
+++ b/src/main/java/freemarker/core/TemplateTemporalFormatFactory.java
@@ -27,8 +27,8 @@ import freemarker.template.Configuration;
/**
* Factory for a certain kind of {@link Temporal} formatting ({@link TemplateTemporalFormat}). Usually a singleton
* (one-per-VM, or one-per-{@link Configuration}), and so must be thread-safe.
- *
- * TODO [FREEMARKER-35] @see Configurable#setCustomTemporalFormats(java.util.Map)
+ *
+ * @see Configurable#setCustomTemporalFormats(java.util.Map)
*
* @since 2.3.32
*/
diff --git a/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
index acc81ef..c217931 100644
--- a/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
+++ b/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
@@ -70,7 +70,7 @@ public class BaseNTemplateNumberFormatFactory extends TemplateNumberFormatFactor
"A format parameter is required to specify the numerical system base.");
}
throw new InvalidFormatParametersException(
- "The format paramter must be an integer, but was (shown quoted): "
+ "The format parameter must be an integer, but was (shown quoted): "
+ StringUtil.jQuote(params));
}
if (base < 2) {
diff --git a/src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java b/src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java
index 00f56f3..23c0199 100644
--- a/src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java
+++ b/src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java
@@ -46,7 +46,7 @@ public class EpochMillisDivTemplateDateFormatFactory extends TemplateDateFormatF
"A format parameter is required, which specifies the divisor.");
}
throw new InvalidFormatParametersException(
- "The format paramter must be an integer, but was (shown quoted): " + StringUtil.jQuote(params));
+ "The format parameter must be an integer, but was (shown quoted): " + StringUtil.jQuote(params));
}
return new EpochMillisDivTemplateDateFormat(divisor);
}
diff --git a/src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java b/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
similarity index 57%
copy from src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java
copy to src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
index 00f56f3..ff5ba63 100644
--- a/src/test/java/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java
+++ b/src/test/java/freemarker/core/EpochMillisDivTemplateTemporalFormatFactory.java
@@ -18,25 +18,30 @@
*/
package freemarker.core;
-import java.util.Date;
+import java.time.Instant;
+import java.time.temporal.Temporal;
import java.util.Locale;
import java.util.TimeZone;
-import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
import freemarker.template.utility.StringUtil;
-public class EpochMillisDivTemplateDateFormatFactory extends TemplateDateFormatFactory {
+public class EpochMillisDivTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
- public static final EpochMillisDivTemplateDateFormatFactory INSTANCE = new EpochMillisDivTemplateDateFormatFactory();
+ public static final EpochMillisDivTemplateTemporalFormatFactory
+ INSTANCE = new EpochMillisDivTemplateTemporalFormatFactory();
- private EpochMillisDivTemplateDateFormatFactory() {
+ private EpochMillisDivTemplateTemporalFormatFactory() {
// Defined to decrease visibility
}
@Override
- public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
- Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+ public TemplateTemporalFormat get(
+ String params,
+ Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone,
+ Environment env)
+ throws TemplateValueFormatException {
int divisor;
try {
divisor = Integer.parseInt(params);
@@ -46,23 +51,30 @@ public class EpochMillisDivTemplateDateFormatFactory extends TemplateDateFormatF
"A format parameter is required, which specifies the divisor.");
}
throw new InvalidFormatParametersException(
- "The format paramter must be an integer, but was (shown quoted): " + StringUtil.jQuote(params));
+ "The format parameter must be an integer, but was (shown quoted): " + StringUtil.jQuote(params));
}
- return new EpochMillisDivTemplateDateFormat(divisor);
+ return new EpochMillisDivTemplateTemporalFormat(divisor);
}
- private static class EpochMillisDivTemplateDateFormat extends TemplateDateFormat {
+ private static class EpochMillisDivTemplateTemporalFormat extends TemplateTemporalFormat {
private final int divisor;
- private EpochMillisDivTemplateDateFormat(int divisor) {
+ private EpochMillisDivTemplateTemporalFormat(int divisor) {
this.divisor = divisor;
}
@Override
- public String formatToPlainText(TemplateDateModel dateModel)
+ public String formatToPlainText(TemplateTemporalModel temporalModel)
throws UnformattableValueException, TemplateModelException {
- return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime() / divisor);
+ Temporal temporal = TemplateFormatUtil.getNonNullTemporal(temporalModel);
+ long epochMillis;
+ try {
+ epochMillis = temporal.query(Instant::from).toEpochMilli();
+ } catch (Exception e) {
+ throw new UnformattableValueException("Can't extract epoch millis from " + temporal.getClass().getName() + " object.");
+ }
+ return String.valueOf(epochMillis / divisor);
}
@Override
@@ -76,15 +88,6 @@ public class EpochMillisDivTemplateDateFormatFactory extends TemplateDateFormatF
}
@Override
- public Date parse(String s, int dateType) throws UnparsableValueException {
- try {
- return new Date(Long.parseLong(s));
- } catch (NumberFormatException e) {
- throw new UnparsableValueException("Malformed long");
- }
- }
-
- @Override
public String getDescription() {
return "millis since the epoch";
}
diff --git a/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
new file mode 100644
index 0000000..a51b6ad
--- /dev/null
+++ b/src/test/java/freemarker/core/EpochMillisTemplateTemporalFormatFactory.java
@@ -0,0 +1,83 @@
+/*
+ * 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.Instant;
+import java.time.temporal.Temporal;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
+
+public class EpochMillisTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
+
+ public static final EpochMillisTemplateTemporalFormatFactory INSTANCE
+ = new EpochMillisTemplateTemporalFormatFactory();
+
+ private EpochMillisTemplateTemporalFormatFactory() {
+ // Defined to decrease visibility
+ }
+
+ @Override
+ public TemplateTemporalFormat get(
+ String params, Class<? extends Temporal> temporalClass,
+ Locale locale, TimeZone timeZone, Environment env)
+ throws InvalidFormatParametersException {
+ TemplateFormatUtil.checkHasNoParameters(params);
+ return EpochMillisTemplateTemporalFormat.INSTANCE;
+ }
+
+ private static class EpochMillisTemplateTemporalFormat extends TemplateTemporalFormat {
+
+ private static final EpochMillisTemplateTemporalFormat INSTANCE = new EpochMillisTemplateTemporalFormat();
+
+ private EpochMillisTemplateTemporalFormat() { }
+
+ @Override
+ public String formatToPlainText(TemplateTemporalModel temporalModel)
+ throws UnformattableValueException, TemplateModelException {
+ Temporal temporal = TemplateFormatUtil.getNonNullTemporal(temporalModel);
+ long epochMillis;
+ try {
+ epochMillis = temporal.query(Instant::from).toEpochMilli();
+ } catch (Exception e) {
+ throw new UnformattableValueException("Can't extract epoch millis from " + temporal.getClass().getName() + " object.");
+ }
+ return String.valueOf(epochMillis);
+ }
+
+ @Override
+ public boolean isLocaleBound() {
+ return false;
+ }
+
+ @Override
+ public boolean isTimeZoneBound() {
+ return false;
+ }
+
+ @Override
+ public String getDescription() {
+ return "millis since the epoch";
+ }
+
+ }
+
+}
diff --git a/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java b/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
new file mode 100644
index 0000000..5226fd2
--- /dev/null
+++ b/src/test/java/freemarker/core/HTMLISOTemplateTemporalFormatFactory.java
@@ -0,0 +1,86 @@
+/*
+ * 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.temporal.Temporal;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
+
+public class HTMLISOTemplateTemporalFormatFactory extends TemplateTemporalFormatFactory {
+
+ public static final HTMLISOTemplateTemporalFormatFactory INSTANCE = new HTMLISOTemplateTemporalFormatFactory();
+
+ private HTMLISOTemplateTemporalFormatFactory() {
+ // Defined to decrease visibility
+ }
+
+ @Override
+ public TemplateTemporalFormat get(
+ String params,
+ Class<? extends Temporal> temporalClass, Locale locale, TimeZone timeZone,
+ Environment env)
+ throws TemplateValueFormatException {
+ TemplateFormatUtil.checkHasNoParameters(params);
+ TemplateTemporalFormat isoFormat = ISOTemplateTemporalFormatFactory.INSTANCE
+ .get("", temporalClass, locale, timeZone, env);
+ return new HTMLISOTemplateTemporalFormat(isoFormat);
+ }
+
+ private static class HTMLISOTemplateTemporalFormat extends TemplateTemporalFormat {
+
+ private final TemplateTemporalFormat isoFormat;
+
+ private HTMLISOTemplateTemporalFormat(TemplateTemporalFormat isoFormat) {
+ this.isoFormat = isoFormat;
+ }
+
+ @Override
+ public String formatToPlainText(TemplateTemporalModel temporalModel)
+ throws TemplateValueFormatException, TemplateModelException {
+ return isoFormat.formatToPlainText(temporalModel);
+ }
+
+ @Override
+ public boolean isLocaleBound() {
+ return false;
+ }
+
+ @Override
+ public boolean isTimeZoneBound() {
+ return false;
+ }
+
+ @Override
+ public Object format(TemplateTemporalModel temporalModel) throws TemplateValueFormatException, TemplateModelException {
+ return HTMLOutputFormat.INSTANCE.fromMarkup(
+ formatToPlainText(temporalModel).replace("T", "<span class='T'>T</span>"));
+ }
+
+ @Override
+ public String getDescription() {
+ return "ISO UTC HTML";
+ }
+
+ }
+
+}
+
\ No newline at end of file
diff --git a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java b/src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java
index ea2b6f7..51cbe94 100644
--- a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java
+++ b/src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java
@@ -53,7 +53,7 @@ public class LocAndTZSensitiveTemplateDateFormatFactory extends TemplateDateForm
@Override
public String formatToPlainText(TemplateDateModel dateModel)
throws UnformattableValueException, TemplateModelException {
- return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime() + "@" + locale + ":" + timeZone.getID());
+ return TemplateFormatUtil.getNonNullDate(dateModel).getTime() + "@" + locale + ":" + timeZone.getID();
}
@Override
diff --git a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java b/src/test/java/freemarker/core/LocAndTZSensitiveTemporalFormatFactory.java
similarity index 53%
copy from src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java
copy to src/test/java/freemarker/core/LocAndTZSensitiveTemporalFormatFactory.java
index ea2b6f7..4b7b46a 100644
--- a/src/test/java/freemarker/core/LocAndTZSensitiveTemplateDateFormatFactory.java
+++ b/src/test/java/freemarker/core/LocAndTZSensitiveTemporalFormatFactory.java
@@ -18,42 +18,53 @@
*/
package freemarker.core;
-import java.util.Date;
+import java.time.Instant;
+import java.time.temporal.Temporal;
import java.util.Locale;
import java.util.TimeZone;
-import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTemporalModel;
-public class LocAndTZSensitiveTemplateDateFormatFactory extends TemplateDateFormatFactory {
+public class LocAndTZSensitiveTemporalFormatFactory extends TemplateTemporalFormatFactory {
- public static final LocAndTZSensitiveTemplateDateFormatFactory INSTANCE = new LocAndTZSensitiveTemplateDateFormatFactory();
+ public static final LocAndTZSensitiveTemporalFormatFactory INSTANCE = new LocAndTZSensitiveTemporalFormatFactory();
- private LocAndTZSensitiveTemplateDateFormatFactory() {
+ private LocAndTZSensitiveTemporalFormatFactory() {
// Defined to decrease visibility
}
@Override
- public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
- Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+ public TemplateTemporalFormat get(
+ String params, Class<? extends Temporal> temporalClass,
+ Locale locale, TimeZone timeZone,
+ Environment env)
+ throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
TemplateFormatUtil.checkHasNoParameters(params);
- return new LocAndTZSensitiveTemplateDateFormat(locale, timeZone);
+ return new LocAndTZSensitiveTemplateTemporalFormat(locale, timeZone);
}
- private static class LocAndTZSensitiveTemplateDateFormat extends TemplateDateFormat {
+ private static class LocAndTZSensitiveTemplateTemporalFormat extends TemplateTemporalFormat {
private final Locale locale;
private final TimeZone timeZone;
- public LocAndTZSensitiveTemplateDateFormat(Locale locale, TimeZone timeZone) {
+ public LocAndTZSensitiveTemplateTemporalFormat(Locale locale, TimeZone timeZone) {
this.locale = locale;
this.timeZone = timeZone;
}
@Override
- public String formatToPlainText(TemplateDateModel dateModel)
+ public String formatToPlainText(TemplateTemporalModel temporalModel)
throws UnformattableValueException, TemplateModelException {
- return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime() + "@" + locale + ":" + timeZone.getID());
+ Temporal temporal = TemplateFormatUtil.getNonNullTemporal(temporalModel);
+ long epochMillis;
+ try {
+ epochMillis = temporal.query(Instant::from).toEpochMilli();
+ } catch (Exception e) {
+ throw new UnformattableValueException("Can't extract epoch millis from " + temporal.getClass().getName() + " object.");
+ }
+ return epochMillis + "@" + locale + ":" + timeZone.getID();
}
@Override
@@ -67,19 +78,6 @@ public class LocAndTZSensitiveTemplateDateFormatFactory extends TemplateDateForm
}
@Override
- public Date parse(String s, int dateType) throws UnparsableValueException {
- try {
- int atIdx = s.indexOf("@");
- if (atIdx == -1) {
- throw new UnparsableValueException("Missing @");
- }
- return new Date(Long.parseLong(s.substring(0, atIdx)));
- } catch (NumberFormatException e) {
- throw new UnparsableValueException("Malformed long");
- }
- }
-
- @Override
public String getDescription() {
return "millis since the epoch";
}
diff --git a/src/test/java/freemarker/core/TemplateConfigurationTest.java b/src/test/java/freemarker/core/TemplateConfigurationTest.java
index 57ed326..c5aa08b 100644
--- a/src/test/java/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/freemarker/core/TemplateConfigurationTest.java
@@ -190,6 +190,8 @@ public class TemplateConfigurationTest {
ImmutableMap.of("dummy", HexTemplateNumberFormatFactory.INSTANCE));
SETTING_ASSIGNMENTS.put("customDateFormats",
ImmutableMap.of("dummy", EpochMillisTemplateDateFormatFactory.INSTANCE));
+ SETTING_ASSIGNMENTS.put("customTemporalFormats",
+ ImmutableMap.of("dummy", EpochMillisTemplateTemporalFormatFactory.INSTANCE));
SETTING_ASSIGNMENTS.put("truncateBuiltinAlgorithm", DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE);
// Parser-only settings:
diff --git a/src/test/java/freemarker/core/TemporalFormatTest2.java b/src/test/java/freemarker/core/TemporalFormatTest2.java
new file mode 100644
index 0000000..215eb7c
--- /dev/null
+++ b/src/test/java/freemarker/core/TemporalFormatTest2.java
@@ -0,0 +1,76 @@
+/*
+ * 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 java.time.ZoneOffset;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.Configuration;
+import freemarker.test.TemplateTest;
+
+/**
+ * Like {@link TemporalFormatTest}, but this one contains the tests that utilize {@link TemplateTest}.
+ */
+public class TemporalFormatTest2 extends TemplateTest {
+
+ @Before
+ public void setup() {
+ Configuration cfg = getConfiguration();
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_32);
+ cfg.setLocale(Locale.US);
+ cfg.setTimeZone(TimeZone.getTimeZone("GMT+01:00"));
+
+ cfg.setCustomTemporalFormats(ImmutableMap.of(
+ "epoch", EpochMillisTemplateTemporalFormatFactory.INSTANCE,
+ "loc", LocAndTZSensitiveTemporalFormatFactory.INSTANCE,
+ "div", EpochMillisDivTemplateTemporalFormatFactory.INSTANCE,
+ "htmlIso", HTMLISOTemplateTemporalFormatFactory.INSTANCE));
+ }
+
+ @Test
+ public void testCustomFormat() throws Exception {
+ addToDataModel("d", OffsetDateTime.of(
+ 1970, 1, 2,
+ 10, 17, 36, 789000000,
+ ZoneOffset.ofHours(0)));
+ assertOutput(
+ "${d?string.@epoch} ${d?string.@epoch} <#setting locale='de_DE'>${d?string.@epoch}",
+ "123456789 123456789 123456789");
+
+ getConfiguration().setOffsetDateTimeFormat("@epoch");
+ assertOutput(
+ "${d} ${d?string} <#setting locale='de_DE'>${d}",
+ "123456789 123456789 123456789");
+
+ getConfiguration().setOffsetDateTimeFormat("@htmlIso");
+ assertOutput(
+ "${d} ${d?string} <#setting locale='de_DE'>${d}",
+ "1970-01-02<span class='T'>T</span>10:17:36.789Z "
+ + "1970-01-02T10:17:36.789Z "
+ + "1970-01-02<span class='T'>T</span>10:17:36.789Z");
+ }
+}