diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 8cf83380..afe40c4c 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -95,6 +95,13 @@ public class Configurable {
/** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
public static final String NUMBER_FORMAT_KEY = NUMBER_FORMAT_KEY_SNAKE_CASE;
+ /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+ public static final String CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE = "custom_number_formats";
+ /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+ public static final String CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE = "customNumberFormats";
+ /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+ public static final String CUSTOM_NUMBER_FORMATS_KEY = CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE;
+
/** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
public static final String TIME_FORMAT_KEY_SNAKE_CASE = "time_format";
/** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
@@ -109,6 +116,13 @@ public class Configurable {
/** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
public static final String DATE_FORMAT_KEY = DATE_FORMAT_KEY_SNAKE_CASE;
+ /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+ public static final String CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE = "custom_date_formats";
+ /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+ public static final String CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE = "customDateFormats";
+ /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+ public static final String CUSTOM_DATE_FORMATS_KEY = CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE;
+
/** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
public static final String DATETIME_FORMAT_KEY_SNAKE_CASE = "datetime_format";
/** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
@@ -232,6 +246,8 @@ public class Configurable {
AUTO_FLUSH_KEY_SNAKE_CASE,
BOOLEAN_FORMAT_KEY_SNAKE_CASE,
CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
+ CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
+ CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
DATE_FORMAT_KEY_SNAKE_CASE,
DATETIME_FORMAT_KEY_SNAKE_CASE,
LOCALE_KEY_SNAKE_CASE,
@@ -256,6 +272,8 @@ public class Configurable {
AUTO_FLUSH_KEY_CAMEL_CASE,
BOOLEAN_FORMAT_KEY_CAMEL_CASE,
CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
+ CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
+ CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
DATE_FORMAT_KEY_CAMEL_CASE,
DATETIME_FORMAT_KEY_CAMEL_CASE,
LOCALE_KEY_CAMEL_CASE,
@@ -1486,6 +1504,16 @@ public boolean isLogTemplateExceptionsSet() {
*
String value: {@code "true"}, {@code "false"}, also since 2.3.20 {@code 0} or {@code 1} or {@code 2}.
* (Also accepts {@code "yes"}, {@code "no"}, {@code "t"}, {@code "f"}, {@code "y"}, {@code "n"}.)
* Case insensitive.
+ *
+ *
{@code "custom_number_formats"}: See {@link #setCustomNumberFormats(Map)}.
+ *
String value: Interpreted as an object builder expression.
+ *
Example: { "hex": com.example.HexTemplateNumberFormatFactory,
+ * "gps": com.example.GPSTemplateNumberFormatFactory }
+ *
+ *
{@code "custom_date_formats"}: See {@link #setCustomDateFormats(Map)}.
+ *
String value: Interpreted as an object builder expression.
+ *
Example: { "trade": com.example.TradeTemplateDateFormatFactory,
+ * "log": com.example.LogTemplateDateFormatFactory }
*
*
{@code "template_exception_handler"}:
* See {@link #setTemplateExceptionHandler(TemplateExceptionHandler)}.
@@ -1848,12 +1876,26 @@ public void setSetting(String name, String value) throws TemplateException {
setLocale(StringUtil.deduceLocale(value));
} else if (NUMBER_FORMAT_KEY_SNAKE_CASE.equals(name) || NUMBER_FORMAT_KEY_CAMEL_CASE.equals(name)) {
setNumberFormat(value);
+ } else if (CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE.equals(name)
+ || CUSTOM_NUMBER_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", TemplateNumberFormatFactory.class, map.values());
+ setCustomNumberFormats(map);
} else if (TIME_FORMAT_KEY_SNAKE_CASE.equals(name) || TIME_FORMAT_KEY_CAMEL_CASE.equals(name)) {
setTimeFormat(value);
} else if (DATE_FORMAT_KEY_SNAKE_CASE.equals(name) || DATE_FORMAT_KEY_CAMEL_CASE.equals(name)) {
setDateFormat(value);
} else if (DATETIME_FORMAT_KEY_SNAKE_CASE.equals(name) || DATETIME_FORMAT_KEY_CAMEL_CASE.equals(name)) {
setDateTimeFormat(value);
+ } else if (CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE.equals(name)
+ || CUSTOM_DATE_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", TemplateDateFormatFactory.class, map.values());
+ setCustomDateFormats(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/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 8b301ff8..6b657afa 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -17,12 +17,14 @@
package freemarker.core;
import java.io.Writer;
+import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import freemarker.template.Template;
import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.utility.ClassUtil;
/**
@@ -134,5 +136,21 @@ static final public void checkHasNoNestedContent(TemplateDirectiveBody body)
static final public void replaceText(TextBlock textBlock, String text) {
textBlock.replaceText(text);
}
+
+ /**
+ * @throws IllegalArgumentException
+ * if the type of the some of the values isn't as expected
+ */
+ public static void checkSettingValueItemsType(String somethingsSentenceStart, Class> expectedClass,
+ Collection extends Object> values) {
+ if (values == null) return;
+ for (Object value : values) {
+ if (!expectedClass.isInstance(value)) {
+ throw new IllegalArgumentException(somethingsSentenceStart + " must be instances of "
+ + ClassUtil.getShortClassName(expectedClass) + ", but one of them was a(n) "
+ + ClassUtil.getShortClassNameOfObject(value) + ".");
+ }
+ }
+ }
}
diff --git a/src/manual/book.xml b/src/manual/book.xml
index 22f97176..3479faf1 100644
--- a/src/manual/book.xml
+++ b/src/manual/book.xml
@@ -25583,11 +25583,12 @@ TemplateModel x = env.getVariable("x"); // get variable x