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
commit d43c096cee175c9b252bd0fbc2bfe40f93063690 Author: ddekany <[email protected]> AuthorDate: Mon Nov 8 01:37:49 2021 +0100 [FREEMARKER-35] Added temporalSupport property to DefaultObjectWrapper, and BeansWrapper, which only defaults to true starting from incompatible_improvements 2.3.32. --- .../java/freemarker/ext/beans/BeansModelCache.java | 5 ++ .../java/freemarker/ext/beans/BeansWrapper.java | 54 ++++++++++++++++--- .../ext/beans/BeansWrapperConfiguration.java | 15 ++++++ .../java/freemarker/ext/beans/TemporalModel.java | 61 ++++++++++++++++++++++ .../java/freemarker/template/Configuration.java | 9 ++++ .../freemarker/template/DefaultObjectWrapper.java | 7 ++- .../freemarker/template/utility/ClassUtil.java | 3 ++ .../freemarker/core/TemporalErrorMessagesTest.java | 7 ++- .../freemarker/ext/beans/BeansWrapperMiscTest.java | 31 ++++++++++- .../template/DefaultObjectWrapperTest.java | 16 +++++- .../freemarker/test/templatesuite/testcases.xml | 4 +- 11 files changed, 196 insertions(+), 16 deletions(-) diff --git a/src/main/java/freemarker/ext/beans/BeansModelCache.java b/src/main/java/freemarker/ext/beans/BeansModelCache.java index 99374b6..ef214f0 100644 --- a/src/main/java/freemarker/ext/beans/BeansModelCache.java +++ b/src/main/java/freemarker/ext/beans/BeansModelCache.java @@ -71,4 +71,9 @@ public class BeansModelCache extends ModelCache { return factory.create(object, wrapper); } + + void clearClassToFactoryMap() { + classToFactory.clear(); + } + } diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java index 7968aac..d654a63 100644 --- a/src/main/java/freemarker/ext/beans/BeansWrapper.java +++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java @@ -45,7 +45,6 @@ import freemarker.core.BugException; import freemarker.core._DelayedFTLTypeDescription; import freemarker.core._DelayedShortClassName; import freemarker.core._TemplateModelException; -import freemarker.ext.util.ModelCache; import freemarker.ext.util.ModelFactory; import freemarker.ext.util.WrapperTemplateModel; import freemarker.log.Logger; @@ -170,7 +169,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { * Object to wrapped object cache; not used by default. * This object only belongs to a single {@link BeansWrapper}. */ - private final ModelCache modelCache; + private final BeansModelCache modelCache; private final BooleanModel falseModel; private final BooleanModel trueModel; @@ -189,7 +188,8 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { private boolean simpleMapWrapper; // initialized from the BeansWrapperConfiguration private boolean strict; // initialized from the BeansWrapperConfiguration private boolean preferIndexedReadMethod; // initialized from the BeansWrapperConfiguration - + private boolean temporalSupport; // initialized from the BeansWrapperConfiguration + private final Version incompatibleImprovements; /** @@ -258,6 +258,12 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { * The default of the {@link #setPreferIndexedReadMethod(boolean) preferIndexedReadMethod} setting changes * from {@code true} to {@code false}. * </li> + * <li> + * <p>2.3.32 (or higher): + * The default of {@link #setTemporalSupport(boolean) temporalSupport} changes to {@code true}, + * and thus {@link Temporal}-s (the Java 8 date/time classes) are wrapped into {@link TemplateTemporalModel}. + * Before that, {@link Temporal}-s were treated as generic Java objects. + * {@link TemplateTemporalModel}) was added in FreeMarker 2.3.32. * </ul> * * <p>Note that the version will be normalized to the lowest version where the same incompatible @@ -346,6 +352,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { simpleMapWrapper = bwConf.isSimpleMapWrapper(); preferIndexedReadMethod = bwConf.getPreferIndexedReadMethod(); + temporalSupport = bwConf.getTemporalSupport(); defaultDateType = bwConf.getDefaultDateType(); outerIdentity = bwConf.getOuterIdentity() != null ? bwConf.getOuterIdentity() : this; strict = bwConf.isStrict(); @@ -562,6 +569,33 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { } /** + * Getter pair of {@link #setTemporalSupport(boolean)} + * + * @since 2.3.32 + */ + public boolean getTemporalSupport() { + return temporalSupport; + } + + /** + * Sets if {@link Temporal}-s (the Java 8 date/time classes) are wrapped into a {@link TemplateTemporalModel}, or + * just are wrapped like generic java objects (and thus won't be formatted by FreeMarker properly, nor the built-ins + * made for them will work on them). The recommended value is {@code true}. But the defaults is {@code false} if the + * {@link BeansWrapper#BeansWrapper(Version) incompatibleImprovements} of the {@link BeansWrapper} is less than + * {@code 2.3.32}, otherwise it's {@code true}. + * + * @since 2.3.32 + */ + public void setTemporalSupport(boolean temporalSupport) { + checkModifiable(); + if (temporalSupport != this.temporalSupport) { + this.temporalSupport = temporalSupport; + getModelCache().clearClassToFactoryMap(); + getModelCache().clearCache(); + } + } + + /** * Sets the method exposure level. By default, set to <code>EXPOSE_SAFE</code>. * @param exposureLevel can be any of the <code>EXPOSE_xxx</code> * constants. @@ -888,7 +922,8 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { */ protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) { _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements); - return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_27 ? Configuration.VERSION_2_3_27 + return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_32 ? Configuration.VERSION_2_3_32 + : incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_27 ? Configuration.VERSION_2_3_27 : incompatibleImprovements.intValue() == _TemplateAPI.VERSION_INT_2_3_26 ? Configuration.VERSION_2_3_26 : is2324Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_24 : is2321Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_21 @@ -921,7 +956,8 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { * <li>if the object is null, returns the {@link #setNullModel(TemplateModel) null model},</li> * <li>if the object is a Number returns a {@link NumberModel} for it,</li> * <li>if the object is a Date returns a {@link DateModel} for it,</li> - * <li>if the object is a Boolean returns + * <li>if the object is a java.time.Temporal returns a {@link TemporalModel} for it,</li> + * <li>if the object is a Boolean returns * {@link freemarker.template.TemplateBooleanModel#TRUE} or * {@link freemarker.template.TemplateBooleanModel#FALSE}</li> * <li>if the object is already a TemplateModel, returns it unchanged,</li> @@ -1012,7 +1048,10 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { if (Date.class.isAssignableFrom(clazz)) { return DateModel.FACTORY; } - if (Boolean.class == clazz) { // Boolean is final + if (temporalSupport && Temporal.class.isAssignableFrom(clazz)) { + return TemporalModel.FACTORY; + } + if (Boolean.class == clazz) { // Boolean is final return BOOLEAN_FACTORY; } if (ResourceBundle.class.isAssignableFrom(clazz)) { @@ -1631,7 +1670,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { } /** For Unit tests only */ - ModelCache getModelCache() { + BeansModelCache getModelCache() { return modelCache; } @@ -1850,6 +1889,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { + "exposureLevel=" + classIntrospector.getExposureLevel() + ", " + "exposeFields=" + classIntrospector.getExposeFields() + ", " + "preferIndexedReadMethod=" + preferIndexedReadMethod + ", " + + "temporalSupport=" + temporalSupport + ", " + "treatDefaultMethodsAsBeanMembers=" + classIntrospector.getTreatDefaultMethodsAsBeanMembers() + ", " + "sharedClassIntrospCache=" diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java index 7be7ee4..f8b5151 100644 --- a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java +++ b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java @@ -47,6 +47,7 @@ public abstract class BeansWrapperConfiguration implements Cloneable { // Properties and their *defaults*: private boolean simpleMapWrapper = false; private boolean preferIndexedReadMethod; + private boolean temporalSupport; private int defaultDateType = TemplateDateModel.UNKNOWN; private ObjectWrapper outerIdentity = null; private boolean strict = false; @@ -90,6 +91,8 @@ public abstract class BeansWrapperConfiguration implements Cloneable { this.incompatibleImprovements = incompatibleImprovements; preferIndexedReadMethod = incompatibleImprovements.intValue() < _TemplateAPI.VERSION_INT_2_3_27; + + temporalSupport = incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_32; classIntrospectorBuilder = new ClassIntrospectorBuilder(incompatibleImprovements); } @@ -108,6 +111,7 @@ public abstract class BeansWrapperConfiguration implements Cloneable { result = prime * result + incompatibleImprovements.hashCode(); result = prime * result + (simpleMapWrapper ? 1231 : 1237); result = prime * result + (preferIndexedReadMethod ? 1231 : 1237); + result = prime * result + (temporalSupport ? 1231 : 1237); result = prime * result + defaultDateType; result = prime * result + (outerIdentity != null ? outerIdentity.hashCode() : 0); result = prime * result + (strict ? 1231 : 1237); @@ -130,6 +134,7 @@ public abstract class BeansWrapperConfiguration implements Cloneable { if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false; if (simpleMapWrapper != other.simpleMapWrapper) return false; if (preferIndexedReadMethod != other.preferIndexedReadMethod) return false; + if (temporalSupport != other.temporalSupport) return false; if (defaultDateType != other.defaultDateType) return false; if (outerIdentity != other.outerIdentity) return false; if (strict != other.strict) return false; @@ -171,6 +176,16 @@ public abstract class BeansWrapperConfiguration implements Cloneable { this.preferIndexedReadMethod = preferIndexedReadMethod; } + /** @since 2.3.32 */ + public boolean getTemporalSupport() { + return temporalSupport; + } + + /** See {@link BeansWrapper#setTemporalSupport(boolean)}. @since 2.3.32 */ + public void setTemporalSupport(boolean temporalSupport) { + this.temporalSupport = temporalSupport; + } + public int getDefaultDateType() { return defaultDateType; } diff --git a/src/main/java/freemarker/ext/beans/TemporalModel.java b/src/main/java/freemarker/ext/beans/TemporalModel.java new file mode 100644 index 0000000..16a69fe --- /dev/null +++ b/src/main/java/freemarker/ext/beans/TemporalModel.java @@ -0,0 +1,61 @@ +/* + * 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.ext.beans; + +import java.time.temporal.Temporal; +import java.util.Date; + +import freemarker.ext.util.ModelFactory; +import freemarker.template.ObjectWrapper; +import freemarker.template.TemplateDateModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateTemporalModel; + +/** + * Wraps arbitrary subclass of {@link Date} into a reflective model. + * Beside acting as a {@link TemplateDateModel}, you can call all Java methods + * on these objects as well. + */ +public class TemporalModel extends BeanModel implements TemplateTemporalModel { + static final ModelFactory FACTORY = + new ModelFactory() + { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return new TemporalModel((Temporal) object, (BeansWrapper) wrapper); + } + }; + + private final Temporal temporal; + + public TemporalModel(Temporal temporal, BeansWrapper wrapper) { + super(temporal, wrapper); + if (temporal == null) { + throw new IllegalArgumentException("temporal == null"); + } + this.temporal = temporal; + } + + @Override + public Temporal getAsTemporal() { + return temporal; + } + +} diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java index ed47cba..e6772d0 100644 --- a/src/main/java/freemarker/template/Configuration.java +++ b/src/main/java/freemarker/template/Configuration.java @@ -26,6 +26,7 @@ import java.lang.reflect.InvocationTargetException; import java.net.URLConnection; import java.text.DecimalFormat; import java.text.SimpleDateFormat; +import java.time.temporal.Temporal; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -945,6 +946,14 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * U+221E, and U+FFFD. * </ul> * </li> + * <li><p> + * 2.3.32 (or higher): + * <ul> + * <li>2.3.32 (or higher): + * {@link BeansWrapper} and {@link DefaultObjectWrapper} now wraps {@link Temporal}-s into + * {@link SimpleTemporal}. Before that, {@link Temporal}-s were treated as generic Java objects; + * see {@link BeansWrapper#BeansWrapper(Version)}. + * </li> * </ul> * * @throws IllegalArgumentException diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java index 2636424..56bef33 100644 --- a/src/main/java/freemarker/template/DefaultObjectWrapper.java +++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java @@ -81,7 +81,7 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { /** * Creates a new instance with the incompatible-improvements-version specified in * {@link Configuration#DEFAULT_INCOMPATIBLE_IMPROVEMENTS}. - * + * * @deprecated Use {@link DefaultObjectWrapperBuilder}, or in rare cases, * {@link #DefaultObjectWrapper(Version)} instead. */ @@ -89,7 +89,7 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { public DefaultObjectWrapper() { this(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); } - + /** * Use {@link DefaultObjectWrapperBuilder} instead if possible. Instances created with this constructor won't share * the class introspection caches with other instances. See {@link BeansWrapper#BeansWrapper(Version)} (the @@ -112,7 +112,6 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { * the default). This adapter is cleaner than {@link EnumerationModel} as it only implements the * minimally required FTL type, which avoids some ambiguous situations. (Note that Java API methods * aren't exposed anymore as subvariables; if you really need them, you can use {@code ?api}). - * </li> * </ul> * * @since 2.3.21 @@ -211,7 +210,7 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { } return new SimpleDate((java.util.Date) obj, getDefaultDateType()); } - if (obj instanceof Temporal) { + if (getTemporalSupport() && obj instanceof Temporal) { return new SimpleTemporal((Temporal) obj); } final Class<?> objClass = obj.getClass(); diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java index 3fa4aa4..8faddc4 100644 --- a/src/main/java/freemarker/template/utility/ClassUtil.java +++ b/src/main/java/freemarker/template/utility/ClassUtil.java @@ -44,6 +44,7 @@ import freemarker.ext.beans.NumberModel; import freemarker.ext.beans.OverloadedMethodsModel; import freemarker.ext.beans.SimpleMethodModel; import freemarker.ext.beans.StringModel; +import freemarker.ext.beans.TemporalModel; import freemarker.ext.util.WrapperTemplateModel; import freemarker.template.AdapterTemplateModel; import freemarker.template.TemplateBooleanModel; @@ -214,6 +215,8 @@ public class ClassUtil { return TemplateBooleanModel.class; } else if (tm instanceof DateModel) { return TemplateDateModel.class; + } else if (tm instanceof TemporalModel) { + return TemporalModel.class; } else if (tm instanceof StringModel) { Object wrapped = ((BeanModel) tm).getWrappedObject(); return wrapped instanceof String diff --git a/src/test/java/freemarker/core/TemporalErrorMessagesTest.java b/src/test/java/freemarker/core/TemporalErrorMessagesTest.java index e9c2791..1448d25 100644 --- a/src/test/java/freemarker/core/TemporalErrorMessagesTest.java +++ b/src/test/java/freemarker/core/TemporalErrorMessagesTest.java @@ -19,16 +19,21 @@ package freemarker.core; -import java.time.Instant; import java.time.LocalTime; import org.junit.Test; +import freemarker.template.Configuration; import freemarker.template.TemplateException; import freemarker.test.TemplateTest; public class TemporalErrorMessagesTest extends TemplateTest { + @Override + protected Configuration createConfiguration() throws Exception { + return new Configuration(Configuration.VERSION_2_3_32); + } + @Test public void testExplicitFormatString() throws TemplateException { addToDataModel("t", LocalTime.now()); diff --git a/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java b/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java index ac7a7b7..0bc4273 100644 --- a/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java +++ b/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.lang.reflect.Modifier; +import java.time.LocalDate; import java.util.Collections; import org.junit.Test; @@ -37,6 +38,7 @@ import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateScalarModel; import freemarker.template.TemplateSequenceModel; +import freemarker.template.TemplateTemporalModel; import freemarker.template.Version; import freemarker.template.utility.Constants; @@ -117,7 +119,34 @@ public class BeansWrapperMiscTest { assertNull(barTM); // all read methods inaccessible } } - + + @Test + public void testTemporalWrappingICI() throws TemplateModelException { + LocalDate localDate = LocalDate.of(2021, 10, 31); + { + BeansWrapper bw = new BeansWrapper(Configuration.VERSION_2_3_31); + assertFalse(bw.getTemporalSupport()); + assertThat( + bw.wrap(localDate), + not(instanceOf(TemplateTemporalModel.class))); + bw.setTemporalSupport(true); + assertThat( + bw.wrap(localDate), + instanceOf(TemporalModel.class)); + } + { + BeansWrapper bw = new BeansWrapper(Configuration.VERSION_2_3_32); + assertTrue(bw.getTemporalSupport()); + assertThat( + bw.wrap(localDate), + instanceOf(TemporalModel.class)); + bw.setTemporalSupport(false); + assertThat( + bw.wrap(localDate), + not(instanceOf(TemplateTemporalModel.class))); + } + } + public static class BeanWithBothIndexedAndArrayProperty { private final static String[] FOO = new String[] { "a", "b" }; diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java index 17c6702..ee2a6b5 100644 --- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java +++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.*; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -103,7 +104,7 @@ public class DefaultObjectWrapperTest { expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.29 expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.30 expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.31 - expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.32 + expected.add(Configuration.VERSION_2_3_32); List<Version> actual = new ArrayList<>(); for (int i = _TemplateAPI.VERSION_INT_2_3_0; i <= Configuration.getVersion().intValue(); i++) { @@ -1090,7 +1091,18 @@ public class DefaultObjectWrapperTest { assertFalse(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build().getPreferIndexedReadMethod()); assertFalse(new DefaultObjectWrapper(Configuration.VERSION_2_3_27).getPreferIndexedReadMethod()); } - + + @Test + public void testTemporalWrappingICI() throws TemplateModelException { + LocalDate localDate = LocalDate.of(2021, 10, 31); + assertThat( + new DefaultObjectWrapper(Configuration.VERSION_2_3_31).wrap(localDate), + not(instanceOf(TemplateTemporalModel.class))); + assertThat( + new DefaultObjectWrapper(Configuration.VERSION_2_3_32).wrap(localDate), + instanceOf(SimpleTemporal.class)); + } + private void assertSizeThroughAPIModel(int expectedSize, TemplateModel normalModel) throws TemplateModelException { if (!(normalModel instanceof TemplateModelWithAPISupport)) { fail(); diff --git a/src/test/resources/freemarker/test/templatesuite/testcases.xml b/src/test/resources/freemarker/test/templatesuite/testcases.xml index c918926..d0f66bb 100644 --- a/src/test/resources/freemarker/test/templatesuite/testcases.xml +++ b/src/test/resources/freemarker/test/templatesuite/testcases.xml @@ -220,7 +220,9 @@ <setting incompatible_improvements="2.3.24, max"/> </testCase> <testCase name="date-type-builtins" noOutput="true" /> - <testCase name="temporal" noOutput="true" /> + <testCase name="temporal" noOutput="true"> + <setting incompatible_improvements="2.3.32, max"/> + </testCase> <testCase name="url" noOutput="true" /> <testCase name="var-layers"/> <testCase name="variables"/>
