freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [12/25] incubator-freemarker git commit: 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 introspect
Date Mon, 13 Mar 2017 10:43:28 GMT
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 <ddekany@apache.org>
Authored: Tue Mar 7 15:31:30 2017 +0100
Committer: ddekany <ddekany@apache.org>
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++) {


Mime
View raw message