http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java 
b/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
new file mode 100644
index 0000000..9c483d6
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
@@ -0,0 +1,362 @@
+/*
+ * 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.ObjectWrapper;
+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");
+    
+    static final ModelFactory FACTORY =
+        new ModelFactory()
+        {
+            @Override
+            public TemplateModel create(Object object, ObjectWrapper wrapper) {
+                return new BeanModel(object, (DefaultObjectWrapper) wrapper);
+            }
+        };
+
+    // 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 create 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 getObject(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 {
+            if (wrapper.isMethodsShadowItems()) {
+                Object fd = classInfo.get(key);
+                if (fd != null) {
+                    retval = invokeThroughDescriptor(fd, classInfo);
+                } else {
+                    retval = invokeGenericGet(classInfo, clazz, key);
+                }
+            } else {
+                TemplateModel model = invokeGenericGet(classInfo, clazz, key);
+                final TemplateModel nullModel = wrapper.wrap(null);
+                if (model != nullModel && model != UNKNOWN) {
+                    return model;
+                }
+                Object fd = classInfo.get(key);
+                if (fd != null) {
+                    retval = invokeThroughDescriptor(fd, classInfo);
+                    if (retval == UNKNOWN && model == nullModel) {
+                        // This is the (somewhat subtle) case where the 
generic get() returns null
+                        // and we have no bean info, so we respect the fact 
that
+                        // the generic get() returns null and return null. (JR)
+                        retval = nullModel;
+                    }
+                }
+            }
+            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 IndexedPropertyDescriptor) {
+            Method readMethod = ((IndexedPropertyDescriptor) 
desc).getIndexedReadMethod(); 
+            resultModel = cachedModel = 
+                new SimpleMethodModel(object, readMethod, 
+                        ClassIntrospector.getArgTypes(classInfo, readMethod), 
wrapper);
+        } else if (desc instanceof PropertyDescriptor) {
+            PropertyDescriptor pd = (PropertyDescriptor) desc;
+            resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), 
null);
+            // cachedModel remains null, as we don't cache these
+        } 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 SimpleMethodModel(
+                    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/051a0822/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java 
b/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java
new file mode 100644
index 0000000..7808d48
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java
@@ -0,0 +1,73 @@
+/*
+ * 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.TemplateModel;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+public class BeansModelCache extends ModelCache {
+    private final Map classToFactory = new ConcurrentHashMap();
+    private final Set mappedClassNames = new HashSet();
+
+    private final DefaultObjectWrapper wrapper;
+    
+    BeansModelCache(DefaultObjectWrapper wrapper) {
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    protected boolean isCacheable(Object object) {
+        return object.getClass() != Boolean.class; 
+    }
+    
+    @Override
+    @SuppressFBWarnings(value="JLM_JSR166_UTILCONCURRENT_MONITORENTER", 
justification="Locks for factory creation only")
+    protected TemplateModel create(Object object) {
+        Class clazz = object.getClass();
+        
+        ModelFactory factory = (ModelFactory) classToFactory.get(clazz);
+        
+        if (factory == null) {
+            // Synchronized so that we won't unnecessarily create the same 
factory for multiple times in parallel.
+            synchronized (classToFactory) {
+                factory = (ModelFactory) classToFactory.get(clazz);
+                if (factory == null) {
+                    String className = clazz.getName();
+                    // clear mappings when class reloading is detected
+                    if (!mappedClassNames.add(className)) {
+                        classToFactory.clear();
+                        mappedClassNames.clear();
+                        mappedClassNames.add(className);
+                    }
+                    factory = wrapper.getModelFactory(clazz);
+                    classToFactory.put(clazz, factory);
+                }
+            }
+        }
+        
+        return factory.create(object, wrapper);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java 
b/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java
new file mode 100644
index 0000000..9db421f
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java
@@ -0,0 +1,40 @@
+/*
+ * 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.TemplateBooleanModel;
+
+/**
+ * <p>A class that will wrap instances of {@link java.lang.Boolean} into a
+ * {@link TemplateBooleanModel}.
+ */
+public class BooleanModel extends BeanModel implements TemplateBooleanModel {
+    private final boolean value;
+    
+    public BooleanModel(Boolean bool, DefaultObjectWrapper wrapper) {
+        super(bool, wrapper, false);
+        value = bool.booleanValue();
+    }
+
+    @Override
+    public boolean getAsBoolean() {
+        return value;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
 
b/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java
new file mode 100644
index 0000000..5548796
--- /dev/null
+++ 
b/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/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java 
b/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java
new file mode 100644
index 0000000..0e31d80
--- /dev/null
+++ b/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/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
 
b/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
new file mode 100644
index 0000000..8b72f2b
--- /dev/null
+++ 
b/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/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java 
b/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java
new file mode 100644
index 0000000..ddca496
--- /dev/null
+++ 
b/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);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java 
b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
new file mode 100644
index 0000000..4dd8931
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -0,0 +1,807 @@
+/*
+ * 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.BeanInfo;
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.MethodDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core._CoreLogs;
+import 
org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.DecisionInput;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+
+/**
+ * Returns information about a {@link Class} that's useful for FreeMarker. 
Encapsulates a cache for this. Thread-safe,
+ * doesn't even require "proper publishing" starting from 2.3.24 or Java 5. 
Immutable, with the exception of the
+ * internal caches.
+ * 
+ * <p>
+ * Note that instances of this are cached on the level of FreeMarker's 
defining class loader. Hence, it must not do
+ * operations that depend on the Thread Context Class Loader, such as 
resolving class names.
+ */
+class ClassIntrospector {
+
+    // Attention: This class must be thread-safe (not just after proper 
publishing). This is important as some of
+    // these are shared by many object wrappers, and concurrency related 
glitches due to user errors must remain
+    // local to the object wrappers, not corrupting the shared 
ClassIntrospector.
+
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+
+    private static final String JREBEL_SDK_CLASS_NAME = 
"org.zeroturnaround.javarebel.ClassEventListener";
+    private static final String JREBEL_INTEGRATION_ERROR_MSG
+            = "Error initializing JRebel integration. JRebel integration 
disabled.";
+
+    private static final ClassChangeNotifier CLASS_CHANGE_NOTIFIER;
+    static {
+        boolean jRebelAvailable;
+        try {
+            Class.forName(JREBEL_SDK_CLASS_NAME);
+            jRebelAvailable = true;
+        } catch (Throwable e) {
+            jRebelAvailable = false;
+            try {
+                if (!(e instanceof ClassNotFoundException)) {
+                    LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e);
+                }
+            } catch (Throwable loggingE) {
+                // ignore
+            }
+        }
+
+        ClassChangeNotifier classChangeNotifier;
+        if (jRebelAvailable) {
+            try {
+                classChangeNotifier = (ClassChangeNotifier)
+                        
Class.forName("org.apache.freemarker.core.model.impl.JRebelClassChangeNotifier").newInstance();
+            } catch (Throwable e) {
+                classChangeNotifier = null;
+                try {
+                    LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e);
+                } catch (Throwable loggingE) {
+                    // ignore
+                }
+            }
+        } else {
+            classChangeNotifier = null;
+        }
+
+        CLASS_CHANGE_NOTIFIER = classChangeNotifier;
+    }
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Introspection info Map keys:
+
+    /** Key in the class info Map to the Map that maps method to argument type 
arrays */
+    private static final Object ARG_TYPES_BY_METHOD_KEY = new Object();
+    /** Key in the class info Map to the object that represents the 
constructors (one or multiple due to overloading) */
+    static final Object CONSTRUCTORS_KEY = new Object();
+    /** Key in the class info Map to the get(String|Object) Method */
+    static final Object GENERIC_GET_KEY = new Object();
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Introspection configuration properties:
+
+    // Note: These all must be *declared* final (or else synchronization is 
needed everywhere where they are accessed).
+
+    final int exposureLevel;
+    final boolean exposeFields;
+    final MethodAppearanceFineTuner methodAppearanceFineTuner;
+    final MethodSorter methodSorter;
+
+    /** See {@link #getHasSharedInstanceRestrictons()} */
+    final private boolean hasSharedInstanceRestrictons;
+
+    /** See {@link #isShared()} */
+    final private boolean shared;
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // State fields:
+
+    private final Object sharedLock;
+    private final Map<Class<?>, Map<Object, Object>> cache
+            = new ConcurrentHashMap<>(0, 0.75f, 16);
+    private final Set<String> cacheClassNames = new HashSet<>(0);
+    private final Set<Class<?>> classIntrospectionsInProgress = new 
HashSet<>(0);
+
+    private final 
List<WeakReference<Object/*ClassBasedModelFactory|ModelCache>*/>> modelFactories
+            = new LinkedList<>();
+    private final ReferenceQueue<Object> modelFactoriesRefQueue = new 
ReferenceQueue<>();
+
+    private int clearingCounter;
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Instantiation:
+
+    /**
+     * Creates a new instance, that is hence surely not shared (singleton) 
instance.
+     * 
+     * @param pa
+     *            Stores what the values of the JavaBean properties of the 
returned instance will be. Not {@code null}.
+     */
+    ClassIntrospector(ClassIntrospectorBuilder pa, Object sharedLock) {
+        this(pa, sharedLock, false, false);
+    }
+
+    /**
+     * @param hasSharedInstanceRestrictons
+     *            {@code true} exactly if we are creating a new instance with 
{@link ClassIntrospectorBuilder}. Then
+     *            it's {@code true} even if it won't put the instance into the 
cache.
+     */
+    ClassIntrospector(ClassIntrospectorBuilder builder, Object sharedLock,
+            boolean hasSharedInstanceRestrictons, boolean shared) {
+        _NullArgumentException.check("sharedLock", sharedLock);
+
+        exposureLevel = builder.getExposureLevel();
+        exposeFields = builder.getExposeFields();
+        methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner();
+        methodSorter = builder.getMethodSorter();
+
+        this.sharedLock = sharedLock;
+
+        this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons;
+        this.shared = shared;
+
+        if (CLASS_CHANGE_NOTIFIER != null) {
+            CLASS_CHANGE_NOTIFIER.subscribe(this);
+        }
+    }
+
+    /**
+     * Returns a {@link ClassIntrospectorBuilder}-s that could be used to 
create an identical {@link #ClassIntrospector}
+     * . The returned {@link ClassIntrospectorBuilder} can be modified without 
interfering with anything.
+     */
+    ClassIntrospectorBuilder getPropertyAssignments() {
+        return new ClassIntrospectorBuilder(this);
+    }
+
+    // 
------------------------------------------------------------------------------------------------------------------
+    // Introspection:
+
+    /**
+     * Gets the class introspection data from {@link #cache}, automatically 
creating the cache entry if it's missing.
+     * 
+     * @return A {@link Map} where each key is a property/method/field name 
(or a special {@link Object} key like
+     *         {@link #CONSTRUCTORS_KEY}), each value is a {@link 
PropertyDescriptor} or {@link Method} or
+     *         {@link OverloadedMethods} or {@link Field} (but better check 
the source code...).
+     */
+    Map<Object, Object> get(Class<?> clazz) {
+        {
+            Map<Object, Object> introspData = cache.get(clazz);
+            if (introspData != null) return introspData;
+        }
+
+        String className;
+        synchronized (sharedLock) {
+            Map<Object, Object> introspData = cache.get(clazz);
+            if (introspData != null) return introspData;
+
+            className = clazz.getName();
+            if (cacheClassNames.contains(className)) {
+                onSameNameClassesDetected(className);
+            }
+
+            while (introspData == null && 
classIntrospectionsInProgress.contains(clazz)) {
+                // Another thread is already introspecting this class;
+                // waiting for its result.
+                try {
+                    sharedLock.wait();
+                    introspData = cache.get(clazz);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(
+                            "Class inrospection data lookup aborded: " + e);
+                }
+            }
+            if (introspData != null) return introspData;
+
+            // This will be the thread that introspects this class.
+            classIntrospectionsInProgress.add(clazz);
+        }
+        try {
+            Map<Object, Object> introspData = 
createClassIntrospectionData(clazz);
+            synchronized (sharedLock) {
+                cache.put(clazz, introspData);
+                cacheClassNames.add(className);
+            }
+            return introspData;
+        } finally {
+            synchronized (sharedLock) {
+                classIntrospectionsInProgress.remove(clazz);
+                sharedLock.notifyAll();
+            }
+        }
+    }
+
+    /**
+     * Creates a {@link Map} with the content as described for the return 
value of {@link #get(Class)}.
+     */
+    private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) {
+        final Map<Object, Object> introspData = new HashMap<>();
+
+        if (exposeFields) {
+            addFieldsToClassIntrospectionData(introspData, clazz);
+        }
+
+        final Map<MethodSignature, List<Method>> accessibleMethods = 
discoverAccessibleMethods(clazz);
+
+        addGenericGetToClassIntrospectionData(introspData, accessibleMethods);
+
+        if (exposureLevel != DefaultObjectWrapper.EXPOSE_NOTHING) {
+            try {
+                addBeanInfoToClassIntrospectionData(introspData, clazz, 
accessibleMethods);
+            } catch (IntrospectionException e) {
+                LOG.warn("Couldn't properly perform introspection for class 
{}", clazz.getName(), e);
+                introspData.clear(); // FIXME NBC: Don't drop everything here.
+            }
+        }
+
+        addConstructorsToClassIntrospectionData(introspData, clazz);
+
+        if (introspData.size() > 1) {
+            return introspData;
+        } else if (introspData.size() == 0) {
+            return Collections.emptyMap();
+        } else { // map.size() == 1
+            Entry<Object, Object> e = introspData.entrySet().iterator().next();
+            return Collections.singletonMap(e.getKey(), e.getValue());
+        }
+    }
+
+    private void addFieldsToClassIntrospectionData(Map<Object, Object> 
introspData, Class<?> clazz)
+            throws SecurityException {
+        for (Field field : clazz.getFields()) {
+            if ((field.getModifiers() & Modifier.STATIC) == 0) {
+                introspData.put(field.getName(), field);
+            }
+        }
+    }
+
+    private void addBeanInfoToClassIntrospectionData(
+            Map<Object, Object> introspData, Class<?> clazz, 
Map<MethodSignature, List<Method>> accessibleMethods)
+            throws IntrospectionException {
+        BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
+
+        PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors();
+        if (pda != null) {
+            int pdaLength = pda.length;
+            for (int i = pdaLength - 1; i >= 0; --i) {
+                addPropertyDescriptorToClassIntrospectionData(
+                        introspData, pda[i], clazz,
+                        accessibleMethods);
+            }
+        }
+
+        if (exposureLevel < DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) {
+            final MethodAppearanceFineTuner.Decision decision = new 
MethodAppearanceFineTuner.Decision();
+            DecisionInput decisionInput = null;
+            final MethodDescriptor[] mda = 
sortMethodDescriptors(beanInfo.getMethodDescriptors());
+            if (mda != null) {
+                int mdaLength = mda.length;
+                for (int i = mdaLength - 1; i >= 0; --i) {
+                    final MethodDescriptor md = mda[i];
+                    final Method method = 
getMatchingAccessibleMethod(md.getMethod(), accessibleMethods);
+                    if (method != null && isAllowedToExpose(method)) {
+                        decision.setDefaults(method);
+                        if (methodAppearanceFineTuner != null) {
+                            if (decisionInput == null) {
+                                decisionInput = new DecisionInput();
+                            }
+                            decisionInput.setContainingClass(clazz);
+                            decisionInput.setMethod(method);
+    
+                            methodAppearanceFineTuner.process(decisionInput, 
decision);
+                        }
+    
+                        PropertyDescriptor propDesc = 
decision.getExposeAsProperty();
+                        if (propDesc != null && 
!(introspData.get(propDesc.getName()) instanceof PropertyDescriptor)) {
+                            addPropertyDescriptorToClassIntrospectionData(
+                                    introspData, propDesc, clazz, 
accessibleMethods);
+                        }
+    
+                        String methodKey = decision.getExposeMethodAs();
+                        if (methodKey != null) {
+                            Object previous = introspData.get(methodKey);
+                            if (previous instanceof Method) {
+                                // Overloaded method - replace Method with a 
OverloadedMethods
+                                OverloadedMethods overloadedMethods = new 
OverloadedMethods();
+                                overloadedMethods.addMethod((Method) previous);
+                                overloadedMethods.addMethod(method);
+                                introspData.put(methodKey, overloadedMethods);
+                                // Remove parameter type information
+                                
getArgTypesByMethod(introspData).remove(previous);
+                            } else if (previous instanceof OverloadedMethods) {
+                                // Already overloaded method - add new overload
+                                ((OverloadedMethods) 
previous).addMethod(method);
+                            } else if (decision.getMethodShadowsProperty()
+                                    || !(previous instanceof 
PropertyDescriptor)) {
+                                // Simple method (this far)
+                                introspData.put(methodKey, method);
+                                getArgTypesByMethod(introspData).put(method,
+                                        method.getParameterTypes());
+                            }
+                        }
+                    }
+                } // for each in mda
+            } // if mda != null
+        } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY)
+    }
+
+    private void addPropertyDescriptorToClassIntrospectionData(Map<Object, 
Object> introspData,
+            PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, 
List<Method>> accessibleMethods) {
+        if (pd instanceof IndexedPropertyDescriptor) {
+            IndexedPropertyDescriptor ipd =
+                    (IndexedPropertyDescriptor) pd;
+            Method readMethod = ipd.getIndexedReadMethod();
+            Method publicReadMethod = getMatchingAccessibleMethod(readMethod, 
accessibleMethods);
+            if (publicReadMethod != null && 
isAllowedToExpose(publicReadMethod)) {
+                try {
+                    if (readMethod != publicReadMethod) {
+                        ipd = new IndexedPropertyDescriptor(
+                                ipd.getName(), ipd.getReadMethod(),
+                                null, publicReadMethod,
+                                null);
+                    }
+                    introspData.put(ipd.getName(), ipd);
+                    getArgTypesByMethod(introspData).put(publicReadMethod, 
publicReadMethod.getParameterTypes());
+                } catch (IntrospectionException e) {
+                    LOG.warn("Failed creating a publicly-accessible property 
descriptor "
+                            + "for {} indexed property {}, read method {}",
+                            clazz.getName(), pd.getName(), publicReadMethod,
+                            e);
+                }
+            }
+        } else {
+            Method readMethod = pd.getReadMethod();
+            Method publicReadMethod = getMatchingAccessibleMethod(readMethod, 
accessibleMethods);
+            if (publicReadMethod != null && 
isAllowedToExpose(publicReadMethod)) {
+                try {
+                    if (readMethod != publicReadMethod) {
+                        pd = new PropertyDescriptor(pd.getName(), 
publicReadMethod, null);
+                        pd.setReadMethod(publicReadMethod);
+                    }
+                    introspData.put(pd.getName(), pd);
+                } catch (IntrospectionException e) {
+                    LOG.warn("Failed creating a publicly-accessible property 
descriptor "
+                            + "for {} property {}, read method {}",
+                            clazz.getName(), pd.getName(), publicReadMethod,
+                            e);
+                }
+            }
+        }
+    }
+
+    private void addGenericGetToClassIntrospectionData(Map<Object, Object> 
introspData,
+            Map<MethodSignature, List<Method>> accessibleMethods) {
+        Method genericGet = getFirstAccessibleMethod(
+                MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+        if (genericGet == null) {
+            genericGet = getFirstAccessibleMethod(
+                    MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+        }
+        if (genericGet != null) {
+            introspData.put(GENERIC_GET_KEY, genericGet);
+        }
+    }
+
+    private void addConstructorsToClassIntrospectionData(final Map<Object, 
Object> introspData,
+            Class<?> clazz) {
+        try {
+            Constructor<?>[] ctors = clazz.getConstructors();
+            if (ctors.length == 1) {
+                Constructor<?> ctor = ctors[0];
+                introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, 
ctor.getParameterTypes()));
+            } else if (ctors.length > 1) {
+                OverloadedMethods overloadedCtors = new OverloadedMethods();
+                for (Constructor<?> ctor : ctors) {
+                    overloadedCtors.addConstructor(ctor);
+                }
+                introspData.put(CONSTRUCTORS_KEY, overloadedCtors);
+            }
+        } catch (SecurityException e) {
+            LOG.warn("Can't discover constructors for class {}", 
clazz.getName(), e);
+        }
+    }
+
+    /**
+     * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of 
accessible methods for a class. In case the
+     * class is not public, retrieves methods with same signature as its 
public methods from public superclasses and
+     * interfaces. Basically upcasts every method to the nearest accessible 
method.
+     */
+    private static Map<MethodSignature, List<Method>> 
discoverAccessibleMethods(Class<?> clazz) {
+        Map<MethodSignature, List<Method>> accessibles = new HashMap<>();
+        discoverAccessibleMethods(clazz, accessibles);
+        return accessibles;
+    }
+
+    private static void discoverAccessibleMethods(Class<?> clazz, 
Map<MethodSignature, List<Method>> accessibles) {
+        if (Modifier.isPublic(clazz.getModifiers())) {
+            try {
+                Method[] methods = clazz.getMethods();
+                for (Method method : methods) {
+                    MethodSignature sig = new MethodSignature(method);
+                    // Contrary to intuition, a class can actually have several
+                    // different methods with same signature *but* different
+                    // return types. These can't be constructed using Java the
+                    // language, as this is illegal on source code level, but
+                    // the compiler can emit synthetic methods as part of
+                    // generic type reification that will have same signature
+                    // yet different return type than an existing explicitly
+                    // declared method. Consider:
+                    // public interface I<T> { T m(); }
+                    // public class C implements I<Integer> { Integer m() { 
return 42; } }
+                    // C.class will have both "Object m()" and "Integer m()" 
methods.
+                    List<Method> methodList = accessibles.get(sig);
+                    if (methodList == null) {
+                        // TODO Collection.singletonList is more efficient, 
though read only.
+                        methodList = new LinkedList<>();
+                        accessibles.put(sig, methodList);
+                    }
+                    methodList.add(method);
+                }
+                return;
+            } catch (SecurityException e) {
+                LOG.warn("Could not discover accessible methods of class {}, 
attemping superclasses/interfaces.",
+                        clazz.getName(), e);
+                // Fall through and attempt to discover superclass/interface 
methods
+            }
+        }
+
+        Class<?>[] interfaces = clazz.getInterfaces();
+        for (Class<?> anInterface : interfaces) {
+            discoverAccessibleMethods(anInterface, accessibles);
+        }
+        Class<?> superclass = clazz.getSuperclass();
+        if (superclass != null) {
+            discoverAccessibleMethods(superclass, accessibles);
+        }
+    }
+
+    private static Method getMatchingAccessibleMethod(Method m, 
Map<MethodSignature, List<Method>> accessibles) {
+        if (m == null) {
+            return null;
+        }
+        MethodSignature sig = new MethodSignature(m);
+        List<Method> ams = accessibles.get(sig);
+        if (ams == null) {
+            return null;
+        }
+        for (Method am : ams) {
+            if (am.getReturnType() == m.getReturnType()) {
+                return am;
+            }
+        }
+        return null;
+    }
+
+    private static Method getFirstAccessibleMethod(MethodSignature sig, 
Map<MethodSignature, List<Method>> accessibles) {
+        List<Method> ams = accessibles.get(sig);
+        if (ams == null || ams.isEmpty()) {
+            return null;
+        }
+        return ams.get(0);
+    }
+
+    /**
+     * As of this writing, this is only used for testing if method order 
really doesn't mater.
+     */
+    private MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] 
methodDescriptors) {
+        return methodSorter != null ? 
methodSorter.sortMethodDescriptors(methodDescriptors) : methodDescriptors;
+    }
+
+    boolean isAllowedToExpose(Method method) {
+        return exposureLevel < DefaultObjectWrapper.EXPOSE_SAFE || 
!UnsafeMethods.isUnsafeMethod(method);
+    }
+
+    private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, 
Object> classInfo) {
+        @SuppressWarnings("unchecked")
+        Map<Method, Class<?>[]> argTypes = (Map<Method, Class<?>[]>) 
classInfo.get(ARG_TYPES_BY_METHOD_KEY);
+        if (argTypes == null) {
+            argTypes = new HashMap<>();
+            classInfo.put(ARG_TYPES_BY_METHOD_KEY, argTypes);
+        }
+        return argTypes;
+    }
+
+    private static final class MethodSignature {
+        private static final MethodSignature GET_STRING_SIGNATURE =
+                new MethodSignature("get", new Class[] { String.class });
+        private static final MethodSignature GET_OBJECT_SIGNATURE =
+                new MethodSignature("get", new Class[] { Object.class });
+
+        private final String name;
+        private final Class<?>[] args;
+
+        private MethodSignature(String name, Class<?>[] args) {
+            this.name = name;
+            this.args = args;
+        }
+
+        MethodSignature(Method method) {
+            this(method.getName(), method.getParameterTypes());
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof MethodSignature) {
+                MethodSignature ms = (MethodSignature) o;
+                return ms.name.equals(name) && Arrays.equals(args, ms.args);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return name.hashCode() ^ args.length; // TODO That's a poor 
quality hash... isn't this a problem?
+        }
+    }
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Cache management:
+
+    /**
+     * Corresponds to {@link 
DefaultObjectWrapper#clearClassIntrospecitonCache()}.
+     * 
+     * @since 2.3.20
+     */
+    void clearCache() {
+        if (getHasSharedInstanceRestrictons()) {
+            throw new IllegalStateException(
+                    "It's not allowed to clear the whole cache in a read-only 
" + getClass().getName() +
+                            "instance. Use 
removeFromClassIntrospectionCache(String prefix) instead.");
+        }
+        forcedClearCache();
+    }
+
+    private void forcedClearCache() {
+        synchronized (sharedLock) {
+            cache.clear();
+            cacheClassNames.clear();
+            clearingCounter++;
+
+            for (WeakReference<Object> regedMfREf : modelFactories) {
+                Object regedMf = regedMfREf.get();
+                if (regedMf != null) {
+                    if (regedMf instanceof ClassBasedModelFactory) {
+                        ((ClassBasedModelFactory) regedMf).clearCache();
+                    } else if (regedMf instanceof ModelCache) {
+                        ((ModelCache) regedMf).clearCache();
+                    } else {
+                        throw new BugException();
+                    }
+                }
+            }
+
+            removeClearedModelFactoryReferences();
+        }
+    }
+
+    /**
+     * Corresponds to {@link 
DefaultObjectWrapper#removeFromClassIntrospectionCache(Class)}.
+     * 
+     * @since 2.3.20
+     */
+    void remove(Class<?> clazz) {
+        synchronized (sharedLock) {
+            cache.remove(clazz);
+            cacheClassNames.remove(clazz.getName());
+            clearingCounter++;
+
+            for (WeakReference<Object> regedMfREf : modelFactories) {
+                Object regedMf = regedMfREf.get();
+                if (regedMf != null) {
+                    if (regedMf instanceof ClassBasedModelFactory) {
+                        ((ClassBasedModelFactory) 
regedMf).removeFromCache(clazz);
+                    } else if (regedMf instanceof ModelCache) {
+                        ((ModelCache) regedMf).clearCache(); // doesn't 
support selective clearing ATM
+                    } else {
+                        throw new BugException();
+                    }
+                }
+            }
+
+            removeClearedModelFactoryReferences();
+        }
+    }
+
+    /**
+     * Returns the number of events so far that could make class introspection 
data returned earlier outdated.
+     */
+    int getClearingCounter() {
+        synchronized (sharedLock) {
+            return clearingCounter;
+        }
+    }
+
+    private void onSameNameClassesDetected(String className) {
+        // TODO: This behavior should be pluggable, as in environments where
+        // some classes are often reloaded or multiple versions of the
+        // same class is normal (OSGi), this will drop the cache contents
+        // too often.
+        LOG.info(
+                "Detected multiple classes with the same name, \"{}\". "
+                + "Assuming it was a class-reloading. Clearing class 
introspection caches to release old data.",
+                className);
+        forcedClearCache();
+    }
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Managing dependent objects:
+
+    void registerModelFactory(ClassBasedModelFactory mf) {
+        registerModelFactory((Object) mf);
+    }
+
+    void registerModelFactory(ModelCache mf) {
+        registerModelFactory((Object) mf);
+    }
+
+    private void registerModelFactory(Object mf) {
+        // Note that this `synchronized (sharedLock)` is also need for the 
DefaultObjectWrapper constructor to work safely.
+        synchronized (sharedLock) {
+            modelFactories.add(new WeakReference<>(mf, 
modelFactoriesRefQueue));
+            removeClearedModelFactoryReferences();
+        }
+    }
+
+    void unregisterModelFactory(ClassBasedModelFactory mf) {
+        unregisterModelFactory((Object) mf);
+    }
+
+    void unregisterModelFactory(ModelCache mf) {
+        unregisterModelFactory((Object) mf);
+    }
+
+    void unregisterModelFactory(Object mf) {
+        synchronized (sharedLock) {
+            for (Iterator<WeakReference<Object>> it = 
modelFactories.iterator(); it.hasNext(); ) {
+                Object regedMf = it.next().get();
+                if (regedMf == mf) {
+                    it.remove();
+                }
+            }
+
+        }
+    }
+
+    private void removeClearedModelFactoryReferences() {
+        Reference<?> cleardRef;
+        while ((cleardRef = modelFactoriesRefQueue.poll()) != null) {
+            synchronized (sharedLock) {
+                findClearedRef: for (Iterator<WeakReference<Object>> it = 
modelFactories.iterator(); it.hasNext(); ) {
+                    if (it.next() == cleardRef) {
+                        it.remove();
+                        break findClearedRef;
+                    }
+                }
+            }
+        }
+    }
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Extracting from introspection info:
+
+    static Class<?>[] getArgTypes(Map<Object, Object> classInfo, Method 
method) {
+        @SuppressWarnings("unchecked")
+        Map<Method, Class<?>[]> argTypesByMethod = (Map<Method, Class<?>[]>) 
classInfo.get(ARG_TYPES_BY_METHOD_KEY);
+        return argTypesByMethod.get(method);
+    }
+
+    /**
+     * Returns the number of introspected methods/properties that should be 
available via the TemplateHashModel
+     * interface.
+     */
+    int keyCount(Class<?> clazz) {
+        Map<Object, Object> map = get(clazz);
+        int count = map.size();
+        if (map.containsKey(CONSTRUCTORS_KEY)) count--;
+        if (map.containsKey(GENERIC_GET_KEY)) count--;
+        if (map.containsKey(ARG_TYPES_BY_METHOD_KEY)) count--;
+        return count;
+    }
+
+    /**
+     * Returns the Set of names of introspected methods/properties that should 
be available via the TemplateHashModel
+     * interface.
+     */
+    Set<Object> keySet(Class<?> clazz) {
+        Set<Object> set = new HashSet<>(get(clazz).keySet());
+        set.remove(CONSTRUCTORS_KEY);
+        set.remove(GENERIC_GET_KEY);
+        set.remove(ARG_TYPES_BY_METHOD_KEY);
+        return set;
+    }
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Properties
+
+    int getExposureLevel() {
+        return exposureLevel;
+    }
+
+    boolean getExposeFields() {
+        return exposeFields;
+    }
+
+    MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+        return methodAppearanceFineTuner;
+    }
+
+    MethodSorter getMethodSorter() {
+        return methodSorter;
+    }
+
+    /**
+     * Returns {@code true} if this instance was created with {@link 
ClassIntrospectorBuilder}, even if it wasn't
+     * actually put into the cache (as we reserve the right to do so in later 
versions).
+     */
+    boolean getHasSharedInstanceRestrictons() {
+        return hasSharedInstanceRestrictons;
+    }
+
+    /**
+     * Tells if this instance is (potentially) shared among {@link 
DefaultObjectWrapper} instances.
+     * 
+     * @see #getHasSharedInstanceRestrictons()
+     */
+    boolean isShared() {
+        return shared;
+    }
+
+    /**
+     * Almost always, you want to use {@link 
DefaultObjectWrapper#getSharedIntrospectionLock()}, not this! The only 
exception is
+     * when you get this to set the field returned by {@link 
DefaultObjectWrapper#getSharedIntrospectionLock()}.
+     */
+    Object getSharedLock() {
+        return sharedLock;
+    }
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Monitoring:
+
+    /** For unit testing only */
+    Object[] getRegisteredModelFactoriesSnapshot() {
+        synchronized (sharedLock) {
+            return modelFactories.toArray();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java
 
b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java
new file mode 100644
index 0000000..b17a49e
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java
@@ -0,0 +1,190 @@
+/*
+ * 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.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+final class ClassIntrospectorBuilder implements Cloneable {
+
+    private static final Map/*<PropertyAssignments, 
Reference<ClassIntrospector>>*/ INSTANCE_CACHE = new HashMap();
+    private static final ReferenceQueue INSTANCE_CACHE_REF_QUEUE = new 
ReferenceQueue(); 
+    
+    // Properties and their *defaults*:
+    private int exposureLevel = DefaultObjectWrapper.EXPOSE_SAFE;
+    private boolean exposeFields;
+    private MethodAppearanceFineTuner methodAppearanceFineTuner;
+    private MethodSorter methodSorter;
+    // Attention:
+    // - This is also used as a cache key, so non-normalized field values 
should be avoided.
+    // - If some field has a default value, it must be set until the end of 
the constructor. No field that has a
+    //   default can be left unset (like null).
+    // - If you add a new field, review all methods in this class, also the 
ClassIntrospector constructor
+    
+    ClassIntrospectorBuilder(ClassIntrospector ci) {
+        exposureLevel = ci.exposureLevel;
+        exposeFields = ci.exposeFields;
+        methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
+        methodSorter = ci.methodSorter; 
+    }
+    
+    ClassIntrospectorBuilder(Version incompatibleImprovements) {
+        // Warning: incompatibleImprovements must not affect this object at 
versions increments where there's no
+        // change in the 
DefaultObjectWrapper.normalizeIncompatibleImprovements results. That is, this 
class may don't react
+        // to some version changes that affects DefaultObjectWrapper, but not 
the other way around.
+        _NullArgumentException.check(incompatibleImprovements);
+        // Currently nothing depends on incompatibleImprovements
+    }
+    
+    @Override
+    protected Object clone() {
+        try {
+            return super.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new RuntimeException("Failed to clone 
ClassIntrospectorBuilder", e);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (exposeFields ? 1231 : 1237);
+        result = prime * result + exposureLevel;
+        result = prime * result + 
System.identityHashCode(methodAppearanceFineTuner);
+        result = prime * result + System.identityHashCode(methodSorter);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        ClassIntrospectorBuilder other = (ClassIntrospectorBuilder) obj;
+        
+        if (exposeFields != other.exposeFields) return false;
+        if (exposureLevel != other.exposureLevel) return false;
+        if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) 
return false;
+        return methodSorter == other.methodSorter;
+    }
+    
+    public int getExposureLevel() {
+        return exposureLevel;
+    }
+
+    /** See {@link DefaultObjectWrapper#setExposureLevel(int)}. */
+    public void setExposureLevel(int exposureLevel) {
+        if (exposureLevel < DefaultObjectWrapper.EXPOSE_ALL || exposureLevel > 
DefaultObjectWrapper.EXPOSE_NOTHING) {
+            throw new IllegalArgumentException("Illegal exposure level: " + 
exposureLevel);
+        }
+        
+        this.exposureLevel = exposureLevel;
+    }
+
+    public boolean getExposeFields() {
+        return exposeFields;
+    }
+
+    /** See {@link DefaultObjectWrapper#setExposeFields(boolean)}. */
+    public void setExposeFields(boolean exposeFields) {
+        this.exposeFields = exposeFields;
+    }
+
+    public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+        return methodAppearanceFineTuner;
+    }
+
+    public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner 
methodAppearanceFineTuner) {
+        this.methodAppearanceFineTuner = methodAppearanceFineTuner;
+    }
+
+    public MethodSorter getMethodSorter() {
+        return methodSorter;
+    }
+
+    public void setMethodSorter(MethodSorter methodSorter) {
+        this.methodSorter = methodSorter;
+    }
+
+    private static void removeClearedReferencesFromInstanceCache() {
+        Reference clearedRef;
+        while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) {
+            synchronized (INSTANCE_CACHE) {
+                findClearedRef: for (Iterator it = 
INSTANCE_CACHE.values().iterator(); it.hasNext(); ) {
+                    if (it.next() == clearedRef) {
+                        it.remove();
+                        break findClearedRef;
+                    }
+                }
+            }
+        }
+    }
+
+    /** For unit testing only */
+    static void clearInstanceCache() {
+        synchronized (INSTANCE_CACHE) {
+            INSTANCE_CACHE.clear();
+        }
+    }
+    
+    /** For unit testing only */
+    static Map getInstanceCache() {
+        return INSTANCE_CACHE;
+    }
+
+    /**
+     * Returns an instance that is possibly shared (singleton). Note that this 
comes with its own "shared lock",
+     * since everyone who uses this object will have to lock with that common 
object.
+     */
+    ClassIntrospector build() {
+        if ((methodAppearanceFineTuner == null || methodAppearanceFineTuner 
instanceof SingletonCustomizer)
+                && (methodSorter == null || methodSorter instanceof 
SingletonCustomizer)) {
+            // Instance can be cached.
+            ClassIntrospector instance;
+            synchronized (INSTANCE_CACHE) {
+                Reference instanceRef = (Reference) INSTANCE_CACHE.get(this);
+                instance = instanceRef != null ? (ClassIntrospector) 
instanceRef.get() : null;
+                if (instance == null) {
+                    ClassIntrospectorBuilder thisClone = 
(ClassIntrospectorBuilder) clone();  // prevent any aliasing issues
+                    instance = new ClassIntrospector(thisClone, new Object(), 
true, true);
+                    INSTANCE_CACHE.put(thisClone, new WeakReference(instance, 
INSTANCE_CACHE_REF_QUEUE));
+                }
+            }
+            
+            removeClearedReferencesFromInstanceCache();
+            
+            return instance;
+        } else {
+            // If methodAppearanceFineTuner or methodSorter is specified and 
isn't marked as a singleton, the
+            // ClassIntrospector can't be shared/cached as those objects could 
contain a back-reference to the
+            // DefaultObjectWrapper.
+            return new ClassIntrospector(this, new Object(), true, false);
+        }
+    }
+    
+}
\ 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/CollectionAdapter.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java 
b/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
new file mode 100644
index 0000000..70caa66
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * 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.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+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;
+
+/**
+ * Adapts a {@link TemplateCollectionModel} to  {@link Collection}.
+ */
+class CollectionAdapter extends AbstractCollection implements 
TemplateModelAdapter {
+    private final DefaultObjectWrapper wrapper;
+    private final TemplateCollectionModel model;
+    
+    CollectionAdapter(TemplateCollectionModel model, DefaultObjectWrapper 
wrapper) {
+        this.model = model;
+        this.wrapper = wrapper;
+    }
+    
+    @Override
+    public TemplateModel getTemplateModel() {
+        return model;
+    }
+    
+    @Override
+    public int size() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Iterator iterator() {
+        try {
+            return new Iterator() {
+                final TemplateModelIterator i = model.iterator();
+    
+                @Override
+                public boolean hasNext() {
+                    try {
+                        return i.hasNext();
+                    } catch (TemplateModelException e) {
+                        throw new UndeclaredThrowableException(e);
+                    }
+                }
+                
+                @Override
+                public Object next() {
+                    try {
+                        return wrapper.unwrap(i.next());
+                    } catch (TemplateModelException e) {
+                        throw new UndeclaredThrowableException(e);
+                    }
+                }
+                
+                @Override
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        } catch (TemplateModelException e) {
+            throw new UndeclaredThrowableException(e);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java 
b/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java
new file mode 100644
index 0000000..36c6947
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java
@@ -0,0 +1,109 @@
+/*
+ * 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.Collection;
+import java.util.List;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+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;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * <p>A special case of {@link BeanModel} that can wrap Java collections
+ * and that implements the {@link TemplateCollectionModel} in order to be 
usable 
+ * in a <tt>&lt;#list&gt;</tt> block.</p>
+ */
+public class CollectionModel
+extends
+    StringModel
+implements
+    TemplateCollectionModel,
+    TemplateSequenceModel {
+    static final ModelFactory FACTORY =
+        new ModelFactory()
+        {
+            @Override
+            public TemplateModel create(Object object, ObjectWrapper wrapper) {
+                return new CollectionModel((Collection) object, 
(DefaultObjectWrapper) wrapper);
+            }
+        };
+
+
+    /**
+     * Creates a new model that wraps the specified collection object.
+     * @param collection the collection 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 CollectionModel(Collection collection, DefaultObjectWrapper 
wrapper) {
+        super(collection, wrapper);
+    }
+
+    /**
+     * Retrieves the i-th object from the collection, wrapped as a 
TemplateModel.
+     * @throws TemplateModelException if the index is out of bounds, or the
+     * underlying collection is not a List.
+     */
+    @Override
+    public TemplateModel get(int index)
+    throws TemplateModelException {
+        // Don't forget to keep getSupportsIndexedAccess in sync with this!
+        if (object instanceof List) {
+            try {
+                return wrap(((List) object).get(index));
+            } catch (IndexOutOfBoundsException e) {
+                return null;
+//                throw new TemplateModelException("Index out of bounds: " + 
index);
+            }
+        } else {
+            throw new TemplateModelException("Underlying collection is not a 
list, it's " + object.getClass().getName());
+        }
+    }
+    
+    /**
+     * Tells if {@link #get(int)} will always fail for this object.
+     * As this object implements {@link TemplateSequenceModel},
+     * {@link #get(int)} should always work, but due to a design flaw, for
+     * non-{@link List} wrapped objects {@link #get(int)} will always fail.
+     * This method exists to ease working this problem around.
+     * 
+     * @since 2.3.17 
+     */
+    public boolean getSupportsIndexedAccess() {
+        return object instanceof List;
+    }
+    
+    @Override
+    public TemplateModelIterator iterator() {
+        return new IteratorModel(((Collection) object).iterator(), wrapper);
+    }
+
+    @Override
+    public int size() {
+        return ((Collection) object).size();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java 
b/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java
new file mode 100644
index 0000000..e16e375
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java
@@ -0,0 +1,76 @@
+/*
+ * 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.Date;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Wraps arbitrary subclass of {@link java.util.Date} into a reflective model.
+ * Beside acting as a {@link TemplateDateModel}, you can call all Java methods
+ * on these objects as well.
+ */
+public class DateModel extends BeanModel implements
+    TemplateDateModel {
+    static final ModelFactory FACTORY =
+        new ModelFactory()
+        {
+            @Override
+            public TemplateModel create(Object object, ObjectWrapper wrapper) {
+                return new DateModel((Date) object, (DefaultObjectWrapper) 
wrapper);
+            }
+        };
+
+    private final int type;
+    
+    /**
+     * Creates a new model that wraps the specified date object.
+     * @param date the date 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 DateModel(Date date, DefaultObjectWrapper wrapper) {
+        super(date, wrapper);
+        if (date instanceof java.sql.Date) {
+            type = DATE;
+        } else if (date instanceof java.sql.Time) {
+            type = TIME;
+        } else if (date instanceof java.sql.Timestamp) {
+            type = DATETIME;
+        } else {
+            type = wrapper.getDefaultDateType();
+        }
+    }
+
+    @Override
+    public Date getAsDate() {
+        return (Date) object;
+    }
+
+    @Override
+    public int getDateType() {
+        return type;
+    }
+}


Reply via email to