This is an automated email from the ASF dual-hosted git repository. ddekany pushed a commit to branch FREEMARKER-183 in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit 4c760f6e2283304d47e1395472f74772f60cfd5a Author: ddekany <[email protected]> AuthorDate: Sat Jan 13 18:12:11 2024 +0100 Added support for marking obj.prop and obj.prop() to be the same in templates (and equally obj["prop"], and obj["prop"]()). Made Java zero argument methods to be such properties by default, if incompatibleImprovements is at least 2.3.33. Added ZeroArgumentNonVoidMethodPolicy, and BeansWrapperConfiguration.nonRecordZeroArgumentNonVoidMethodPolicy, and recordZeroArgumentNonVoidMethodPolicy to implement these. Also,added GenericObjectModel which implements MethodCallAwareTemplateHashMode [...] --- build.gradle.kts | 2 +- .../main/java/freemarker/core/Configurable.java | 2 +- .../src/main/java/freemarker/core/Dot.java | 21 +- .../java/freemarker/core/DotBeforeMethodCall.java | 59 ++++ .../main/java/freemarker/core/DynamicKeyName.java | 16 +- .../core/DynamicKeyNameBeforeMethodCall.java | 50 +++ .../src/main/java/freemarker/core/MethodCall.java | 3 + .../main/java/freemarker/ext/beans/APIModel.java | 13 +- .../main/java/freemarker/ext/beans/BeanModel.java | 108 ++++++- .../java/freemarker/ext/beans/BeansWrapper.java | 140 ++++++-- .../ext/beans/BeansWrapperConfiguration.java | 36 +++ .../freemarker/ext/beans/ClassIntrospector.java | 88 +++++- .../ext/beans/ClassIntrospectorBuilder.java | 48 ++- .../ext/beans/FastPropertyDescriptor.java | 18 +- .../freemarker/ext/beans/GenericObjectModel.java | 72 +++++ .../ext/beans/MethodAppearanceFineTuner.java | 20 +- .../java/freemarker/ext/beans/StringModel.java | 15 +- .../ext/beans/ZeroArgumentNonVoidMethodPolicy.java | 65 ++++ .../main/java/freemarker/ext/beans/_BeansAPI.java | 4 +- .../template/MethodCallAwareTemplateHashModel.java | 124 ++++++++ .../src/main/javacc/freemarker/core/FTL.jj | 7 + .../freemarker/template/ConfigurationTest.java | 4 +- .../beans/TestZeroArgumentNonVoidMethodPolicy.java | 352 +++++++++++++++++++++ .../template/DefaultObjectWrapperTest.java | 4 +- freemarker-manual/src/main/docgen/en_US/book.xml | 130 +++++++- .../main/java/freemarker/test/TemplateTest.java | 15 +- 26 files changed, 1342 insertions(+), 74 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 114ac085..82f8917a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,7 +61,7 @@ freemarkerRoot { configureSourceSet("jython20") configureSourceSet("jython22") configureSourceSet("jython25") { enableTests() } - configureSourceSet("core16", "16") + configureSourceSet("core16", "16") { enableTests() } configureGeneratedSourceSet("jakartaServlet") { val jakartaSourceGenerators = generateJakartaSources("javaxServlet") diff --git a/freemarker-core/src/main/java/freemarker/core/Configurable.java b/freemarker-core/src/main/java/freemarker/core/Configurable.java index fc98db58..6eaf8eda 100644 --- a/freemarker-core/src/main/java/freemarker/core/Configurable.java +++ b/freemarker-core/src/main/java/freemarker/core/Configurable.java @@ -2553,7 +2553,7 @@ public class Configurable { * <p>If you have no constructor arguments and property setters, and the <code><i>className</i></code> class has * a public static {@code INSTANCE} field, the value of that filed will be the value of the expression, and * the constructor won't be called. Note that if you use the backward compatible - * syntax, where these's no parenthesis after the class name, then it will not look for {@code INSTANCE}. + * syntax, where there's no parenthesis after the class name, then it will not look for {@code INSTANCE}. * </li> * <li> * <p>If there exists a class named <code><i>className</i>Builder</code>, then that class will be instantiated diff --git a/freemarker-core/src/main/java/freemarker/core/Dot.java b/freemarker-core/src/main/java/freemarker/core/Dot.java index 54bae57b..f360a955 100644 --- a/freemarker-core/src/main/java/freemarker/core/Dot.java +++ b/freemarker-core/src/main/java/freemarker/core/Dot.java @@ -27,7 +27,7 @@ import freemarker.template.TemplateModel; * The dot operator. Used to reference items inside a * <code>TemplateHashModel</code>. */ -final class Dot extends Expression { +class Dot extends Expression { private final Expression target; private final String key; @@ -36,11 +36,20 @@ final class Dot extends Expression { this.key = key; } + /** + * Shallow copy constructor + */ + Dot(Dot dot) { + this(dot.target, dot.key); + this.constantValue = dot.constantValue; // Probably always will be null here + copyFieldsFrom(dot); + } + @Override TemplateModel _eval(Environment env) throws TemplateException { TemplateModel leftModel = target.eval(env); if (leftModel instanceof TemplateHashModel) { - return ((TemplateHashModel) leftModel).get(key); + return evalOnHash((TemplateHashModel) leftModel); } if (leftModel == null && env.isClassicCompatible()) { return null; // ${noSuchVar.foo} has just printed nothing in FM 1. @@ -48,6 +57,14 @@ final class Dot extends Expression { throw new NonHashException(target, leftModel, env); } + protected TemplateModel evalOnHash(TemplateHashModel leftModel) throws TemplateException { + return leftModel.get(key); + } + + String getKey() { + return key; + } + @Override public String getCanonicalForm() { return target.getCanonicalForm() + getNodeTypeSymbol() + _CoreStringUtils.toFTLIdentifierReferenceAfterDot(key); diff --git a/freemarker-core/src/main/java/freemarker/core/DotBeforeMethodCall.java b/freemarker-core/src/main/java/freemarker/core/DotBeforeMethodCall.java new file mode 100644 index 00000000..c842c7d6 --- /dev/null +++ b/freemarker-core/src/main/java/freemarker/core/DotBeforeMethodCall.java @@ -0,0 +1,59 @@ +/* + * 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 freemarker.ext.beans.BeansWrapper; +import freemarker.template.MethodCallAwareTemplateHashModel; +import freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy; +import freemarker.template.TemplateException; +import freemarker.template.TemplateHashModel; +import freemarker.template.TemplateModel; + +/** + * Like {@link Dot}, but when used before method call (but as of 2.3.33, before 0-argument calls only), as in + * {@code obj.key()}. The reason it's only used before 0-argument calls (as of 2.3.33 at least) is that it adds some + * overhead, and this {@link Dot} subclass was added to implement + * {@link ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD} + * (via {@link BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}). We don't + * necessarily want to go beyond that hack, as we don't have separate method namespace in the template language. + */ +class DotBeforeMethodCall extends Dot { + public DotBeforeMethodCall(Dot dot) { + super(dot); + } + + @Override + protected TemplateModel evalOnHash(TemplateHashModel leftModel) throws TemplateException { + if (leftModel instanceof MethodCallAwareTemplateHashModel) { + try { + return ((MethodCallAwareTemplateHashModel) leftModel).getBeforeMethodCall(getKey()); + } catch (MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) { + String hint = e.getHint(); + throw new NonMethodException( + this, + e.getActualValue(), + hint != null ? new String[] { hint } : null, + Environment.getCurrentEnvironment()); + } + } else { + return super.evalOnHash(leftModel); + } + } +} diff --git a/freemarker-core/src/main/java/freemarker/core/DynamicKeyName.java b/freemarker-core/src/main/java/freemarker/core/DynamicKeyName.java index f8cef3a4..d2fa8225 100644 --- a/freemarker-core/src/main/java/freemarker/core/DynamicKeyName.java +++ b/freemarker-core/src/main/java/freemarker/core/DynamicKeyName.java @@ -43,7 +43,7 @@ import freemarker.template.utility.Constants; * {@code target[keyExpression]}, where, in FM 2.3, {@code keyExpression} can be string, a number or a range, * and {@code target} can be a hash or a sequence. */ -final class DynamicKeyName extends Expression { +class DynamicKeyName extends Expression { private static final int UNKNOWN_RESULT_SIZE = -1; @@ -58,6 +58,13 @@ final class DynamicKeyName extends Expression { target.enableLazilyGeneratedResult(); } + DynamicKeyName(DynamicKeyName dynamicKeyName) { + this(dynamicKeyName.target, dynamicKeyName.keyExpression); + this.lazilyGeneratedResultEnabled = dynamicKeyName.lazilyGeneratedResultEnabled; + this.constantValue = dynamicKeyName.constantValue; // Probably always will be null here + copyFieldsFrom(dynamicKeyName); + } + @Override TemplateModel _eval(Environment env) throws TemplateException { TemplateModel targetModel = target.eval(env); @@ -163,11 +170,16 @@ final class DynamicKeyName extends Expression { private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env) throws TemplateException { if (targetModel instanceof TemplateHashModel) { - return((TemplateHashModel) targetModel).get(key); + return getFromHashModelWithStringKey((TemplateHashModel) targetModel, key); } throw new NonHashException(target, targetModel, env); } + protected TemplateModel getFromHashModelWithStringKey(TemplateHashModel targetModel, String key) + throws TemplateException { + return targetModel.get(key); + } + private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env) throws TemplateException { // We can have 3 kind of left hand operands ("targets"): sequence, lazily generated sequence, string diff --git a/freemarker-core/src/main/java/freemarker/core/DynamicKeyNameBeforeMethodCall.java b/freemarker-core/src/main/java/freemarker/core/DynamicKeyNameBeforeMethodCall.java new file mode 100644 index 00000000..f9678cac --- /dev/null +++ b/freemarker-core/src/main/java/freemarker/core/DynamicKeyNameBeforeMethodCall.java @@ -0,0 +1,50 @@ +/* + * 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 freemarker.template.MethodCallAwareTemplateHashModel; +import freemarker.template.TemplateException; +import freemarker.template.TemplateHashModel; +import freemarker.template.TemplateModel; + +class DynamicKeyNameBeforeMethodCall extends DynamicKeyName { + DynamicKeyNameBeforeMethodCall(DynamicKeyName dynamicKeyName) { + super(dynamicKeyName); + } + + @Override + protected TemplateModel getFromHashModelWithStringKey(TemplateHashModel targetModel, String key) + throws TemplateException { + if (targetModel instanceof MethodCallAwareTemplateHashModel) { + try { + return ((MethodCallAwareTemplateHashModel) targetModel).getBeforeMethodCall(key); + } catch (MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) { + String hint = e.getHint(); + throw new NonMethodException( + this, + e.getActualValue(), + hint != null ? new String[] { hint } : null, + Environment.getCurrentEnvironment()); + } + } else { + return super.getFromHashModelWithStringKey(targetModel, key); + } + } +} diff --git a/freemarker-core/src/main/java/freemarker/core/MethodCall.java b/freemarker-core/src/main/java/freemarker/core/MethodCall.java index 2ece2a0a..1eebbe59 100644 --- a/freemarker-core/src/main/java/freemarker/core/MethodCall.java +++ b/freemarker-core/src/main/java/freemarker/core/MethodCall.java @@ -66,6 +66,9 @@ final class MethodCall extends Expression { } else { throw new NonMethodException(target, targetModel, true, false, null, env); } + // ATTENTION! If you add support for calling any new type, ensure that + // freemarker.ext.beans.BeanModel.invokeThroughDescriptor sees that type as callable too, + // where it deals with the beforeMethodCall logic! } @Override diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java b/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java index 4580ac8d..2bc875e2 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java @@ -19,6 +19,10 @@ package freemarker.ext.beans; +import freemarker.template.MethodCallAwareTemplateHashModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + /** * Exposes the Java API (and properties) of an object. * @@ -32,7 +36,7 @@ package freemarker.ext.beans; * * @since 2.3.22 */ -final class APIModel extends BeanModel { +final class APIModel extends BeanModel implements MethodCallAwareTemplateHashModel { APIModel(Object object, BeansWrapper wrapper) { super(object, wrapper, false); @@ -41,5 +45,10 @@ final class APIModel extends BeanModel { protected boolean isMethodsShadowItems() { return true; } - + + @Override + public TemplateModel getBeforeMethodCall(String key) throws TemplateModelException, + ShouldNotBeGetAsMethodException { + return super.getBeforeMethodCall(key); + } } diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java b/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java index 79110b90..fd92bab8 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java @@ -30,7 +30,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import freemarker.core.BugException; import freemarker.core.CollectionAndSequence; +import freemarker.core.Macro; import freemarker.core._DelayedFTLTypeDescription; import freemarker.core._DelayedJQuote; import freemarker.core._TemplateModelException; @@ -38,16 +40,20 @@ import freemarker.ext.util.ModelFactory; import freemarker.ext.util.WrapperTemplateModel; import freemarker.log.Logger; import freemarker.template.AdapterTemplateModel; +import freemarker.template.MethodCallAwareTemplateHashModel; import freemarker.template.ObjectWrapper; import freemarker.template.SimpleScalar; import freemarker.template.SimpleSequence; import freemarker.template.TemplateCollectionModel; import freemarker.template.TemplateHashModelEx; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelIterator; import freemarker.template.TemplateModelWithAPISupport; import freemarker.template.TemplateScalarModel; +import freemarker.template.utility.CollectionUtils; import freemarker.template.utility.StringUtil; /** @@ -134,12 +140,67 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp * then {@code non-void-return-type get(java.lang.Object)}, or * alternatively (if the wrapped object is a resource bundle) * {@code Object getObject(java.lang.String)}. + * + * <p>As of 2.3.33, the default implementation of this method delegates to {@link #get(String, boolean)}. It's + * better to override that, instead of this method. Otherwise, unwanted behavior can arise if the model class also + * implements {@link MethodCallAwareTemplateHashModel}, as that will certainly call {@link #get(String, boolean)} + * internally, and not the overridden version of this method. + * * @throws TemplateModelException if there was no property nor method nor * a generic {@code get} method to invoke. */ @Override - public TemplateModel get(String key) - throws TemplateModelException { + public TemplateModel get(String key) throws TemplateModelException { + try { + return get(key, false); + } catch (MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) { + throw new BugException(e); + } + } + + /** + * Can be overridden to be public, to implement {@link MethodCallAwareTemplateHashModel}. We don't implement that + * in {@link BeanModel} for backward compatibility, but the functionality is present. If you expose this method by + * implementing {@link MethodCallAwareTemplateHashModel}, then be sure that {@link #get(String)} is + * not overridden in custom subclasses; if it is, then those subclasses should be modernized to override + * {@link #get(String, boolean)} instead. + * + * @since 2.3.33 + */ + protected TemplateModel getBeforeMethodCall(String key) + throws TemplateModelException, MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException { + TemplateModel result = get(key, true); + if (result instanceof TemplateMethodModelEx) { + return (TemplateMethodModelEx) result; + } + if (result == null) { + return null; + } + throw new MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException(result, null); + } + + /** + * Override this if you want to customize the behavior of {@link #get(String)}. + * In standard implementations at least, this is what {@link #get(String)}, and + * {@link MethodCallAwareTemplateHashModel#getBeforeMethodCall(String)} delegates to. + * + * @param key + * Same as the parameter of {@link #get(String)}. + * @param beforeMethodCall + * This is a hint that tells that the returned value will be called in the template. This was added to + * implement {@link BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}. + * This parameter is {@code false} when {@link #get(String)} is called, and + * {@code true} when {@link MethodCallAwareTemplateHashModel#getBeforeMethodCall(String)} is called. + * If this is {@code true}, this method should return a {@link TemplateMethodModelEx}, or {@code null}, + * or fail with {@link MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException}. + * + * @since 2.3.33 + */ + // Before calling this from FreeMarker classes, consider that some users may have overridden {@link #get(String)} + // instead, as this class didn't exist before 2.3.33. So with incompatibleImprovements before that, that should be + // the only place where this gets called, or else the behavior of the model will be inconsistent. + protected TemplateModel get(String key, boolean beforeMethodCall) + throws TemplateModelException, MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException { Class<?> clazz = object.getClass(); Map<Object, Object> classInfo = wrapper.getClassIntrospector().get(clazz); TemplateModel retval = null; @@ -148,7 +209,7 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp if (wrapper.isMethodsShadowItems()) { Object fd = classInfo.get(key); if (fd != null) { - retval = invokeThroughDescriptor(fd, classInfo); + retval = invokeThroughDescriptor(fd, classInfo, beforeMethodCall); } else { retval = invokeGenericGet(classInfo, clazz, key); } @@ -160,7 +221,7 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp } Object fd = classInfo.get(key); if (fd != null) { - retval = invokeThroughDescriptor(fd, classInfo); + retval = invokeThroughDescriptor(fd, classInfo, beforeMethodCall); if (retval == UNKNOWN && model == nullModel) { // This is the (somewhat subtle) case where the generic get() returns null // and we have no bean info, so we respect the fact that @@ -178,9 +239,12 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp retval = wrapper.wrap(null); } return retval; - } catch (TemplateModelException e) { + } catch (TemplateModelException | MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) { throw e; } catch (Exception e) { + if (beforeMethodCall && e instanceof MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException) { + throw (MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException) e; + } throw new _TemplateModelException(e, "An error has occurred when reading existing sub-variable ", new _DelayedJQuote(key), "; see cause exception! The type of the containing value was: ", @@ -203,8 +267,9 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp return wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY) != null; } - private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo) - throws IllegalAccessException, InvocationTargetException, TemplateModelException { + private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo, boolean beforeMethodCall) + throws IllegalAccessException, InvocationTargetException, TemplateModelException, + MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException { // See if this particular instance has a cached implementation for the requested feature descriptor TemplateModel cachedModel; synchronized (this) { @@ -215,6 +280,9 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp return cachedModel; } + // ATTENTION! As the value of beforeMethodCall is not part of the cache lookup key, it's very important that we + // don't cache the value for desc-s where beforeMethodCall can have influence on the result! + TemplateModel resultModel = UNKNOWN; if (desc instanceof FastPropertyDescriptor) { FastPropertyDescriptor pd = (FastPropertyDescriptor) desc; @@ -229,8 +297,30 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp ClassIntrospector.getArgTypes(classInfo, indexedReadMethod), wrapper); } } else { - resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null); - // cachedModel remains null, as we don't cache these + // cachedModel must remains null in this branch, because the result is influenced by beforeMethodCall, + // which wasn't part of the cache key! + + if (!beforeMethodCall) { + resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null); + // cachedModel remains null, as we don't cache these + } else { + if (pd.isMethodInsteadOfPropertyValueBeforeCall()) { + // Do not cache this result! See comments earlier! + resultModel = new SimpleMethodModel( + object, pd.getReadMethod(), CollectionUtils.EMPTY_CLASS_ARRAY, wrapper); + } else { + resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null); + + // Checks if freemarker.core.MethodCall would accept this result: + if (!(resultModel instanceof TemplateMethodModel || resultModel instanceof Macro)) { + throw new MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException( + resultModel, + "This member of the parent object is seen by templates as a property of it " + + "(with other words, an attribute, or a field), not a method of it. " + + "Thus, to get its value, it must not be called as a method."); + } + } + } } } else if (desc instanceof Field) { resultModel = wrapper.readField(object, (Field) desc); diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java index be2455eb..0fbf3561 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java @@ -19,6 +19,7 @@ package freemarker.ext.beans; +import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.AccessibleObject; @@ -153,7 +154,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { * performance. In theory that's not needed, but apps might fail to keep the rules. */ private ClassIntrospector classIntrospector; - + /** * {@link String} class name to {@link StaticModel} cache. * This object only belongs to a single {@link BeansWrapper}. @@ -193,9 +194,10 @@ 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 final Version incompatibleImprovements; - + + /** * Creates a new instance with the incompatible-improvements-version specified in * {@link Configuration#DEFAULT_INCOMPATIBLE_IMPROVEMENTS}. @@ -262,6 +264,16 @@ 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.33 (or higher): + * The default of {@link BeansWrapper#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)} + * has changes to {@link ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}, from + * {@link ZeroArgumentNonVoidMethodPolicy#METHOD_ONLY}. This means that Java records public methods with + * 0-arguments and non-void return type are now exposed both as properties, and as methods, while earlier they + * were only exposed as methods. That is, if in a record you have {@code public String name()}, now in + * templates the value can be accessed both as {@code obj.name} (like a property), and as {@code obj.name()} + * (for better backward compatibility only - it's bad style). + * </li> * </ul> * * <p>Note that the version will be normalized to the lowest version where the same incompatible @@ -289,7 +301,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { } /** - * Initializes the instance based on the the {@link BeansWrapperConfiguration} specified. + * Initializes the instance based on the {@link BeansWrapperConfiguration} specified. * * @param writeProtected Makes the instance's configuration settings read-only via * {@link WriteProtectable#writeProtect()}; this way it can use the shared class introspection cache. @@ -320,7 +332,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { } } catch (Throwable e) { // The security manager sometimes doesn't allow this - LOG.info("Failed to check if finetuneMethodAppearance is overidden in " + thisClass.getName() + LOG.info("Failed to check if finetuneMethodAppearance is overridden in " + thisClass.getName() + "; acting like if it was, but this way it won't utilize the shared class introspection " + "cache.", e); @@ -353,7 +365,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { defaultDateType = bwConf.getDefaultDateType(); outerIdentity = bwConf.getOuterIdentity() != null ? bwConf.getOuterIdentity() : this; strict = bwConf.isStrict(); - + if (!writeProtected) { // As this is not a read-only BeansWrapper, the classIntrospector will be possibly replaced for a few times, // but we need to use the same sharedInrospectionLock forever, because that's what the model factories @@ -367,7 +379,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { classIntrospector = _BeansAPI.getClassIntrospectorBuilder(bwConf).build(); sharedIntrospectionLock = classIntrospector.getSharedLock(); } - + falseModel = new BooleanModel(Boolean.FALSE, this); trueModel = new BooleanModel(Boolean.TRUE, this); @@ -633,7 +645,45 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { replaceClassIntrospector(builder); } } - + + /** + * Sets the {@link ZeroArgumentNonVoidMethodPolicy} for classes that are not Java records; + * defaults to {@link ZeroArgumentNonVoidMethodPolicy#METHOD_ONLY}. + * + * <p>Note that methods in this class are inherited by {@link DefaultObjectWrapper}, which is what you normally use. + * + * @since 2.3.33 + */ + public void setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy nonRecordZeroArgumentNonVoidMethodPolicy) { + checkModifiable(); + + if (classIntrospector.getNonRecordZeroArgumentNonVoidMethodPolicy() != nonRecordZeroArgumentNonVoidMethodPolicy) { + ClassIntrospectorBuilder builder = classIntrospector.createBuilder(); + builder.setNonRecordZeroArgumentNonVoidMethodPolicy(nonRecordZeroArgumentNonVoidMethodPolicy); + replaceClassIntrospector(builder); + } + } + + /** + * Sets the {@link ZeroArgumentNonVoidMethodPolicy} for classes that are Java records; if the + * {@code BeansWrapper#BeansWrapper(Version) incompatibleImprovements} of the object wrapper is at least 2.3.33, + * then it defaults to {@link ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}, otherwise it defaults to + * {@link ZeroArgumentNonVoidMethodPolicy#METHOD_ONLY}. + * + * <p>Note that methods in this class are inherited by {@link DefaultObjectWrapper}, which is what you normally use. + * + * @since 2.3.33 + */ + public void setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy) { + checkModifiable(); + + if (classIntrospector.getRecordZeroArgumentNonVoidMethodPolicy() != recordZeroArgumentNonVoidMethodPolicy) { + ClassIntrospectorBuilder builder = classIntrospector.createBuilder(); + builder.setRecordZeroArgumentNonVoidMethodPolicy(recordZeroArgumentNonVoidMethodPolicy); + replaceClassIntrospector(builder); + } + } + /** * Returns whether exposure of public instance fields of classes is * enabled. See {@link #setExposeFields(boolean)} for details. @@ -651,7 +701,25 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { public boolean getTreatDefaultMethodsAsBeanMembers() { return classIntrospector.getTreatDefaultMethodsAsBeanMembers(); } - + + /** + * See {@link #setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}. + * + * @since 2.3.33 + */ + public ZeroArgumentNonVoidMethodPolicy getNonRecordZeroArgumentNonVoidMethodPolicy() { + return classIntrospector.getNonRecordZeroArgumentNonVoidMethodPolicy(); + } + + /** + * See {@link #setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}. + * + * @since 2.3.33 + */ + public ZeroArgumentNonVoidMethodPolicy getRecordZeroArgumentNonVoidMethodPolicy() { + return classIntrospector.getRecordZeroArgumentNonVoidMethodPolicy(); + } + public MethodAppearanceFineTuner getMethodAppearanceFineTuner() { return classIntrospector.getMethodAppearanceFineTuner(); } @@ -865,7 +933,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { /** * Returns the version given with {@link #BeansWrapper(Version)}, normalized to the lowest version where a change * has occurred. Thus, this is not necessarily the same version than that was given to the constructor. - * + * * @since 2.3.21 */ public Version getIncompatibleImprovements() { @@ -894,7 +962,8 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { */ protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) { _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements); - return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_27 ? Configuration.VERSION_2_3_27 + return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 ? Configuration.VERSION_2_3_33 + : incompatibleImprovements.intValue() >= _VersionInts.V_2_3_27 ? Configuration.VERSION_2_3_27 : incompatibleImprovements.intValue() == _VersionInts.V_2_3_26 ? Configuration.VERSION_2_3_26 : is2324Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_24 : is2321Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_21 @@ -937,7 +1006,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { * <li>if the object is an Iterator, returns a {@link IteratorModel} for it * <li>if the object is an Enumeration, returns a {@link EnumerationModel} for it * <li>if the object is a String, returns a {@link StringModel} for it - * <li>otherwise, returns a generic {@link StringModel} for it. + * <li>otherwise, returns a {@link GenericObjectModel} for it. * </ul> */ @Override @@ -1033,7 +1102,7 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { if (clazz.isArray()) { return ArrayModel.FACTORY; } - return StringModel.FACTORY; + return GenericObjectModel.FACTORY; } /** @@ -1855,15 +1924,33 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { */ static public final class MethodAppearanceDecision { private PropertyDescriptor exposeAsProperty; + private boolean methodInsteadOfPropertyValueBeforeCall; private boolean replaceExistingProperty; private String exposeMethodAs; private boolean methodShadowsProperty; - - void setDefaults(Method m) { - exposeAsProperty = null; - replaceExistingProperty = false; - exposeMethodAs = m.getName(); + + /** + * @param appliedZeroArgumentNonVoidMethodPolicy + * {@code null} if this is not a zero argument method with non-void return type. + */ + void setDefaults(Method m, ZeroArgumentNonVoidMethodPolicy appliedZeroArgumentNonVoidMethodPolicy) { + if (appliedZeroArgumentNonVoidMethodPolicy != null + && appliedZeroArgumentNonVoidMethodPolicy != ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY) { + try { + exposeAsProperty = new PropertyDescriptor(m.getName(), m, null); + } catch (IntrospectionException e) { + throw new BugException("Failed to create PropertyDescriptor for " + m, e); + } + methodInsteadOfPropertyValueBeforeCall = appliedZeroArgumentNonVoidMethodPolicy == + ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD; + } else { + exposeAsProperty = null; + methodInsteadOfPropertyValueBeforeCall = false; + } + exposeMethodAs = appliedZeroArgumentNonVoidMethodPolicy != ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY + ? m.getName() : null; methodShadowsProperty = true; + replaceExistingProperty = false; } /** @@ -1935,6 +2022,23 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { this.methodShadowsProperty = shadowEarlierProperty; } + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + * + * @since 2.3.33 + */ + public boolean isMethodInsteadOfPropertyValueBeforeCall() { + return methodInsteadOfPropertyValueBeforeCall; + } + + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + * + * @since 2.3.33 + */ + public void setMethodInsteadOfPropertyValueBeforeCall(boolean methodInsteadOfPropertyValueBeforeCall) { + this.methodInsteadOfPropertyValueBeforeCall = methodInsteadOfPropertyValueBeforeCall; + } } /** diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java index 5daaa909..a49fabbc 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java @@ -251,6 +251,42 @@ public abstract class BeansWrapperConfiguration implements Cloneable { classIntrospectorBuilder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers); } + /** + * @since 2.3.33 + */ + public ZeroArgumentNonVoidMethodPolicy getNonRecordZeroArgumentNonVoidMethodPolicy() { + return classIntrospectorBuilder.getNonRecordZeroArgumentNonVoidMethodPolicy(); + } + + /** + * See {@link BeansWrapper#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}. + * + * <p>Note that methods in this class are inherited by {@link DefaultObjectWrapperBuilder}, which is what you normally use. + * + * @since 2.3.33 + */ + public void setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy nonRecordZeroArgumentNonVoidMethodPolicy) { + classIntrospectorBuilder.setNonRecordZeroArgumentNonVoidMethodPolicy(nonRecordZeroArgumentNonVoidMethodPolicy); + } + + /** + * @since 2.3.33 + */ + public ZeroArgumentNonVoidMethodPolicy getRecordZeroArgumentNonVoidMethodPolicy() { + return classIntrospectorBuilder.getRecordZeroArgumentNonVoidMethodPolicy(); + } + + /** + * See {@link BeansWrapper#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)} + * + * <p>Note that methods in this class are inherited by {@link DefaultObjectWrapperBuilder}, which is what you normally use. + * + * @since 2.3.33 + */ + public void setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy) { + classIntrospectorBuilder.setRecordZeroArgumentNonVoidMethodPolicy(recordZeroArgumentNonVoidMethodPolicy); + } + public MethodAppearanceFineTuner getMethodAppearanceFineTuner() { return classIntrospectorBuilder.getMethodAppearanceFineTuner(); } diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java index bf867814..81f4ffe0 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java @@ -48,11 +48,13 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import freemarker.core.BugException; +import freemarker.core._JavaVersions; import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision; import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput; import freemarker.ext.util.ModelCache; import freemarker.log.Logger; import freemarker.template.Version; +import freemarker.template.utility.CollectionUtils; import freemarker.template.utility.NullArgumentException; import freemarker.template.utility.SecurityUtilities; @@ -82,7 +84,7 @@ class ClassIntrospector { private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE = new ExecutableMemberSignature("get", new Class[] { Object.class }); private static final ExecutableMemberSignature TO_STRING_SIGNATURE = - new ExecutableMemberSignature("toString", new Class[0]); + new ExecutableMemberSignature("toString", CollectionUtils.EMPTY_CLASS_ARRAY); /** * When this property is true, some things are stricter. This is mostly to catch suspicious things in development @@ -151,6 +153,9 @@ class ClassIntrospector { final MethodAppearanceFineTuner methodAppearanceFineTuner; final MethodSorter methodSorter; final boolean treatDefaultMethodsAsBeanMembers; + final ZeroArgumentNonVoidMethodPolicy nonRecordZeroArgumentNonVoidMethodPolicy; + final ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy; + final private boolean recordAware; final Version incompatibleImprovements; /** See {@link #getHasSharedInstanceRestrictions()} */ @@ -192,6 +197,14 @@ class ClassIntrospector { this.methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner(); this.methodSorter = builder.getMethodSorter(); this.treatDefaultMethodsAsBeanMembers = builder.getTreatDefaultMethodsAsBeanMembers(); + this.nonRecordZeroArgumentNonVoidMethodPolicy = builder.getNonRecordZeroArgumentNonVoidMethodPolicy(); + this.recordZeroArgumentNonVoidMethodPolicy = builder.getRecordZeroArgumentNonVoidMethodPolicy(); + this.recordAware = nonRecordZeroArgumentNonVoidMethodPolicy != recordZeroArgumentNonVoidMethodPolicy; + if (recordAware && _JavaVersions.JAVA_16 == null) { + throw new IllegalArgumentException( + "nonRecordZeroArgumentNonVoidMethodPolicy != recordZeroArgumentNonVoidMethodPolicy, " + + "but Java 16 support is not available."); + } this.incompatibleImprovements = builder.getIncompatibleImprovements(); this.sharedLock = sharedLock; @@ -329,13 +342,26 @@ class ClassIntrospector { Map<ExecutableMemberSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy effClassMemberAccessPolicy) throws IntrospectionException { BeanInfo beanInfo = Introspector.getBeanInfo(clazz); + + boolean treatClassAsRecord = recordAware && _JavaVersions.JAVA_16.isRecord(clazz); + ZeroArgumentNonVoidMethodPolicy zeroArgumentNonVoidMethodPolicy = treatClassAsRecord + ? recordZeroArgumentNonVoidMethodPolicy + : nonRecordZeroArgumentNonVoidMethodPolicy; + + // For real Java Beans properties only, used to exclude them from creating fake properties based on ZeroArgumentNonVoidMethod. + Set<String> beanPropertyReadMethodNameCollector = zeroArgumentNonVoidMethodPolicy != ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY + ? new HashSet<String>() + : null; + List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz); int pdasLength = pdas.size(); // Reverse order shouldn't mater, but we keep it to not risk backward incompatibility. for (int i = pdasLength - 1; i >= 0; --i) { addPropertyDescriptorToClassIntrospectionData( - introspData, pdas.get(i), - accessibleMethods, effClassMemberAccessPolicy); + introspData, pdas.get(i), false, + accessibleMethods, + beanPropertyReadMethodNameCollector, + effClassMemberAccessPolicy); } if (exposureLevel < BeansWrapper.EXPOSE_PROPERTIES_ONLY) { @@ -348,7 +374,11 @@ class ClassIntrospector { for (int i = mdsSize - 1; i >= 0; --i) { final Method method = getMatchingAccessibleMethod(mds.get(i).getMethod(), accessibleMethods); if (method != null && effClassMemberAccessPolicy.isMethodExposed(method)) { - decision.setDefaults(method); + ZeroArgumentNonVoidMethodPolicy appliedZeroArgumentNonVoidMethodPolicy = + getAppliedZeroArgumentNonVoidMethodPolicy( + method, beanPropertyReadMethodNameCollector, zeroArgumentNonVoidMethodPolicy); + + decision.setDefaults(method, appliedZeroArgumentNonVoidMethodPolicy); if (methodAppearanceFineTuner != null) { if (decisionInput == null) { decisionInput = new MethodAppearanceDecisionInput(); @@ -359,24 +389,31 @@ class ClassIntrospector { methodAppearanceFineTuner.process(decisionInput, decision); } + String exposedMethodName = decision.getExposeMethodAs(); + PropertyDescriptor propDesc = decision.getExposeAsProperty(); if (propDesc != null && (decision.getReplaceExistingProperty() || !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor))) { + boolean methodInsteadOfPropertyValueBeforeCall = decision.isMethodInsteadOfPropertyValueBeforeCall(); addPropertyDescriptorToClassIntrospectionData( - introspData, propDesc, accessibleMethods, effClassMemberAccessPolicy); + introspData, propDesc, methodInsteadOfPropertyValueBeforeCall, + accessibleMethods, null, effClassMemberAccessPolicy); + if (methodInsteadOfPropertyValueBeforeCall + && exposedMethodName != null && exposedMethodName.equals(propDesc.getName())) { + exposedMethodName = null; // We have already exposed this as property with the method name + } } - String methodKey = decision.getExposeMethodAs(); - if (methodKey != null) { - Object previous = introspData.get(methodKey); + if (exposedMethodName != null) { + Object previous = introspData.get(exposedMethodName); if (previous instanceof Method) { // Overloaded method - replace Method with a OverloadedMethods OverloadedMethods overloadedMethods = new OverloadedMethods(is2321Bugfixed()); overloadedMethods.addMethod((Method) previous); overloadedMethods.addMethod(method); - introspData.put(methodKey, overloadedMethods); + introspData.put(exposedMethodName, overloadedMethods); // Remove parameter type information (unless an indexed property reader needs it): if (argTypesUsedByIndexerPropReaders == null || !argTypesUsedByIndexerPropReaders.containsKey(previous)) { @@ -388,7 +425,7 @@ class ClassIntrospector { } else if (decision.getMethodShadowsProperty() || !(previous instanceof FastPropertyDescriptor)) { // Simple method (so far) - introspData.put(methodKey, method); + introspData.put(exposedMethodName, method); Class<?>[] replaced = getArgTypesByMethod(introspData).put(method, method.getParameterTypes()); if (replaced != null) { @@ -404,6 +441,18 @@ class ClassIntrospector { } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY) } + private static ZeroArgumentNonVoidMethodPolicy getAppliedZeroArgumentNonVoidMethodPolicy(Method method, Set<String> beanPropertyReadMethodNameCollector, ZeroArgumentNonVoidMethodPolicy zeroArgumentNonVoidMethodPolicy) { + if (method.getParameterCount() == 0 && method.getReturnType() != void.class) { + if (beanPropertyReadMethodNameCollector != null && beanPropertyReadMethodNameCollector.contains(method.getName())) { + return ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY; + } else { + return zeroArgumentNonVoidMethodPolicy; + } + } else { + return null; + } + } + /** * Very similar to {@link BeanInfo#getPropertyDescriptors()}, but can deal with Java 8 default methods too. */ @@ -673,8 +722,9 @@ class ClassIntrospector { } private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData, - PropertyDescriptor pd, + PropertyDescriptor pd, boolean methodInsteadOfPropertyValueBeforeCall, Map<ExecutableMemberSignature, List<Method>> accessibleMethods, + Set<String> beanPropertyReadMethodNameCollector, ClassMemberAccessPolicy effClassMemberAccessPolicy) { Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods); if (readMethod != null && !effClassMemberAccessPolicy.isMethodExposed(readMethod)) { @@ -697,7 +747,13 @@ class ClassIntrospector { } if (readMethod != null || indexedReadMethod != null) { - introspData.put(pd.getName(), new FastPropertyDescriptor(readMethod, indexedReadMethod)); + introspData.put(pd.getName(), new FastPropertyDescriptor( + readMethod, indexedReadMethod, + methodInsteadOfPropertyValueBeforeCall)); + } + + if (readMethod != null && beanPropertyReadMethodNameCollector != null) { + beanPropertyReadMethodNameCollector.add(readMethod.getName()); } } @@ -1076,6 +1132,14 @@ class ClassIntrospector { return treatDefaultMethodsAsBeanMembers; } + ZeroArgumentNonVoidMethodPolicy getNonRecordZeroArgumentNonVoidMethodPolicy() { + return nonRecordZeroArgumentNonVoidMethodPolicy; + } + + ZeroArgumentNonVoidMethodPolicy getRecordZeroArgumentNonVoidMethodPolicy() { + return recordZeroArgumentNonVoidMethodPolicy; + } + MethodAppearanceFineTuner getMethodAppearanceFineTuner() { return methodAppearanceFineTuner; } diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java index 76e42318..24ad2735 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import freemarker.core._JavaVersions; import freemarker.template.Configuration; import freemarker.template.Version; import freemarker.template._TemplateAPI; @@ -46,6 +47,8 @@ final class ClassIntrospectorBuilder implements Cloneable { private boolean exposeFields; private MemberAccessPolicy memberAccessPolicy; private boolean treatDefaultMethodsAsBeanMembers; + private ZeroArgumentNonVoidMethodPolicy nonRecordZeroArgumentNonVoidMethodPolicy; + private ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy; private MethodAppearanceFineTuner methodAppearanceFineTuner; private MethodSorter methodSorter; // Attention: @@ -60,6 +63,8 @@ final class ClassIntrospectorBuilder implements Cloneable { exposeFields = ci.exposeFields; memberAccessPolicy = ci.memberAccessPolicy; treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers; + nonRecordZeroArgumentNonVoidMethodPolicy = ci.nonRecordZeroArgumentNonVoidMethodPolicy; + recordZeroArgumentNonVoidMethodPolicy = ci.recordZeroArgumentNonVoidMethodPolicy; methodAppearanceFineTuner = ci.methodAppearanceFineTuner; methodSorter = ci.methodSorter; } @@ -69,15 +74,18 @@ final class ClassIntrospectorBuilder implements Cloneable { // change in the BeansWrapper.normalizeIncompatibleImprovements results. That is, this class may don't react // to some version changes that affects BeansWrapper, but not the other way around. this.incompatibleImprovements = normalizeIncompatibleImprovementsVersion(incompatibleImprovements); - treatDefaultMethodsAsBeanMembers - = incompatibleImprovements.intValue() >= _VersionInts.V_2_3_26; + treatDefaultMethodsAsBeanMembers = incompatibleImprovements.intValue() >= _VersionInts.V_2_3_26; + nonRecordZeroArgumentNonVoidMethodPolicy = ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY; + recordZeroArgumentNonVoidMethodPolicy = incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 && _JavaVersions.JAVA_16 != null + ? ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD : ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY; memberAccessPolicy = DefaultMemberAccessPolicy.getInstance(this.incompatibleImprovements); } private static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) { _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements); // All breakpoints here must occur in BeansWrapper.normalizeIncompatibleImprovements! - return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_30 ? Configuration.VERSION_2_3_30 + return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 ? Configuration.VERSION_2_3_33 + : incompatibleImprovements.intValue() >= _VersionInts.V_2_3_30 ? Configuration.VERSION_2_3_30 : incompatibleImprovements.intValue() >= _VersionInts.V_2_3_21 ? Configuration.VERSION_2_3_21 : Configuration.VERSION_2_3_0; } @@ -98,6 +106,8 @@ final class ClassIntrospectorBuilder implements Cloneable { result = prime * result + incompatibleImprovements.hashCode(); result = prime * result + (exposeFields ? 1231 : 1237); result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 1237); + result = prime * result + nonRecordZeroArgumentNonVoidMethodPolicy.hashCode(); + result = prime * result + recordZeroArgumentNonVoidMethodPolicy.hashCode(); result = prime * result + exposureLevel; result = prime * result + memberAccessPolicy.hashCode(); result = prime * result + System.identityHashCode(methodAppearanceFineTuner); @@ -115,6 +125,8 @@ final class ClassIntrospectorBuilder implements Cloneable { if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false; if (exposeFields != other.exposeFields) return false; if (treatDefaultMethodsAsBeanMembers != other.treatDefaultMethodsAsBeanMembers) return false; + if (nonRecordZeroArgumentNonVoidMethodPolicy != other.nonRecordZeroArgumentNonVoidMethodPolicy) return false; + if (recordZeroArgumentNonVoidMethodPolicy != other.recordZeroArgumentNonVoidMethodPolicy) return false; if (exposureLevel != other.exposureLevel) return false; if (!memberAccessPolicy.equals(other.memberAccessPolicy)) return false; if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false; @@ -153,6 +165,36 @@ final class ClassIntrospectorBuilder implements Cloneable { this.treatDefaultMethodsAsBeanMembers = treatDefaultMethodsAsBeanMembers; } + /** + * @since 2.3.33 + */ + public ZeroArgumentNonVoidMethodPolicy getNonRecordZeroArgumentNonVoidMethodPolicy() { + return nonRecordZeroArgumentNonVoidMethodPolicy; + } + + /** + * @since 2.3.33 + */ + public void setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy nonRecordZeroArgumentNonVoidMethodPolicy) { + NullArgumentException.check(nonRecordZeroArgumentNonVoidMethodPolicy); + this.nonRecordZeroArgumentNonVoidMethodPolicy = nonRecordZeroArgumentNonVoidMethodPolicy; + } + + /** + * @since 2.3.33 + */ + public ZeroArgumentNonVoidMethodPolicy getRecordZeroArgumentNonVoidMethodPolicy() { + return recordZeroArgumentNonVoidMethodPolicy; + } + + /** + * @since 2.3.33 + */ + public void setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy) { + NullArgumentException.check(recordZeroArgumentNonVoidMethodPolicy); + this.recordZeroArgumentNonVoidMethodPolicy = recordZeroArgumentNonVoidMethodPolicy; + } + public MemberAccessPolicy getMemberAccessPolicy() { return memberAccessPolicy; } diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java b/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java index 12d43de1..7ae8b674 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java @@ -29,10 +29,13 @@ import java.lang.reflect.Method; final class FastPropertyDescriptor { private final Method readMethod; private final Method indexedReadMethod; - - public FastPropertyDescriptor(Method readMethod, Method indexedReadMethod) { + private final boolean methodInsteadOfPropertyValueBeforeCall; + + public FastPropertyDescriptor( + Method readMethod, Method indexedReadMethod, boolean methodInsteadOfPropertyValueBeforeCall) { this.readMethod = readMethod; this.indexedReadMethod = indexedReadMethod; + this.methodInsteadOfPropertyValueBeforeCall = methodInsteadOfPropertyValueBeforeCall; } public Method getReadMethod() { @@ -42,5 +45,14 @@ final class FastPropertyDescriptor { public Method getIndexedReadMethod() { return indexedReadMethod; } - + + /** + * If this is true, and the property value is referred directly before it's called in a template, then + * the instead of the property value, it the value should be the read method (which therefore will be called). + * + * @since 2.3.33 + */ + public boolean isMethodInsteadOfPropertyValueBeforeCall() { + return methodInsteadOfPropertyValueBeforeCall; + } } diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/GenericObjectModel.java b/freemarker-core/src/main/java/freemarker/ext/beans/GenericObjectModel.java new file mode 100644 index 00000000..bb1723f2 --- /dev/null +++ b/freemarker-core/src/main/java/freemarker/ext/beans/GenericObjectModel.java @@ -0,0 +1,72 @@ +/* + * 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.util.Collection; +import java.util.Map; + +import freemarker.ext.util.ModelFactory; +import freemarker.template.MethodCallAwareTemplateHashModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +/** + * This is used for wrapping objects that has no special treatment (unlike {@link Map}-s, {@link Collection}-s, + * {@link Number}-s, {@link Boolean}-s, and some more, which have), hence they are just "generic" Java + * objects. Users usually just want to call the public Java methods on such objects. + * These objects can also be used as string values in templates, and that value is provided by + * the {@link Object#toString()} method of the wrapped object. + * + * <p>This extends {@link StringModel} for backward compatibility, as now {@link BeansWrapper} returns instances of + * {@link GenericObjectModel} instead of {@link StringModel}-s, but user code may have {@code insteanceof StringModel}, + * or casing to {@link StringModel}. {@link StringModel} served the same purpose as this class, but didn't implement + * {@link MethodCallAwareTemplateHashModel}. + * + * @since 2.3.33 + */ +public class GenericObjectModel extends StringModel implements MethodCallAwareTemplateHashModel { + static final ModelFactory FACTORY = (object, wrapper) -> new GenericObjectModel(object, (BeansWrapper) wrapper); + + /** + * Creates a new model that wraps the specified object with BeanModel + scalar functionality. + * + * @param object + * the object to wrap into a model. + * @param wrapper + * the {@link BeansWrapper} associated with this model. Every model has to have an associated + * {@link BeansWrapper} instance. The model gains many attributes from its wrapper, including the caching + * behavior, method exposure level, method-over-item shadowing policy etc. + */ + public GenericObjectModel(Object object, BeansWrapper wrapper) { + super(object, wrapper); + } + + // Made this final, to ensure that users override get(key, boolean) instead. + @Override + public final TemplateModel get(String key) throws TemplateModelException { + return super.get(key); + } + + @Override + public TemplateModel getBeforeMethodCall(String key) throws TemplateModelException, + ShouldNotBeGetAsMethodException { + return super.getBeforeMethodCall(key); + } +} diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java b/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java index 98c6416c..bfa6bc9f 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java @@ -53,7 +53,10 @@ public interface MethodAppearanceFineTuner { * <li>Show the method with a different name in the data-model than its * real name by calling * {@link MethodAppearanceDecision#setExposeMethodAs(String)} - * with non-{@code null} parameter. + * with non-{@code null} parameter. Also, if set to {@code null}, the method won't be exposed. + * The default is the name of the method. Note that if {@code methodInsteadOfPropertyValueBeforeCall} is + * {@code true}, the method is not exposed if the method name set here is the same as the name of the property + * set for this method with {@link MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}. * <li>Create a fake JavaBean property for this method by calling * {@link MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}. * For example, if you have {@code int size()} in a class, but you @@ -76,6 +79,21 @@ public interface MethodAppearanceFineTuner { * of the same name was already assigned earlier, it won't be * replaced by the new one by default, however this can be changed with * {@link MethodAppearanceDecision#setReplaceExistingProperty(boolean)}. + * <li>If something is exposed as property via + * {@link MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)} (and not only because it's a real + * JavaBeans property), and you also want the property value to be accessible in templates as the return value + * of a 0-argument method of the same name, then call + * {@link MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)} with {@code true}. + * Here's an example to explain that. Let's say, you have a class that contains "public String name()", and you + * exposed that as a property via {@link MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}. So + * far, you can access the property value from templates as {@code user.name}, but {@code user.name()} will + * fail, saying that you try to call a {@code String} (because you apply the {@code ()} operator on the result + * of {@code user.name}). But with + * {@link MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)} {@code true}, + * both {@code user.name}, and {@code user.name()} will do the same. + * The default of this is influenced by + * {@link BeansWrapperConfiguration#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}, + * {@link BeansWrapperConfiguration#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}. * <li>Prevent the method to hide a JavaBeans property (fake or real) of * the same name by calling * {@link MethodAppearanceDecision#setMethodShadowsProperty(boolean)} diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java b/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java index b53872d0..7936fcab 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java @@ -20,25 +20,20 @@ package freemarker.ext.beans; import freemarker.ext.util.ModelFactory; -import freemarker.template.ObjectWrapper; -import freemarker.template.TemplateModel; +import freemarker.template.MethodCallAwareTemplateHashModel; import freemarker.template.TemplateScalarModel; /** * Subclass of {@link BeanModel} that exposes the return value of the {@link * java.lang.Object#toString()} method through the {@link TemplateScalarModel} * interface. + * + * @deprecated Use {@link GenericObjectModel} instead, which implements {@link MethodCallAwareTemplateHashModel}. */ +@Deprecated public class StringModel extends BeanModel implements TemplateScalarModel { - static final ModelFactory FACTORY = - new ModelFactory() - { - @Override - public TemplateModel create(Object object, ObjectWrapper wrapper) { - return new StringModel(object, (BeansWrapper) wrapper); - } - }; + static final ModelFactory FACTORY = (object, wrapper) -> new StringModel(object, (BeansWrapper) wrapper); // Package visible for testing static final String TO_STRING_NOT_EXPOSED = "[toString not exposed]"; diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/ZeroArgumentNonVoidMethodPolicy.java b/freemarker-core/src/main/java/freemarker/ext/beans/ZeroArgumentNonVoidMethodPolicy.java new file mode 100644 index 00000000..85f2390a --- /dev/null +++ b/freemarker-core/src/main/java/freemarker/ext/beans/ZeroArgumentNonVoidMethodPolicy.java @@ -0,0 +1,65 @@ +/* + * 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 freemarker.template.DefaultObjectWrapper; + +/** + * How to show 0 argument non-void public methods to templates, which are not standard Java Beans read methods. + * Used in {@link BeansWrapper}, and therefore in {@link DefaultObjectWrapper}. + * This policy doesn't apply to methods that Java Beans introspector discovers as a property read method (which + * typically look like {@code getSomething()}/{@code getSomething()}). It's only applicable to methods like + * {@code something()}, including the component read methods of Java records. + * + * @see BeansWrapperConfiguration#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy) + * @see BeansWrapperConfiguration#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy) + * @see BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean) + * + * @since 2.3.33 + */ +public enum ZeroArgumentNonVoidMethodPolicy { + + /** + * Both {@code obj.m}, and {@code obj.m()} gives back the value that the {@code m} Java method returns, and it's + * not possible to get the method itself. + * + * <p>This is a parse-time trick that only works when the result of the dot operator is called immediately in a + * template (and therefore the dot operator knows that you will call the result of it). The practical reason for + * this feature is that the convention of having {@code SomeType something()} instead of + * {@code SomeType getSomething()} spreads in the Java ecosystem (and is a standard in some other JVM languages), + * and thus we can't tell anymore if {@code SomeType something()} just reads a value, and hence should be accessed + * like {@code obj.something}, or it's more like an operation that has side effect, and therefore should be + * accessed like {@code obj.something()}. So with be allowing both, the template author is free to decide which is + * the more fitting. Also, for accessing Java records components, the proper way is {@code obj.something}, but + * before FreeMarker was aware of records (and hence that those methods are like property read methods), the + * only way that worked was {@code obj.something()}, so to be more backward compatible, we have to support both. + */ + BOTH_PROPERTY_AND_METHOD, + + /** + * Only {@code obj.m()} gives back the value, {@code obj.m} just gives the method itself. + */ + METHOD_ONLY, + + /** + * {@code obj.m} in gives back the value, and the method itself can't be get. + */ + PROPERTY_ONLY +} diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java b/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java index 64d9797e..a8a6c775 100644 --- a/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java +++ b/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java @@ -116,7 +116,7 @@ public class _BeansAPI { packedArgs = new Object[fixedArgCnt + 1]; for (int i = 0; i < fixedArgCnt; i++) { - packedArgs[i] = args[i]; +packedArgs[i] = args[i]; } final Class<?> compType = paramTypes[fixedArgCnt].getComponentType(); @@ -226,5 +226,5 @@ public class _BeansAPI { public static ClassIntrospectorBuilder getClassIntrospectorBuilder(BeansWrapperConfiguration bwc) { return bwc.getClassIntrospectorBuilder(); } - + } diff --git a/freemarker-core/src/main/java/freemarker/template/MethodCallAwareTemplateHashModel.java b/freemarker-core/src/main/java/freemarker/template/MethodCallAwareTemplateHashModel.java new file mode 100644 index 00000000..6c4912e2 --- /dev/null +++ b/freemarker-core/src/main/java/freemarker/template/MethodCallAwareTemplateHashModel.java @@ -0,0 +1,124 @@ +/* + * 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.template; + +import java.util.Collection; +import java.util.Map; + +import freemarker.core.Macro; +import freemarker.core.NonMethodException; +import freemarker.ext.beans.BeansWrapper; +import freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy; +import freemarker.template.utility.NullArgumentException; + +/** + * Adds a getter method to {@link TemplateHashModel}, that can return different result than {@link #get(String)}, + * knowing that the result of it will be called as a method. At least as of 2.3.33, this is only utilized by the + * template language for 0-argument method calls directly after the dot operator and the key. For example, if in the + * template you have {@code someRecord.someComponent()}, and there {@code someRecord} was wrapped by the + * {@link ObjectWrapper} into a {@link TemplateHashModel} that also implements this interface, then the dot operator + * will call {@link #getBeforeMethodCall(String) getBeforeMethodCall("someComponent")}, rather than + * {@link #get(String) get("someComponent")}. This is needed to implement subtle features like + * {@link BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}, + * which is needed to implement {@link ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}. + * + * <p>While technically we could do the same for method calls with more the 0 arguments, as of 2.3.33 at least we + * don't want to generalize this to that case. The FreeMarker 2.x template language doesn't have separated namespace for + * methods, so this is already a hack as is, but we had to address the issue with Java records (see that at + * {@link BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}). + * + * <p>Objects wrapped with {@link BeansWrapper}, and hence with {@link DefaultObjectWrapper}, will implement this + * interface, when they are "generic" objects (that is, when they are not classes with special wrapper, like + * {@link Map}-s, {@link Collection}-s, {@link Number}-s, etc.). + * + * @since 2.3.33 + */ +public interface MethodCallAwareTemplateHashModel extends TemplateHashModel { + + /** + * This is called instead of {@link #get(String)}, if we know that the return value should be callable like a + * method. The advantage of this is that we can coerce the value to a method when desirable, and otherwise can give + * a more specific error message in the resulting exception than the standard {@link NonMethodException} would. + * + * @param key + * Same as for {@link #get(String)} + * + * @return + * Same as for just like {@link #get(String)}, except it should return a + * {@link TemplateMethodModelEx}, or a {@link TemplateMethodModel}, or in very rare case a {@link Macro} + * that was created with the {@code function} directive. Or, {@code null} in the same case as + * {@link #get(String)}. The method should never return something that's not callable in the template language + * as a method or function. + * + * @throws ShouldNotBeGetAsMethodException + * If the value for the given key exists, but it shouldn't be coerced something callable as a method. This will + * be converted to {@link NonMethodException} by the engine, but in this exception you can optionally give a + * more specific explanation, and that will be added to the resulting {@link NonMethodException} as a hint to + * the user. + */ + TemplateModel getBeforeMethodCall(String key) + throws TemplateModelException, ShouldNotBeGetAsMethodException; + + /** + * Thrown by {@link #getBeforeMethodCall(String)}; see there. + */ + final class ShouldNotBeGetAsMethodException extends Exception { + private final TemplateModel actualValue; + private final String hint; + + /** + * Same as {@link ShouldNotBeGetAsMethodException(TemplateModel, String, Throwable)}, with {@code null} + * cause exception argument. + */ + public ShouldNotBeGetAsMethodException(TemplateModel actualValue, String hint) { + this(actualValue, hint, null); + } + + /** + * @param actualValue + * The actual value we got instead of a method; can't be {@code null}! + * @param hint + * Hint for the user, that's added to the error message; {@code null} if you just want the plain + * {@link NonMethodException} error message. + * @param cause + * Can be {@code null}. + */ + public ShouldNotBeGetAsMethodException(TemplateModel actualValue, String hint, Throwable cause) { + super(null, cause, true, false); + NullArgumentException.check(actualValue); + this.actualValue = actualValue; + this.hint = hint; + } + + /** + * The actual value we got instead of a method; not {@code null}. + */ + public TemplateModel getActualValue() { + return actualValue; + } + + /** + * Additional hint for the user; maybe {@code null}. + */ + public String getHint() { + return hint; + } + } +} diff --git a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj index f4390862..e4950431 100644 --- a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj +++ b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj @@ -2467,6 +2467,13 @@ MethodCall MethodArgs(Expression exp) : end = <CLOSE_PAREN> { args.trimToSize(); + if (args.isEmpty()) { + if (exp instanceof Dot) { + exp = new DotBeforeMethodCall((Dot) exp); + } else if (exp instanceof DynamicKeyName) { + exp = new DynamicKeyNameBeforeMethodCall((DynamicKeyName) exp); + } + } MethodCall result = new MethodCall(exp, args); result.setLocation(template, exp, end); return result; diff --git a/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java b/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java index 6a08ea91..4f4e6f94 100644 --- a/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java +++ b/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java @@ -90,10 +90,10 @@ import freemarker.core.XMLOutputFormat; import freemarker.core.XSCFormat; import freemarker.core._CoreStringUtils; import freemarker.ext.beans.BeansWrapperBuilder; +import freemarker.ext.beans.GenericObjectModel; import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy; import freemarker.ext.beans.MemberAccessPolicy; import freemarker.ext.beans.MemberSelectorListMemberAccessPolicy; -import freemarker.ext.beans.StringModel; import freemarker.ext.beans.WhitelistMemberAccessPolicy; import freemarker.template.utility.DateUtil; import freemarker.template.utility.NullArgumentException; @@ -1316,7 +1316,7 @@ public class ConfigurationTest extends TestCase { { TemplateScalarModel aVal = (TemplateScalarModel) cfg.getSharedVariable("a"); assertEquals("aa", aVal.getAsString()); - assertEquals(StringModel.class, aVal.getClass()); + assertEquals(GenericObjectModel.class, aVal.getClass()); TemplateScalarModel bVal = (TemplateScalarModel) cfg.getSharedVariable("b"); assertEquals("bbLegacy", bVal.getAsString()); diff --git a/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java b/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java new file mode 100644 index 00000000..56b80d53 --- /dev/null +++ b/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java @@ -0,0 +1,352 @@ +/* + * 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 static freemarker.template.Configuration.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import org.junit.Test; + +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.ObjectWrapper; +import freemarker.template.SimpleHash; +import freemarker.template.TemplateException; +import freemarker.test.TemplateTest; + +public class TestZeroArgumentNonVoidMethodPolicy extends TemplateTest { + private static final Pattern DOT_REPLACE_PATTERN = Pattern.compile("\\.(\\w+)"); + + private static String withDotOrSquareBracket(String s, boolean dot) { + if (dot) { + return s; + } + return DOT_REPLACE_PATTERN.matcher(s).replaceFirst(key -> "['" + key.group(1) + "']"); + } + + @Override + protected Configuration createConfiguration() throws Exception { + Configuration cfg = super.createConfiguration(); + // Don't use default, as then the object wrapper is a shared static mutable object: + cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_32); + return cfg; + } + + @Test + public void testDefaultWithHighIncompatibleImprovements() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)){ + setupDataModel( + () -> new DefaultObjectWrapper(VERSION_2_3_33), + cacheTopLevelVars); + assertRecIsBothPropertyAndMethod(); + assertNrcIsMethodOnly(); + } + } + + @Test + public void testDefaultWithLowIncompatibleImprovements() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)) { + setupDataModel( + () -> new DefaultObjectWrapper(VERSION_2_3_32), + cacheTopLevelVars); + assertRecIsMethodOnly(); + assertNrcIsMethodOnly(); + } + } + + @Test + public void testDefaultWithLowIncompatibleImprovements2() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)) { + setupDataModel( + () -> { + DefaultObjectWrapper beansWrapper = new DefaultObjectWrapper(VERSION_2_3_32); + beansWrapper.setRecordZeroArgumentNonVoidMethodPolicy( + ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD); + return beansWrapper; + }, + cacheTopLevelVars); + assertRecIsBothPropertyAndMethod(); + assertNrcIsMethodOnly(); + } + } + + @Test + public void testDefaultWithRecordsPropertyOnly() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)) { + setupDataModel( + () -> { + DefaultObjectWrapper beansWrapper = new DefaultObjectWrapper(VERSION_2_3_32); + beansWrapper.setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY); + return beansWrapper; + }, + cacheTopLevelVars); + assertRecIsPropertyOnly(); + assertNrcIsMethodOnly(); + } + } + + @Test + public void testDefaultWithRecordsPropertyOnly2() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)) { + setupDataModel( + () -> { + DefaultObjectWrapper beansWrapper = new DefaultObjectWrapper(VERSION_2_3_33); + beansWrapper.setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY); + return beansWrapper; + }, + cacheTopLevelVars); + assertRecIsPropertyOnly(); + assertNrcIsMethodOnly(); + } + } + + @Test + public void testDefaultWithNonRecordsPropertyOnly() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)) { + setupDataModel( + () -> { + DefaultObjectWrapper beansWrapper = new DefaultObjectWrapper(VERSION_2_3_32); + beansWrapper.setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY); + return beansWrapper; + }, + cacheTopLevelVars); + assertRecIsMethodOnly(); + assertNrcIsPropertyOnly(); + } + } + + @Test + public void testDefaultWithBothPropertyAndMethod() throws TemplateException, IOException { + for (boolean cacheTopLevelVars : List.of(true, false)) { + setupDataModel( + () -> { + DefaultObjectWrapper beansWrapper = new DefaultObjectWrapper(VERSION_2_3_33); + beansWrapper.setNonRecordZeroArgumentNonVoidMethodPolicy( + ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD); + return beansWrapper; + }, + cacheTopLevelVars); + assertRecIsBothPropertyAndMethod(); + assertNrcIsBothPropertyAndMethod(); + } + } + + @Test + public void testSettings() throws TemplateException, IOException { + getConfiguration().setSetting( + "objectWrapper", + "DefaultObjectWrapper(2.3.33, nonRecordZeroArgumentNonVoidMethodPolicy=freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD)"); + setupDataModel(() -> getConfiguration().getObjectWrapper(), false); + assertRecIsBothPropertyAndMethod(); + assertNrcIsBothPropertyAndMethod(); + } + + private void setupDataModel(Supplier<? extends ObjectWrapper> objectWrapperSupplier, boolean cacheTopLevelVars) { + ObjectWrapper objectWrapper = objectWrapperSupplier.get(); + getConfiguration().setObjectWrapper(objectWrapper); + + setDataModel(cacheTopLevelVars ? new SimpleHash(objectWrapper) : new HashMap<>()); + + addToDataModel("rec", new TestRecord(1, "S")); + addToDataModel("nrc", new TestNonRecord()); + } + + private void assertRecIsBothPropertyAndMethod() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertOutput(withDotOrSquareBracket("${rec.x}", dot), "1"); + assertOutput(withDotOrSquareBracket("${rec.x()}", dot), "1"); + assertOutput(withDotOrSquareBracket("${rec.s}", dot), "S"); + assertOutput(withDotOrSquareBracket("${rec.s()}", dot), "S"); + assertOutput(withDotOrSquareBracket("${rec.y}", dot), "2"); + assertOutput(withDotOrSquareBracket("${rec.y()}", dot), "2"); + assertOutput(withDotOrSquareBracket("${rec.tenX}", dot), "10"); + assertOutput(withDotOrSquareBracket("${rec.tenX()}", dot), "10"); + } + assertRecPolicyIndependentMembers(); + } + + private void assertRecIsMethodOnly() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertErrorContains(withDotOrSquareBracket("${rec.x}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${rec.x()}", dot), "1"); + assertErrorContains(withDotOrSquareBracket("${rec.s}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${rec.s()}", dot), "S"); + assertErrorContains(withDotOrSquareBracket("${rec.y}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${rec.y()}", dot), "2"); + assertErrorContains(withDotOrSquareBracket("${rec.tenX}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${rec.tenX()}", dot), "10"); + } + assertRecPolicyIndependentMembers(); + } + + private void assertRecIsPropertyOnly() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertOutput(withDotOrSquareBracket("${rec.x}", dot), "1"); + assertErrorContains(withDotOrSquareBracket("${rec.x()}", dot), "SimpleNumber", "must not be called as a method"); + assertOutput(withDotOrSquareBracket("${rec.s}", dot), "S"); + assertErrorContains(withDotOrSquareBracket("${rec.s()}", dot), "SimpleScalar"); + assertOutput(withDotOrSquareBracket("${rec.y}", dot), "2"); + assertErrorContains(withDotOrSquareBracket("${rec.y()}", dot), "SimpleNumber"); + assertOutput(withDotOrSquareBracket("${rec.tenX}", dot), "10"); + assertErrorContains(withDotOrSquareBracket("${rec.tenX()}", dot), "SimpleNumber"); + } + assertRecPolicyIndependentMembers(); + } + + private void assertRecPolicyIndependentMembers() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertOutput(withDotOrSquareBracket("${rec.z}", dot), "3"); + assertErrorContains(withDotOrSquareBracket("${rec.z()}", dot), "SimpleNumber"); + assertOutput(withDotOrSquareBracket("${rec.getZ()}", dot), "3"); + assertOutput(withDotOrSquareBracket("${rec.xTimes(5)}", dot), "5"); + assertErrorContains(withDotOrSquareBracket("${rec.xTimes}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${rec.voidMethod()}", dot), ""); + assertErrorContains(withDotOrSquareBracket("${rec.voidMethod}", dot), "SimpleMethodModel"); + } + } + + private void assertNrcIsMethodOnly() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertErrorContains(withDotOrSquareBracket("${nrc.x}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${nrc.x()}", dot), "1"); + assertErrorContains(withDotOrSquareBracket("${nrc.y}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${nrc.y()}", dot), "2"); + assertErrorContains(withDotOrSquareBracket("${nrc.tenX}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${nrc.tenX()}", dot), "10"); + } + assertNrcPolicyIndependentMembers(); + } + + private void assertNrcIsBothPropertyAndMethod() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertOutput(withDotOrSquareBracket("${nrc.x}", dot), "1"); + assertOutput(withDotOrSquareBracket("${nrc.x()}", dot), "1"); + assertOutput(withDotOrSquareBracket("${nrc.y}", dot), "2"); + assertOutput(withDotOrSquareBracket("${nrc.y()}", dot), "2"); + assertOutput(withDotOrSquareBracket("${nrc.tenX}", dot), "10"); + assertOutput(withDotOrSquareBracket("${nrc.tenX()}", dot), "10"); + } + assertNrcPolicyIndependentMembers(); + } + + private void assertNrcIsPropertyOnly() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertOutput(withDotOrSquareBracket("${nrc.x}", dot), "1"); + assertErrorContains(withDotOrSquareBracket("${nrc.x()}", dot), "SimpleNumber", "must not be called as a method"); + assertOutput(withDotOrSquareBracket("${nrc.y}", dot), "2"); + assertErrorContains(withDotOrSquareBracket("${nrc.y()}", dot), "SimpleNumber"); + assertOutput(withDotOrSquareBracket("${nrc.tenX}", dot), "10"); + assertErrorContains(withDotOrSquareBracket("${nrc.tenX()}", dot), "SimpleNumber"); + } + assertNrcPolicyIndependentMembers(); + } + + private void assertNrcPolicyIndependentMembers() throws IOException, TemplateException { + for (boolean dot : List.of(true, false)) { + assertOutput(withDotOrSquareBracket("${nrc.z}", dot), "3"); + assertErrorContains(withDotOrSquareBracket("${nrc.z()}", dot), "SimpleNumber"); + assertOutput(withDotOrSquareBracket("${nrc.getZ()}", dot), "3"); + assertOutput(withDotOrSquareBracket("${nrc.xTimes(5)}", dot), "5"); + assertErrorContains(withDotOrSquareBracket("${nrc.xTimes}", dot), "SimpleMethodModel"); + assertOutput(withDotOrSquareBracket("${nrc.voidMethod()}", dot), ""); + assertErrorContains(withDotOrSquareBracket("${nrc.voidMethod}", dot), "SimpleMethodModel"); + } + } + + public interface TestInterface { + int y(); + + /** + * Defines a real JavaBeans property, "z", so the {@link ZeroArgumentNonVoidMethodPolicy} shouldn't affect this + */ + int getZ(); + } + + /** + * Defines record component readers for "x" and "s", and some other non-record-component methods that are still + * potentially exposed as if there were properties. + */ + public record TestRecord(int x, String s) implements TestInterface { + @Override + public int y() { + return 2; + } + + @Override + public int getZ() { + return 3; + } + + public int tenX() { + return x * 10; + } + + /** + * Has an argument, so this never should be exposed as property. + */ + public int xTimes(int m) { + return x * m; + } + + /** + * Has a void return type, so this never should be exposed as property. + */ + public void voidMethod() { + // do nothing + } + } + + public static class TestNonRecord implements TestInterface { + public int x() { + return 1; + } + + @Override + public int y() { + return 2; + } + + @Override + public int getZ() { + return 3; + } + + public int tenX() { + return x() * 10; + } + + public int xTimes(int m) { + return x() * m; + } + + /** + * Has a void return type, so this never should be exposed as property. + */ + public void voidMethod() { + // do nothing + } + } + +} diff --git a/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java index d67d1950..51152998 100644 --- a/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java +++ b/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java @@ -104,7 +104,7 @@ public class DefaultObjectWrapperTest { 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_27); // no non-BC change in 2.3.33 + expected.add(Configuration.VERSION_2_3_33); List<Version> actual = new ArrayList<>(); for (int i = _VersionInts.V_2_3_0; i <= Configuration.getVersion().intValue(); i++) { @@ -383,7 +383,7 @@ public class DefaultObjectWrapperTest { assertTrue(ow.getUseAdaptersForContainers()); assertTrue(ow.getForceLegacyNonListCollections()); } - + try { new DefaultObjectWrapper(new Version(99, 9, 9)); fail(); diff --git a/freemarker-manual/src/main/docgen/en_US/book.xml b/freemarker-manual/src/main/docgen/en_US/book.xml index 06fa9f9e..56750583 100644 --- a/freemarker-manual/src/main/docgen/en_US/book.xml +++ b/freemarker-manual/src/main/docgen/en_US/book.xml @@ -20,7 +20,10 @@ <book conformance="docgen" version="5.0" xml:lang="en" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" -> + xmlns:ns5="http://www.w3.org/1999/xhtml" + xmlns:ns4="http://www.w3.org/2000/svg" + xmlns:ns3="http://www.w3.org/1998/Math/MathML" + xmlns:ns="http://docbook.org/ns/docbook"> <info> <title>Apache FreeMarker Manual</title> @@ -30126,6 +30129,18 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <title>Changes on the FTL side</title> <itemizedlist> + <listitem> + <para><link + xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-183">FREEMARKER-183</link>: + If FreeMarker is configured like so, values in Java records can + now be referred like <literal>obj.price</literal>, instead of + like <literal>obj.price()</literal>. Furthermore, FreeMarker can + now be configured to allow this for all 0-argument + non-<literal>void</literal> methods. See more details in the + <link linkend="version_hisotry_freemarker_183_java_side">Changes + on the Java side</link> section below.</para> + </listitem> + <listitem> <para><link xlink:href="https://github.com/apache/freemarker/pull/87">GitHub @@ -30229,10 +30244,121 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </itemizedlist> </section> - <section> + <section xml:id="version_hisotry_freemarker_183_java_side"> <title>Changes on the Java side</title> <itemizedlist> + <listitem> + <para><link + xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-183">FREEMARKER-183</link>: + Better support for Java records, if you set the <link + linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal> + setting</link> to 2.3.33 or higher (or if you create your own + <literal>ObjectWrapper</literal>, then set its + <literal>incompatible_improvements</literal>, or just its + <literal>recordZeroArgumentNonVoidMethodPolicy</literal> + property to <literal>BOTH_PROPERTY_AND_METHOD</literal>). If in + a Java record you have something like <literal>int + price()</literal>, earlier you could only read the value in + templates as <literal>obj.price()</literal>. With this + improvement <literal>obj.price</literal> will do the same (and + similarly, <literal>obj["price"]()</literal>, and + <literal>obj["price"]</literal> will do the same). This has + always worked for JavaBeans properties, like <literal>int + getPrice()</literal> could always be used in templates as + <literal>obj.price</literal>, in additionally to as + <literal>obj.getPrice()</literal>. Now this also works for Java + records, as there we simply treat all methods that has 0 + arguments, and non-<literal>void</literal> return type as if it + was a JavaBean property read method. Except, here the name of + the method is exactly the same as the name of the faked + JavaBeans property (<literal>price</literal>), while with real + JavaBeans the read method name typically would be + <literal>getPrice</literal>, and the property name would be + <literal>price</literal> (so we have two separate names). There + are some strange technical tricks involved for the same name to + be usable in both ways, but as far as most users care, it just + works.</para> + + <para>Some more technical changes:</para> + + <itemizedlist> + <listitem> + <para>Added two new settings to + <literal>BeansWrapper</literal>, and therefore + <literal>DefaultObjectWrapper</literal>: + <literal>recordZeroArgumentNonVoidMethodPolicy</literal>, + and + <literal>nonRecordZeroArgumentNonVoidMethodPolicy</literal>. + Each has enum type + <literal>freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy</literal>, + that can be <literal>METHOD_ONLY</literal>, + <literal>PROPERTY_ONLY</literal>, or + <literal>BOTH_PROPERTY_AND_METHOD</literal>. + Therefore:</para> + + <itemizedlist> + <listitem> + <para>Note that with + <literal>nonRecordZeroArgumentNonVoidMethodPolicy</literal> + you can set similar behavior to non-records. That is, + you can call 0 argument non-void methods without + <literal>()</literal>, if you want. It's only meant to + be used for methods that are mere value readers, and has + no side effect.</para> + </listitem> + + <listitem> + <para>For records, you can enforce proper style with + setting + <literal>recordZeroArgumentNonVoidMethodPolicy</literal> + to <literal>PROPERTY_ONLY</literal>. The default with + <literal>incompatible_improvements</literal> 2.3.33 is + more lenient, as there using <literal>()</literal> is + allowed (for backward compatibility, and because people + often just use the Java syntax).</para> + </listitem> + </itemizedlist> + </listitem> + + <listitem> + <para>Added new interface, + <literal>freemarker.template.MethodCallAwareTemplateHashModel</literal>, + which adds <literal>getBeforeMethodCall(String + key)</literal>. If you have something like + <literal>obj.price()</literal> in a template, where + <literal>obj</literal> (after wrapping) implements that + interface, then + <literal>getBeforeMethodCall("price")</literal> called + instead of + <literal>TemplateHashModel.get("price")</literal>. This is + needed for + <literal>ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD</literal> + to work.</para> + </listitem> + + <listitem> + <para>Added <literal>GenericObjectModel</literal>, which + extends <literal>StringModel</literal> with implementing + <literal>MethodCallAwareTemplateHashModel</literal>, and has + a more telling name. <literal>BeansWrapper</literal>, and + therefore <literal>DefaultObjectWrapper</literal> now + creates <literal>GenericObjectModel</literal>-s instead of + <literal>StringModel</literal>-s. This is like so regardless + of any setting, like regardless of + <literal>incompatible_improvements</literal>.</para> + </listitem> + + <listitem> + <para>You shouldn't override + <literal>BeanModel.get(String)</literal> anymore, but + <literal>BeanModel.get(String, boolean)</literal>. If you + have overridden <literal>get</literal>, then see in the + Javadoc for more.</para> + </listitem> + </itemizedlist> + </listitem> + <listitem> <para><link xlink:href="https://github.com/apache/freemarker/pull/88">GitHub diff --git a/freemarker-test-utils/src/main/java/freemarker/test/TemplateTest.java b/freemarker-test-utils/src/main/java/freemarker/test/TemplateTest.java index a730b3e4..1ab7af45 100644 --- a/freemarker-test-utils/src/main/java/freemarker/test/TemplateTest.java +++ b/freemarker-test-utils/src/main/java/freemarker/test/TemplateTest.java @@ -42,6 +42,7 @@ import freemarker.cache.StringTemplateLoader; import freemarker.cache.TemplateLoader; import freemarker.core.ParseException; import freemarker.template.Configuration; +import freemarker.template.SimpleHash; import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.utility.StringUtil; @@ -146,7 +147,8 @@ public abstract class TemplateTest { protected String getOutput(Template t) throws TemplateException, IOException { StringWriter out = new StringWriter(); - t.process(getDataModel(), new FilterWriter(out) { + Object dataModelObject = getDataModel(); + t.process(dataModelObject, new FilterWriter(out) { private boolean closed; @Override @@ -197,6 +199,11 @@ public abstract class TemplateTest { } return dataModel; } + + protected void setDataModel(Object dataModel) { + this.dataModel = dataModel; + dataModelCreated = true; + } protected Object createDataModel() { return null; @@ -248,6 +255,7 @@ public abstract class TemplateTest { } } + @SuppressWarnings({"unchecked", "rawtypes"}) protected void addToDataModel(String name, Object value) { Object dm = getDataModel(); if (dm == null) { @@ -256,6 +264,9 @@ public abstract class TemplateTest { } if (dm instanceof Map) { ((Map) dm).put(name, value); + } else if (dm instanceof SimpleHash) { + // SimpleHash is interesting, as it caches the top-level TemplateDateModel-s + ((SimpleHash) dm).put(name, value); } else { throw new IllegalStateException("Can't add to non-Map data-model: " + dm); } @@ -289,7 +300,7 @@ public abstract class TemplateTest { t = new Template("adhoc", ftl, getConfiguration()); } t.process(getDataModel(), new StringWriter()); - fail("The tempalte had to fail"); + fail("The template had to fail"); return null; } catch (TemplateException e) { if (exceptionClass != null) {
