Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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).
* </li>
* <li>
* <p>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.
* </li>
* </ul>
*
* <p>Note that the version will be normalized to the lowest version where the same incompatible
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -471,29 +473,38 @@ private List<PropertyDescriptor> getPropertyDescriptors(BeanInfo beanInfo, Class
List<PropertyDescriptor> introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray)
: Collections.<PropertyDescriptor>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
// method), instead of just replacing them in a Map. That's why we have introduced PropertyReaderMethodPair,
// 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<String, PropertyReaderMethodPair>, 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<String, Object /*PropertyReaderMethodPair|Method|PropertyDescriptor*/> 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 */) {
Expand All @@ -514,9 +525,9 @@ private List<PropertyDescriptor> 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;
}

Expand Down Expand Up @@ -1143,6 +1154,10 @@ boolean getTreatDefaultMethodsAsBeanMembers() {
return treatDefaultMethodsAsBeanMembers;
}

boolean getTreatBooleanWrapperIsGettersAsProperties() {
return treatBooleanWrapperIsGettersAsProperties;
}

ZeroArgumentNonVoidMethodPolicy getDefaultZeroArgumentNonVoidMethodPolicy() {
return defaultZeroArgumentNonVoidMethodPolicy;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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()
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
19 changes: 19 additions & 0 deletions freemarker-manual/src/main/docgen/en_US/book.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30598,6 +30598,25 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
reflection and resource access for your application; <link
linkend="pgui_misc_graalvm_native">see more here</link>!</para>
</listitem>

<listitem>
<para><link
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-234">FREEMARKER-234</link>:
<literal>BeansWrapper</literal>, and therefore
<literal>DefaultObjectWrapper</literal>, now exposes
<literal>isFoo()</literal> methods that return
<literal>java.lang.Boolean</literal> (the wrapper class) as the
<literal>foo</literal> bean property, if you set the <link
linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
setting</link> to 2.3.35 or higher. Earlier this only worked if
the return type was primitive <literal>boolean</literal>, which
contradicted the documented behavior stating that
<literal>model.foo</literal> invokes either
<literal>obj.getFoo()</literal> or
<literal>obj.isFoo()</literal>. Static methods, and
<literal>isFoo()</literal> methods with any other return type,
are still not exposed as properties.</para>
</listitem>
</itemizedlist>
</section>
</section>
Expand Down