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 2a7f91250..2a2c1886d 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -274,6 +274,16 @@ public BeansWrapper() {
* 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).
*
+ *
+ *
2.3.35 (or higher):
+ * {@code isFoo()} methods that return {@link Boolean} (the wrapper class) are now exposed as the {@code foo}
+ * bean property, just like {@code isFoo()} methods returning primitive {@code boolean} already were. Before
+ * this, {@link java.beans.Introspector} (which is spec-strict) reported only primitive-{@code boolean}
+ * {@code isFoo()} methods as property readers, so {@code Boolean isFoo()} was accessible only as a method
+ * ({@code obj.isFoo()}), even though the documented behavior claims {@code obj.foo} should work for either
+ * {@code getFoo()} or {@code isFoo()}. Static {@code isFoo()} methods and {@code isFoo()} methods with any
+ * other return type are still not exposed as properties.
+ *
*
*
*
Note that the version will be normalized to the lowest version where the same incompatible
@@ -967,7 +977,8 @@ static boolean is2324Bugfixed(Version version) {
*/
protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
_TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
- return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 ? Configuration.VERSION_2_3_33
+ return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_35 ? Configuration.VERSION_2_3_35
+ : 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
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 5e229937d..4eb05c349 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -154,6 +154,7 @@ class ClassIntrospector {
final MethodAppearanceFineTuner methodAppearanceFineTuner;
final MethodSorter methodSorter;
final boolean treatDefaultMethodsAsBeanMembers;
+ final boolean treatBooleanWrapperIsGettersAsProperties;
final ZeroArgumentNonVoidMethodPolicy defaultZeroArgumentNonVoidMethodPolicy;
final ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy;
final private boolean recordAware;
@@ -198,6 +199,7 @@ class ClassIntrospector {
this.methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner();
this.methodSorter = builder.getMethodSorter();
this.treatDefaultMethodsAsBeanMembers = builder.getTreatDefaultMethodsAsBeanMembers();
+ this.treatBooleanWrapperIsGettersAsProperties = builder.getTreatBooleanWrapperIsGettersAsProperties();
this.defaultZeroArgumentNonVoidMethodPolicy = builder.getDefaultZeroArgumentNonVoidMethodPolicy();
this.recordZeroArgumentNonVoidMethodPolicy = builder.getRecordZeroArgumentNonVoidMethodPolicy();
this.recordAware = defaultZeroArgumentNonVoidMethodPolicy != recordZeroArgumentNonVoidMethodPolicy;
@@ -471,11 +473,11 @@ private List getPropertyDescriptors(BeanInfo beanInfo, Class
List introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray)
: Collections.emptyList();
- if (!treatDefaultMethodsAsBeanMembers) {
+ if (!treatDefaultMethodsAsBeanMembers && !treatBooleanWrapperIsGettersAsProperties) {
// java.beans.Introspector was good enough then.
return introspectorPDs;
}
-
+
// introspectorPDs contains each property exactly once. But as now we will search them manually too, it can
// happen that we find the same property for multiple times. Worse, because of indexed properties, it's possible
// that we have to merge entries (like one has the normal reader method, the other has the indexed reader
@@ -483,17 +485,26 @@ private List getPropertyDescriptors(BeanInfo beanInfo, Class
// which holds the methods belonging to the same property name. IndexedPropertyDescriptor is not good for that,
// as it can't store two methods whose types are incompatible, and we have to wait until all the merging was
// done to see if the incompatibility goes away.
-
+
// This could be Map, but since we rarely need to do merging, we try to avoid
// creating those and use the source objects as much as possible. Also note that we initialize this lazily.
LinkedHashMap mergedPRMPs = null;
- // Collect Java 8 default methods that look like property readers into mergedPRMPs:
+ // Collect methods that look like property readers but that java.beans.Introspector did not report as such:
+ // - Java 8 default interface methods (not reported by Introspector prior to its own support)
+ // - isXxx() returning Boolean wrapper (spec-wise Introspector only recognises primitive boolean here)
// (Note that java.beans.Introspector discovers non-accessible public methods, and to emulate that behavior
// here, we don't utilize the accessibleMethods Map, which we might already have at this point.)
for (Method method : clazz.getMethods()) {
- if (method.isDefault() && method.getReturnType() != void.class
- && !method.isBridge()) {
+ if (method.isBridge() || method.getReturnType() == void.class) {
+ continue;
+ }
+ boolean isDefaultMethodCandidate = treatDefaultMethodsAsBeanMembers && method.isDefault();
+ boolean isBooleanWrapperIsGetterCandidate = treatBooleanWrapperIsGettersAsProperties
+ && !Modifier.isStatic(method.getModifiers())
+ && method.getReturnType() == Boolean.class
+ && method.getName().startsWith("is");
+ if (isDefaultMethodCandidate || isBooleanWrapperIsGetterCandidate) {
Class>[] paramTypes = method.getParameterTypes();
if (paramTypes.length == 0
|| paramTypes.length == 1 && paramTypes[0] == int.class /* indexed property reader */) {
@@ -514,9 +525,9 @@ private List getPropertyDescriptors(BeanInfo beanInfo, Class
}
}
} // for clazz.getMethods()
-
+
if (mergedPRMPs == null) {
- // We had no interfering Java 8 default methods, so we can chose the fast route.
+ // No methods needed supplementing, so we can chose the fast route.
return introspectorPDs;
}
@@ -1143,6 +1154,10 @@ boolean getTreatDefaultMethodsAsBeanMembers() {
return treatDefaultMethodsAsBeanMembers;
}
+ boolean getTreatBooleanWrapperIsGettersAsProperties() {
+ return treatBooleanWrapperIsGettersAsProperties;
+ }
+
ZeroArgumentNonVoidMethodPolicy getDefaultZeroArgumentNonVoidMethodPolicy() {
return defaultZeroArgumentNonVoidMethodPolicy;
}
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 141917ccf..ee1b478d8 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -47,6 +47,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
private boolean exposeFields;
private MemberAccessPolicy memberAccessPolicy;
private boolean treatDefaultMethodsAsBeanMembers;
+ private boolean treatBooleanWrapperIsGettersAsProperties;
private ZeroArgumentNonVoidMethodPolicy defaultZeroArgumentNonVoidMethodPolicy;
private ZeroArgumentNonVoidMethodPolicy recordZeroArgumentNonVoidMethodPolicy;
private MethodAppearanceFineTuner methodAppearanceFineTuner;
@@ -63,6 +64,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
exposeFields = ci.exposeFields;
memberAccessPolicy = ci.memberAccessPolicy;
treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers;
+ treatBooleanWrapperIsGettersAsProperties = ci.treatBooleanWrapperIsGettersAsProperties;
defaultZeroArgumentNonVoidMethodPolicy = ci.defaultZeroArgumentNonVoidMethodPolicy;
recordZeroArgumentNonVoidMethodPolicy = ci.recordZeroArgumentNonVoidMethodPolicy;
methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
@@ -75,6 +77,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
// 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;
+ treatBooleanWrapperIsGettersAsProperties = incompatibleImprovements.intValue() >= _VersionInts.V_2_3_35;
defaultZeroArgumentNonVoidMethodPolicy = ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY;
recordZeroArgumentNonVoidMethodPolicy
= incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 && _Java16.INSTANCE.isSupported()
@@ -86,7 +89,8 @@ final class ClassIntrospectorBuilder implements Cloneable {
private static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
_TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
// All breakpoints here must occur in BeansWrapper.normalizeIncompatibleImprovements!
- return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 ? Configuration.VERSION_2_3_33
+ return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_35 ? Configuration.VERSION_2_3_35
+ : 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;
@@ -108,6 +112,7 @@ public int hashCode() {
result = prime * result + incompatibleImprovements.hashCode();
result = prime * result + (exposeFields ? 1231 : 1237);
result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 1237);
+ result = prime * result + (treatBooleanWrapperIsGettersAsProperties ? 1231 : 1237);
result = prime * result + defaultZeroArgumentNonVoidMethodPolicy.hashCode();
result = prime * result + recordZeroArgumentNonVoidMethodPolicy.hashCode();
result = prime * result + exposureLevel;
@@ -127,6 +132,7 @@ public boolean equals(Object obj) {
if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false;
if (exposeFields != other.exposeFields) return false;
if (treatDefaultMethodsAsBeanMembers != other.treatDefaultMethodsAsBeanMembers) return false;
+ if (treatBooleanWrapperIsGettersAsProperties != other.treatBooleanWrapperIsGettersAsProperties) return false;
if (defaultZeroArgumentNonVoidMethodPolicy != other.defaultZeroArgumentNonVoidMethodPolicy) return false;
if (recordZeroArgumentNonVoidMethodPolicy != other.recordZeroArgumentNonVoidMethodPolicy) return false;
if (exposureLevel != other.exposureLevel) return false;
@@ -167,6 +173,14 @@ public void setTreatDefaultMethodsAsBeanMembers(boolean treatDefaultMethodsAsBea
this.treatDefaultMethodsAsBeanMembers = treatDefaultMethodsAsBeanMembers;
}
+ public boolean getTreatBooleanWrapperIsGettersAsProperties() {
+ return treatBooleanWrapperIsGettersAsProperties;
+ }
+
+ public void setTreatBooleanWrapperIsGettersAsProperties(boolean treatBooleanWrapperIsGettersAsProperties) {
+ this.treatBooleanWrapperIsGettersAsProperties = treatBooleanWrapperIsGettersAsProperties;
+ }
+
/**
* The getter pair of {@link #setDefaultZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}.
*
diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/_MethodUtil.java b/freemarker-core/src/main/java/freemarker/ext/beans/_MethodUtil.java
index 99a733ed6..c7c24c687 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/_MethodUtil.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/_MethodUtil.java
@@ -302,7 +302,7 @@ public static String getBeanPropertyNameFromReaderMethodName(String name, Class<
int start;
if (name.startsWith("get")) {
start = 3;
- } else if (returnType == boolean.class && name.startsWith("is")) {
+ } else if ((returnType == boolean.class || returnType == Boolean.class) && name.startsWith("is")) {
start = 2;
} else {
return null;
diff --git a/freemarker-core/src/main/java/freemarker/template/_VersionInts.java b/freemarker-core/src/main/java/freemarker/template/_VersionInts.java
index b07c9bd73..b0fe1b583 100644
--- a/freemarker-core/src/main/java/freemarker/template/_VersionInts.java
+++ b/freemarker-core/src/main/java/freemarker/template/_VersionInts.java
@@ -48,5 +48,6 @@ private _VersionInts() {
public static final int V_2_3_32 = Configuration.VERSION_2_3_32.intValue();
public static final int V_2_3_33 = Configuration.VERSION_2_3_33.intValue();
public static final int V_2_3_34 = Configuration.VERSION_2_3_34.intValue();
+ public static final int V_2_3_35 = Configuration.VERSION_2_3_35.intValue();
public static final int V_2_4_0 = Version.intValueFor(2, 4, 0);
}
diff --git a/freemarker-core/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java b/freemarker-core/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java
index ac7a7b773..ff9f38733 100644
--- a/freemarker-core/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java
+++ b/freemarker-core/src/test/java/freemarker/ext/beans/BeansWrapperMiscTest.java
@@ -118,6 +118,59 @@ public void java8InaccessibleIndexedAccessibleNonIndexedReadMethodTest() throws
}
}
+ @Test
+ public void booleanWrapperIsGetterAsPropertyTest() throws TemplateModelException {
+ // Pre-2.3.35 (legacy): Boolean isXxx() is NOT exposed as a property, only as a method.
+ {
+ BeansWrapper bw = new BeansWrapperBuilder(Configuration.VERSION_2_3_34).build();
+ TemplateHashModel beanTM = (TemplateHashModel) bw.wrap(new BeanWithBooleanWrapperIsGetter());
+ assertNull(beanTM.get("obsolete"));
+ assertThat(beanTM.get("isObsolete"), instanceOf(TemplateMethodModelEx.class));
+ }
+
+ // At 2.3.35+: Boolean isXxx() is exposed as a property (FREEMARKER-234).
+ {
+ BeansWrapper bw = new BeansWrapperBuilder(Configuration.VERSION_2_3_35).build();
+ TemplateHashModel beanTM = (TemplateHashModel) bw.wrap(new BeanWithBooleanWrapperIsGetter());
+
+ TemplateModel obsoleteTM = beanTM.get("obsolete");
+ assertThat(obsoleteTM, instanceOf(TemplateBooleanModel.class));
+ assertTrue(((TemplateBooleanModel) obsoleteTM).getAsBoolean());
+
+ // Primitive boolean isXxx() still works (unchanged).
+ TemplateModel activeTM = beanTM.get("active");
+ assertThat(activeTM, instanceOf(TemplateBooleanModel.class));
+ assertFalse(((TemplateBooleanModel) activeTM).getAsBoolean());
+
+ // String isXxx() is never a property (not a recognised reader).
+ assertNull(beanTM.get("label"));
+
+ // Static Boolean isXxx() must not be exposed as a property.
+ assertNull(beanTM.get("archived"));
+
+ // The method form is still reachable (method exposure unchanged).
+ assertThat(beanTM.get("isObsolete"), instanceOf(TemplateMethodModelEx.class));
+ }
+ }
+
+ public static class BeanWithBooleanWrapperIsGetter {
+ public Boolean isObsolete() {
+ return Boolean.TRUE;
+ }
+
+ public boolean isActive() {
+ return false;
+ }
+
+ public String isLabel() {
+ return "not a property";
+ }
+
+ public static Boolean isArchived() {
+ return Boolean.TRUE;
+ }
+ }
+
public static class BeanWithBothIndexedAndArrayProperty {
private final static String[] FOO = new String[] { "a", "b" };
diff --git a/freemarker-core/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java b/freemarker-core/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
index d100b775b..082a4a33a 100644
--- a/freemarker-core/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
+++ b/freemarker-core/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
@@ -36,11 +36,12 @@ public void test() {
assertEquals("fooBar", _MethodUtil.getBeanPropertyNameFromReaderMethodName("getFooBar", String.class));
assertEquals("FOoBar", _MethodUtil.getBeanPropertyNameFromReaderMethodName("getFOoBar", String.class));
- assertEquals("foo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("getFoo", boolean.class));
- assertEquals("foo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", boolean.class));
- assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", Boolean.class));
+ assertEquals("foo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("getFoo", boolean.class));
+ assertEquals("foo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", boolean.class));
+ assertEquals("foo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", Boolean.class));
assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", String.class));
- assertEquals("f", _MethodUtil.getBeanPropertyNameFromReaderMethodName("isF", boolean.class));
+ assertEquals("f", _MethodUtil.getBeanPropertyNameFromReaderMethodName("isF", boolean.class));
+ assertEquals("f", _MethodUtil.getBeanPropertyNameFromReaderMethodName("isF", Boolean.class));
assertEquals("foo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("getfoo", String.class));
assertEquals("fo", _MethodUtil.getBeanPropertyNameFromReaderMethodName("getfo", String.class));
diff --git a/freemarker-manual/src/main/docgen/en_US/book.xml b/freemarker-manual/src/main/docgen/en_US/book.xml
index 3f35428e3..33bafe685 100644
--- a/freemarker-manual/src/main/docgen/en_US/book.xml
+++ b/freemarker-manual/src/main/docgen/en_US/book.xml
@@ -30598,6 +30598,25 @@ TemplateModel x = env.getVariable("x"); // get variable x
reflection and resource access for your application; see more here!
+
+
+ FREEMARKER-234:
+ BeansWrapper, and therefore
+ DefaultObjectWrapper, now exposes
+ isFoo() methods that return
+ java.lang.Boolean (the wrapper class) as the
+ foo bean property, if you set the incompatible_improvements
+ setting to 2.3.35 or higher. Earlier this only worked if
+ the return type was primitive boolean, which
+ contradicted the documented behavior stating that
+ model.foo invokes either
+ obj.getFoo() or
+ obj.isFoo(). Static methods, and
+ isFoo() methods with any other return type,
+ are still not exposed as properties.
+