You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/03/16 15:41:23 UTC
incubator-freemarker git commit: Forward ported Java 8 default method
support.
Repository: incubator-freemarker
Updated Branches:
refs/heads/3 8242c4b01 -> 529cdd86f
Forward ported Java 8 default method support.
Also, changed how indexed properties are treated. If for an indexed JavaBean property there's both an indexed read method (like `Foo getFoo(int index)`) and a normal read method (like Foo[] getFoo()), we prefer the normal read method, and so the result will be a clean FTL sequence (not a multi-type value with sequence+method type). If there's only an indexed read method, then we don't expose the property anymore, but the indexed read method can still be called as an usual method (as `myObj.getFoo(index)`). These changes were made because building on the indexed read method we can't create a proper sequence (which the value of the property should be), since sequences are required to support returning their size. (In FreeMarker 2 such sequences has thrown exception on calling size(), which caused more problems and confusion than it solved.)
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/529cdd86
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/529cdd86
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/529cdd86
Branch: refs/heads/3
Commit: 529cdd86f5f068f6ac07af6b637bbff8f0689a10
Parents: 8242c4b
Author: ddekany <dd...@apache.org>
Authored: Thu Mar 16 16:41:17 2017 +0100
Committer: ddekany <dd...@apache.org>
Committed: Thu Mar 16 16:41:17 2017 +0100
----------------------------------------------------------------------
.../freemarker/core/model/impl/BeanModel.java | 22 +-
.../core/model/impl/ClassIntrospector.java | 394 ++++++++++++++++---
.../core/model/impl/MethodSorter.java | 12 +-
.../freemarker/core/model/impl/_MethodUtil.java | 26 ++
src/manual/en_US/FM3-CHANGE-LOG.txt | 9 +-
.../model/impl/AlphabeticalMethodSorter.java | 10 +-
.../core/model/impl/BridgeMethodsBean.java | 30 ++
.../core/model/impl/BridgeMethodsBeanBase.java | 29 ++
...Java8BridgeMethodsWithDefaultMethodBean.java | 29 ++
...ava8BridgeMethodsWithDefaultMethodBean2.java | 23 ++
...8BridgeMethodsWithDefaultMethodBeanBase.java | 31 ++
...BridgeMethodsWithDefaultMethodBeanBase2.java | 28 ++
.../model/impl/Java8DefaultMethodsBean.java | 84 ++++
.../model/impl/Java8DefaultMethodsBeanBase.java | 97 +++++
...a8DefaultObjectWrapperBridgeMethodsTest.java | 65 +++
.../impl/Java8DefaultObjectWrapperTest.java | 160 ++++++++
.../templatesuite/models/BeanTestClass.java | 6 +-
.../templates/default-object-wrapper.ftl | 2 +-
18 files changed, 978 insertions(+), 79 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java b/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
index e5c7de5..556660b 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
@@ -210,15 +210,21 @@ public class BeanModel
}
TemplateModel resultModel = UNKNOWN;
- if (desc instanceof IndexedPropertyDescriptor) {
- Method readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod();
- resultModel = cachedModel =
- new JavaMethodModel(object, readMethod,
- ClassIntrospector.getArgTypes(classInfo, readMethod), wrapper);
- } else if (desc instanceof PropertyDescriptor) {
+ if (desc instanceof PropertyDescriptor) {
PropertyDescriptor pd = (PropertyDescriptor) desc;
- resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null);
- // cachedModel remains null, as we don't cache these
+ Method readMethod = pd.getReadMethod();
+ if (readMethod != null) {
+ // Unlike in FreeMarker 2, we prefer the normal read method even if there's an indexed read method.
+ resultModel = wrapper.invokeMethod(object, readMethod, null);
+ // cachedModel remains null, as we don't cache these
+ } else if (desc instanceof IndexedPropertyDescriptor) {
+ // In FreeMarker 2 we have exposed such indexed properties as sequences, but they can't support
+ // the size() method, so we have discontinued that. People has to call the indexed read method like
+ // any other method.
+ resultModel = UNKNOWN;
+ } else {
+ throw new IllegalStateException("PropertyDescriptor.readMethod shouldn't be null");
+ }
} else if (desc instanceof Field) {
resultModel = wrapper.wrap(((Field) desc).get(object));
// cachedModel remains null, as we don't cache these
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
index 65dcc6a..c19de75 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -32,11 +32,14 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.IdentityHashMap;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -45,8 +48,8 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.freemarker.core._CoreLogs;
-import org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.DecisionInput;
import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._JavaVersions;
import org.apache.freemarker.core.util._NullArgumentException;
import org.slf4j.Logger;
@@ -299,72 +302,347 @@ class ClassIntrospector {
Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods)
throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
-
- PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors();
- if (pda != null) {
- int pdaLength = pda.length;
- for (int i = pdaLength - 1; i >= 0; --i) {
- addPropertyDescriptorToClassIntrospectionData(
- introspData, pda[i], clazz,
- accessibleMethods);
- }
+ 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), clazz,
+ accessibleMethods);
}
if (exposureLevel < DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) {
final MethodAppearanceFineTuner.Decision decision = new MethodAppearanceFineTuner.Decision();
- DecisionInput decisionInput = null;
- final MethodDescriptor[] mda = sortMethodDescriptors(beanInfo.getMethodDescriptors());
- if (mda != null) {
- int mdaLength = mda.length;
- for (int i = mdaLength - 1; i >= 0; --i) {
- final MethodDescriptor md = mda[i];
- final Method method = getMatchingAccessibleMethod(md.getMethod(), accessibleMethods);
- if (method != null && isAllowedToExpose(method)) {
- decision.setDefaults(method);
- if (methodAppearanceFineTuner != null) {
- if (decisionInput == null) {
- decisionInput = new DecisionInput();
- }
- decisionInput.setContainingClass(clazz);
- decisionInput.setMethod(method);
-
- methodAppearanceFineTuner.process(decisionInput, decision);
+ MethodAppearanceFineTuner.DecisionInput decisionInput = null;
+ List<MethodDescriptor> mds = getMethodDescriptors(beanInfo, clazz);
+ sortMethodDescriptors(mds);
+ int mdsSize = mds.size();
+ IdentityHashMap<Method, Void> argTypesUsedByIndexerPropReaders = null;
+ for (int i = mdsSize - 1; i >= 0; --i) {
+ final MethodDescriptor md = mds.get(i);
+ final Method method = getMatchingAccessibleMethod(md.getMethod(), accessibleMethods);
+ if (method != null && isAllowedToExpose(method)) {
+ decision.setDefaults(method);
+ if (methodAppearanceFineTuner != null) {
+ if (decisionInput == null) {
+ decisionInput = new MethodAppearanceFineTuner.DecisionInput();
}
-
- PropertyDescriptor propDesc = decision.getExposeAsProperty();
- if (propDesc != null && !(introspData.get(propDesc.getName()) instanceof PropertyDescriptor)) {
- addPropertyDescriptorToClassIntrospectionData(
- introspData, propDesc, clazz, accessibleMethods);
- }
-
- String methodKey = decision.getExposeMethodAs();
- if (methodKey != null) {
- Object previous = introspData.get(methodKey);
- if (previous instanceof Method) {
- // Overloaded method - replace Method with a OverloadedMethods
- OverloadedMethods overloadedMethods = new OverloadedMethods();
- overloadedMethods.addMethod((Method) previous);
- overloadedMethods.addMethod(method);
- introspData.put(methodKey, overloadedMethods);
- // Remove parameter type information
+ decisionInput.setContainingClass(clazz);
+ decisionInput.setMethod(method);
+
+ methodAppearanceFineTuner.process(decisionInput, decision);
+ }
+
+ PropertyDescriptor propDesc = decision.getExposeAsProperty();
+ if (propDesc != null && !(introspData.get(propDesc.getName()) instanceof PropertyDescriptor)) {
+ addPropertyDescriptorToClassIntrospectionData(
+ introspData, propDesc, clazz, accessibleMethods);
+ }
+
+ String methodKey = decision.getExposeMethodAs();
+ if (methodKey != null) {
+ Object previous = introspData.get(methodKey);
+ if (previous instanceof Method) {
+ // Overloaded method - replace Method with a OverloadedMethods
+ OverloadedMethods overloadedMethods = new OverloadedMethods();
+ overloadedMethods.addMethod((Method) previous);
+ overloadedMethods.addMethod(method);
+ introspData.put(methodKey, overloadedMethods);
+ // Remove parameter type information (unless an indexed property reader needs it):
+ if (argTypesUsedByIndexerPropReaders == null
+ || !argTypesUsedByIndexerPropReaders.containsKey(previous)) {
getArgTypesByMethod(introspData).remove(previous);
- } else if (previous instanceof OverloadedMethods) {
- // Already overloaded method - add new overload
- ((OverloadedMethods) previous).addMethod(method);
- } else if (decision.getMethodShadowsProperty()
- || !(previous instanceof PropertyDescriptor)) {
- // Simple method (this far)
- introspData.put(methodKey, method);
- getArgTypesByMethod(introspData).put(method,
- method.getParameterTypes());
+ }
+ } else if (previous instanceof OverloadedMethods) {
+ // Already overloaded method - add new overload
+ ((OverloadedMethods) previous).addMethod(method);
+ } else if (decision.getMethodShadowsProperty()
+ || !(previous instanceof PropertyDescriptor)) {
+ // Simple method (this far)
+ introspData.put(methodKey, method);
+ Class<?>[] replaced = getArgTypesByMethod(introspData).put(method,
+ method.getParameterTypes());
+ if (replaced != null) {
+ if (argTypesUsedByIndexerPropReaders == null) {
+ argTypesUsedByIndexerPropReaders = new IdentityHashMap<Method, Void>();
+ }
+ argTypesUsedByIndexerPropReaders.put(method, null);
}
}
}
- } // for each in mda
- } // if mda != null
+ }
+ } // for each in mds
} // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY)
}
+ /**
+ * Very similar to {@link BeanInfo#getPropertyDescriptors()}, but can deal with Java 8 default methods too.
+ */
+ private List<PropertyDescriptor> getPropertyDescriptors(BeanInfo beanInfo, Class<?> clazz) {
+ PropertyDescriptor[] introspectorPDsArray = beanInfo.getPropertyDescriptors();
+ List<PropertyDescriptor> introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray)
+ : Collections.<PropertyDescriptor>emptyList();
+
+ if (_JavaVersions.JAVA_8 == null) {
+ // 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:
+ // (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 (_JavaVersions.JAVA_8.isDefaultMethod(method) && method.getReturnType() != void.class
+ && !method.isBridge()) {
+ Class<?>[] paramTypes = method.getParameterTypes();
+ if (paramTypes.length == 0
+ || paramTypes.length == 1 && paramTypes[0] == int.class /* indexed property reader */) {
+ String propName = _MethodUtil.getBeanPropertyNameFromReaderMethodName(
+ method.getName(), method.getReturnType());
+ if (propName != null) {
+ if (mergedPRMPs == null) {
+ // Lazy initialization
+ mergedPRMPs = new LinkedHashMap<String, Object>();
+ }
+ if (paramTypes.length == 0) {
+ mergeInPropertyReaderMethod(mergedPRMPs, propName, method);
+ } else { // It's an indexed property reader method
+ mergeInPropertyReaderMethodPair(mergedPRMPs, propName,
+ new PropertyReaderedMethodPair(null, method));
+ }
+ }
+ }
+ }
+ } // for clazz.getMethods()
+
+ if (mergedPRMPs == null) {
+ // We had no interfering Java 8 default methods, so we can chose the fast route.
+ return introspectorPDs;
+ }
+
+ for (PropertyDescriptor introspectorPD : introspectorPDs) {
+ mergeInPropertyDescriptor(mergedPRMPs, introspectorPD);
+ }
+
+ // Now we convert the PRMPs to PDs, handling case where the normal and the indexed read methods contradict.
+ List<PropertyDescriptor> mergedPDs = new ArrayList<PropertyDescriptor>(mergedPRMPs.size());
+ for (Entry<String, Object> entry : mergedPRMPs.entrySet()) {
+ String propName = entry.getKey();
+ Object propDescObj = entry.getValue();
+ if (propDescObj instanceof PropertyDescriptor) {
+ mergedPDs.add((PropertyDescriptor) propDescObj);
+ } else {
+ Method readMethod;
+ Method indexedReadMethod;
+ if (propDescObj instanceof Method) {
+ readMethod = (Method) propDescObj;
+ indexedReadMethod = null;
+ } else if (propDescObj instanceof PropertyReaderedMethodPair) {
+ PropertyReaderedMethodPair prmp = (PropertyReaderedMethodPair) propDescObj;
+ readMethod = prmp.readMethod;
+ indexedReadMethod = prmp.indexedReadMethod;
+ if (readMethod != null && indexedReadMethod != null
+ && indexedReadMethod.getReturnType() != readMethod.getReturnType().getComponentType()) {
+ // Here we copy the java.beans.Introspector behavior: If the array item class is not exactly the
+ // the same as the indexed read method return type, we say that the property is not indexed.
+ indexedReadMethod = null;
+ }
+ } else {
+ throw new BugException();
+ }
+ try {
+ mergedPDs.add(
+ indexedReadMethod != null
+ ? new IndexedPropertyDescriptor(propName,
+ readMethod, null, indexedReadMethod, null)
+ : new PropertyDescriptor(propName, readMethod, null));
+ } catch (IntrospectionException e) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Failed creating property descriptor for " + clazz.getName() + " property " + propName,
+ e);
+ }
+ }
+ }
+ }
+ return mergedPDs;
+ }
+
+ private static class PropertyReaderedMethodPair {
+ private final Method readMethod;
+ private final Method indexedReadMethod;
+
+ PropertyReaderedMethodPair(Method readerMethod, Method indexedReaderMethod) {
+ this.readMethod = readerMethod;
+ this.indexedReadMethod = indexedReaderMethod;
+ }
+
+ PropertyReaderedMethodPair(PropertyDescriptor pd) {
+ this(
+ pd.getReadMethod(),
+ pd instanceof IndexedPropertyDescriptor
+ ? ((IndexedPropertyDescriptor) pd).getIndexedReadMethod() : null);
+ }
+
+ static PropertyReaderedMethodPair from(Object obj) {
+ if (obj instanceof PropertyReaderedMethodPair) {
+ return (PropertyReaderedMethodPair) obj;
+ } else if (obj instanceof PropertyDescriptor) {
+ return new PropertyReaderedMethodPair((PropertyDescriptor) obj);
+ } else if (obj instanceof Method) {
+ return new PropertyReaderedMethodPair((Method) obj, null);
+ } else {
+ throw new BugException("Unexpected obj type: " + obj.getClass().getName());
+ }
+ }
+
+ static PropertyReaderedMethodPair merge(PropertyReaderedMethodPair oldMethods, PropertyReaderedMethodPair newMethods) {
+ return new PropertyReaderedMethodPair(
+ newMethods.readMethod != null ? newMethods.readMethod : oldMethods.readMethod,
+ newMethods.indexedReadMethod != null ? newMethods.indexedReadMethod
+ : oldMethods.indexedReadMethod);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((indexedReadMethod == null) ? 0 : indexedReadMethod.hashCode());
+ result = prime * result + ((readMethod == null) ? 0 : readMethod.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ PropertyReaderedMethodPair other = (PropertyReaderedMethodPair) obj;
+ return other.readMethod == readMethod && other.indexedReadMethod == indexedReadMethod;
+ }
+
+ }
+
+ private void mergeInPropertyDescriptor(LinkedHashMap<String, Object> mergedPRMPs, PropertyDescriptor pd) {
+ String propName = pd.getName();
+ Object replaced = mergedPRMPs.put(propName, pd);
+ if (replaced != null) {
+ PropertyReaderedMethodPair newPRMP = new PropertyReaderedMethodPair(pd);
+ putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRMP);
+ }
+ }
+
+ private void mergeInPropertyReaderMethodPair(LinkedHashMap<String, Object> mergedPRMPs,
+ String propName, PropertyReaderedMethodPair newPRM) {
+ Object replaced = mergedPRMPs.put(propName, newPRM);
+ if (replaced != null) {
+ putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRM);
+ }
+ }
+
+ private void mergeInPropertyReaderMethod(LinkedHashMap<String, Object> mergedPRMPs,
+ String propName, Method readerMethod) {
+ Object replaced = mergedPRMPs.put(propName, readerMethod);
+ if (replaced != null) {
+ putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName,
+ replaced, new PropertyReaderedMethodPair(readerMethod, null));
+ }
+ }
+
+ private void putIfMergedPropertyReaderMethodPairDiffers(LinkedHashMap<String, Object> mergedPRMPs,
+ String propName, Object replaced, PropertyReaderedMethodPair newPRMP) {
+ PropertyReaderedMethodPair replacedPRMP = PropertyReaderedMethodPair.from(replaced);
+ PropertyReaderedMethodPair mergedPRMP = PropertyReaderedMethodPair.merge(replacedPRMP, newPRMP);
+ if (!mergedPRMP.equals(newPRMP)) {
+ mergedPRMPs.put(propName, mergedPRMP);
+ }
+ }
+
+ /**
+ * Very similar to {@link BeanInfo#getMethodDescriptors()}, but can deal with Java 8 default methods too.
+ */
+ private List<MethodDescriptor> getMethodDescriptors(BeanInfo beanInfo, Class<?> clazz) {
+ MethodDescriptor[] introspectorMDArray = beanInfo.getMethodDescriptors();
+ List<MethodDescriptor> introspectionMDs = introspectorMDArray != null && introspectorMDArray.length != 0
+ ? Arrays.asList(introspectorMDArray) : Collections.<MethodDescriptor>emptyList();
+
+ if (_JavaVersions.JAVA_8 == null) {
+ // java.beans.Introspector was good enough then.
+ return introspectionMDs;
+ }
+
+ Map<String, List<Method>> defaultMethodsToAddByName = null;
+ for (Method method : clazz.getMethods()) {
+ if (_JavaVersions.JAVA_8.isDefaultMethod(method) && !method.isBridge()) {
+ if (defaultMethodsToAddByName == null) {
+ defaultMethodsToAddByName = new HashMap<String, List<Method>>();
+ }
+ List<Method> overloads = defaultMethodsToAddByName.get(method.getName());
+ if (overloads == null) {
+ overloads = new ArrayList<Method>(0);
+ defaultMethodsToAddByName.put(method.getName(), overloads);
+ }
+ overloads.add(method);
+ }
+ }
+
+ if (defaultMethodsToAddByName == null) {
+ // We had no interfering default methods:
+ return introspectionMDs;
+ }
+
+ // Recreate introspectionMDs so that its size can grow:
+ ArrayList<MethodDescriptor> newIntrospectionMDs
+ = new ArrayList<MethodDescriptor>(introspectionMDs.size() + 16);
+ for (MethodDescriptor introspectorMD : introspectionMDs) {
+ Method introspectorM = introspectorMD.getMethod();
+ // Prevent cases where the same method is added with different return types both from the list of default
+ // methods and from the list of Introspector-discovered methods, as that would lead to overloaded method
+ // selection ambiguity later. This is known to happen when the default method in an interface has reified
+ // return type, and then the interface is implemented by a class where the compiler generates an override
+ // for the bridge method only. (Other tricky cases might exist.)
+ if (!containsMethodWithSameParameterTypes(
+ defaultMethodsToAddByName.get(introspectorM.getName()), introspectorM)) {
+ newIntrospectionMDs.add(introspectorMD);
+ }
+ }
+ introspectionMDs = newIntrospectionMDs;
+
+ // Add default methods:
+ for (Entry<String, List<Method>> entry : defaultMethodsToAddByName.entrySet()) {
+ for (Method method : entry.getValue()) {
+ introspectionMDs.add(new MethodDescriptor(method));
+ }
+ }
+
+ return introspectionMDs;
+ }
+
+ private boolean containsMethodWithSameParameterTypes(List<Method> overloads, Method m) {
+ if (overloads == null) {
+ return false;
+ }
+
+ Class<?>[] paramTypes = m.getParameterTypes();
+ for (Method overload : overloads) {
+ if (Arrays.equals(overload.getParameterTypes(), paramTypes)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) {
if (pd instanceof IndexedPropertyDescriptor) {
@@ -522,8 +800,10 @@ class ClassIntrospector {
/**
* As of this writing, this is only used for testing if method order really doesn't mater.
*/
- private MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] methodDescriptors) {
- return methodSorter != null ? methodSorter.sortMethodDescriptors(methodDescriptors) : methodDescriptors;
+ private void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors) {
+ if (methodSorter != null) {
+ methodSorter.sortMethodDescriptors(methodDescriptors);
+ }
}
boolean isAllowedToExpose(Method method) {
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java b/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
index c1edd7c..773e8f3 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
@@ -20,13 +20,17 @@
package org.apache.freemarker.core.model.impl;
import java.beans.MethodDescriptor;
+import java.util.List;
/**
* Used for JUnit testing method-order dependence bugs via
- * {@link DefaultObjectWrapperBuilder#setMethodSorter(MethodSorter)}.
+ * {@link DefaultObjectWrapper#setMethodSorter(MethodSorter)}.
*/
interface MethodSorter {
- MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] methodDescriptors);
-
-}
+ /**
+ * Sorts the methods in place (that is, by modifying the parameter list).
+ */
+ void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors);
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java b/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
index cc063cc..82da455 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtil.java
@@ -290,4 +290,30 @@ public final class _MethodUtil {
"; see cause exception in the Java stack trace.");
}
+ /**
+ * Extracts the JavaBeans property from a reader method name, or returns {@code null} if the method name doesn't
+ * look like a reader method name.
+ */
+ public static String getBeanPropertyNameFromReaderMethodName(String name, Class<?> returnType) {
+ int start;
+ if (name.startsWith("get")) {
+ start = 3;
+ } else if (returnType == boolean.class && name.startsWith("is")) {
+ start = 2;
+ } else {
+ return null;
+ }
+ int ln = name.length();
+
+ if (start == ln) {
+ return null;
+ }
+ char c1 = name.charAt(start);
+
+ return start + 1 < ln && Character.isUpperCase(name.charAt(start + 1)) && Character.isUpperCase(c1)
+ ? name.substring(start) // getFOOBar => "FOOBar" (not lower case) according the JavaBeans spec.
+ : new StringBuilder(ln - start).append(Character.toLowerCase(c1)).append(name, start + 1, ln)
+ .toString();
+ }
+
}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/manual/en_US/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/src/manual/en_US/FM3-CHANGE-LOG.txt b/src/manual/en_US/FM3-CHANGE-LOG.txt
index 8a2dd81..f59ed0b 100644
--- a/src/manual/en_US/FM3-CHANGE-LOG.txt
+++ b/src/manual/en_US/FM3-CHANGE-LOG.txt
@@ -143,4 +143,11 @@ the FreeMarer 3 changelog here:
- <#call ...> (deprecated by <@... />)
- <#comment>...</#comment> (deprecated by <#-- ... -->)
- <#transform ...>...</#transform> (deprecated by <@....@...>)
- - <#foreach x in xs>...</#foreach> (deprecated by <#list xs as x>...</#list>)
\ No newline at end of file
+ - <#foreach x in xs>...</#foreach> (deprecated by <#list xs as x>...</#list>)
+- If for an indexed JavaBean property there's both an indexed read method (like `Foo getFoo(int index)`) and a normal read method
+ (like Foo[] getFoo()), we prefer the normal read method, and so the result will be a clean FTL sequence (not a multi-type value
+ with sequence+method type). If there's only an indexed read method, then we don't expose the property anymore, but the indexed
+ read method can still be called as an usual method (as `myObj.getFoo(index)`). These changes were made because building on the
+ indexed read method we can't create a proper sequence (which the value of the property should be), since sequences are required
+ to support returning their size. (In FreeMarker 2 such sequences has thrown exception on calling size(), which caused more
+ problems and confusion than it solved.)
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java b/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
index 6080c5a..149ad0d 100644
--- a/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
+++ b/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
@@ -20,10 +20,9 @@
package org.apache.freemarker.core.model.impl;
import java.beans.MethodDescriptor;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.List;
class AlphabeticalMethodSorter implements MethodSorter {
@@ -34,16 +33,13 @@ class AlphabeticalMethodSorter implements MethodSorter {
}
@Override
- public MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] methodDescriptors) {
- ArrayList<MethodDescriptor> ls = new ArrayList<>(Arrays.asList(methodDescriptors));
- Collections.sort(ls, new Comparator<MethodDescriptor>() {
- @Override
+ public void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors) {
+ Collections.sort(methodDescriptors, new Comparator<MethodDescriptor>() {
public int compare(MethodDescriptor o1, MethodDescriptor o2) {
int res = o1.getMethod().toString().compareTo(o2.getMethod().toString());
return desc ? -res : res;
}
});
- return ls.toArray(new MethodDescriptor[ls.size()]);
}
}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java b/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java
new file mode 100644
index 0000000..2c9d4e9
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBean.java
@@ -0,0 +1,30 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public class BridgeMethodsBean extends BridgeMethodsBeanBase<String> {
+
+ static final String M1_RETURN_VALUE = "m1ReturnValue";
+
+ @Override
+ public String m1() {
+ return M1_RETURN_VALUE;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java b/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java
new file mode 100644
index 0000000..4ecec7c
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/BridgeMethodsBeanBase.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public abstract class BridgeMethodsBeanBase<T> {
+
+ public abstract T m1();
+
+ public T m2() {
+ return null;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java
new file mode 100644
index 0000000..c7d27a6
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public class Java8BridgeMethodsWithDefaultMethodBean implements Java8BridgeMethodsWithDefaultMethodBeanBase<String> {
+
+ static final String M1_RETURN_VALUE = "m1ReturnValue";
+
+ public String m1() {
+ return M1_RETURN_VALUE;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java
new file mode 100644
index 0000000..7dfb39a
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBean2.java
@@ -0,0 +1,23 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public class Java8BridgeMethodsWithDefaultMethodBean2 implements Java8BridgeMethodsWithDefaultMethodBeanBase2 {
+ // All inherited
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java
new file mode 100644
index 0000000..fdd8821
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase.java
@@ -0,0 +1,31 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public interface Java8BridgeMethodsWithDefaultMethodBeanBase<T> {
+
+ default T m1() {
+ return null;
+ }
+
+ default T m2() {
+ return null;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java
new file mode 100644
index 0000000..6f68dc7
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8BridgeMethodsWithDefaultMethodBeanBase2.java
@@ -0,0 +1,28 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public interface Java8BridgeMethodsWithDefaultMethodBeanBase2 extends Java8BridgeMethodsWithDefaultMethodBeanBase<String> {
+
+ @Override
+ default String m1() {
+ return Java8BridgeMethodsWithDefaultMethodBean.M1_RETURN_VALUE;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java
new file mode 100644
index 0000000..eabc3d0
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBean.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public class Java8DefaultMethodsBean implements Java8DefaultMethodsBeanBase {
+
+ static final String NORMAL_PROP = "normalProp";
+ static final String NORMAL_PROP_VALUE = "normalPropValue";
+ static final String PROP_2_OVERRIDE_VALUE = "prop2OverrideValue";
+ static final int NOT_AN_INDEXED_PROP_VALUE = 1;
+ static final String ARRAY_PROP_2_VALUE_0 = "arrayProp2[0].value";
+ private static final int NOT_AN_INDEXED_PROP_3_VALUE = 3;
+ private static final String NOT_AN_INDEXED_PROP_2_VALUE = "notAnIndecedProp2Value";
+ static final String INDEXED_PROP_4 = "indexedProp4";
+ static final String INDEXED_PROP_GETTER_4 = "getIndexedProp4";
+ static final String INDEXED_PROP_4_VALUE = "indexedProp4Value[0]";
+ static final String NORMAL_ACTION = "normalAction";
+ static final String NORMAL_ACTION_RETURN_VALUE = "normalActionReturnValue";
+ static final String OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE = "overriddenValue";
+
+ public String getNormalProp() {
+ return NORMAL_PROP_VALUE;
+ }
+
+ @Override
+ public String getDefaultMethodProp2() {
+ return PROP_2_OVERRIDE_VALUE;
+ }
+
+ public String[] getDefaultMethodIndexedProp2() {
+ return new String[] { ARRAY_PROP_2_VALUE_0 };
+ }
+
+ /**
+ * There's a matching non-indexed reader method in the base class, but as this is indexed, it takes over.
+ */
+ public String getDefaultMethodIndexedProp3(int index) {
+ return "";
+ }
+
+ public int getDefaultMethodNotAnIndexedProp() {
+ return NOT_AN_INDEXED_PROP_VALUE;
+ }
+
+ /** Actually, this will be indexed if the default method support is off. */
+ public String getDefaultMethodNotAnIndexedProp2(int index) {
+ return NOT_AN_INDEXED_PROP_2_VALUE;
+ }
+
+ /** Actually, this will be indexed if the default method support is off. */
+ public int getDefaultMethodNotAnIndexedProp3(int index) {
+ return NOT_AN_INDEXED_PROP_3_VALUE;
+ }
+
+ public String getIndexedProp4(int index) {
+ return INDEXED_PROP_4_VALUE;
+ }
+
+ public String normalAction() {
+ return NORMAL_ACTION_RETURN_VALUE;
+ }
+
+ @Override
+ public String overriddenDefaultMethodAction() {
+ return OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java
new file mode 100644
index 0000000..c01422e
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultMethodsBeanBase.java
@@ -0,0 +1,97 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+public interface Java8DefaultMethodsBeanBase {
+
+ static final String DEFAULT_METHOD_PROP = "defaultMethodProp";
+ static final String DEFAULT_METHOD_PROP_VALUE = "defaultMethodPropValue";
+ static final String DEFAULT_METHOD_PROP_2 = "defaultMethodProp2";
+ static final String DEFAULT_METHOD_INDEXED_PROP = "defaultMethodIndexedProp";
+ static final String DEFAULT_METHOD_INDEXED_PROP_GETTER = "getDefaultMethodIndexedProp";
+ static final String DEFAULT_METHOD_INDEXED_PROP_VALUE = "defaultMethodIndexedPropValue";
+ static final String DEFAULT_METHOD_INDEXED_PROP_2 = "defaultMethodIndexedProp2";
+ static final String DEFAULT_METHOD_INDEXED_PROP_2_VALUE_0 = "defaultMethodIndexedProp2(0).value";
+ static final String DEFAULT_METHOD_INDEXED_PROP_3 = "defaultMethodIndexedProp3";
+ static final String DEFAULT_METHOD_INDEXED_PROP_3_VALUE_0 = "indexedProp3Value[0]";
+ static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP = "defaultMethodNotAnIndexedProp";
+ static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_VALUE = "defaultMethodNotAnIndexedPropValue";
+ static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2 = "defaultMethodNotAnIndexedProp2";
+ static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2_VALUE = "defaultMethodNotAnIndexedProp2Value";
+ static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3 = "defaultMethodNotAnIndexedProp3";
+ static final String DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3_VALUE_0 = "defaultMethodNotAnIndexedProp3Value[0]";
+ static final String DEFAULT_METHOD_ACTION = "defaultMethodAction";
+ static final String DEFAULT_METHOD_ACTION_RETURN_VALUE = "defaultMethodActionReturnValue";
+ static final String OVERRIDDEN_DEFAULT_METHOD_ACTION = "overriddenDefaultMethodAction";
+
+ default String getDefaultMethodProp() {
+ return DEFAULT_METHOD_PROP_VALUE;
+ }
+
+ default String getDefaultMethodProp2() {
+ return "";
+ }
+
+ /**
+ * Will be kept as there's no non-indexed read methods for this.
+ */
+ default String getDefaultMethodIndexedProp(int i) {
+ return DEFAULT_METHOD_INDEXED_PROP_VALUE;
+ }
+
+ /**
+ * Will be kept as there will be a matching non-indexed read method in the subclass.
+ * However, as of FM3, the non-indexed read method is used if it's available.
+ */
+ default String getDefaultMethodIndexedProp2(int i) {
+ return DEFAULT_METHOD_INDEXED_PROP_2_VALUE_0;
+ }
+
+ /**
+ * This is not an indexed reader method, but a matching indexed reader method will be added in the subclass.
+ * However, as of FM3, the non-indexed read method is used if it's available.
+ */
+ default String[] getDefaultMethodIndexedProp3() {
+ return new String[] {DEFAULT_METHOD_INDEXED_PROP_3_VALUE_0};
+ }
+
+ /** Will be discarded because of a non-matching non-indexed read method in a subclass */
+ default String getDefaultMethodNotAnIndexedProp(int i) {
+ return "";
+ }
+
+ /** The subclass will try to override this with a non-matching indexed reader, but this will be stronger. */
+ default String getDefaultMethodNotAnIndexedProp2() {
+ return DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2_VALUE;
+ }
+
+ /** The subclass will try to override this with a non-matching indexed reader, but this will be stronger. */
+ default String[] getDefaultMethodNotAnIndexedProp3() {
+ return new String[] { DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3_VALUE_0 };
+ }
+
+ default String defaultMethodAction() {
+ return DEFAULT_METHOD_ACTION_RETURN_VALUE;
+ }
+
+ default Object overriddenDefaultMethodAction() {
+ return null;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.java
new file mode 100644
index 0000000..57b8810
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperBridgeMethodsTest.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 org.apache.freemarker.core.model.impl;
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+public class Java8DefaultObjectWrapperBridgeMethodsTest {
+
+ @Test
+ public void testWithoutDefaultMethod() throws TemplateModelException {
+ test(BridgeMethodsBean.class);
+ }
+
+ @Test
+ public void testWithDefaultMethod() throws TemplateModelException {
+ test(Java8BridgeMethodsWithDefaultMethodBean.class);
+ }
+
+ @Test
+ public void testWithDefaultMethod2() throws TemplateModelException {
+ test(Java8BridgeMethodsWithDefaultMethodBean2.class);
+ }
+
+ private void test(Class<?> pClass) throws TemplateModelException {
+ DefaultObjectWrapper ow = new DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+ TemplateHashModel wrapped;
+ try {
+ wrapped = (TemplateHashModel) ow.wrap(pClass.newInstance());
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+
+ TemplateMethodModelEx m1 = (TemplateMethodModelEx) wrapped.get("m1");
+ assertEquals(BridgeMethodsBean.M1_RETURN_VALUE, "" + m1.exec(Collections.emptyList()));
+
+ TemplateMethodModelEx m2 = (TemplateMethodModelEx) wrapped.get("m2");
+ assertNull(m2.exec(Collections.emptyList()));
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java
new file mode 100644
index 0000000..c4fe82f
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/Java8DefaultObjectWrapperTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 org.apache.freemarker.core.model.impl;
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+public class Java8DefaultObjectWrapperTest {
+
+ @Test
+ public void testDefaultMethodRecognized() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+ DefaultObjectWrapper ow = owb.build();
+ TemplateHashModel wrappedBean = (TemplateHashModel) ow.wrap(new Java8DefaultMethodsBean());
+
+ {
+ TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(Java8DefaultMethodsBean.NORMAL_PROP);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBean.NORMAL_PROP_VALUE, prop.getAsString());
+ }
+ {
+ // This is overridden in the subclass, so it's visible even without default method support:
+ TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_PROP_2);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBean.PROP_2_OVERRIDE_VALUE, prop.getAsString());
+ }
+ {
+ TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(
+ Java8DefaultMethodsBeanBase.DEFAULT_METHOD_PROP);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_PROP_VALUE, prop.getAsString());
+ }
+ {
+ // Has only indexed read method, so it's not exposed as a property
+ assertNull(wrappedBean.get(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP));
+
+ TemplateMethodModelEx indexedReadMethod = (TemplateMethodModelEx) wrappedBean.get(
+ Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_GETTER);
+ assertNotNull(indexedReadMethod);
+ assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_VALUE,
+ ((TemplateScalarModel) indexedReadMethod.exec(Collections.singletonList(new SimpleNumber(0))))
+ .getAsString
+ ());
+ }
+ {
+ // We see default method indexed read method, but it's invalidated by normal getter in the subclass
+ TemplateNumberModel prop = (TemplateNumberModel) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBean.NOT_AN_INDEXED_PROP_VALUE, prop.getAsNumber());
+ }
+ {
+ // The default method read method invalidates the indexed read method in the subclass
+ TemplateScalarModel prop = (TemplateScalarModel) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2_VALUE, prop.getAsString());
+ }
+ {
+ // The default method read method invalidates the indexed read method in the subclass
+ TemplateSequenceModel prop = (TemplateSequenceModel) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3_VALUE_0,
+ ((TemplateScalarModel) prop.get(0)).getAsString());
+ }
+ {
+ // We see the default method indexed reader, which overrides the plain array reader in the subclass.
+ TemplateSequenceModel prop = (TemplateSequenceModel) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_INDEXED_PROP_2);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBean.ARRAY_PROP_2_VALUE_0,
+ ((TemplateScalarModel) prop.get(0)).getAsString());
+ }
+ {
+ // We do see the default method non-indexed reader, but the subclass has a matching indexed reader, so that
+ // takes over.
+ TemplateSequenceModel prop = (TemplateSequenceModel) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_INDEXED_PROP_3);
+ assertNotNull(prop);
+ assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_3_VALUE_0,
+ ((TemplateScalarModel) prop.get(0)).getAsString());
+ }
+ {
+ // Only present in the subclass.
+
+ // Has only indexed read method, so it's not exposed as a property
+ assertNull(wrappedBean.get(Java8DefaultMethodsBean.INDEXED_PROP_4));
+
+ TemplateMethodModelEx indexedReadMethod = (TemplateMethodModelEx) wrappedBean.get(
+ Java8DefaultMethodsBean.INDEXED_PROP_GETTER_4);
+ assertNotNull(indexedReadMethod);
+ assertEquals(Java8DefaultMethodsBean.INDEXED_PROP_4_VALUE,
+ ((TemplateScalarModel) indexedReadMethod.exec(Collections.singletonList(new SimpleNumber(0))))
+ .getAsString());
+ }
+ {
+ TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+ Java8DefaultMethodsBean.NORMAL_ACTION);
+ assertNotNull(action);
+ assertEquals(
+ Java8DefaultMethodsBean.NORMAL_ACTION_RETURN_VALUE,
+ ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+ }
+
+ {
+ TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+ Java8DefaultMethodsBean.NORMAL_ACTION);
+ assertNotNull(action);
+ assertEquals(
+ Java8DefaultMethodsBean.NORMAL_ACTION_RETURN_VALUE,
+ ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+ }
+ {
+ TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_ACTION);
+ assertNotNull(action);
+ assertEquals(
+ Java8DefaultMethodsBean.DEFAULT_METHOD_ACTION_RETURN_VALUE,
+ ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+ }
+ {
+ TemplateMethodModelEx action = (TemplateMethodModelEx) wrappedBean.get(
+ Java8DefaultMethodsBean.OVERRIDDEN_DEFAULT_METHOD_ACTION);
+ assertNotNull(action);
+ assertEquals(
+ Java8DefaultMethodsBean.OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE,
+ ((TemplateScalarModel) action.exec(Collections.emptyList())).getAsString());
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/java/org/apache/freemarker/test/templatesuite/models/BeanTestClass.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/test/templatesuite/models/BeanTestClass.java b/src/test/java/org/apache/freemarker/test/templatesuite/models/BeanTestClass.java
index 1c2a312..7e7fa82 100644
--- a/src/test/java/org/apache/freemarker/test/templatesuite/models/BeanTestClass.java
+++ b/src/test/java/org/apache/freemarker/test/templatesuite/models/BeanTestClass.java
@@ -32,7 +32,11 @@ public class BeanTestClass extends BeanTestSuperclass implements BeanTestInterfa
public String getBar(int index) {
return "bar-value-" + index;
}
-
+
+ public String[] getBar() {
+ return new String[] { "bar-value-0", "bar-value-1", "bar-value-2" };
+ }
+
public String overloaded(int i) {
return "overloaded-int-" + i;
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/529cdd86/src/test/resources/org/apache/freemarker/test/templatesuite/templates/default-object-wrapper.ftl
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/freemarker/test/templatesuite/templates/default-object-wrapper.ftl b/src/test/resources/org/apache/freemarker/test/templatesuite/templates/default-object-wrapper.ftl
index df45543..bcf903d 100644
--- a/src/test/resources/org/apache/freemarker/test/templatesuite/templates/default-object-wrapper.ftl
+++ b/src/test/resources/org/apache/freemarker/test/templatesuite/templates/default-object-wrapper.ftl
@@ -33,7 +33,7 @@ ${map?api.get(objKey)}
${obj.foo}
<#if obj.foo?exists>hasfoo<#else>nofoo</#if>
<#if obj.baz?exists>hasbaz<#else>nobaz</#if>
-${obj.bar(0)}
+${obj.bar[0]}
${obj.getFoo()}
${obj.overloaded(1?int)}
${obj.overloaded("String")}