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 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 - Added - Configurable.setCustomNumberFormats(Map<String, + Added custom_number_formats and + custom_date_formats settings + (Configurable.setCustomNumberFormats(Map<String, TemplateNumberFormatFactory>) and Configurable.setCustomDateFormats(Map<String, - TemplateDateFormatFactory>) with which you can + TemplateDateFormatFactory>)) with which you can register your own formats. If you set incompatible_improvements to @@ -25608,7 +25609,14 @@ TemplateModel x = env.getVariable("x"); // get variable x format strings is reserved for this purpose with incompatible_improvements to 2.3.24, and need to be escaped as @@ if it has to be - there literally. + there literally. Note that the + custom_number_formats and + custom_date_formats settings can be set + per-template (via the new + template_configurers settings) or + per-Environment too, thus + @foo can mean something different in + different templates. diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java index de1de827..4b3132a6 100644 --- a/src/test/java/freemarker/template/ConfigurationTest.java +++ b/src/test/java/freemarker/template/ConfigurationTest.java @@ -48,6 +48,7 @@ import freemarker.cache.TemplateLookupResult; import freemarker.cache.TemplateLookupStrategy; import freemarker.cache.TemplateNameFormat; +import freemarker.core.BaseNTemplateNumberFormatFactory; import freemarker.core.CombinedMarkupOutputFormat; import freemarker.core.Configurable; import freemarker.core.Configurable.SettingValueAssignmentException; @@ -56,6 +57,7 @@ import freemarker.core.CustomHTMLOutputFormat; import freemarker.core.DummyOutputFormat; import freemarker.core.Environment; +import freemarker.core.EpochMillisDivTemplateDateFormatFactory; import freemarker.core.EpochMillisTemplateDateFormatFactory; import freemarker.core.HTMLOutputFormat; import freemarker.core.HexTemplateNumberFormatFactory; @@ -63,6 +65,8 @@ import freemarker.core.OutputFormat; import freemarker.core.ParseException; import freemarker.core.RTFOutputFormat; +import freemarker.core.TemplateDateFormatFactory; +import freemarker.core.TemplateNumberFormatFactory; import freemarker.core.UndefinedOutputFormat; import freemarker.core.UnregisteredOutputFormatException; import freemarker.core.XMLOutputFormat; @@ -1298,7 +1302,7 @@ public void testTemplateUpdateDelay() throws IOException, TemplateException { } @Test - public void testSetCustomNumberFormat() { + public void testSetCustomNumberFormat() throws Exception { Configuration cfg = new Configuration(Configuration.VERSION_2_3_0); try { @@ -1337,10 +1341,40 @@ public void testSetCustomNumberFormat() { } catch (IllegalArgumentException e) { assertThat(e.getMessage(), containsString("@wrong")); } + + cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE, + "{ 'base': " + BaseNTemplateNumberFormatFactory.class.getName() + "() }"); + assertEquals( + Collections.singletonMap("base", BaseNTemplateNumberFormatFactory.INSTANCE), + cfg.getCustomNumberFormats()); + + cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE, + "{ " + + "'base': " + BaseNTemplateNumberFormatFactory.class.getName() + "(), " + + "'hex': " + HexTemplateNumberFormatFactory.class.getName() + "()" + + " }"); + assertEquals( + ImmutableMap.of( + "base", BaseNTemplateNumberFormatFactory.INSTANCE, + "hex", HexTemplateNumberFormatFactory.INSTANCE), + cfg.getCustomNumberFormats()); + + cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY, "{}"); + assertEquals(Collections.emptyMap(), cfg.getCustomNumberFormats()); + + try { + cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE, + "{ 'x': " + EpochMillisTemplateDateFormatFactory.class.getName() + "() }"); + fail(); + } catch (TemplateException e) { + assertThat(e.getCause().getMessage(), allOf( + containsString(EpochMillisTemplateDateFormatFactory.class.getName()), + containsString(TemplateNumberFormatFactory.class.getName()))); + } } @Test - public void testSetCustomDateFormat() { + public void testSetCustomDateFormat() throws Exception { Configuration cfg = new Configuration(Configuration.VERSION_2_3_0); try { @@ -1379,6 +1413,36 @@ public void testSetCustomDateFormat() { } catch (IllegalArgumentException e) { assertThat(e.getMessage(), containsString("@wrong")); } + + cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE, + "{ 'epoch': " + EpochMillisTemplateDateFormatFactory.class.getName() + "() }"); + assertEquals( + Collections.singletonMap("epoch", EpochMillisTemplateDateFormatFactory.INSTANCE), + cfg.getCustomDateFormats()); + + cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE, + "{ " + + "'epoch': " + EpochMillisTemplateDateFormatFactory.class.getName() + "(), " + + "'epochDiv': " + EpochMillisDivTemplateDateFormatFactory.class.getName() + "()" + + " }"); + assertEquals( + ImmutableMap.of( + "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE, + "epochDiv", EpochMillisDivTemplateDateFormatFactory.INSTANCE), + cfg.getCustomDateFormats()); + + cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY, "{}"); + assertEquals(Collections.emptyMap(), cfg.getCustomDateFormats()); + + try { + cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE, + "{ 'x': " + HexTemplateNumberFormatFactory.class.getName() + "() }"); + fail(); + } catch (TemplateException e) { + assertThat(e.getCause().getMessage(), allOf( + containsString(HexTemplateNumberFormatFactory.class.getName()), + containsString(TemplateDateFormatFactory.class.getName()))); + } } public void testNamingConventionSetSetting() throws TemplateException {