Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id E987F200C39 for ; Thu, 16 Mar 2017 16:41:33 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id E82CC160B7A; Thu, 16 Mar 2017 15:41:33 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id CD8B2160B78 for ; Thu, 16 Mar 2017 16:41:31 +0100 (CET) Received: (qmail 34866 invoked by uid 500); 16 Mar 2017 15:41:31 -0000 Mailing-List: contact notifications-help@freemarker.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@freemarker.incubator.apache.org Delivered-To: mailing list notifications@freemarker.incubator.apache.org Received: (qmail 34857 invoked by uid 99); 16 Mar 2017 15:41:31 -0000 Received: from pnap-us-west-generic-nat.apache.org (HELO spamd1-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 16 Mar 2017 15:41:31 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd1-us-west.apache.org (ASF Mail Server at spamd1-us-west.apache.org) with ESMTP id 77553C0327 for ; Thu, 16 Mar 2017 15:41:30 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd1-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: -3.569 X-Spam-Level: X-Spam-Status: No, score=-3.569 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RP_MATCHES_RCVD=-0.001, SPF_NEUTRAL=0.652] autolearn=disabled Received: from mx1-lw-eu.apache.org ([10.40.0.8]) by localhost (spamd1-us-west.apache.org [10.40.0.7]) (amavisd-new, port 10024) with ESMTP id hCq1ZGL6TU_p for ; Thu, 16 Mar 2017 15:41:26 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-lw-eu.apache.org (ASF Mail Server at mx1-lw-eu.apache.org) with SMTP id 034EE5F473 for ; Thu, 16 Mar 2017 15:41:23 +0000 (UTC) Received: (qmail 34781 invoked by uid 99); 16 Mar 2017 15:41:23 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 16 Mar 2017 15:41:23 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 0B672DFE8F; Thu, 16 Mar 2017 15:41:23 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: ddekany@apache.org To: notifications@freemarker.incubator.apache.org Message-Id: X-Mailer: ASF-Git Admin Mailer Subject: incubator-freemarker git commit: Forward ported Java 8 default method support. Date: Thu, 16 Mar 2017 15:41:23 +0000 (UTC) archived-at: Thu, 16 Mar 2017 15:41:34 -0000 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 Authored: Thu Mar 16 16:41:17 2017 +0100 Committer: ddekany 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 introspData, Class clazz, Map> 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 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 mds = getMethodDescriptors(beanInfo, clazz); + sortMethodDescriptors(mds); + int mdsSize = mds.size(); + IdentityHashMap 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(); + } + 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 getPropertyDescriptors(BeanInfo beanInfo, Class clazz) { + PropertyDescriptor[] introspectorPDsArray = beanInfo.getPropertyDescriptors(); + List introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray) + : Collections.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, 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: + // (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(); + } + 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 mergedPDs = new ArrayList(mergedPRMPs.size()); + for (Entry 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 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 mergedPRMPs, + String propName, PropertyReaderedMethodPair newPRM) { + Object replaced = mergedPRMPs.put(propName, newPRM); + if (replaced != null) { + putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRM); + } + } + + private void mergeInPropertyReaderMethod(LinkedHashMap 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 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 getMethodDescriptors(BeanInfo beanInfo, Class clazz) { + MethodDescriptor[] introspectorMDArray = beanInfo.getMethodDescriptors(); + List introspectionMDs = introspectorMDArray != null && introspectorMDArray.length != 0 + ? Arrays.asList(introspectorMDArray) : Collections.emptyList(); + + if (_JavaVersions.JAVA_8 == null) { + // java.beans.Introspector was good enough then. + return introspectionMDs; + } + + Map> defaultMethodsToAddByName = null; + for (Method method : clazz.getMethods()) { + if (_JavaVersions.JAVA_8.isDefaultMethod(method) && !method.isBridge()) { + if (defaultMethodsToAddByName == null) { + defaultMethodsToAddByName = new HashMap>(); + } + List overloads = defaultMethodsToAddByName.get(method.getName()); + if (overloads == null) { + overloads = new ArrayList(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 newIntrospectionMDs + = new ArrayList(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> entry : defaultMethodsToAddByName.entrySet()) { + for (Method method : entry.getValue()) { + introspectionMDs.add(new MethodDescriptor(method)); + } + } + + return introspectionMDs; + } + + private boolean containsMethodWithSameParameterTypes(List 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 introspData, PropertyDescriptor pd, Class clazz, Map> 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 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 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>... (deprecated by <#-- ... -->) - <#transform ...>... (deprecated by <@...>...) - - <#foreach x in xs>... (deprecated by <#list xs as x>...) \ No newline at end of file + - <#foreach x in xs>... (deprecated by <#list xs as x>...) +- 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 ls = new ArrayList<>(Arrays.asList(methodDescriptors)); - Collections.sort(ls, new Comparator() { - @Override + public void sortMethodDescriptors(List methodDescriptors) { + Collections.sort(methodDescriptors, new Comparator() { 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 { + + 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 { + + 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 { + + 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 { + + 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 { + + @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 obj.baz?exists>hasbaz<#else>nobaz -${obj.bar(0)} +${obj.bar[0]} ${obj.getFoo()} ${obj.overloaded(1?int)} ${obj.overloaded("String")}