freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [17/20] incubator-freemarker git commit: Removed BeansWrapper, merging it into DefaultObjectWrapper (the o.a.f.core.model.impl.beans packageis gone now). It works, but there's a lot of unused classes and logic now, which will have to be removed.
Date Tue, 28 Feb 2017 22:57:51 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
index 307d278..a2ca780 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
@@ -20,8 +20,9 @@
 package org.apache.freemarker.core.model.impl;
 
 import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreAPI;
 import org.apache.freemarker.core.model.ObjectWrapper;
-import org.apache.freemarker.core.model.impl.beans.BeansWrapperConfiguration;
+import org.apache.freemarker.core.model.TemplateDateModel;
 
 /**
  * Holds {@link DefaultObjectWrapper} configuration settings and defines their defaults.
@@ -29,13 +30,187 @@ import org.apache.freemarker.core.model.impl.beans.BeansWrapperConfiguration;
  * Unless, you are developing a builder for a custom {@link DefaultObjectWrapper} subclass. In that case, note that
  * overriding the {@link #equals} and {@link #hashCode} is important, as these objects are used as {@link ObjectWrapper}
  * singleton lookup keys.
- * 
- * @since 2.3.22
  */
-public abstract class DefaultObjectWrapperConfiguration extends BeansWrapperConfiguration {
-    
+public abstract class DefaultObjectWrapperConfiguration implements Cloneable {
+
+    private final Version incompatibleImprovements;
+
+    protected ClassIntrospectorBuilder classIntrospectorFactory;
+
+    // Properties and their *defaults*:
+    private boolean simpleMapWrapper = false;
+    private int defaultDateType = TemplateDateModel.UNKNOWN;
+    private ObjectWrapper outerIdentity = null;
+    private boolean strict = false;
+    private boolean useModelCache = false;
+    // Attention!
+    // - As this object is a cache key, non-normalized field values should be avoided.
+    // - Fields with default values must be set until the end of the constructor to ensure that when the lookup happens,
+    //   there will be no unset fields.
+    // - If you add a new field, review all methods in this class
+
+    /**
+     * @param incompatibleImprovements
+     *            See the corresponding parameter of {@link DefaultObjectWrapper#DefaultObjectWrapper(Version)}. Not {@code null}. Note
+     *            that the version will be normalized to the lowest version where the same incompatible
+     *            {@link DefaultObjectWrapper} improvements were already present, so for the returned instance
+     *            {@link #getIncompatibleImprovements()} might returns a lower version than what you have specified
+     *            here.
+     * @param isIncompImprsAlreadyNormalized
+     *            Tells if the {@code incompatibleImprovements} parameter contains an <em>already normalized</em> value.
+     *            This parameter meant to be {@code true} when the class that extends {@link DefaultObjectWrapper} needs to add
+     *            additional breaking versions over those of {@link DefaultObjectWrapper}. Thus, if this parameter is
+     *            {@code true}, the versions where {@link DefaultObjectWrapper} had breaking changes must be already factored
+     *            into the {@code incompatibleImprovements} parameter value, as no more normalization will happen. (You
+     *            can use {@link DefaultObjectWrapper#normalizeIncompatibleImprovementsVersion(Version)} to discover those.)
+     *
+     * @since 2.3.22
+     */
+    protected DefaultObjectWrapperConfiguration(Version incompatibleImprovements, boolean isIncompImprsAlreadyNormalized) {
+        _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+
+        incompatibleImprovements = isIncompImprsAlreadyNormalized
+                ? incompatibleImprovements
+                : DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
+        this.incompatibleImprovements = incompatibleImprovements;
+
+        classIntrospectorFactory = new ClassIntrospectorBuilder(incompatibleImprovements);
+    }
+
+    /**
+     * Same as {@link #DefaultObjectWrapperConfiguration(Version, boolean) DefaultObjectWrapperConfiguration(Version, false)}.
+     */
     protected DefaultObjectWrapperConfiguration(Version incompatibleImprovements) {
-        super(DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements), true);
+        this(incompatibleImprovements, false);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + incompatibleImprovements.hashCode();
+        result = prime * result + (simpleMapWrapper ? 1231 : 1237);
+        result = prime * result + defaultDateType;
+        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();
+        return result;
+    }
+
+    /**
+     * Two {@link DefaultObjectWrapperConfiguration}-s are equal exactly if their classes are identical ({@code ==}), and their
+     * field values are equal.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        DefaultObjectWrapperConfiguration other = (DefaultObjectWrapperConfiguration) obj;
+
+        if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false;
+        if (simpleMapWrapper != other.simpleMapWrapper) return false;
+        if (defaultDateType != other.defaultDateType) return false;
+        if (outerIdentity != other.outerIdentity) return false;
+        if (strict != other.strict) return false;
+        if (useModelCache != other.useModelCache) return false;
+        return classIntrospectorFactory.equals(other.classIntrospectorFactory);
+    }
+
+    protected Object clone(boolean deepCloneKey) {
+        try {
+            DefaultObjectWrapperConfiguration clone = (DefaultObjectWrapperConfiguration) super.clone();
+            if (deepCloneKey) {
+                clone.classIntrospectorFactory
+                        = (ClassIntrospectorBuilder) classIntrospectorFactory.clone();
+            }
+            return clone;
+        } catch (CloneNotSupportedException e) {
+            throw new RuntimeException("Failed to clone DefaultObjectWrapperConfiguration", e);
+        }
+    }
+
+    public int getDefaultDateType() {
+        return defaultDateType;
+    }
+
+    /** See {@link DefaultObjectWrapper#setDefaultDateType(int)}. */
+    public void setDefaultDateType(int defaultDateType) {
+        this.defaultDateType = defaultDateType;
+    }
+
+    public ObjectWrapper getOuterIdentity() {
+        return outerIdentity;
+    }
+
+    /**
+     * See {@link DefaultObjectWrapper#setOuterIdentity(ObjectWrapper)}, except here the default is {@code null} that means
+     * the {@link ObjectWrapper} that you will set up with this {@link DefaultObjectWrapperBuilder} object.
+     */
+    public void setOuterIdentity(ObjectWrapper outerIdentity) {
+        this.outerIdentity = outerIdentity;
+    }
+
+    public boolean isStrict() {
+        return strict;
+    }
+
+    /** See {@link DefaultObjectWrapper#setStrict(boolean)}. */
+    public void setStrict(boolean strict) {
+        this.strict = strict;
+    }
+
+    public boolean getUseModelCache() {
+        return useModelCache;
+    }
+
+    /** See {@link DefaultObjectWrapper#setUseModelCache(boolean)} (it means the same). */
+    public void setUseModelCache(boolean useModelCache) {
+        this.useModelCache = useModelCache;
+    }
+
+    public Version getIncompatibleImprovements() {
+        return incompatibleImprovements;
+    }
+
+    public int getExposureLevel() {
+        return classIntrospectorFactory.getExposureLevel();
+    }
+
+    /** See {@link DefaultObjectWrapper#setExposureLevel(int)}. */
+    public void setExposureLevel(int exposureLevel) {
+        classIntrospectorFactory.setExposureLevel(exposureLevel);
+    }
+
+    public boolean getExposeFields() {
+        return classIntrospectorFactory.getExposeFields();
+    }
+
+    /** See {@link DefaultObjectWrapper#setExposeFields(boolean)}. */
+    public void setExposeFields(boolean exposeFields) {
+        classIntrospectorFactory.setExposeFields(exposeFields);
+    }
+
+    public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+        return classIntrospectorFactory.getMethodAppearanceFineTuner();
+    }
+
+    /**
+     * See {@link DefaultObjectWrapper#setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}; additionally,
+     * note that currently setting this to non-{@code null} will disable class introspection cache sharing, unless
+     * the value implements {@link SingletonCustomizer}.
+     */
+    public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+        classIntrospectorFactory.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+    }
+
+    MethodSorter getMethodSorter() {
+        return classIntrospectorFactory.getMethodSorter();
+    }
+
+    void setMethodSorter(MethodSorter methodSorter) {
+        classIntrospectorFactory.setMethodSorter(methodSorter);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java b/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java
new file mode 100644
index 0000000..3ec8e87
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/EmptyCallableMemberDescriptor.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Represents that no member was chosen. Why it wasn't is represented by the two singleton instances,
+ * {@link #NO_SUCH_METHOD} and {@link #AMBIGUOUS_METHOD}. (Note that instances of these are cached associated with the
+ * argument types, thus it shouldn't store details that are specific to the actual argument values. In fact, it better
+ * remains a set of singletons.)     
+ */
+final class EmptyCallableMemberDescriptor extends MaybeEmptyCallableMemberDescriptor {
+    
+    static final EmptyCallableMemberDescriptor NO_SUCH_METHOD = new EmptyCallableMemberDescriptor();
+    static final EmptyCallableMemberDescriptor AMBIGUOUS_METHOD = new EmptyCallableMemberDescriptor();
+    
+    private EmptyCallableMemberDescriptor() { }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java b/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java
new file mode 100644
index 0000000..25987d7
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/EmptyMemberAndArguments.java
@@ -0,0 +1,93 @@
+/*
+ * 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._DelayedOrdinal;
+
+/**
+ * Describes a failed member lookup. Instances of this must not be cached as instances may store the actual argument
+ * values.
+ */
+final class EmptyMemberAndArguments extends MaybeEmptyMemberAndArguments {
+    
+    static final EmptyMemberAndArguments WRONG_NUMBER_OF_ARGUMENTS
+            = new EmptyMemberAndArguments(
+                    "No compatible overloaded variation was found; wrong number of arguments.", true, null);
+    
+    private final Object errorDescription;
+    private final boolean numberOfArgumentsWrong;
+    private final Object[] unwrappedArguments;
+    
+    private EmptyMemberAndArguments(
+            Object errorDescription, boolean numberOfArgumentsWrong, Object[] unwrappedArguments) {
+        this.errorDescription = errorDescription;
+        this.numberOfArgumentsWrong = numberOfArgumentsWrong;
+        this.unwrappedArguments = unwrappedArguments;
+    }
+
+    static EmptyMemberAndArguments noCompatibleOverload(int unwrappableIndex) {
+        return new EmptyMemberAndArguments(
+                new Object[] { "No compatible overloaded variation was found; can't convert (unwrap) the ",
+                new _DelayedOrdinal(Integer.valueOf(unwrappableIndex)), " argument to the desired Java type." },
+                false,
+                null);
+    }
+    
+    static EmptyMemberAndArguments noCompatibleOverload(Object[] unwrappedArgs) {
+        return new EmptyMemberAndArguments(
+                "No compatible overloaded variation was found; declared parameter types and argument value types mismatch.",
+                false,
+                unwrappedArgs);
+    }
+
+    static EmptyMemberAndArguments ambiguous(Object[] unwrappedArgs) {
+        return new EmptyMemberAndArguments(
+                "Multiple compatible overloaded variations were found with the same priority.",
+                false,
+                unwrappedArgs);
+    }
+
+    static MaybeEmptyMemberAndArguments from(
+            EmptyCallableMemberDescriptor emtpyMemberDesc, Object[] unwrappedArgs) {
+        if (emtpyMemberDesc == EmptyCallableMemberDescriptor.NO_SUCH_METHOD) {
+            return noCompatibleOverload(unwrappedArgs);
+        } else if (emtpyMemberDesc == EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD) {
+            return ambiguous(unwrappedArgs);
+        } else {
+            throw new IllegalArgumentException("Unrecognized constant: " + emtpyMemberDesc);
+        }
+    }
+
+    Object getErrorDescription() {
+        return errorDescription;
+    }
+
+    /**
+     * @return {@code null} if the error has occurred earlier than the full argument list was unwrapped.
+     */
+    Object[] getUnwrappedArguments() {
+        return unwrappedArguments;
+    }
+
+    public boolean isNumberOfArgumentsWrong() {
+        return numberOfArgumentsWrong;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/EnumerationModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/EnumerationModel.java b/src/main/java/org/apache/freemarker/core/model/impl/EnumerationModel.java
new file mode 100644
index 0000000..d7b14cc
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/EnumerationModel.java
@@ -0,0 +1,108 @@
+/*
+ * 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.Enumeration;
+import java.util.NoSuchElementException;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+
+/**
+ * <p>A class that adds {@link TemplateModelIterator} functionality to the
+ * {@link Enumeration} interface implementers. 
+ * </p> <p>Using the model as a collection model is NOT thread-safe, as 
+ * enumerations are inherently not thread-safe.
+ * Further, you can iterate over it only once. Attempts to call the
+ * {@link #iterator()} method after it was already driven to the end once will 
+ * throw an exception.</p>
+ */
+
+public class EnumerationModel
+extends
+    BeanModel
+implements
+    TemplateModelIterator,
+    TemplateCollectionModel {
+    private boolean accessed = false;
+    
+    /**
+     * Creates a new model that wraps the specified enumeration object.
+     * @param enumeration the enumeration 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 EnumerationModel(Enumeration enumeration, DefaultObjectWrapper wrapper) {
+        super(enumeration, wrapper);
+    }
+
+    /**
+     * This allows the enumeration to be used in a <tt>&lt;#list&gt;</tt> block.
+     * @return "this"
+     */
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        synchronized (this) {
+            if (accessed) {
+                throw new TemplateModelException(
+                    "This collection is stateful and can not be iterated over the" +
+                    " second time.");
+            }
+            accessed = true;
+        }
+        return this;
+    }
+    
+    /**
+     * Calls underlying {@link Enumeration#nextElement()}.
+     */
+    @Override
+    public boolean hasNext() {
+        return ((Enumeration) object).hasMoreElements();
+    }
+
+
+    /**
+     * Calls underlying {@link Enumeration#nextElement()} and wraps the result.
+     */
+    @Override
+    public TemplateModel next()
+    throws TemplateModelException {
+        try {
+            return wrap(((Enumeration) object).nextElement());
+        } catch (NoSuchElementException e) {
+            throw new TemplateModelException(
+                "No more elements in the enumeration.");
+        }
+    }
+
+    /**
+     * Returns {@link Enumeration#hasMoreElements()}. Therefore, an
+     * enumeration that has no more element evaluates to false, and an 
+     * enumeration that has further elements evaluates to true.
+     */
+    public boolean getAsBoolean() {
+        return hasNext();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java b/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java
new file mode 100644
index 0000000..7fefec9
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/HashAdapter.java
@@ -0,0 +1,180 @@
+/*
+ * 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.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+/**
+ */
+public class HashAdapter extends AbstractMap implements TemplateModelAdapter {
+    private final DefaultObjectWrapper wrapper;
+    private final TemplateHashModel model;
+    private Set entrySet;
+    
+    HashAdapter(TemplateHashModel model, DefaultObjectWrapper wrapper) {
+        this.model = model;
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    public TemplateModel getTemplateModel() {
+        return model;
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        try {
+            return model.isEmpty();
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+    
+    @Override
+    public Object get(Object key) {
+        try {
+            return wrapper.unwrap(model.get(String.valueOf(key)));
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        // A quick check that doesn't require TemplateHashModelEx 
+        if (get(key) != null) {
+            return true;
+        }
+        return super.containsKey(key);
+    }
+    
+    @Override
+    public Set entrySet() {
+        if (entrySet != null) {
+            return entrySet;
+        }
+        return entrySet = new AbstractSet() {
+            @Override
+            public Iterator iterator() {
+                final TemplateModelIterator i;
+                try {
+                     i = getModelEx().keys().iterator();
+                } catch (TemplateModelException e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+                return new Iterator() {
+                    @Override
+                    public boolean hasNext() {
+                        try {
+                            return i.hasNext();
+                        } catch (TemplateModelException e) {
+                            throw new UndeclaredThrowableException(e);
+                        }
+                    }
+                    
+                    @Override
+                    public Object next() {
+                        final Object key;
+                        try {
+                            key = wrapper.unwrap(i.next());
+                        } catch (TemplateModelException e) {
+                            throw new UndeclaredThrowableException(e);
+                        }
+                        return new Map.Entry() {
+                            @Override
+                            public Object getKey() {
+                                return key;
+                            }
+                            
+                            @Override
+                            public Object getValue() {
+                                return get(key);
+                            }
+                            
+                            @Override
+                            public Object setValue(Object value) {
+                                throw new UnsupportedOperationException();
+                            }
+                            
+                            @Override
+                            public boolean equals(Object o) {
+                                if (!(o instanceof Map.Entry))
+                                    return false;
+                                Map.Entry e = (Map.Entry) o;
+                                Object k1 = getKey();
+                                Object k2 = e.getKey();
+                                if (k1 == k2 || (k1 != null && k1.equals(k2))) {
+                                    Object v1 = getValue();
+                                    Object v2 = e.getValue();
+                                    if (v1 == v2 || (v1 != null && v1.equals(v2))) 
+                                        return true;
+                                }
+                                return false;
+                            }
+                        
+                            @Override
+                            public int hashCode() {
+                                Object value = getValue();
+                                return (key == null ? 0 : key.hashCode()) ^
+                                       (value == null ? 0 : value.hashCode());
+                            }
+                        };
+                    }
+                    
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+            
+            @Override
+            public int size() {
+                try {
+                    return getModelEx().size();
+                } catch (TemplateModelException e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+            }
+        };
+    }
+    
+    private TemplateHashModelEx getModelEx() {
+        if (model instanceof TemplateHashModelEx) {
+            return ((TemplateHashModelEx) model);
+        }
+        throw new UnsupportedOperationException(
+                "Operation supported only on TemplateHashModelEx. " + 
+                model.getClass().getName() + " does not implement it though.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java b/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java
new file mode 100644
index 0000000..41197ca
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/InvalidPropertyException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.TemplateModelException;
+
+/**
+ * An exception thrown when there is an attempt to access
+ * an invalid bean property when we are in a "strict bean" mode
+ */
+
+public class InvalidPropertyException extends TemplateModelException {
+	
+    public InvalidPropertyException(String description) {
+        super(description);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/IteratorModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/IteratorModel.java b/src/main/java/org/apache/freemarker/core/model/impl/IteratorModel.java
new file mode 100644
index 0000000..0eabf21
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/IteratorModel.java
@@ -0,0 +1,112 @@
+/*
+ * 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.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+
+/**
+ * <p>A class that adds {@link TemplateModelIterator} functionality to the
+ * {@link Iterator} interface implementers. 
+ * </p>
+ * <p>It differs from the {@link org.apache.freemarker.core.model.impl.SimpleCollection} in that 
+ * it inherits from {@link BeanModel}, and therefore you can call methods on 
+ * it directly, even to the effect of calling <tt>iterator.remove()</tt> in 
+ * the template.</p> <p>Using the model as a collection model is NOT 
+ * thread-safe, as iterators are inherently not thread-safe.
+ * Further, you can iterate over it only once. Attempts to call the
+ * {@link #iterator()} method after it was already driven to the end once will 
+ * throw an exception.</p>
+ */
+
+public class IteratorModel
+extends
+    BeanModel
+implements
+    TemplateModelIterator,
+    TemplateCollectionModel {
+    private boolean accessed = false;
+    
+    /**
+     * Creates a new model that wraps the specified iterator object.
+     * @param iterator the iterator 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 IteratorModel(Iterator iterator, DefaultObjectWrapper wrapper) {
+        super(iterator, wrapper);
+    }
+
+    /**
+     * This allows the iterator to be used in a <tt>&lt;#list&gt;</tt> block.
+     * @return "this"
+     */
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        synchronized (this) {
+            if (accessed) {
+                throw new TemplateModelException(
+                    "This collection is stateful and can not be iterated over the" +
+                    " second time.");
+            }
+            accessed = true;
+        }
+        return this;
+    }
+    
+    /**
+     * Calls underlying {@link Iterator#hasNext()}.
+     */
+    @Override
+    public boolean hasNext() {
+        return ((Iterator) object).hasNext();
+    }
+
+
+    /**
+     * Calls underlying {@link Iterator#next()} and wraps the result.
+     */
+    @Override
+    public TemplateModel next()
+    throws TemplateModelException {
+        try {
+            return wrap(((Iterator) object).next());
+        } catch (NoSuchElementException e) {
+            throw new TemplateModelException(
+                "No more elements in the iterator.", e);
+        }
+    }
+
+    /**
+     * Returns {@link Iterator#hasNext()}. Therefore, an
+     * iterator that has no more element evaluates to false, and an 
+     * iterator that has further elements evaluates to true.
+     */
+    public boolean getAsBoolean() {
+        return hasNext();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java b/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java
new file mode 100644
index 0000000..5f8e222
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/JRebelClassChangeNotifier.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ref.WeakReference;
+
+import org.zeroturnaround.javarebel.ClassEventListener;
+import org.zeroturnaround.javarebel.ReloaderFactory;
+
+class JRebelClassChangeNotifier implements ClassChangeNotifier {
+
+    static void testAvailability() {
+        ReloaderFactory.getInstance();
+    }
+
+    @Override
+    public void subscribe(ClassIntrospector classIntrospector) {
+        ReloaderFactory.getInstance().addClassReloadListener(
+                new ClassIntrospectorCacheInvalidator(classIntrospector));
+    }
+
+    private static class ClassIntrospectorCacheInvalidator
+            implements ClassEventListener {
+        private final WeakReference ref;
+
+        ClassIntrospectorCacheInvalidator(ClassIntrospector w) {
+            ref = new WeakReference(w);
+        }
+
+        @Override
+        public void onClassEvent(int eventType, Class pClass) {
+            ClassIntrospector ci = (ClassIntrospector) ref.get();
+            if (ci == null) {
+                ReloaderFactory.getInstance().removeClassReloadListener(this);
+            } else if (eventType == ClassEventListener.EVENT_RELOADED) {
+                ci.remove(pClass);
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/MapModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MapModel.java b/src/main/java/org/apache/freemarker/core/model/impl/MapModel.java
new file mode 100644
index 0000000..95abb2a
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MapModel.java
@@ -0,0 +1,120 @@
+/*
+ * 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.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * <p>A special case of {@link BeanModel} that adds implementation
+ * for {@link TemplateMethodModelEx} on map objects that is a shortcut for the
+ * <tt>Map.get()</tt> method. Note that if the passed argument itself is a
+ * reflection-wrapper model, then the map lookup will be performed using the
+ * wrapped object as the key. Note that you can call <tt>get()</tt> using the
+ * <tt>map.key</tt> syntax inherited from {@link BeanModel} as well, 
+ * however in that case the key is always a string.</p>
+ * <p>The class itself does not implement the {@link org.apache.freemarker.core.model.TemplateCollectionModel}.
+ * You can, however use <tt>map.entrySet()</tt>, <tt>map.keySet()</tt>, or
+ * <tt>map.values()</tt> to obtain {@link org.apache.freemarker.core.model.TemplateCollectionModel} instances for 
+ * various aspects of the map.</p>
+ */
+public class MapModel
+extends
+    StringModel
+implements
+    TemplateMethodModelEx {
+    static final ModelFactory FACTORY =
+        new ModelFactory()
+        {
+            @Override
+            public TemplateModel create(Object object, ObjectWrapper wrapper) {
+                return new MapModel((Map) object, (DefaultObjectWrapper) wrapper);
+            }
+        };
+
+    /**
+     * Creates a new model that wraps the specified map object.
+     * @param map the map 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 MapModel(Map map, DefaultObjectWrapper wrapper) {
+        super(map, wrapper);
+    }
+
+    /**
+     * The first argument is used as a key to call the map's <tt>get</tt> method.
+     */
+    @Override
+    public Object exec(List arguments)
+    throws TemplateModelException {
+        Object key = unwrap((TemplateModel) arguments.get(0));
+        return wrap(((Map) object).get(key));
+    }
+
+    /**
+     * Overridden to invoke the generic get method by casting to Map instead of 
+     * through reflection - should yield better performance.
+     */
+    @Override
+    protected TemplateModel invokeGenericGet(Map keyMap, Class clazz, String key)
+    throws TemplateModelException {
+        Map map = (Map) object;
+        Object val = map.get(key);
+        if (val == null) {
+            if (key.length() == 1) {
+                // just check for Character key if this is a single-character string
+                Character charKey = Character.valueOf(key.charAt(0));
+                val = map.get(charKey);
+                if (val == null && !(map.containsKey(key) || map.containsKey(charKey))) {
+                    return UNKNOWN;
+                }
+            } else if (!map.containsKey(key)) {
+                return UNKNOWN;
+            }
+        }
+        return wrap(val);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return ((Map) object).isEmpty() && super.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        return keySet().size();
+    }
+
+    @Override
+    protected Set keySet() {
+        Set set = super.keySet();
+        set.addAll(((Map) object).keySet());
+        return set;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java b/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java
new file mode 100644
index 0000000..1b62bf8
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyCallableMemberDescriptor.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * Superclass of the {@link EmptyCallableMemberDescriptor} and {@link CallableMemberDescriptor} "case classes".
+ */
+abstract class MaybeEmptyCallableMemberDescriptor { }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java b/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java
new file mode 100644
index 0000000..bfc88ee
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MaybeEmptyMemberAndArguments.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+abstract class MaybeEmptyMemberAndArguments { }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java b/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java
new file mode 100644
index 0000000..e7c735c
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MemberAndArguments.java
@@ -0,0 +1,64 @@
+/*
+ * 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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ */
+class MemberAndArguments extends MaybeEmptyMemberAndArguments {
+    
+    private final CallableMemberDescriptor callableMemberDesc;
+    private final Object[] args;
+    
+    /**
+     * @param args The already unwrapped arguments
+     */
+    MemberAndArguments(CallableMemberDescriptor callableMemberDesc, Object[] args) {
+        this.callableMemberDesc = callableMemberDesc;
+        this.args = args;
+    }
+    
+    /**
+     * The already unwrapped arguments.
+     */
+    Object[] getArgs() {
+        return args;
+    }
+    
+    TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj)
+            throws TemplateModelException, InvocationTargetException, IllegalAccessException {
+        return callableMemberDesc.invokeMethod(ow, obj, args);
+    }
+
+    Object invokeConstructor(DefaultObjectWrapper ow)
+            throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException,
+            TemplateModelException {
+        return callableMemberDesc.invokeConstructor(ow, args);
+    }
+    
+    CallableMemberDescriptor getCallableMemberDescriptor() {
+        return callableMemberDesc;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java b/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java
new file mode 100644
index 0000000..3235c51
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MethodAppearanceFineTuner.java
@@ -0,0 +1,156 @@
+/*
+ * 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.Method;
+
+/**
+ * Used for customizing how the methods are visible from templates, via
+ * {@link DefaultObjectWrapper#setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}.
+ * The object that implements this should also implement {@link SingletonCustomizer} whenever possible.
+ * 
+ * @since 2.3.21
+ */
+public interface MethodAppearanceFineTuner {
+
+    /**
+     * Implement this to tweak certain aspects of how methods appear in the
+     * data-model. {@link DefaultObjectWrapper} will pass in all Java methods here that
+     * it intends to expose in the data-model as methods (so you can do
+     * <tt>obj.foo()</tt> in the template).
+     * With this method you can do the following tweaks:
+     * <ul>
+     *   <li>Hide a method that would be otherwise shown by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setExposeMethodAs(String)}
+     *     with <tt>null</tt> parameter. Note that you can't un-hide methods
+     *     that are not public or are considered to by unsafe
+     *     (like {@link Object#wait()}) because
+     *     {@link #process} is not called for those.</li>
+     *   <li>Show the method with a different name in the data-model than its
+     *     real name by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setExposeMethodAs(String)}
+     *     with non-<tt>null</tt> parameter.
+     *   <li>Create a fake JavaBean property for this method by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setExposeAsProperty(PropertyDescriptor)}.
+     *     For example, if you have <tt>int size()</tt> in a class, but you
+     *     want it to be accessed from the templates as <tt>obj.size</tt>,
+     *     rather than as <tt>obj.size()</tt>, you can do that with this.
+     *     The default is {@code null}, which means that no fake property is
+     *     created for the method. You need not and shouldn't set this
+     *     to non-<tt>null</tt> for the getter methods of real JavaBean
+     *     properties, as those are automatically shown as properties anyway.
+     *     The property name in the {@link PropertyDescriptor} can be anything,
+     *     but the method (or methods) in it must belong to the class that
+     *     is given as the <tt>clazz</tt> parameter or it must be inherited from
+     *     that class, or else whatever errors can occur later.
+     *     {@link IndexedPropertyDescriptor}-s are supported.
+     *     If a real JavaBean property of the same name exists, it won't be
+     *     replaced by the fake one. Also if a fake property of the same name
+     *     was assigned earlier, it won't be replaced.
+     *   <li>Prevent the method to hide a JavaBean property (fake or real) of
+     *     the same name by calling
+     *     {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.Decision#setMethodShadowsProperty(boolean)}
+     *     with <tt>false</tt>. The default is <tt>true</tt>, so if you have
+     *     both a property and a method called "foo", then in the template
+     *     <tt>myObject.foo</tt> will return the method itself instead
+     *     of the property value, which is often undesirable.
+     * </ul>
+     * 
+     * <p>Note that you can expose a Java method both as a method and as a
+     * JavaBean property on the same time, however you have to chose different
+     * names for them to prevent shadowing. 
+     * 
+     * @param in Describes the method about which the decision will have to be made.
+     *  
+     * @param out Stores how the method will be exposed in the
+     *   data-model after {@link #process} returns.
+     *   This is initialized so that it reflects the default
+     *   behavior of {@link DefaultObjectWrapper}, so you don't have to do anything with this
+     *   when you don't want to change the default behavior.
+     */
+    void process(DecisionInput in, Decision out);
+
+    /**
+     * Used for {@link MethodAppearanceFineTuner#process} to store the results.
+     */
+    final class Decision {
+        private PropertyDescriptor exposeAsProperty;
+        private String exposeMethodAs;
+        private boolean methodShadowsProperty;
+
+        void setDefaults(Method m) {
+            exposeAsProperty = null;
+            exposeMethodAs = m.getName();
+            methodShadowsProperty = true;
+        }
+
+        public PropertyDescriptor getExposeAsProperty() {
+            return exposeAsProperty;
+        }
+
+        public void setExposeAsProperty(PropertyDescriptor exposeAsProperty) {
+            this.exposeAsProperty = exposeAsProperty;
+        }
+
+        public String getExposeMethodAs() {
+            return exposeMethodAs;
+        }
+
+        public void setExposeMethodAs(String exposeMethodAs) {
+            this.exposeMethodAs = exposeMethodAs;
+        }
+
+        public boolean getMethodShadowsProperty() {
+            return methodShadowsProperty;
+        }
+
+        public void setMethodShadowsProperty(boolean methodShadowsProperty) {
+            this.methodShadowsProperty = methodShadowsProperty;
+        }
+
+    }
+
+    /**
+     * Used for {@link org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner#process} as input parameter.
+     */
+    final class DecisionInput {
+        private Method method;
+        private Class<?> containingClass;
+
+        void setMethod(Method method) {
+            this.method = method;
+        }
+
+        void setContainingClass(Class<?> containingClass) {
+            this.containingClass = containingClass;
+        }
+
+        public Method getMethod() {
+            return method;
+        }
+
+        public Class/*<?>*/ getContainingClass() {
+            return containingClass;
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java b/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
new file mode 100644
index 0000000..b893bfe
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.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;
+
+import java.beans.MethodDescriptor;
+
+/**
+ * Used for JUnit testing method-order dependence bugs via
+ * {@link DefaultObjectWrapperBuilder#setMethodSorter(MethodSorter)}.
+ */
+interface MethodSorter {
+
+    MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] methodDescriptors);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ModelCache.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ModelCache.java b/src/main/java/org/apache/freemarker/core/model/impl/ModelCache.java
new file mode 100644
index 0000000..529b988
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/ModelCache.java
@@ -0,0 +1,143 @@
+/*
+ * 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.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+
+/**
+ * Internally used by various wrapper implementations to implement model
+ * caching.
+ */
+public abstract class ModelCache {
+    private boolean useCache = false;
+    private Map<Object, ModelReference> modelCache = null;
+    private ReferenceQueue<TemplateModel> refQueue = null;
+    
+    protected ModelCache() {
+    }
+    
+    /**
+     * Sets whether this wrapper caches model instances. Default is false.
+     * When set to true, calling {@link #getInstance(Object)} 
+     * multiple times for the same object will return the same model.
+     */
+    public synchronized void setUseCache(boolean useCache) {
+        this.useCache = useCache;
+        if (useCache) {
+            modelCache = new IdentityHashMap<>();
+            refQueue = new ReferenceQueue<>();
+        } else {
+            modelCache = null;
+            refQueue = null;
+        }
+    }
+
+    /**
+     * @since 2.3.21
+     */
+    public synchronized boolean getUseCache() {
+        return useCache;
+    }
+    
+    public TemplateModel getInstance(Object object) {
+        if (object instanceof TemplateModel) {
+            return (TemplateModel) object;
+        }
+        if (object instanceof TemplateModelAdapter) {
+            return ((TemplateModelAdapter) object).getTemplateModel();
+        }
+        if (useCache && isCacheable(object)) {
+            TemplateModel model = lookup(object);
+            if (model == null) {
+                model = create(object);
+                register(model, object);
+            }
+            return model;
+        } else {
+            return create(object);
+        }
+    }
+    
+    protected abstract TemplateModel create(Object object);
+    protected abstract boolean isCacheable(Object object);
+    
+    public void clearCache() {
+        if (modelCache != null) {
+            synchronized (modelCache) {
+                modelCache.clear();
+            }
+        }
+    }
+
+    private TemplateModel lookup(Object object) {
+        ModelReference ref = null;
+        // NOTE: we're doing minimal synchronizations -- which can lead to
+        // duplicate wrapper creation. However, this has no harmful side-effects and
+        // is a lesser performance hit.
+        synchronized (modelCache) {
+            ref = modelCache.get(object);
+        }
+
+        if (ref != null)
+            return ref.getModel();
+
+        return null;
+    }
+
+    private void register(TemplateModel model, Object object) {
+        synchronized (modelCache) {
+            // Remove cleared references
+            for (; ; ) {
+                ModelReference queuedRef = (ModelReference) refQueue.poll();
+                if (queuedRef == null) {
+                    break;
+                }
+                modelCache.remove(queuedRef.object);
+            }
+            // Register new reference
+            modelCache.put(object, new ModelReference(model, object, refQueue));
+        }
+    }
+
+    /**
+     * A special soft reference that is registered in the modelCache.
+     * When it gets cleared (that is, the model became unreachable)
+     * it will remove itself from the model cache.
+     */
+    private static final class ModelReference extends SoftReference<TemplateModel> {
+        Object object;
+
+        ModelReference(TemplateModel ref, Object object, ReferenceQueue<TemplateModel> refQueue) {
+            super(ref, refQueue);
+            this.object = object;
+        }
+
+        TemplateModel getModel() {
+            return get();
+        }
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ModelFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ModelFactory.java b/src/main/java/org/apache/freemarker/core/model/impl/ModelFactory.java
new file mode 100644
index 0000000..cd74342
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/ModelFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Interface used to create various wrapper models in the {@link ModelCache}.
+ */
+public interface ModelFactory {
+    /**
+     * Create a wrapping model for the specified object that belongs to
+     * the specified wrapper.
+     */
+    TemplateModel create(Object object, ObjectWrapper wrapper);
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java b/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java
new file mode 100644
index 0000000..2babe9f
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/NonPrimitiveArrayBackedReadOnlyList.java
@@ -0,0 +1,42 @@
+/*
+ * 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.AbstractList;
+
+class NonPrimitiveArrayBackedReadOnlyList extends AbstractList {
+    
+    private final Object[] array;
+    
+    NonPrimitiveArrayBackedReadOnlyList(Object[] array) {
+        this.array = array;
+    }
+
+    @Override
+    public Object get(int index) {
+        return array[index];
+    }
+
+    @Override
+    public int size() {
+        return array.length;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/NumberModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/NumberModel.java b/src/main/java/org/apache/freemarker/core/model/impl/NumberModel.java
new file mode 100644
index 0000000..70b50d9
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/NumberModel.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+/**
+ * Wraps arbitrary subclass of {@link java.lang.Number} into a reflective model.
+ * Beside acting as a {@link TemplateNumberModel}, you can call all Java methods on
+ * these objects as well.
+ */
+public class NumberModel
+extends
+    BeanModel
+implements
+    TemplateNumberModel {
+    static final ModelFactory FACTORY =
+        new ModelFactory()
+        {
+            @Override
+            public TemplateModel create(Object object, ObjectWrapper wrapper) {
+                return new NumberModel((Number) object, (DefaultObjectWrapper) wrapper);
+            }
+        };
+    /**
+     * Creates a new model that wraps the specified number object.
+     * @param number the number 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 NumberModel(Number number, DefaultObjectWrapper wrapper) {
+        super(number, wrapper);
+    }
+
+    @Override
+    public Number getAsNumber() {
+        return (Number) object;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java
new file mode 100644
index 0000000..5b160b6
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedFixArgsMethods.java
@@ -0,0 +1,99 @@
+/*
+ * 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.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * Stores the non-varargs methods for a {@link OverloadedMethods} object.
+ */
+class OverloadedFixArgsMethods extends OverloadedMethodsSubset {
+    
+    OverloadedFixArgsMethods() {
+        super();
+    }
+
+    @Override
+    Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc) {
+        return memberDesc.getParamTypes();
+    }
+    
+    @Override
+    void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
+        // Do nothing
+    }
+
+    @Override
+    MaybeEmptyMemberAndArguments getMemberAndArguments(List tmArgs, DefaultObjectWrapper unwrapper)
+    throws TemplateModelException {
+        if (tmArgs == null) {
+            // null is treated as empty args
+            tmArgs = Collections.EMPTY_LIST;
+        }
+        final int argCount = tmArgs.size();
+        final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
+        if (unwrappingHintsByParamCount.length <= argCount) {
+            return EmptyMemberAndArguments.WRONG_NUMBER_OF_ARGUMENTS;
+        }
+        Class[] unwarppingHints = unwrappingHintsByParamCount[argCount];
+        if (unwarppingHints == null) {
+            return EmptyMemberAndArguments.WRONG_NUMBER_OF_ARGUMENTS;
+        }
+        
+        Object[] pojoArgs = new Object[argCount];
+        
+        int[] typeFlags = getTypeFlags(argCount);
+        if (typeFlags == ALL_ZEROS_ARRAY) {
+            typeFlags = null;
+        }
+
+        Iterator it = tmArgs.iterator();
+        for (int i = 0; i < argCount; ++i) {
+            Object pojo = unwrapper.tryUnwrapTo(
+                    (TemplateModel) it.next(),
+                    unwarppingHints[i],
+                    typeFlags != null ? typeFlags[i] : 0);
+            if (pojo == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                return EmptyMemberAndArguments.noCompatibleOverload(i + 1);
+            }
+            pojoArgs[i] = pojo;
+        }
+        
+        MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, false);
+        if (maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
+            CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
+            if (typeFlags != null) {
+                // Note that overloaded method selection has already accounted for overflow errors when the method
+                // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+                // BigDecimal is allowed to overflow for backward-compatibility.
+                forceNumberArgumentsToParameterTypes(pojoArgs, memberDesc.getParamTypes(), typeFlags);
+            }
+            return new MemberAndArguments(memberDesc, pojoArgs);
+        } else {
+            return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc, pojoArgs);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java
new file mode 100644
index 0000000..e352bcf
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethods.java
@@ -0,0 +1,271 @@
+/*
+ * 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.Method;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.freemarker.core._DelayedConversionToString;
+import org.apache.freemarker.core._ErrorDescriptionBuilder;
+import org.apache.freemarker.core._TemplateModelException;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.FTLUtil;
+import org.apache.freemarker.core.util._ClassUtil;
+
+/**
+ * Used instead of {@link java.lang.reflect.Method} or {@link java.lang.reflect.Constructor} for overloaded methods and
+ * constructors.
+ * 
+ * <p>After the initialization with the {@link #addMethod(Method)} and {@link #addConstructor(Constructor)} calls are
+ * done, the instance must be thread-safe. Before that, it's the responsibility of the caller of those methods to
+ * ensure that the object is properly publishing to other threads.
+ */
+final class OverloadedMethods {
+
+    private final OverloadedMethodsSubset fixArgMethods;
+    private OverloadedMethodsSubset varargMethods;
+    
+    OverloadedMethods() {
+        fixArgMethods = new OverloadedFixArgsMethods();
+    }
+    
+    void addMethod(Method method) {
+        final Class[] paramTypes = method.getParameterTypes();
+        addCallableMemberDescriptor(new ReflectionCallableMemberDescriptor(method, paramTypes));
+    }
+
+    void addConstructor(Constructor constr) {
+        final Class[] paramTypes = constr.getParameterTypes();
+        addCallableMemberDescriptor(new ReflectionCallableMemberDescriptor(constr, paramTypes));
+    }
+    
+    private void addCallableMemberDescriptor(ReflectionCallableMemberDescriptor memberDesc) {
+        // Note: "varargs" methods are always callable as oms args, with a sequence (array) as the last parameter.
+        fixArgMethods.addCallableMemberDescriptor(memberDesc);
+        if (memberDesc.isVarargs()) {
+            if (varargMethods == null) {
+                varargMethods = new OverloadedVarArgsMethods();
+            }
+            varargMethods.addCallableMemberDescriptor(memberDesc);
+        }
+    }
+    
+    MemberAndArguments getMemberAndArguments(List/*<TemplateModel>*/ tmArgs, DefaultObjectWrapper unwrapper)
+    throws TemplateModelException {
+        // Try to find a oms args match:
+        MaybeEmptyMemberAndArguments fixArgsRes = fixArgMethods.getMemberAndArguments(tmArgs, unwrapper);
+        if (fixArgsRes instanceof MemberAndArguments) {
+            return (MemberAndArguments) fixArgsRes;
+        }
+
+        // Try to find a varargs match:
+        MaybeEmptyMemberAndArguments varargsRes;
+        if (varargMethods != null) {
+            varargsRes = varargMethods.getMemberAndArguments(tmArgs, unwrapper);
+            if (varargsRes instanceof MemberAndArguments) {
+                return (MemberAndArguments) varargsRes;
+            }
+        } else {
+            varargsRes = null;
+        }
+        
+        _ErrorDescriptionBuilder edb = new _ErrorDescriptionBuilder(
+                toCompositeErrorMessage(
+                        (EmptyMemberAndArguments) fixArgsRes,
+                        (EmptyMemberAndArguments) varargsRes,
+                        tmArgs),
+                "\nThe matching overload was searched among these members:\n",
+                memberListToString());
+        addMarkupBITipAfterNoNoMarchIfApplicable(edb, tmArgs);
+        throw new _TemplateModelException(edb);
+    }
+
+    private Object[] toCompositeErrorMessage(
+            final EmptyMemberAndArguments fixArgsEmptyRes, final EmptyMemberAndArguments varargsEmptyRes,
+            List tmArgs) {
+        final Object[] argsErrorMsg;
+        if (varargsEmptyRes != null) {
+            if (fixArgsEmptyRes == null || fixArgsEmptyRes.isNumberOfArgumentsWrong()) {
+                argsErrorMsg = toErrorMessage(varargsEmptyRes, tmArgs);
+            } else {
+                argsErrorMsg = new Object[] {
+                        "When trying to call the non-varargs overloads:\n",
+                        toErrorMessage(fixArgsEmptyRes, tmArgs),
+                        "\nWhen trying to call the varargs overloads:\n",
+                        toErrorMessage(varargsEmptyRes, null)
+                };
+            }
+        } else {
+            argsErrorMsg = toErrorMessage(fixArgsEmptyRes, tmArgs);
+        }
+        return argsErrorMsg;
+    }
+
+    private Object[] toErrorMessage(EmptyMemberAndArguments res, List/*<TemplateModel>*/ tmArgs) {
+        final Object[] unwrappedArgs = res.getUnwrappedArguments();
+        return new Object[] {
+                res.getErrorDescription(),
+                tmArgs != null
+                        ? new Object[] {
+                                "\nThe FTL type of the argument values were: ", getTMActualParameterTypes(tmArgs), "." }
+                        : "",
+                unwrappedArgs != null
+                        ? new Object[] {
+                                "\nThe Java type of the argument values were: ",
+                                getUnwrappedActualParameterTypes(unwrappedArgs) + "." }
+                        : ""};
+    }
+
+    private _DelayedConversionToString memberListToString() {
+        return new _DelayedConversionToString(null) {
+            
+            @Override
+            protected String doConversion(Object obj) {
+                final Iterator fixArgMethodsIter = fixArgMethods.getMemberDescriptors();
+                final Iterator varargMethodsIter = varargMethods != null ? varargMethods.getMemberDescriptors() : null;
+                
+                boolean hasMethods = fixArgMethodsIter.hasNext() || (varargMethodsIter != null && varargMethodsIter.hasNext()); 
+                if (hasMethods) {
+                    StringBuilder sb = new StringBuilder();
+                    HashSet fixArgMethods = new HashSet();
+                    while (fixArgMethodsIter.hasNext()) {
+                        if (sb.length() != 0) sb.append(",\n");
+                        sb.append("    ");
+                        CallableMemberDescriptor callableMemberDesc = (CallableMemberDescriptor) fixArgMethodsIter.next();
+                        fixArgMethods.add(callableMemberDesc);
+                        sb.append(callableMemberDesc.getDeclaration());
+                    }
+                    if (varargMethodsIter != null) {
+                        while (varargMethodsIter.hasNext()) {
+                            CallableMemberDescriptor callableMemberDesc = (CallableMemberDescriptor) varargMethodsIter.next();
+                            if (!fixArgMethods.contains(callableMemberDesc)) {
+                                if (sb.length() != 0) sb.append(",\n");
+                                sb.append("    ");
+                                sb.append(callableMemberDesc.getDeclaration());
+                            }
+                        }
+                    }
+                    return sb.toString();
+                } else {
+                    return "No members";
+                }
+            }
+            
+        };
+    }
+    
+    /**
+     * Adds tip to the error message if converting a {@link TemplateMarkupOutputModel} argument to {@link String} might
+     * allows finding a matching overload. 
+     */
+    private void addMarkupBITipAfterNoNoMarchIfApplicable(_ErrorDescriptionBuilder edb,
+            List tmArgs) {
+        for (int argIdx = 0; argIdx < tmArgs.size(); argIdx++) {
+            Object tmArg = tmArgs.get(argIdx);
+            if (tmArg instanceof TemplateMarkupOutputModel) {
+                for (Iterator membDescs = fixArgMethods.getMemberDescriptors(); membDescs.hasNext();) {
+                    CallableMemberDescriptor membDesc = (CallableMemberDescriptor) membDescs.next();
+                    Class[] paramTypes = membDesc.getParamTypes();
+                    
+                    Class paramType = null;
+                    if (membDesc.isVarargs() && argIdx >= paramTypes.length - 1) {
+                        paramType = paramTypes[paramTypes.length - 1];
+                        if (paramType.isArray()) {
+                            paramType = paramType.getComponentType();
+                        }
+                    }
+                    if (paramType == null && argIdx < paramTypes.length) {
+                        paramType = paramTypes[argIdx];
+                    }
+                    if (paramType != null) {
+                        if (paramType.isAssignableFrom(String.class) && !paramType.isAssignableFrom(tmArg.getClass())) {
+                            edb.tip(SimpleMethodModel.MARKUP_OUTPUT_TO_STRING_TIP);
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private _DelayedConversionToString getTMActualParameterTypes(List arguments) {
+        final String[] argumentTypeDescs = new String[arguments.size()];
+        for (int i = 0; i < arguments.size(); i++) {
+            argumentTypeDescs[i] = FTLUtil.getTypeDescription((TemplateModel) arguments.get(i));
+        }
+        
+        return new DelayedCallSignatureToString(argumentTypeDescs) {
+
+            @Override
+            String argumentToString(Object argType) {
+                return (String) argType;
+            }
+            
+        };
+    }
+    
+    private Object getUnwrappedActualParameterTypes(Object[] unwrappedArgs) {
+        final Class[] argumentTypes = new Class[unwrappedArgs.length];
+        for (int i = 0; i < unwrappedArgs.length; i++) {
+            Object unwrappedArg = unwrappedArgs[i];
+            argumentTypes[i] = unwrappedArg != null ? unwrappedArg.getClass() : null;
+        }
+        
+        return new DelayedCallSignatureToString(argumentTypes) {
+
+            @Override
+            String argumentToString(Object argType) {
+                return argType != null
+                        ? _ClassUtil.getShortClassName((Class) argType)
+                        : _ClassUtil.getShortClassNameOfObject(null);
+            }
+            
+        };
+    }
+    
+    private abstract class DelayedCallSignatureToString extends _DelayedConversionToString {
+
+        public DelayedCallSignatureToString(Object[] argTypeArray) {
+            super(argTypeArray);
+        }
+
+        @Override
+        protected String doConversion(Object obj) {
+            Object[] argTypes = (Object[]) obj;
+            
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < argTypes.length; i++) {
+                if (i != 0) sb.append(", ");
+                sb.append(argumentToString(argTypes[i]));
+            }
+            
+            return sb.toString();
+        }
+        
+        abstract String argumentToString(Object argType);
+        
+    }
+
+}


Mime
View raw message