FREEMARKER-24: Added workaround (not enabled by default) to expose Java 8 
default methods (and the bean properties they define) to templates, despite 
that java.beans.Introspector (the official JavaBeans introspector) ignores 
them, at least as of JRE 1.8.0_66. To enable this workaround, either increase 
the value of the incompatibleImprovements constructor argument of 
DefaultObjectWrapper or BeansWrapper the used to 2.3.26, or set its 
treatDefaultMethodsAsBeanMembers setting to true. Note that if you leave the 
object_wrapper setting of the Configuration on its default, it's enough to 
increase the incompatibleImprovements setting of the Configuration to 2.3.26, 
as that's inherited by the default object_wrapper.


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/173abfe9
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/173abfe9
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/173abfe9

Branch: refs/heads/2.3
Commit: 173abfe9bbcf7544e73346da471bd563809c7c85
Parents: 393d3be
Author: ddekany <[email protected]>
Authored: Tue Mar 7 15:31:30 2017 +0100
Committer: ddekany <[email protected]>
Committed: Sat Mar 11 00:34:55 2017 +0100

----------------------------------------------------------------------
 README                                          |   2 -
 .../java/freemarker/ext/beans/BeansWrapper.java |  77 +++-
 .../ext/beans/BeansWrapperConfiguration.java    |  37 +-
 .../freemarker/ext/beans/ClassIntrospector.java | 364 ++++++++++++++++---
 .../ext/beans/ClassIntrospectorBuilder.java     |  15 +
 .../java/freemarker/ext/beans/MethodSorter.java |   6 +-
 .../java/freemarker/ext/beans/_MethodUtil.java  |  26 ++
 .../java/freemarker/template/Configuration.java |   8 +
 src/manual/en_US/book.xml                       |  22 ++
 .../ext/beans/AlphabeticalMethodSorter.java     |   9 +-
 .../ext/beans/BeansWrapperJava8Test.java        | 221 +++++++++++
 ...GetPropertyNameFromReaderMethodNameTest.java |  43 +++
 .../ext/beans/Java8DefaultMethodsBean.java      |  65 ++++
 .../ext/beans/Java8DefaultMethodsBeanBase.java  |  72 ++++
 .../freemarker/template/ConfigurationTest.java  |   5 +
 .../template/DefaultObjectWrapperTest.java      |   2 +-
 16 files changed, 876 insertions(+), 98 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/README
----------------------------------------------------------------------
diff --git a/README b/README
index d5ab3b7..bcd8f20 100644
--- a/README
+++ b/README
@@ -200,8 +200,6 @@ apply it to your development environment:
    - Press "Finish"
 - Eclipse will indicate many errors at this point; it's expected, read on.
 - Project -> Properties -> Java Compiler
-  - Set "Compiler Compliance Level" to "1.5" (you will have to uncheck
-    "Use compliance from execution environment" for that)
   - In Errors/Warnings, check in "Enable project specific settings", then set
     "Forbidden reference (access rules)" from "Error" to "Warning".
 - You will still have errors on these java files (because different java

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/main/java/freemarker/ext/beans/BeansWrapper.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java 
b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index a9a3cbe..1803684 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -241,6 +241,12 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      *       empty exactly if it has no elements left. (Note that this bug has 
never affected basic functionality, like
      *       {@code <#list ...>}.) 
      *     </li>  
+     *     <li>
+     *       <p>2.3.26 (or higher):
+     *       The default of {@link 
BeansWrapper#getTreatDefaultMethodsAsBeanMembers()} changes from {@code false} 
to
+     *       {@code true}. Thus, Java 8 default methods (and the bean 
properties they define) are exposed, despite that
+     *       {@link java.beans.Introspector} (the official JavaBeans 
introspector) ignores them, at least as of Java 8. 
+     *     </li>  
      *   </ul>
      *   
      *   <p>Note that the version will be normalized to the lowest version 
where the same incompatible
@@ -336,11 +342,11 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
             // but we need to use the same sharedInrospectionLock forever, 
because that's what the model factories
             // synchronize on, even during the classIntrospector is being 
replaced.
             sharedIntrospectionLock = new Object();
-            classIntrospector = new 
ClassIntrospector(bwConf.classIntrospectorFactory, sharedIntrospectionLock);
+            classIntrospector = new 
ClassIntrospector(bwConf.classIntrospectorBuilder, sharedIntrospectionLock);
         } else {
             // As this is a read-only BeansWrapper, the classIntrospector is 
never replaced, and since it's shared by
             // other BeansWrapper instances, we use the lock belonging to the 
shared ClassIntrospector.
-            classIntrospector = bwConf.classIntrospectorFactory.build();
+            classIntrospector = bwConf.classIntrospectorBuilder.build();
             sharedIntrospectionLock = classIntrospector.getSharedLock(); 
         }
         
@@ -539,9 +545,9 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         checkModifiable();
      
         if (classIntrospector.getExposureLevel() != exposureLevel) {
-            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
-            pa.setExposureLevel(exposureLevel);
-            replaceClassIntrospector(pa);
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            builder.setExposureLevel(exposureLevel);
+            replaceClassIntrospector(builder);
         }
     }
     
@@ -566,9 +572,34 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         checkModifiable();
         
         if (classIntrospector.getExposeFields() != exposeFields) {
-            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
-            pa.setExposeFields(exposeFields);
-            replaceClassIntrospector(pa);
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            builder.setExposeFields(exposeFields);
+            replaceClassIntrospector(builder);
+        }
+    }
+    
+    /**
+     * Controls whether Java 8 default methods that weren't overridden in a 
class will be recognized as bean property
+     * accessors and/or bean actions, and thus will be visible from templates. 
(We expose bean properties and bean
+     * actions, not methods in general.) Before {@link 
#getIncompatibleImprovements incompatibleImprovements} 2.3.26
+     * this defaults to {@code false} for backward compatibility. Starting 
with {@link #getIncompatibleImprovements
+     * incompatibleImprovements} 2.3.26 it defaults to {@code true}.
+     * <p>
+     * Some explanation: FreeMarker uses {@link java.beans.Introspector} to 
discover the bean properties and actions of
+     * classes, for maximum conformance to the JavaBeans specification. But 
for some reason (perhaps just a bug in the
+     * Oracle/OpenJDK Java 8 implementation) that ignores the Java 8 default 
methods coming from the interfaces. When
+     * this setting is {@code true}, we search for non-overridden default 
methods ourselves, and add them to the set of
+     * discovered bean members.
+     * 
+     * @since 2.3.26
+     */
+    public void setTreatDefaultMethodsAsBeanMembers(boolean 
treatDefaultMethodsAsBeanMembers) {
+        checkModifiable();
+        
+        if (classIntrospector.getTreatDefaultMethodsAsBeanMembers() != 
treatDefaultMethodsAsBeanMembers) {
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            
builder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers);
+            replaceClassIntrospector(builder);
         }
     }
     
@@ -576,11 +607,20 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      * Returns whether exposure of public instance fields of classes is 
      * enabled. See {@link #setExposeFields(boolean)} for details.
      * @return true if public instance fields are exposed, false otherwise.
+     * 
+     * @since 2.3.26
      */
     public boolean isExposeFields() {
         return classIntrospector.getExposeFields();
     }
     
+    /**
+     * See {@link #setTreatDefaultMethodsAsBeanMembers(boolean)}.
+     */
+    public boolean getTreatDefaultMethodsAsBeanMembers() {
+        return classIntrospector.getTreatDefaultMethodsAsBeanMembers();
+    }
+    
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return classIntrospector.getMethodAppearanceFineTuner();
     }
@@ -593,9 +633,9 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         checkModifiable();
         
         if (classIntrospector.getMethodAppearanceFineTuner() != 
methodAppearanceFineTuner) {
-            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
-            pa.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
-            replaceClassIntrospector(pa);
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            builder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+            replaceClassIntrospector(builder);
         }
     }
 
@@ -607,9 +647,9 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         checkModifiable();
         
         if (classIntrospector.getMethodSorter() != methodSorter) {
-            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
-            pa.setMethodSorter(methodSorter);
-            replaceClassIntrospector(pa);
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            builder.setMethodSorter(methodSorter);
+            replaceClassIntrospector(builder);
         }
     }
     
@@ -631,10 +671,10 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      * Replaces the value of {@link #classIntrospector}, but first it 
unregisters
      * the model factories in the old {@link #classIntrospector}.
      */
-    private void replaceClassIntrospector(ClassIntrospectorBuilder pa) {
+    private void replaceClassIntrospector(ClassIntrospectorBuilder builder) {
         checkModifiable();
         
-        final ClassIntrospector newCI = new ClassIntrospector(pa, 
sharedIntrospectionLock);
+        final ClassIntrospector newCI = new ClassIntrospector(builder, 
sharedIntrospectionLock);
         final ClassIntrospector oldCI;
         
         // In principle this need not be synchronized, but as apps might 
publish the configuration improperly, or
@@ -803,7 +843,8 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         if (incompatibleImprovements.intValue() < 
_TemplateAPI.VERSION_INT_2_3_0) {
             throw new IllegalArgumentException("Version must be at least 
2.3.0.");
         }
-        return is2324Bugfixed(incompatibleImprovements) ? 
Configuration.VERSION_2_3_24
+        return incompatibleImprovements.intValue() >= 
_TemplateAPI.VERSION_INT_2_3_26 ? Configuration.VERSION_2_3_26
+                : is2324Bugfixed(incompatibleImprovements) ? 
Configuration.VERSION_2_3_24
                 : is2321Bugfixed(incompatibleImprovements) ? 
Configuration.VERSION_2_3_21
                 : Configuration.VERSION_2_3_0;
     }
@@ -1692,6 +1733,8 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         return "simpleMapWrapper=" + simpleMapWrapper + ", "
                + "exposureLevel=" + classIntrospector.getExposureLevel() + ", "
                + "exposeFields=" + classIntrospector.getExposeFields() + ", "
+               + "treatDefaultMethodsAsBeanMembers="
+               + classIntrospector.getTreatDefaultMethodsAsBeanMembers() + ", "
                + "sharedClassIntrospCache="
                + (classIntrospector.isShared() ? "@" + 
System.identityHashCode(classIntrospector) : "none");
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java 
b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
index d80a812..26764e8 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
@@ -42,7 +42,7 @@ public abstract class BeansWrapperConfiguration implements 
Cloneable {
 
     private final Version incompatibleImprovements;
     
-    protected ClassIntrospectorBuilder classIntrospectorFactory;
+    protected ClassIntrospectorBuilder classIntrospectorBuilder;
     
     // Properties and their *defaults*:
     private boolean simpleMapWrapper = false;
@@ -81,7 +81,7 @@ public abstract class BeansWrapperConfiguration implements 
Cloneable {
                 : 
BeansWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
         this.incompatibleImprovements = incompatibleImprovements;
         
-        classIntrospectorFactory = new 
ClassIntrospectorBuilder(incompatibleImprovements);
+        classIntrospectorBuilder = new 
ClassIntrospectorBuilder(incompatibleImprovements);
     }
     
     /**
@@ -101,7 +101,7 @@ public abstract class BeansWrapperConfiguration implements 
Cloneable {
         result = prime * result + (outerIdentity != null ? 
outerIdentity.hashCode() : 0);
         result = prime * result + (strict ? 1231 : 1237);
         result = prime * result + (useModelCache ? 1231 : 1237);
-        result = prime * result + classIntrospectorFactory.hashCode();
+        result = prime * result + classIntrospectorBuilder.hashCode();
         return result;
     }
 
@@ -122,7 +122,7 @@ public abstract class BeansWrapperConfiguration implements 
Cloneable {
         if (outerIdentity != other.outerIdentity) return false;
         if (strict != other.strict) return false;
         if (useModelCache != other.useModelCache) return false;
-        if (!classIntrospectorFactory.equals(other.classIntrospectorFactory)) 
return false;
+        if (!classIntrospectorBuilder.equals(other.classIntrospectorBuilder)) 
return false;
         
         return true;
     }
@@ -131,8 +131,8 @@ public abstract class BeansWrapperConfiguration implements 
Cloneable {
         try {
             BeansWrapperConfiguration clone = (BeansWrapperConfiguration) 
super.clone();
             if (deepCloneKey) {
-                clone.classIntrospectorFactory
-                        = (ClassIntrospectorBuilder) 
classIntrospectorFactory.clone();
+                clone.classIntrospectorBuilder
+                        = (ClassIntrospectorBuilder) 
classIntrospectorBuilder.clone();
             }
             return clone;
         } catch (CloneNotSupportedException e) {
@@ -193,25 +193,34 @@ public abstract class BeansWrapperConfiguration 
implements Cloneable {
     }
     
     public int getExposureLevel() {
-        return classIntrospectorFactory.getExposureLevel();
+        return classIntrospectorBuilder.getExposureLevel();
     }
 
     /** See {@link BeansWrapper#setExposureLevel(int)}. */
     public void setExposureLevel(int exposureLevel) {
-        classIntrospectorFactory.setExposureLevel(exposureLevel);
+        classIntrospectorBuilder.setExposureLevel(exposureLevel);
     }
 
     public boolean getExposeFields() {
-        return classIntrospectorFactory.getExposeFields();
+        return classIntrospectorBuilder.getExposeFields();
     }
 
     /** See {@link BeansWrapper#setExposeFields(boolean)}. */
     public void setExposeFields(boolean exposeFields) {
-        classIntrospectorFactory.setExposeFields(exposeFields);
+        classIntrospectorBuilder.setExposeFields(exposeFields);
+    }
+
+    public boolean getTreatDefaultMethodsAsBeanMembers() {
+        return classIntrospectorBuilder.getTreatDefaultMethodsAsBeanMembers();
+    }
+    
+    /** See {@link BeansWrapper#setTreatDefaultMethodsAsBeanMembers(boolean)} 
*/
+    public void setTreatDefaultMethodsAsBeanMembers(boolean 
treatDefaultMethodsAsBeanMembers) {
+        
classIntrospectorBuilder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers);
     }
 
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
-        return classIntrospectorFactory.getMethodAppearanceFineTuner();
+        return classIntrospectorBuilder.getMethodAppearanceFineTuner();
     }
 
     /**
@@ -220,15 +229,15 @@ public abstract class BeansWrapperConfiguration 
implements Cloneable {
      * the value implements {@link SingletonCustomizer}.
      */
     public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner 
methodAppearanceFineTuner) {
-        
classIntrospectorFactory.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+        
classIntrospectorBuilder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
     }
 
     MethodSorter getMethodSorter() {
-        return classIntrospectorFactory.getMethodSorter();
+        return classIntrospectorBuilder.getMethodSorter();
     }
 
     void setMethodSorter(MethodSorter methodSorter) {
-        classIntrospectorFactory.setMethodSorter(methodSorter);
+        classIntrospectorBuilder.setMethodSorter(methodSorter);
     }
  
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/main/java/freemarker/ext/beans/ClassIntrospector.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java 
b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index 56e510b..8fa0037 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/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,6 +48,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import freemarker.core.BugException;
+import freemarker.core._JavaVersions;
 import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision;
 import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput;
 import freemarker.ext.util.ModelCache;
@@ -136,6 +140,7 @@ class ClassIntrospector {
     final boolean exposeFields;
     final MethodAppearanceFineTuner methodAppearanceFineTuner;
     final MethodSorter methodSorter;
+    final boolean treatDefaultMethodsAsBeanMembers;
     final boolean bugfixed;
 
     /** See {@link #getHasSharedInstanceRestrictons()} */
@@ -185,6 +190,7 @@ class ClassIntrospector {
         this.exposeFields = builder.getExposeFields();
         this.methodAppearanceFineTuner = 
builder.getMethodAppearanceFineTuner();
         this.methodSorter = builder.getMethodSorter();
+        this.treatDefaultMethodsAsBeanMembers = 
builder.getTreatDefaultMethodsAsBeanMembers();
         this.bugfixed = builder.isBugfixed();
 
         this.sharedLock = sharedLock;
@@ -201,7 +207,7 @@ class ClassIntrospector {
      * Returns a {@link ClassIntrospectorBuilder}-s that could be used to 
create an identical {@link #ClassIntrospector}
      * . The returned {@link ClassIntrospectorBuilder} can be modified without 
interfering with anything.
      */
-    ClassIntrospectorBuilder getPropertyAssignments() {
+    ClassIntrospectorBuilder createBuilder() {
         return new ClassIntrospectorBuilder(this);
     }
 
@@ -312,72 +318,311 @@ 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 < BeansWrapper.EXPOSE_PROPERTIES_ONLY) {
             final MethodAppearanceDecision decision = new 
MethodAppearanceDecision();
             MethodAppearanceDecisionInput 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 
MethodAppearanceDecisionInput();
-                            }
-                            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);
+            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 
MethodAppearanceDecisionInput();
                         }
-    
-                        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(bugfixed);
-                                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(bugfixed);
+                            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 (!treatDefaultMethodsAsBeanMembers || _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.isSynthetic()) {
+                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 (!treatDefaultMethodsAsBeanMembers || _JavaVersions.JAVA_8 == null) 
{
+            // java.beans.Introspector was good enough then.
+            return introspectionMDs;
+        }
+        
+        boolean anyDefaultMethodsAdded = false;
+        findDefaultMethods: for (Method method : clazz.getMethods()) {
+            if (_JavaVersions.JAVA_8.isDefaultMethod(method)) {
+                if (!anyDefaultMethodsAdded) {
+                    for (MethodDescriptor methodDescriptor : introspectionMDs) 
{
+                        // Check if java.bean.Introspector now finds default 
methods (it did not in Java 1.8.0_66):
+                        if 
(_JavaVersions.JAVA_8.isDefaultMethod(methodDescriptor.getMethod())) {
+                            break findDefaultMethods;
+                        }
+                        
+                        // Recreate introspectionMDs so that its size can 
grow: 
+                        ArrayList<MethodDescriptor> newIntrospectionMDs
+                                = new 
ArrayList<MethodDescriptor>(introspectionMDs.size() + 16);
+                        newIntrospectionMDs.addAll(introspectionMDs);
+                        introspectionMDs = newIntrospectionMDs;
+                    }
+                    anyDefaultMethodsAdded = true;
+                }
+                introspectionMDs.add(new MethodDescriptor(method));
+            }
+        }
+        
+        return introspectionMDs;
+    }
+
     private void addPropertyDescriptorToClassIntrospectionData(Map<Object, 
Object> introspData,
             PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, 
List<Method>> accessibleMethods) {
         if (pd instanceof IndexedPropertyDescriptor) {
@@ -410,7 +655,6 @@ class ClassIntrospector {
                 try {
                     if (readMethod != publicReadMethod) {
                         pd = new PropertyDescriptor(pd.getName(), 
publicReadMethod, null);
-                        pd.setReadMethod(publicReadMethod);
                     }
                     introspData.put(pd.getName(), pd);
                 } catch (IntrospectionException e) {
@@ -539,8 +783,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) {
@@ -778,6 +1024,10 @@ class ClassIntrospector {
     boolean getExposeFields() {
         return exposeFields;
     }
+    
+    boolean getTreatDefaultMethodsAsBeanMembers() {
+        return treatDefaultMethodsAsBeanMembers;
+    }
 
     MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return methodAppearanceFineTuner;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java 
b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index cbeffc3..25688e5 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -27,6 +27,7 @@ import java.util.Iterator;
 import java.util.Map;
 
 import freemarker.template.Version;
+import freemarker.template._TemplateAPI;
 
 final class ClassIntrospectorBuilder implements Cloneable {
     
@@ -38,6 +39,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
     // Properties and their *defaults*:
     private int exposureLevel = BeansWrapper.EXPOSE_SAFE;
     private boolean exposeFields;
+    private boolean treatDefaultMethodsAsBeanMembers;
     private MethodAppearanceFineTuner methodAppearanceFineTuner;
     private MethodSorter methodSorter;
     // Attention:
@@ -50,6 +52,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
         bugfixed = ci.bugfixed;
         exposureLevel = ci.exposureLevel;
         exposeFields = ci.exposeFields;
+        treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers;
         methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
         methodSorter = ci.methodSorter; 
     }
@@ -59,6 +62,8 @@ final class ClassIntrospectorBuilder implements Cloneable {
         // change in the BeansWrapper.normalizeIncompatibleImprovements 
results. That is, this class may don't react
         // to some version changes that affects BeansWrapper, but not the 
other way around. 
         bugfixed = BeansWrapper.is2321Bugfixed(incompatibleImprovements);
+        treatDefaultMethodsAsBeanMembers
+                = incompatibleImprovements.intValue() >= 
_TemplateAPI.VERSION_INT_2_3_26;
     }
     
     @Override
@@ -76,6 +81,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
         int result = 1;
         result = prime * result + (bugfixed ? 1231 : 1237);
         result = prime * result + (exposeFields ? 1231 : 1237);
+        result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 
1237);
         result = prime * result + exposureLevel;
         result = prime * result + 
System.identityHashCode(methodAppearanceFineTuner);
         result = prime * result + System.identityHashCode(methodSorter);
@@ -91,6 +97,7 @@ final class ClassIntrospectorBuilder implements Cloneable {
         
         if (bugfixed != other.bugfixed) return false;
         if (exposeFields != other.exposeFields) return false;
+        if (treatDefaultMethodsAsBeanMembers != 
other.treatDefaultMethodsAsBeanMembers) return false;
         if (exposureLevel != other.exposureLevel) return false;
         if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) 
return false;
         if (methodSorter != other.methodSorter) return false;
@@ -119,6 +126,14 @@ final class ClassIntrospectorBuilder implements Cloneable {
     public void setExposeFields(boolean exposeFields) {
         this.exposeFields = exposeFields;
     }
+    
+    public boolean getTreatDefaultMethodsAsBeanMembers() {
+        return treatDefaultMethodsAsBeanMembers;
+    }
+
+    public void setTreatDefaultMethodsAsBeanMembers(boolean 
treatDefaultMethodsAsBeanMembers) {
+        this.treatDefaultMethodsAsBeanMembers = 
treatDefaultMethodsAsBeanMembers;
+    }
 
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return methodAppearanceFineTuner;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/main/java/freemarker/ext/beans/MethodSorter.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/MethodSorter.java 
b/src/main/java/freemarker/ext/beans/MethodSorter.java
index f7f7335..64168b4 100644
--- a/src/main/java/freemarker/ext/beans/MethodSorter.java
+++ b/src/main/java/freemarker/ext/beans/MethodSorter.java
@@ -20,6 +20,7 @@
 package freemarker.ext.beans;
 
 import java.beans.MethodDescriptor;
+import java.util.List;
 
 /**
  * Used for JUnit testing method-order dependence bugs via
@@ -27,6 +28,9 @@ import java.beans.MethodDescriptor;
  */
 interface MethodSorter {
 
-    MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] 
methodDescriptors);
+    /**
+     * Sorts the methods in place (that is, by modifying the parameter list).
+     */
+    void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors);
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/main/java/freemarker/ext/beans/_MethodUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/_MethodUtil.java 
b/src/main/java/freemarker/ext/beans/_MethodUtil.java
index c313309..782b944 100644
--- a/src/main/java/freemarker/ext/beans/_MethodUtil.java
+++ b/src/main/java/freemarker/ext/beans/_MethodUtil.java
@@ -291,4 +291,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/173abfe9/src/main/java/freemarker/template/Configuration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/Configuration.java 
b/src/main/java/freemarker/template/Configuration.java
index 4405f70..a34c605 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -814,6 +814,14 @@ public class Configuration extends Configurable implements 
Cloneable, ParserConf
      *          {@link Configurable}-s always do this filtering regardless of 
the incompatible improvements setting. 
      *     </ul>
      *   </li>
+     *   <li><p>
+     *     2.3.26 (or higher):
+     *     <ul>
+     *       <li><p>
+     *          {@link BeansWrapper} and {@link DefaultObjectWrapper} now 
exposes Java 8 default methods (and the bean
+     *          properties they define); see {@link 
BeansWrapper#BeansWrapper(Version)}. 
+     *     </ul>
+     *   </li>
      * </ul>
      * 
      * @throws IllegalArgumentException

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 1b2e8de..3e06a7b 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -26845,6 +26845,28 @@ TemplateModel x = env.getVariable("x");  // get 
variable x</programlisting>
 
           <itemizedlist>
             <listitem>
+              <para><link
+              
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-24";>FREEMARKER-24</link>:
+              Added workaround (not enabled by default) to expose Java 8
+              default methods (and the bean properties they define) to
+              templates, despite that
+              <literal>java.beans.Introspector</literal> (the official
+              JavaBeans introspector) ignores them, at least as of JRE
+              1.8.0_66. To enable this workaround, either increase the value
+              of the <literal>incompatibleImprovements</literal> constructor
+              argument of <literal>DefaultObjectWrapper</literal> or
+              <literal>BeansWrapper</literal> the used to 2.3.26, or set its
+              <literal>treatDefaultMethodsAsBeanMembers</literal> setting to
+              <literal>true</literal>. Note that if you leave the
+              <literal>object_wrapper</literal> setting of the
+              <literal>Configuration</literal> on its default, it's enough to
+              increase the <literal>incompatibleImprovements</literal> setting
+              of the <literal>Configuration</literal> to 2.3.26, as that's
+              inherited by the default <literal>object_wrapper</literal>.
+              </para>
+            </listitem>
+
+            <listitem>
               <para>Added the
               <literal>freemarker.template.TemplateNodeModelEx</literal>
               interface which extends the <literal>TemplateNodeModel</literal>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/test/java/freemarker/ext/beans/AlphabeticalMethodSorter.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/ext/beans/AlphabeticalMethodSorter.java 
b/src/test/java/freemarker/ext/beans/AlphabeticalMethodSorter.java
index 3adbaa8..fa2e7d7 100644
--- a/src/test/java/freemarker/ext/beans/AlphabeticalMethodSorter.java
+++ b/src/test/java/freemarker/ext/beans/AlphabeticalMethodSorter.java
@@ -20,10 +20,9 @@
 package freemarker.ext.beans;
 
 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 {
 
@@ -33,15 +32,13 @@ class AlphabeticalMethodSorter implements MethodSorter {
         this.desc = desc;
     }
 
-    public MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] 
methodDescriptors) {
-        ArrayList<MethodDescriptor> ls = new 
ArrayList<MethodDescriptor>(Arrays.asList(methodDescriptors));
-        Collections.sort(ls, new Comparator<MethodDescriptor>() {
+    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/173abfe9/src/test/java/freemarker/ext/beans/BeansWrapperJava8Test.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/ext/beans/BeansWrapperJava8Test.java 
b/src/test/java/freemarker/ext/beans/BeansWrapperJava8Test.java
new file mode 100644
index 0000000..4312742
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/BeansWrapperJava8Test.java
@@ -0,0 +1,221 @@
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.TemplateSequenceModel;
+
+public class BeansWrapperJava8Test {
+
+    @Test
+    public void testDefaultMethodNotRecognized() throws TemplateModelException 
{
+        BeansWrapperBuilder owb = new 
BeansWrapperBuilder(Configuration.VERSION_2_3_0);
+        // owb.setTreatDefaultMethodsAsBeanMembers(false);
+        BeansWrapper 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());
+        }
+        
assertNull(wrappedBean.get(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_PROP));
+        
assertNull(wrappedBean.get(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP));
+        {
+            // We don't see the default method indexed reader, but see the 
plain reader method 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());
+        }
+        {
+            // We don't see the default method non-indexed reader that would 
spoil the indexed reader in the subclass.
+            TemplateSequenceModel prop = (TemplateSequenceModel) 
wrappedBean.get(
+                    
Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_2);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.NOT_AN_INDEXED_PROP_2_VALUE,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }
+        {
+            // We don't see the default method non-indexed reader that would 
spoil the indexed reader in the subclass.
+            TemplateSequenceModel prop = (TemplateSequenceModel) 
wrappedBean.get(
+                    
Java8DefaultMethodsBean.DEFAULT_METHOD_NOT_AN_INDEXED_PROP_3);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.NOT_AN_INDEXED_PROP_3_VALUE,
+                    ((TemplateNumberModel) prop.get(0)).getAsNumber());
+        }
+        {
+            // We don't see the default method indexed reader, but see 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());
+        }
+        {
+            // Only present in the subclass.
+            TemplateSequenceModel prop = (TemplateSequenceModel) 
wrappedBean.get(
+                    Java8DefaultMethodsBean.INDEXED_PROP_4);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.INDEXED_PROP_4_VALUE,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }        
+        {
+            // We don't see the default method non-indexed reader
+            TemplateSequenceModel prop = (TemplateSequenceModel) 
wrappedBean.get(
+                    Java8DefaultMethodsBean.DEFAULT_METHOD_INDEXED_PROP_3);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.INDEXED_PROP_3_VALUE,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }
+        
+        {
+            TemplateMethodModelEx action = (TemplateMethodModelEx) 
wrappedBean.get(
+                    Java8DefaultMethodsBean.NORMAL_ACTION);
+            assertNotNull(action);
+            assertEquals(
+                    Java8DefaultMethodsBean.NORMAL_ACTION_RETURN_VALUE,
+                    ((TemplateScalarModel) 
action.exec(Collections.emptyList())).getAsString());
+        }
+        
assertNull(wrappedBean.get(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_ACTION));
+        {
+            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());
+        }
+    }
+    
+    @Test
+    public void testDefaultMethodRecognized() throws TemplateModelException {
+        BeansWrapperBuilder owb = new 
BeansWrapperBuilder(Configuration.VERSION_2_3_0);
+        owb.setTreatDefaultMethodsAsBeanMembers(true);
+        BeansWrapper 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());
+        }
+        {
+            TemplateSequenceModel prop = (TemplateSequenceModel) 
wrappedBean.get(
+                    Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP);
+            assertNotNull(prop);
+            
assertEquals(Java8DefaultMethodsBeanBase.DEFAULT_METHOD_INDEXED_PROP_VALUE,
+                    ((TemplateScalarModel) prop.get(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.DEFAULT_METHOD_INDEXED_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(Java8DefaultMethodsBean.INDEXED_PROP_3_VALUE,
+                    ((TemplateScalarModel) prop.get(0)).getAsString());
+        }        
+        {
+            // Only present in the subclass.
+            TemplateSequenceModel prop = (TemplateSequenceModel) 
wrappedBean.get(
+                    Java8DefaultMethodsBean.INDEXED_PROP_4);
+            assertNotNull(prop);
+            assertEquals(Java8DefaultMethodsBean.INDEXED_PROP_4_VALUE,
+                    ((TemplateScalarModel) prop.get(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/173abfe9/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
 
b/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
new file mode 100644
index 0000000..ce95b99
--- /dev/null
+++ 
b/src/test/java/freemarker/ext/beans/GetPropertyNameFromReaderMethodNameTest.java
@@ -0,0 +1,43 @@
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class GetPropertyNameFromReaderMethodNameTest {
+    
+    @Test
+    public void test() {
+       assertEquals("foo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFoo", String.class)); 
+       assertEquals("fo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFo", String.class)); 
+       assertEquals("f", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getF", String.class));
+       
+       assertEquals("FO", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFO", String.class)); 
+       assertEquals("FOo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFOo", String.class)); 
+       assertEquals("FOO", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFOO", String.class));
+       assertEquals("fooBar", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFooBar", String.class));
+       assertEquals("FOoBar", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFOoBar", String.class));
+
+       assertEquals("foo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getFoo", boolean.class)); 
+       assertEquals("foo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", boolean.class)); 
+       assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", 
Boolean.class));
+       assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("isFoo", 
String.class));
+       assertEquals("f", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("isF", boolean.class)); 
+       
+       assertEquals("foo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getfoo", String.class)); 
+       assertEquals("fo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getfo", String.class)); 
+       assertEquals("f", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("getf", String.class));
+       assertEquals("_f", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("get_f", String.class));
+       assertEquals("_F", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("get_F", String.class));
+       assertEquals("_", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("get_", String.class));
+       assertEquals("1f", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("get1f", String.class));
+       assertEquals("1", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("get1", String.class));
+       assertEquals("1F", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("get1F", String.class));
+       assertEquals("foo", 
_MethodUtil.getBeanPropertyNameFromReaderMethodName("isfoo", boolean.class)); 
+       
+       assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("get", 
String.class));
+       assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("is", 
boolean.class));
+       assertNull(_MethodUtil.getBeanPropertyNameFromReaderMethodName("f", 
String.class));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBean.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBean.java 
b/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBean.java
new file mode 100644
index 0000000..0687d84
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBean.java
@@ -0,0 +1,65 @@
+package freemarker.ext.beans;
+
+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 String INDEXED_PROP_3_VALUE = "indexedProp3Value";
+    static final int NOT_AN_INDEXED_PROP_VALUE = 1;
+    static final String ARRAY_PROP_2_VALUE_0 = "arrayProp2[0].value";
+    static final int NOT_AN_INDEXED_PROP_3_VALUE = 3;
+    static final String NOT_AN_INDEXED_PROP_2_VALUE = "notAnIndecedProp2Value";
+    static final String INDEXED_PROP_4 = "indexedProp4";
+    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 INDEXED_PROP_3_VALUE;
+    }
+    
+    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;
+    }
+    
+    public String overriddenDefaultMethodAction() {
+        return OVERRIDDEN_DEFAULT_METHOD_ACTION_RETURN_VALUE;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBeanBase.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBeanBase.java 
b/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBeanBase.java
new file mode 100644
index 0000000..c912ace
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/Java8DefaultMethodsBeanBase.java
@@ -0,0 +1,72 @@
+package freemarker.ext.beans;
+
+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_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_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 "";
+    }
+    
+    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. 
+     */
+    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. 
+     */
+    default String[] getDefaultMethodIndexedProp3() {
+        return new String[] {""};
+    }
+    
+    /** 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/173abfe9/src/test/java/freemarker/template/ConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java 
b/src/test/java/freemarker/template/ConfigurationTest.java
index e3117ad..a4dbfcc 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -162,6 +162,11 @@ public class ConfigurationTest extends TestCase {
         cfg = new Configuration(Configuration.VERSION_2_3_22);
         assertUses2322ObjectWrapper(cfg);
         assertUsesNewTemplateLoader(cfg);
+        
+        cfg = new Configuration(Configuration.VERSION_2_3_25);
+        assertFalse(((DefaultObjectWrapper) 
cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_26);
+        assertTrue(((DefaultObjectWrapper) 
cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
     }
 
     private void assertUses2322ObjectWrapper(Configuration cfg) {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/173abfe9/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java 
b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index 36c12fd..5d3ad2b 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -90,7 +90,7 @@ public class DefaultObjectWrapperTest {
         expected.add(Configuration.VERSION_2_3_22); // no non-BC change in 
2.3.23
         expected.add(Configuration.VERSION_2_3_24);
         expected.add(Configuration.VERSION_2_3_24); // no non-BC change in 
2.3.25
-        expected.add(Configuration.VERSION_2_3_24); // no non-BC change in 
2.3.26
+        expected.add(Configuration.VERSION_2_3_26);
 
         List<Version> actual = new ArrayList<Version>();
         for (int i = _TemplateAPI.VERSION_INT_2_3_0; i <= 
Configuration.getVersion().intValue(); i++) {

Reply via email to