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 MethodCallAwareTemplateHashModel, and create that in BeansWrapper instead of StringModel.
diff --git a/build.gradle.kts b/build.gradle.kts
index 114ac08..82f8917 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -61,7 +61,7 @@
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 fc98db5..6eaf8ed 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 @@
* <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 54bae57..a2d2f27 100644
--- a/freemarker-core/src/main/java/freemarker/core/Dot.java
+++ b/freemarker-core/src/main/java/freemarker/core/Dot.java
@@ -27,20 +27,29 @@
* 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;
+ protected final String key;
Dot(Expression target, String key) {
this.target = target;
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,10 @@
throw new NonHashException(target, leftModel, env);
}
+ protected TemplateModel evalOnHash(TemplateHashModel leftModel) throws TemplateException {
+ return leftModel.get(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 0000000..17ae026
--- /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.ext.beans.ZeroArgumentNonVoidMethodPolicy;
+import freemarker.template.MethodCallAwareTemplateHashModel;
+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(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.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 f8cef3a..d2fa822 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 @@
* {@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 @@
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 @@
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 0000000..f9678ca
--- /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 2ece2a0..1eebbe5 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 @@
} 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 4580ac8..2bc875e 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 @@
*
* @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 @@
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 79110b9..c983ae0 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.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.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,21 +140,55 @@
* 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);
+ }
+ }
+
+ /**
+ * 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;
-
+
try {
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 +200,7 @@
}
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,7 +218,7 @@
retval = wrapper.wrap(null);
}
return retval;
- } catch (TemplateModelException e) {
+ } catch (TemplateModelException | MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) {
throw e;
} catch (Exception e) {
throw new _TemplateModelException(e,
@@ -189,6 +229,24 @@
}
}
+ /**
+ * 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 || result == null) {
+ return result;
+ }
+ throw new MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException(result, null);
+ }
+
private void logNoSuchKey(String key, Map<?, ?> keyMap) {
LOG.debug("Key " + StringUtil.jQuoteNoXSS(key) + " was not found on instance of " +
object.getClass().getName() + ". Introspection information for " +
@@ -203,8 +261,9 @@
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 +274,9 @@
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 +291,30 @@
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 be2455e..0e20266 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 @@
* 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 @@
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 @@
* 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 changed 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 @@
}
/**
- * 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 @@
}
} 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 @@
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 @@
classIntrospector = _BeansAPI.getClassIntrospectorBuilder(bwConf).build();
sharedIntrospectionLock = classIntrospector.getSharedLock();
}
-
+
falseModel = new BooleanModel(Boolean.FALSE, this);
trueModel = new BooleanModel(Boolean.TRUE, this);
@@ -633,7 +645,45 @@
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 this defaults to {@link ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}, otherwise this 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 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 @@
/**
* 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 @@
*/
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 @@
* <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 @@
if (clazz.isArray()) {
return ArrayModel.FACTORY;
}
- return StringModel.FACTORY;
+ return GenericObjectModel.FACTORY;
}
/**
@@ -1855,15 +1924,33 @@
*/
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 @@
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 5daaa90..a49fabb 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 @@
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 bf86781..bc4a5dd 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.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 @@
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 @@
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 @@
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 @@
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<>()
+ : 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 @@
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 @@
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 @@
} 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,16 @@
} // 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) {
+ return beanPropertyReadMethodNameCollector != null && beanPropertyReadMethodNameCollector.contains(method.getName())
+ ? ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY
+ : zeroArgumentNonVoidMethodPolicy;
+ } else {
+ return null;
+ }
+ }
+
/**
* Very similar to {@link BeanInfo#getPropertyDescriptors()}, but can deal with Java 8 default methods too.
*/
@@ -673,8 +720,9 @@
}
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 +745,13 @@
}
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 +1130,14 @@
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 76e4231..24ad273 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.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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 12d43de..d66f06d 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java
@@ -23,16 +23,20 @@
/**
* Used instead of {@link PropertyDescriptor}, because the methods of that are synchronized.
+ * Also, we use this for "fake" Java Beans properties too (see {@link BeansWrapper.MethodAppearanceDecision}).
*
* @since 2.3.27
*/
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 +46,14 @@
public Method getIndexedReadMethod() {
return indexedReadMethod;
}
-
+
+ /**
+ * If this is true, and the property value is referred directly before it's called in a template, then
+ * instead of the property value, the value should be the read method.
+ *
+ * @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 0000000..bb1723f
--- /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 98c6416..bfa6bc9 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 @@
* <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 @@
* 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 b53872d..7936fca 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 0000000..add78f0
--- /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()}, or {@code isSomething()}). 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 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 64d9797..a8a6c77 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 @@
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 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 0000000..a3e8056
--- /dev/null
+++ b/freemarker-core/src/main/java/freemarker/template/MethodCallAwareTemplateHashModel.java
@@ -0,0 +1,125 @@
+/*
+ * 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 an extra 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 non-void method calls directly after the dot operator and the key (like
+ * {@code obj.m()}), or for the equivalent with square brackets ({@code obj["m"]()}). 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 f439086..e495043 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 @@
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 6a08ea9..4f4e6f9 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.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 @@
{
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 0000000..ee834f8
--- /dev/null
+++ b/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java
@@ -0,0 +1,370 @@
+/*
+ * 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 {
+ @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);
+ cfg.setAPIBuiltinEnabled(true);
+ 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 (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertOutput(modifyTemplate("${rec.x}", tempMods), "1");
+ assertOutput(modifyTemplate("${rec.x()}", tempMods), "1");
+ assertOutput(modifyTemplate("${rec.s}", tempMods), "S");
+ assertOutput(modifyTemplate("${rec.s()}", tempMods), "S");
+ assertOutput(modifyTemplate("${rec.y}", tempMods), "2");
+ assertOutput(modifyTemplate("${rec.y()}", tempMods), "2");
+ assertOutput(modifyTemplate("${rec.tenX}", tempMods), "10");
+ assertOutput(modifyTemplate("${rec.tenX()}", tempMods), "10");
+ }
+ assertRecPolicyIndependentMembers();
+ }
+
+ private void assertRecIsMethodOnly() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertErrorContains(modifyTemplate("${rec.x}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${rec.x()}", tempMods), "1");
+ assertErrorContains(modifyTemplate("${rec.s}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${rec.s()}", tempMods), "S");
+ assertErrorContains(modifyTemplate("${rec.y}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${rec.y()}", tempMods), "2");
+ assertErrorContains(modifyTemplate("${rec.tenX}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${rec.tenX()}", tempMods), "10");
+ }
+ assertRecPolicyIndependentMembers();
+ }
+
+ private void assertRecIsPropertyOnly() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertOutput(modifyTemplate("${rec.x}", tempMods), "1");
+ assertErrorContains(modifyTemplate("${rec.x()}", tempMods), "SimpleNumber", "must not be called as a method");
+ assertOutput(modifyTemplate("${rec.s}", tempMods), "S");
+ assertErrorContains(modifyTemplate("${rec.s()}", tempMods), "SimpleScalar");
+ assertOutput(modifyTemplate("${rec.y}", tempMods), "2");
+ assertErrorContains(modifyTemplate("${rec.y()}", tempMods), "SimpleNumber");
+ assertOutput(modifyTemplate("${rec.tenX}", tempMods), "10");
+ assertErrorContains(modifyTemplate("${rec.tenX()}", tempMods), "SimpleNumber");
+ }
+ assertRecPolicyIndependentMembers();
+ }
+
+ private void assertRecPolicyIndependentMembers() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertOutput(modifyTemplate("${rec.z}", tempMods), "3");
+ assertErrorContains(modifyTemplate("${rec.z()}", tempMods), "SimpleNumber");
+ assertOutput(modifyTemplate("${rec.getZ()}", tempMods), "3");
+ assertOutput(modifyTemplate("${rec.xTimes(5)}", tempMods), "5");
+ assertErrorContains(modifyTemplate("${rec.xTimes}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${rec.voidMethod()}", tempMods), "");
+ assertErrorContains(modifyTemplate("${rec.voidMethod}", tempMods), "SimpleMethodModel");
+ }
+ }
+
+ private void assertNrcIsMethodOnly() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertErrorContains(modifyTemplate("${nrc.x}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${nrc.x()}", tempMods), "1");
+ assertErrorContains(modifyTemplate("${nrc.y}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${nrc.y()}", tempMods), "2");
+ assertErrorContains(modifyTemplate("${nrc.tenX}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${nrc.tenX()}", tempMods), "10");
+ }
+ assertNrcPolicyIndependentMembers();
+ }
+
+ private void assertNrcIsBothPropertyAndMethod() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertOutput(modifyTemplate("${nrc.x}", tempMods), "1");
+ assertOutput(modifyTemplate("${nrc.x()}", tempMods), "1");
+ assertOutput(modifyTemplate("${nrc.y}", tempMods), "2");
+ assertOutput(modifyTemplate("${nrc.y()}", tempMods), "2");
+ assertOutput(modifyTemplate("${nrc.tenX}", tempMods), "10");
+ assertOutput(modifyTemplate("${nrc.tenX()}", tempMods), "10");
+ }
+ assertNrcPolicyIndependentMembers();
+ }
+
+ private void assertNrcIsPropertyOnly() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertOutput(modifyTemplate("${nrc.x}", tempMods), "1");
+ assertErrorContains(modifyTemplate("${nrc.x()}", tempMods), "SimpleNumber", "must not be called as a method");
+ assertOutput(modifyTemplate("${nrc.y}", tempMods), "2");
+ assertErrorContains(modifyTemplate("${nrc.y()}", tempMods), "SimpleNumber");
+ assertOutput(modifyTemplate("${nrc.tenX}", tempMods), "10");
+ assertErrorContains(modifyTemplate("${nrc.tenX()}", tempMods), "SimpleNumber");
+ }
+ assertNrcPolicyIndependentMembers();
+ }
+
+ private void assertNrcPolicyIndependentMembers() throws IOException, TemplateException {
+ for (TemplateModifications tempMods : TemplateModifications.values()) {
+ assertOutput(modifyTemplate("${nrc.z}", tempMods), "3");
+ assertErrorContains(modifyTemplate("${nrc.z()}", tempMods), "SimpleNumber");
+ assertOutput(modifyTemplate("${nrc.getZ()}", tempMods), "3");
+ assertOutput(modifyTemplate("${nrc.xTimes(5)}", tempMods), "5");
+ assertErrorContains(modifyTemplate("${nrc.xTimes}", tempMods), "SimpleMethodModel");
+ assertOutput(modifyTemplate("${nrc.voidMethod()}", tempMods), "");
+ assertErrorContains(modifyTemplate("${nrc.voidMethod}", tempMods), "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
+ }
+ }
+
+ private static final Pattern DOT_TO_SQUARE_BRACKETS_REPLACEMENT_PATTERN = Pattern.compile("\\.(\\w+)");
+
+ private static String modifyTemplate(String s, TemplateModifications tempMods) {
+ if (tempMods.useApi) {
+ s = s.replace(".", "?api.");
+ }
+ if (tempMods.doToSquareBrackets) {
+ s = DOT_TO_SQUARE_BRACKETS_REPLACEMENT_PATTERN.matcher(s).replaceFirst(key -> "['" + key.group(1) + "']");
+ }
+ return s;
+ }
+
+ enum TemplateModifications {
+ DOT(true, false), SQUARE_BRACKETS(false, false),
+ API_DOT(true, true), API_SQUARE_BRACKETS(false, true);
+
+ private final boolean doToSquareBrackets;
+ private final boolean useApi;
+
+ TemplateModifications(boolean doToSquareBrackets, boolean useApi) {
+ this.doToSquareBrackets = doToSquareBrackets;
+ this.useApi = useApi;
+ }
+ }
+
+
+}
diff --git a/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index d67d195..5115299 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 @@
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 @@
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 06fa9f9..5675058 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>
@@ -30128,6 +30131,18 @@
<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
PR 87</link> Comparing strings is now way faster, if the <link
linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
@@ -30229,12 +30244,123 @@
</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
PR 88</link>: Added a new possible value for the
<literal>auto_escaping_policy</literal> configuration setting,
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 a730b3e..1ab7af4 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.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 @@
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 @@
}
return dataModel;
}
+
+ protected void setDataModel(Object dataModel) {
+ this.dataModel = dataModel;
+ dataModelCreated = true;
+ }
protected Object createDataModel() {
return null;
@@ -248,6 +255,7 @@
}
}
+ @SuppressWarnings({"unchecked", "rawtypes"})
protected void addToDataModel(String name, Object value) {
Object dm = getDataModel();
if (dm == null) {
@@ -256,6 +264,9 @@
}
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 @@
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) {