You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:53:08 UTC

[25/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java
new file mode 100644
index 0000000..7ae5a71
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+/**
+ * Exposes the Java API (and properties) of an object.
+ * 
+ * <p>
+ * Notes:
+ * <ul>
+ * <li>The exposion level is inherited from the {@link DefaultObjectWrapper}</li>
+ * <li>But methods will always shadow properties and fields with identical name, regardless of {@link DefaultObjectWrapper}
+ * settings</li>
+ * </ul>
+ * 
+ * @since 2.3.22
+ */
+final class APIModel extends BeanModel {
+
+    APIModel(Object object, DefaultObjectWrapper wrapper) {
+        super(object, wrapper, false);
+    }
+
+    protected boolean isMethodsShadowItems() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java
new file mode 100644
index 0000000..3b346e6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java
@@ -0,0 +1,647 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * The argument types of a method call; usable as cache key.
+ */
+final class ArgumentTypes {
+    
+    /**
+     * Conversion difficulty: Lowest; Java Reflection will do it automatically.
+     */
+    private static final int CONVERSION_DIFFICULTY_REFLECTION = 0;
+
+    /**
+     * Conversion difficulty: Medium: Java reflection API won't convert it, FreeMarker has to do it.
+     */
+    private static final int CONVERSION_DIFFICULTY_FREEMARKER = 1;
+    
+    /**
+     * Conversion difficulty: Highest; conversion is not possible.
+     */
+    private static final int CONVERSION_DIFFICULTY_IMPOSSIBLE = 2;
+
+    /**
+     * The types of the arguments; for varags this contains the exploded list (not the array). 
+     */
+    private final Class<?>[] types;
+    
+    /**
+     * @param args The actual arguments. A varargs argument should be present exploded, no as an array.
+     */
+    ArgumentTypes(Object[] args) {
+        int ln = args.length;
+        Class<?>[] typesTmp = new Class[ln];
+        for (int i = 0; i < ln; ++i) {
+            Object arg = args[i];
+            typesTmp[i] = arg == null
+                    ? Null.class
+                    : arg.getClass();
+        }
+        
+        // `typesTmp` is used so the array is only modified before it's stored in the final `types` field (see JSR-133)
+        types = typesTmp;  
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 0;
+        for (Class<?> type : types) {
+            hash ^= type.hashCode();
+        }
+        return hash;
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof ArgumentTypes) {
+            ArgumentTypes cs = (ArgumentTypes) o;
+            if (cs.types.length != types.length) {
+                return false;
+            }
+            for (int i = 0; i < types.length; ++i) {
+                if (cs.types[i] != types[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or
+     *         {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}. 
+     */
+    MaybeEmptyCallableMemberDescriptor getMostSpecific(
+            List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) {
+        LinkedList<CallableMemberDescriptor> applicables = getApplicables(memberDescs, varArg);
+        if (applicables.isEmpty()) {
+            return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
+        }
+        if (applicables.size() == 1) {
+            return applicables.getFirst();
+        }
+        
+        LinkedList<CallableMemberDescriptor> maximals = new LinkedList<>();
+        for (CallableMemberDescriptor applicable : applicables) {
+            boolean lessSpecific = false;
+            for (Iterator<CallableMemberDescriptor> maximalsIter = maximals.iterator(); 
+                maximalsIter.hasNext(); ) {
+                CallableMemberDescriptor maximal = maximalsIter.next();
+                final int cmpRes = compareParameterListPreferability(
+                        applicable.getParamTypes(), maximal.getParamTypes(), varArg); 
+                if (cmpRes > 0) {
+                    maximalsIter.remove();
+                } else if (cmpRes < 0) {
+                    lessSpecific = true;
+                }
+            }
+            if (!lessSpecific) {
+                maximals.addLast(applicable);
+            }
+        }
+        if (maximals.size() > 1) {
+            return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
+        }
+        return maximals.getFirst();
+    }
+
+    /**
+     * Tells if among the parameter list of two methods, which one fits this argument list better.
+     * This method assumes that the parameter lists are applicable to this argument lists; if that's not ensured,
+     * what the result will be is undefined.
+     * 
+     * <p>The decision is made by comparing the preferability of each parameter types of the same position in a loop.
+     * At the end, the parameter list with the more preferred parameters will be the preferred one. If both parameter
+     * lists has the same amount of preferred parameters, the one that has the first (lower index) preferred parameter
+     * is the preferred one. Otherwise the two parameter list are considered to be equal in terms of preferability.
+     * 
+     * <p>If there's no numerical conversion involved, the preferability of two parameter types is decided on how
+     * specific their types are. For example, {@code String} is more specific than {@link Object} (because
+     * {@code Object.class.isAssignableFrom(String.class)}-s), and so {@code String} is preferred. Primitive
+     * types are considered to be more specific than the corresponding boxing class (like {@code boolean} is more
+     * specific than {@code Boolean}, because the former can't store {@code null}). The preferability decision gets
+     * trickier when there's a possibility of numerical conversion from the actual argument type to the type of some of
+     * the parameters. If such conversion is only possible for one of the competing parameter types, that parameter
+     * automatically wins. If it's possible for both, {@link OverloadedNumberUtil#getArgumentConversionPrice} will
+     * be used to calculate the conversion "price", and the parameter type with lowest price wins. There are also
+     * a twist with array-to-list and list-to-array conversions; we try to avoid those, so the parameter where such
+     * conversion isn't needed will always win.
+     * 
+     * @param paramTypes1 The parameter types of one of the competing methods
+     * @param paramTypes2 The parameter types of the other competing method
+     * @param varArg Whether these competing methods are varargs methods. 
+     * @return More than 0 if the first parameter list is preferred, less then 0 if the other is preferred,
+     *        0 if there's no decision 
+     */
+    int compareParameterListPreferability(Class<?>[] paramTypes1, Class<?>[] paramTypes2, boolean varArg) {
+        final int argTypesLen = types.length; 
+        final int paramTypes1Len = paramTypes1.length;
+        final int paramTypes2Len = paramTypes2.length;
+        //assert varArg || paramTypes1Len == paramTypes2Len;
+        
+        int paramList1WeakWinCnt = 0;
+        int paramList2WeakWinCnt = 0;
+        int paramList1WinCnt = 0;
+        int paramList2WinCnt = 0;
+        int paramList1StrongWinCnt = 0;
+        int paramList2StrongWinCnt = 0;
+        int paramList1VeryStrongWinCnt = 0;
+        int paramList2VeryStrongWinCnt = 0;
+        int firstWinerParamList = 0;
+        for (int i = 0; i < argTypesLen; i++) {
+            final Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
+            final Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
+            
+            final int winerParam;  // 1 => paramType1; -1 => paramType2; 0 => draw
+            if (paramType1 == paramType2) {
+                winerParam = 0;
+            } else {
+                final Class<?> argType = types[i];
+                final boolean argIsNum = Number.class.isAssignableFrom(argType);
+                
+                final int numConvPrice1;
+                if (argIsNum && _ClassUtil.isNumerical(paramType1)) {
+                    final Class<?> nonPrimParamType1 = paramType1.isPrimitive()
+                            ? _ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1;
+                    numConvPrice1 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType1);
+                } else {
+                    numConvPrice1 = Integer.MAX_VALUE;
+                }
+                // numConvPrice1 is Integer.MAX_VALUE if either:
+                // - argType and paramType1 aren't both numerical
+                // - FM doesn't know some of the numerical types, or the conversion between them is not allowed    
+                
+                final int numConvPrice2;
+                if (argIsNum && _ClassUtil.isNumerical(paramType2)) {
+                    final Class<?> nonPrimParamType2 = paramType2.isPrimitive()
+                            ? _ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+                    numConvPrice2 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType2);
+                } else {
+                    numConvPrice2 = Integer.MAX_VALUE;
+                }
+                
+                if (numConvPrice1 == Integer.MAX_VALUE) {
+                    if (numConvPrice2 == Integer.MAX_VALUE) {  // No numerical conversions anywhere
+                        // List to array conversions (unwrapping sometimes makes a List instead of an array)
+                        if (List.class.isAssignableFrom(argType)
+                                && (paramType1.isArray() || paramType2.isArray())) {
+                            if (paramType1.isArray()) {
+                                if (paramType2.isArray()) {  // both paramType1 and paramType2 are arrays
+                                    int r = compareParameterListPreferability_cmpTypeSpecificty(
+                                            paramType1.getComponentType(), paramType2.getComponentType());
+                                    // Because we don't know if the List items are instances of the component
+                                    // type or not, we prefer the safer choice, which is the more generic array:
+                                    if (r > 0) {
+                                        winerParam = 2;
+                                        paramList2StrongWinCnt++;
+                                    } else if (r < 0) {
+                                        winerParam = 1;
+                                        paramList1StrongWinCnt++;
+                                    } else {
+                                        winerParam = 0;
+                                    }
+                                } else {  // paramType1 is array, paramType2 isn't
+                                    // Avoid List to array conversion if the other way makes any sense:
+                                    if (Collection.class.isAssignableFrom(paramType2)) {
+                                        winerParam = 2;
+                                        paramList2StrongWinCnt++;
+                                    } else {
+                                        winerParam = 1;
+                                        paramList1WeakWinCnt++;
+                                    }
+                                }
+                            } else {  // paramType2 is array, paramType1 isn't
+                                // Avoid List to array conversion if the other way makes any sense:
+                                if (Collection.class.isAssignableFrom(paramType1)) {
+                                    winerParam = 1;
+                                    paramList1StrongWinCnt++;
+                                } else {
+                                    winerParam = 2;
+                                    paramList2WeakWinCnt++;
+                                }
+                            }
+                        } else if (argType.isArray()
+                                && (List.class.isAssignableFrom(paramType1)
+                                        || List.class.isAssignableFrom(paramType2))) {
+                            // Array to List conversions (unwrapping sometimes makes an array instead of a List)
+                            if (List.class.isAssignableFrom(paramType1)) {
+                                if (List.class.isAssignableFrom(paramType2)) {
+                                    // Both paramType1 and paramType2 extends List
+                                    winerParam = 0;
+                                } else {
+                                    // Only paramType1 extends List
+                                    winerParam = 2;
+                                    paramList2VeryStrongWinCnt++;
+                                }
+                            } else {
+                                // Only paramType2 extends List
+                                winerParam = 1;
+                                paramList1VeryStrongWinCnt++;
+                            }
+                        } else {  // No list to/from array conversion
+                            final int r = compareParameterListPreferability_cmpTypeSpecificty(
+                                    paramType1, paramType2);
+                            if (r > 0) {
+                                winerParam = 1;
+                                if (r > 1) {
+                                    paramList1WinCnt++;
+                                } else {
+                                    paramList1WeakWinCnt++;
+                                }
+                            } else if (r < 0) {
+                                winerParam = -1;
+                                if (r < -1) {
+                                    paramList2WinCnt++;
+                                } else {
+                                    paramList2WeakWinCnt++;
+                                }
+                            } else {
+                                winerParam = 0;
+                            }
+                        }
+                    } else {  // No num. conv. of param1, num. conv. of param2
+                        winerParam = -1;
+                        paramList2WinCnt++;
+                    }
+                } else if (numConvPrice2 == Integer.MAX_VALUE) {  // Num. conv. of param1, not of param2
+                    winerParam = 1;
+                    paramList1WinCnt++;
+                } else {  // Num. conv. of both param1 and param2
+                    if (numConvPrice1 != numConvPrice2) {
+                        if (numConvPrice1 < numConvPrice2) {
+                            winerParam = 1;
+                            if (numConvPrice1 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
+                                    && numConvPrice2 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) {
+                                paramList1StrongWinCnt++;
+                            } else {
+                                paramList1WinCnt++;
+                            }
+                        } else {
+                            winerParam = -1;
+                            if (numConvPrice2 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
+                                    && numConvPrice1 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) {
+                                paramList2StrongWinCnt++;
+                            } else {
+                                paramList2WinCnt++;
+                            }
+                        }
+                    } else {
+                        winerParam = (paramType1.isPrimitive() ? 1 : 0) - (paramType2.isPrimitive() ? 1 : 0);
+                        if (winerParam == 1) paramList1WeakWinCnt++;
+                        else if (winerParam == -1) paramList2WeakWinCnt++;
+                    }
+                }
+            }  // when paramType1 != paramType2
+            
+            if (firstWinerParamList == 0 && winerParam != 0) {
+                firstWinerParamList = winerParam; 
+            }
+        }  // for each parameter types
+        
+        if (paramList1VeryStrongWinCnt != paramList2VeryStrongWinCnt) {
+            return paramList1VeryStrongWinCnt - paramList2VeryStrongWinCnt;
+        } else if (paramList1StrongWinCnt != paramList2StrongWinCnt) {
+            return paramList1StrongWinCnt - paramList2StrongWinCnt;
+        } else if (paramList1WinCnt != paramList2WinCnt) {
+            return paramList1WinCnt - paramList2WinCnt;
+        } else if (paramList1WeakWinCnt != paramList2WeakWinCnt) {
+            return paramList1WeakWinCnt - paramList2WeakWinCnt;
+        } else if (firstWinerParamList != 0) {  // paramList1WinCnt == paramList2WinCnt
+            return firstWinerParamList;
+        } else { // still undecided
+            if (varArg) {
+                if (paramTypes1Len == paramTypes2Len) {
+                    // If we had a 0-length varargs array in both methods, we also compare the types at the
+                    // index of the varargs parameter, like if we had a single varargs argument. However, this
+                    // time we don't have an argument type, so we can only decide based on type specificity:
+                    if (argTypesLen == paramTypes1Len - 1) {
+                        Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, argTypesLen, true);
+                        Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, argTypesLen, true);
+                        if (_ClassUtil.isNumerical(paramType1) && _ClassUtil.isNumerical(paramType2)) {
+                            int r = OverloadedNumberUtil.compareNumberTypeSpecificity(paramType1, paramType2);
+                            if (r != 0) return r;
+                            // falls through
+                        }
+                        return compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
+                    } else {
+                        return 0;
+                    }
+                } else {
+                    // The method with more oms parameters wins:
+                    return paramTypes1Len - paramTypes2Len;
+                }
+            } else {
+                return 0;
+            }
+        }
+    }
+    
+    /**
+     * Trivial comparison of type specificities; unaware of numerical conversions. 
+     * 
+     * @return Less-than-0, 0, or more-than-0 depending on which side is more specific. The absolute value is 1 if
+     *     the difference is only in primitive VS non-primitive, more otherwise.
+     */
+    private int compareParameterListPreferability_cmpTypeSpecificty(
+            final Class<?> paramType1, final Class<?> paramType2) {
+        // The more specific (smaller) type wins.
+        
+        final Class<?> nonPrimParamType1 = paramType1.isPrimitive()
+                ? _ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1;
+        final Class<?> nonPrimParamType2 = paramType2.isPrimitive()
+                ? _ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+                
+        if (nonPrimParamType1 == nonPrimParamType2) {
+            if (nonPrimParamType1 != paramType1) {
+                if (nonPrimParamType2 != paramType2) {
+                    return 0;  // identical prim. types; shouldn't ever be reached
+                } else {
+                    return 1;  // param1 is prim., param2 is non prim.
+                }
+            } else if (nonPrimParamType2 != paramType2) {
+                return -1;  // param1 is non-prim., param2 is prim.
+            } else {
+                return 0;  // identical non-prim. types
+            }
+        } else if (nonPrimParamType2.isAssignableFrom(nonPrimParamType1)) {
+            return 2;
+        } else if (nonPrimParamType1.isAssignableFrom(nonPrimParamType2)) {
+            return -2;
+        } if (nonPrimParamType1 == Character.class && nonPrimParamType2.isAssignableFrom(String.class)) {
+            return 2;  // A character is a 1 long string in FTL, so we pretend that it's a String subtype.
+        } if (nonPrimParamType2 == Character.class && nonPrimParamType1.isAssignableFrom(String.class)) {
+            return -2;
+        } else {
+            return 0;  // unrelated types
+        }
+    }
+
+    private static Class<?> getParamType(Class<?>[] paramTypes, int paramTypesLen, int i, boolean varArg) {
+        return varArg && i >= paramTypesLen - 1
+                ? paramTypes[paramTypesLen - 1].getComponentType()
+                : paramTypes[i];
+    }
+    
+    /**
+     * Returns all methods that are applicable to actual
+     * parameter types represented by this ArgumentTypes object.
+     */
+    LinkedList<CallableMemberDescriptor> getApplicables(
+            List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) {
+        LinkedList<CallableMemberDescriptor> applicables = new LinkedList<>();
+        for (ReflectionCallableMemberDescriptor memberDesc : memberDescs) {
+            int difficulty = isApplicable(memberDesc, varArg);
+            if (difficulty != CONVERSION_DIFFICULTY_IMPOSSIBLE) {
+                if (difficulty == CONVERSION_DIFFICULTY_REFLECTION) {
+                    applicables.add(memberDesc);
+                } else if (difficulty == CONVERSION_DIFFICULTY_FREEMARKER) {
+                    applicables.add(new SpecialConversionCallableMemberDescriptor(memberDesc));
+                } else {
+                    throw new BugException();
+                }
+            }
+        }
+        return applicables;
+    }
+    
+    /**
+     * Returns if the supplied method is applicable to actual
+     * parameter types represented by this ArgumentTypes object, also tells
+     * how difficult that conversion is.
+     * 
+     * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants.
+     */
+    private int isApplicable(ReflectionCallableMemberDescriptor memberDesc, boolean varArg) {
+        final Class<?>[] paramTypes = memberDesc.getParamTypes(); 
+        final int cl = types.length;
+        final int fl = paramTypes.length - (varArg ? 1 : 0);
+        if (varArg) {
+            if (cl < fl) {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+        } else {
+            if (cl != fl) {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+        }
+        
+        int maxDifficulty = 0;
+        for (int i = 0; i < fl; ++i) {
+            int difficulty = isMethodInvocationConvertible(paramTypes[i], types[i]);
+            if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+            if (maxDifficulty < difficulty) {
+                maxDifficulty = difficulty;
+            }
+        }
+        if (varArg) {
+            Class<?> varArgParamType = paramTypes[fl].getComponentType();
+            for (int i = fl; i < cl; ++i) {
+                int difficulty = isMethodInvocationConvertible(varArgParamType, types[i]); 
+                if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) {
+                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+                }
+                if (maxDifficulty < difficulty) {
+                    maxDifficulty = difficulty;
+                }
+            }
+        }
+        return maxDifficulty;
+    }
+
+    /**
+     * Determines whether a type is convertible to another type via 
+     * method invocation conversion, and if so, what kind of conversion is needed.
+     * It treates the object type counterpart of primitive types as if they were the primitive types
+     * (that is, a Boolean actual parameter type matches boolean primitive formal type). This behavior
+     * is because this method is used to determine applicable methods for 
+     * an actual parameter list, and primitive types are represented by 
+     * their object duals in reflective method calls.
+     * @param formal the parameter type to which the actual 
+     * parameter type should be convertible; possibly a primitive type
+     * @param actual the argument type; not a primitive type, maybe {@link Null}.
+     * 
+     * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants.
+     */
+    private int isMethodInvocationConvertible(final Class<?> formal, final Class<?> actual) {
+        // Check for identity or widening reference conversion
+        if (formal.isAssignableFrom(actual) && actual != CharacterOrString.class) {
+            return CONVERSION_DIFFICULTY_REFLECTION;
+        } else {
+            final Class<?> formalNP;
+            if (formal.isPrimitive()) {
+                if (actual == Null.class) {
+                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+                }
+                
+                formalNP = _ClassUtil.primitiveClassToBoxingClass(formal);
+                if (actual == formalNP) {
+                    // Character and char, etc.
+                    return CONVERSION_DIFFICULTY_REFLECTION;
+                }
+            } else {  // formal is non-primitive
+                if (actual == Null.class) {
+                    return CONVERSION_DIFFICULTY_REFLECTION;
+                }
+                
+                formalNP = formal;
+            }
+            if (Number.class.isAssignableFrom(actual) && Number.class.isAssignableFrom(formalNP)) {
+                return OverloadedNumberUtil.getArgumentConversionPrice(actual, formalNP) == Integer.MAX_VALUE
+                        ? CONVERSION_DIFFICULTY_IMPOSSIBLE : CONVERSION_DIFFICULTY_REFLECTION;
+            } else if (formal.isArray()) {
+                // DefaultObjectWrapper method/constructor calls convert from List to array automatically
+                return List.class.isAssignableFrom(actual)
+                        ? CONVERSION_DIFFICULTY_FREEMARKER : CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            } else if (actual.isArray() && formal.isAssignableFrom(List.class)) {
+                // DefaultObjectWrapper method/constructor calls convert from array to List automatically
+                return CONVERSION_DIFFICULTY_FREEMARKER;
+            } else if (actual == CharacterOrString.class
+                    && (formal.isAssignableFrom(String.class)
+                            || formal.isAssignableFrom(Character.class) || formal == char.class)) {
+                return CONVERSION_DIFFICULTY_FREEMARKER;
+            } else {
+                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
+            }
+        }
+    }
+    
+    /**
+     * Symbolizes the class of null (it's missing from Java).
+     */
+    private static class Null {
+        
+        // Can't be instantiated
+        private Null() { }
+        
+    }
+    
+    /**
+     * Used instead of {@link ReflectionCallableMemberDescriptor} when the method is only applicable
+     * ({@link #isApplicable}) with conversion that Java reflection won't do. It delegates to a
+     * {@link ReflectionCallableMemberDescriptor}, but it adds the necessary conversions to the invocation methods. 
+     */
+    private static final class SpecialConversionCallableMemberDescriptor extends CallableMemberDescriptor {
+        
+        private final ReflectionCallableMemberDescriptor callableMemberDesc;
+
+        SpecialConversionCallableMemberDescriptor(ReflectionCallableMemberDescriptor callableMemberDesc) {
+            this.callableMemberDesc = callableMemberDesc;
+        }
+
+        @Override
+        TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args) throws TemplateModelException,
+                InvocationTargetException, IllegalAccessException {
+            convertArgsToReflectionCompatible(ow, args);
+            return callableMemberDesc.invokeMethod(ow, obj, args);
+        }
+
+        @Override
+        Object invokeConstructor(DefaultObjectWrapper ow, Object[] args) throws IllegalArgumentException,
+                InstantiationException, IllegalAccessException, InvocationTargetException, TemplateModelException {
+            convertArgsToReflectionCompatible(ow, args);
+            return callableMemberDesc.invokeConstructor(ow, args);
+        }
+
+        @Override
+        String getDeclaration() {
+            return callableMemberDesc.getDeclaration();
+        }
+
+        @Override
+        boolean isConstructor() {
+            return callableMemberDesc.isConstructor();
+        }
+
+        @Override
+        boolean isStatic() {
+            return callableMemberDesc.isStatic();
+        }
+
+        @Override
+        boolean isVarargs() {
+            return callableMemberDesc.isVarargs();
+        }
+
+        @Override
+        Class<?>[] getParamTypes() {
+            return callableMemberDesc.getParamTypes();
+        }
+        
+        @Override
+        String getName() {
+            return callableMemberDesc.getName();
+        }
+
+        private void convertArgsToReflectionCompatible(DefaultObjectWrapper ow, Object[] args) throws TemplateModelException {
+            Class<?>[] paramTypes = callableMemberDesc.getParamTypes();
+            int ln = paramTypes.length;
+            for (int i = 0; i < ln; i++) {
+                Class<?> paramType = paramTypes[i];
+                final Object arg = args[i];
+                if (arg == null) continue;
+                
+                // Handle conversion between List and array types, in both directions. Java reflection won't do such
+                // conversion, so we have to.
+                // Most reflection-incompatible conversions were already addressed by the unwrapping. The reason
+                // this one isn't is that for overloaded methods the hint of a given parameter position is often vague,
+                // so we may end up with a List even if some parameter types at that position are arrays (remember, we
+                // have to chose one unwrapping target type, despite that we have many possible overloaded methods), or
+                // the other way around (that happens when AdapterTemplateMoldel returns an array).
+                // Later, the overloaded method selection will assume that a List argument is applicable to an array
+                // parameter, and that an array argument is applicable to a List parameter, so we end up with this
+                // situation.
+                if (paramType.isArray() && arg instanceof List) {
+                   args[i] = ow.listToArray((List<?>) arg, paramType, null);
+                }
+                if (arg.getClass().isArray() && paramType.isAssignableFrom(List.class)) {
+                    args[i] = ow.arrayToList(arg);
+                }
+                
+                // Handle the conversion from CharacterOrString to Character or String:
+                if (arg instanceof CharacterOrString) {
+                    if (paramType == Character.class || paramType == char.class
+                            || (!paramType.isAssignableFrom(String.class)
+                                    && paramType.isAssignableFrom(Character.class))) {
+                        args[i] = Character.valueOf(((CharacterOrString) arg).getAsChar());
+                    } else {
+                        args[i] = ((CharacterOrString) arg).getAsString();
+                    }
+                }
+            }
+        }
+
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
new file mode 100644
index 0000000..c154bba
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import org.apache.freemarker.core.model.TemplateScalarModel;
+
+/**
+ * Subclass of {@link BeanModel} that exposes the return value of the {@link
+ * java.lang.Object#toString()} method through the {@link TemplateScalarModel}
+ * interface.
+ */
+// [FM3] Treating all beans as FTL strings was certainly a bad idea in FM2.
+public class BeanAndStringModel extends BeanModel implements TemplateScalarModel {
+
+    /**
+     * Creates a new model that wraps the specified object with BeanModel + scalar
+     * functionality.
+     * @param object the object to wrap into a model.
+     * @param wrapper the {@link DefaultObjectWrapper} associated with this model.
+     * Every model has to have an associated {@link DefaultObjectWrapper} instance. The
+     * model gains many attributes from its wrapper, including the caching 
+     * behavior, method exposure level, method-over-item shadowing policy etc.
+     */
+    public BeanAndStringModel(Object object, DefaultObjectWrapper wrapper) {
+        super(object, wrapper);
+    }
+
+    /**
+     * Returns the result of calling {@link Object#toString()} on the wrapped
+     * object.
+     */
+    @Override
+    public String getAsString() {
+        return object.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
new file mode 100644
index 0000000..91fe9dc
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._DelayedFTLTypeDescription;
+import org.apache.freemarker.core._DelayedJQuote;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * A class that will wrap an arbitrary object into {@link org.apache.freemarker.core.model.TemplateHashModel}
+ * interface allowing calls to arbitrary property getters and invocation of
+ * accessible methods on the object from a template using the
+ * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to
+ * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to
+ * access indexed properties. It uses Beans {@link java.beans.Introspector}
+ * to dynamically discover the properties and methods. 
+ */
+
+public class BeanModel
+        implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport {
+    
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+    
+    protected final Object object;
+    protected final DefaultObjectWrapper wrapper;
+    
+    // We use this to represent an unknown value as opposed to known value of null (JR)
+    static final TemplateModel UNKNOWN = new SimpleScalar("UNKNOWN");
+
+    // I've tried to use a volatile ConcurrentHashMap field instead of HashMap + synchronized(this), but oddly it was
+    // a bit slower, at least on Java 8 u66. 
+    private HashMap<Object, TemplateModel> memberCache;
+
+    /**
+     * Creates a new model that wraps the specified object. Note that there are
+     * specialized subclasses of this class for wrapping arrays, collections,
+     * enumeration, iterators, and maps. Note also that the superclass can be
+     * used to wrap String objects if only scalar functionality is needed. You
+     * can also choose to delegate the choice over which model class is used for
+     * wrapping to {@link DefaultObjectWrapper#wrap(Object)}.
+     * @param object the object to wrap into a model.
+     * @param wrapper the {@link DefaultObjectWrapper} associated with this model.
+     * Every model has to have an associated {@link DefaultObjectWrapper} instance. The
+     * model gains many attributes from its wrapper, including the caching 
+     * behavior, method exposure level, method-over-item shadowing policy etc.
+     */
+    public BeanModel(Object object, DefaultObjectWrapper wrapper) {
+        // [2.4]: All models were introspected here, then the results was discareded, and get() will just do the
+        // introspection again. So is this necessary? (The inrospectNow parameter was added in 2.3.21 to allow
+        // lazy-introspecting DefaultObjectWrapper.trueModel|falseModel.)
+        this(object, wrapper, true);
+    }
+
+    /** @since 2.3.21 */
+    BeanModel(Object object, DefaultObjectWrapper wrapper, boolean inrospectNow) {
+        this.object = object;
+        this.wrapper = wrapper;
+        if (inrospectNow && object != null) {
+            // [2.4]: Could this be removed?
+            wrapper.getClassIntrospector().get(object.getClass());
+        }
+    }
+    
+    /**
+     * Uses Beans introspection to locate a property or method with name
+     * matching the key name. If a method or property is found, it's wrapped
+     * into {@link org.apache.freemarker.core.model.TemplateMethodModelEx} (for a method or
+     * indexed property), or evaluated on-the-fly and the return value wrapped
+     * into appropriate model (for a simple property) Models for various
+     * properties and methods are cached on a per-class basis, so the costly
+     * introspection is performed only once per property or method of a class.
+     * (Side-note: this also implies that any class whose method has been called
+     * will be strongly referred to by the framework and will not become
+     * unloadable until this class has been unloaded first. Normally this is not
+     * an issue, but can be in a rare scenario where you invoke many classes on-
+     * the-fly. Also, as the cache grows with new classes and methods introduced
+     * to the framework, it may appear as if it were leaking memory. The
+     * framework does, however detect class reloads (if you happen to be in an
+     * environment that does this kind of things--servlet containers do it when
+     * they reload a web application) and flushes the cache. If no method or
+     * property matching the key is found, the framework will try to invoke
+     * methods with signature
+     * <tt>non-void-return-type get(java.lang.String)</tt>,
+     * then <tt>non-void-return-type get(java.lang.Object)</tt>, or 
+     * alternatively (if the wrapped object is a resource bundle) 
+     * <tt>Object get(java.lang.String)</tt>.
+     * @throws TemplateModelException if there was no property nor method nor
+     * a generic <tt>get</tt> method to invoke.
+     */
+    @Override
+    public TemplateModel get(String key)
+        throws TemplateModelException {
+        Class<?> clazz = object.getClass();
+        Map<Object, Object> classInfo = wrapper.getClassIntrospector().get(clazz);
+        TemplateModel retval = null;
+        
+        try {
+            Object fd = classInfo.get(key);
+            if (fd != null) {
+                retval = invokeThroughDescriptor(fd, classInfo);
+            } else {
+                retval = invokeGenericGet(classInfo, clazz, key);
+            }
+            if (retval == UNKNOWN) {
+                if (wrapper.isStrict()) {
+                    throw new InvalidPropertyException("No such bean property: " + key);
+                } else {
+                    logNoSuchKey(key, classInfo);
+                }
+                retval = wrapper.wrap(null);
+            }
+            return retval;
+        } catch (TemplateModelException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new _TemplateModelException(e,
+                    "An error has occurred when reading existing sub-variable ", new _DelayedJQuote(key),
+                    "; see cause exception! The type of the containing value was: ",
+                    new _DelayedFTLTypeDescription(this)
+            );
+        }
+    }
+
+    private void logNoSuchKey(String key, Map<?, ?> keyMap) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Key " + _StringUtil.jQuoteNoXSS(key) + " was not found on instance of " + 
+                object.getClass().getName() + ". Introspection information for " +
+                "the class is: " + keyMap);
+        }
+    }
+    
+    /**
+     * Whether the model has a plain get(String) or get(Object) method
+     */
+    
+    protected boolean hasPlainGetMethod() {
+        return wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY) != null;
+    }
+    
+    private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo)
+            throws IllegalAccessException, InvocationTargetException, TemplateModelException {
+        // See if this particular instance has a cached implementation for the requested feature descriptor
+        TemplateModel cachedModel;
+        synchronized (this) {
+            cachedModel = memberCache != null ? memberCache.get(desc) : null;
+        }
+
+        if (cachedModel != null) {
+            return cachedModel;
+        }
+
+        TemplateModel resultModel = UNKNOWN;
+        if (desc instanceof PropertyDescriptor) {
+            PropertyDescriptor pd = (PropertyDescriptor) desc;
+            Method readMethod = pd.getReadMethod();
+            if (readMethod != null) {
+                // Unlike in FreeMarker 2, we prefer the normal read method even if there's an indexed read method.
+                resultModel = wrapper.invokeMethod(object, readMethod, null);
+                // cachedModel remains null, as we don't cache these
+            } else if (desc instanceof IndexedPropertyDescriptor) {
+                // In FreeMarker 2 we have exposed such indexed properties as sequences, but they can't support
+                // the size() method, so we have discontinued that. People has to call the indexed read method like
+                // any other method.
+                resultModel = UNKNOWN;
+            } else {
+                throw new IllegalStateException("PropertyDescriptor.readMethod shouldn't be null");
+            }
+        } else if (desc instanceof Field) {
+            resultModel = wrapper.wrap(((Field) desc).get(object));
+            // cachedModel remains null, as we don't cache these
+        } else if (desc instanceof Method) {
+            Method method = (Method) desc;
+            resultModel = cachedModel = new JavaMethodModel(
+                    object, method, ClassIntrospector.getArgTypes(classInfo, method), wrapper);
+        } else if (desc instanceof OverloadedMethods) {
+            resultModel = cachedModel = new OverloadedMethodsModel(
+                    object, (OverloadedMethods) desc, wrapper);
+        }
+        
+        // If new cachedModel was created, cache it
+        if (cachedModel != null) {
+            synchronized (this) {
+                if (memberCache == null) {
+                    memberCache = new HashMap<>();
+                }
+                memberCache.put(desc, cachedModel);
+            }
+        }
+        return resultModel;
+    }
+    
+    void clearMemberCache() {
+        synchronized (this) {
+            memberCache = null;
+        }
+    }
+
+    protected TemplateModel invokeGenericGet(Map/*<Object, Object>*/ classInfo, Class<?> clazz, String key)
+            throws IllegalAccessException, InvocationTargetException,
+        TemplateModelException {
+        Method genericGet = (Method) classInfo.get(ClassIntrospector.GENERIC_GET_KEY);
+        if (genericGet == null) {
+            return UNKNOWN;
+        }
+
+        return wrapper.invokeMethod(object, genericGet, new Object[] { key });
+    }
+
+    protected TemplateModel wrap(Object obj)
+    throws TemplateModelException {
+        return wrapper.getOuterIdentity().wrap(obj);
+    }
+    
+    protected Object unwrap(TemplateModel model)
+    throws TemplateModelException {
+        return wrapper.unwrap(model);
+    }
+
+    /**
+     * Tells whether the model is considered to be empty.
+     * It is empty if the wrapped object is a 0 length {@link String}, or an empty {@link Collection} or and empty
+     * {@link Map}, or an {@link Iterator} that has no more items, or a {@link Boolean#FALSE}, or {@code null}. 
+     */
+    @Override
+    public boolean isEmpty() {
+        if (object instanceof String) {
+            return ((String) object).length() == 0;
+        }
+        if (object instanceof Collection) {
+            return ((Collection<?>) object).isEmpty();
+        }
+        if (object instanceof Iterator) {
+            return !((Iterator<?>) object).hasNext();
+        }
+        if (object instanceof Map) {
+            return ((Map<?,?>) object).isEmpty();
+        }
+        // [FM3] Why's FALSE empty? 
+        return object == null || Boolean.FALSE.equals(object);
+    }
+    
+    /**
+     * Returns the same as {@link #getWrappedObject()}; to ensure that, this method will be final starting from 2.4.
+     * This behavior of {@link BeanModel} is assumed by some FreeMarker code. 
+     */
+    @Override
+    public Object getAdaptedObject(Class<?> hint) {
+        return object;  // return getWrappedObject(); starting from 2.4
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return object;
+    }
+    
+    @Override
+    public int size() {
+        return wrapper.getClassIntrospector().keyCount(object.getClass());
+    }
+
+    @Override
+    public TemplateCollectionModel keys() {
+        return new CollectionAndSequence(new SimpleSequence(keySet(), wrapper));
+    }
+
+    @Override
+    public TemplateCollectionModel values() throws TemplateModelException {
+        List<Object> values = new ArrayList<>(size());
+        TemplateModelIterator it = keys().iterator();
+        while (it.hasNext()) {
+            String key = ((TemplateScalarModel) it.next()).getAsString();
+            values.add(get(key));
+        }
+        return new CollectionAndSequence(new SimpleSequence(values, wrapper));
+    }
+    
+    @Override
+    public String toString() {
+        return object.toString();
+    }
+
+    /**
+     * Helper method to support TemplateHashModelEx. Returns the Set of
+     * Strings which are available via the TemplateHashModel
+     * interface. Subclasses that override <tt>invokeGenericGet</tt> to
+     * provide additional hash keys should also override this method.
+     */
+    protected Set/*<Object>*/ keySet() {
+        return wrapper.getClassIntrospector().keySet(object.getClass());
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return wrapper.wrapAsAPI(object);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
new file mode 100644
index 0000000..bbaf6bd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Packs a {@link Method} or {@link Constructor} together with its parameter types. The actual
+ * {@link Method} or {@link Constructor} is not exposed by the API, because in rare cases calling them require
+ * type conversion that the Java reflection API can't do, hence the developer shouldn't be tempted to call them
+ * directly. 
+ */
+abstract class CallableMemberDescriptor extends MaybeEmptyCallableMemberDescriptor {
+
+    abstract TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args)
+            throws TemplateModelException, InvocationTargetException, IllegalAccessException;
+
+    abstract Object invokeConstructor(DefaultObjectWrapper ow, Object[] args)
+            throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException,
+            TemplateModelException;
+    
+    abstract String getDeclaration();
+    
+    abstract boolean isConstructor();
+    
+    abstract boolean isStatic();
+
+    abstract boolean isVarargs();
+
+    abstract Class[] getParamTypes();
+
+    abstract String getName();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
new file mode 100644
index 0000000..6026011
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import org.apache.freemarker.core.model.TemplateScalarModel;
+
+/**
+ * Represents value unwrapped both to {@link Character} and {@link String}. This is needed for unwrapped overloaded
+ * method parameters where both {@link Character} and {@link String} occurs on the same parameter position when the
+ * {@link TemplateScalarModel} to unwrapp contains a {@link String} of length 1.
+ */
+final class CharacterOrString {
+
+    private final String stringValue;
+
+    CharacterOrString(String stringValue) {
+        this.stringValue = stringValue;
+    }
+    
+    String getAsString() {
+        return stringValue;
+    }
+
+    char getAsChar() {
+        return stringValue.charAt(0);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
new file mode 100644
index 0000000..3fd3a2d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * Base class for hash models keyed by Java class names. 
+ */
+abstract class ClassBasedModelFactory implements TemplateHashModel {
+    private final DefaultObjectWrapper wrapper;
+    
+    private final Map/*<String,TemplateModel>*/ cache = new ConcurrentHashMap();
+    private final Set classIntrospectionsInProgress = new HashSet();
+    
+    protected ClassBasedModelFactory(DefaultObjectWrapper wrapper) {
+        this.wrapper = wrapper;
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        try {
+            return getInternal(key);
+        } catch (Exception e) {
+            if (e instanceof TemplateModelException) {
+                throw (TemplateModelException) e;
+            } else {
+                throw new TemplateModelException(e);
+            }
+        }
+    }
+
+    private TemplateModel getInternal(String key) throws TemplateModelException, ClassNotFoundException {
+        {
+            TemplateModel model = (TemplateModel) cache.get(key);
+            if (model != null) return model;
+        }
+
+        final ClassIntrospector classIntrospector;
+        int classIntrospectorClearingCounter;
+        final Object sharedLock = wrapper.getSharedIntrospectionLock();
+        synchronized (sharedLock) {
+            TemplateModel model = (TemplateModel) cache.get(key);
+            if (model != null) return model;
+            
+            while (model == null
+                    && classIntrospectionsInProgress.contains(key)) {
+                // Another thread is already introspecting this class;
+                // waiting for its result.
+                try {
+                    sharedLock.wait();
+                    model = (TemplateModel) cache.get(key);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(
+                            "Class inrospection data lookup aborded: " + e);
+                }
+            }
+            if (model != null) return model;
+            
+            // This will be the thread that introspects this class.
+            classIntrospectionsInProgress.add(key);
+
+            // While the classIntrospector should not be changed from another thread, badly written apps can do that,
+            // and it's cheap to get the classIntrospector from inside the lock here:   
+            classIntrospector = wrapper.getClassIntrospector();
+            classIntrospectorClearingCounter = classIntrospector.getClearingCounter();
+        }
+        try {
+            final Class clazz = _ClassUtil.forName(key);
+            
+            // This is called so that we trigger the
+            // class-reloading detector. If clazz is a reloaded class,
+            // the wrapper will in turn call our clearCache method.
+            // TODO: Why do we check it now and only now?
+            classIntrospector.get(clazz);
+            
+            TemplateModel model = createModel(clazz);
+            // Warning: model will be null if the class is not good for the subclass.
+            // For example, EnumModels#createModel returns null if clazz is not an enum.
+            
+            if (model != null) {
+                synchronized (sharedLock) {
+                    // Save it into the cache, but only if nothing relevant has changed while we were outside the lock: 
+                    if (classIntrospector == wrapper.getClassIntrospector()
+                            && classIntrospectorClearingCounter == classIntrospector.getClearingCounter()) {  
+                        cache.put(key, model);
+                    }
+                }
+            }
+            return model;
+        } finally {
+            synchronized (sharedLock) {
+                classIntrospectionsInProgress.remove(key);
+                sharedLock.notifyAll();
+            }
+        }
+    }
+    
+    void clearCache() {
+        synchronized (wrapper.getSharedIntrospectionLock()) {
+            cache.clear();
+        }
+    }
+    
+    void removeFromCache(Class clazz) {
+        synchronized (wrapper.getSharedIntrospectionLock()) {
+            cache.remove(clazz.getName());
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+    
+    protected abstract TemplateModel createModel(Class clazz) 
+    throws TemplateModelException;
+    
+    protected DefaultObjectWrapper getWrapper() {
+        return wrapper;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
new file mode 100644
index 0000000..52321f0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+/**
+ * Reports when the non-private interface of a class was changed to the subscribers.   
+ */
+interface ClassChangeNotifier {
+    
+    /**
+     * @param classIntrospector Should only be weak-referenced from the monitor object.
+     */
+    void subscribe(ClassIntrospector classIntrospector);
+
+}