http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
 
b/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
new file mode 100644
index 0000000..ec20b9f
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
@@ -0,0 +1,121 @@
+/*
+ * 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.io.Serializable;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+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.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Adapts an {@link Enumeration} to the corresponding {@link TemplateModel} 
interface(s), most importantly to
+ * {@link TemplateCollectionModel}. Putting aside that it wraps an {@link 
Enumeration} instead of an {@link Iterator},
+ * this is identical to {@link DefaultIteratorAdapter}, so see further details 
there.
+ */
+// TODO JUnit
+public class DefaultEnumerationAdapter extends WrappingTemplateModel 
implements TemplateCollectionModel,
+        AdapterTemplateModel, WrapperTemplateModel, Serializable {
+
+    @SuppressFBWarnings(value="SE_BAD_FIELD", justification="We hope it's 
Seralizable")
+    private final Enumeration enumeration;
+    private boolean enumerationOwnedBySomeone;
+
+    /**
+     * Factory method for creating new adapter instances.
+     *
+     * @param iterator
+     *            The enumeration to adapt; can't be {@code null}.
+     */
+    public static DefaultEnumerationAdapter adapt(Enumeration iterator, 
ObjectWrapper wrapper) {
+        return new DefaultEnumerationAdapter(iterator, wrapper);
+    }
+
+    private DefaultEnumerationAdapter(Enumeration enumeration, ObjectWrapper 
wrapper) {
+        super(wrapper);
+        this.enumeration = enumeration;
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return enumeration;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return getWrappedObject();
+    }
+
+    @Override
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        return new SimpleTemplateModelIterator();
+    }
+
+    /**
+     * Not thread-safe.
+     */
+    private class SimpleTemplateModelIterator implements TemplateModelIterator 
{
+
+        private boolean enumerationOwnedByMe;
+
+        @Override
+        public TemplateModel next() throws TemplateModelException {
+            if (!enumerationOwnedByMe) {
+                checkNotOwner();
+                enumerationOwnedBySomeone = true;
+                enumerationOwnedByMe = true;
+            }
+
+            if (!enumeration.hasMoreElements()) {
+                throw new TemplateModelException("The collection has no more 
items.");
+            }
+
+            Object value = enumeration.nextElement();
+            return value instanceof TemplateModel ? (TemplateModel) value : 
wrap(value);
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateModelException {
+            // Calling hasNext may looks safe, but I have met sync. problems.
+            if (!enumerationOwnedByMe) {
+                checkNotOwner();
+            }
+
+            return enumeration.hasMoreElements();
+        }
+
+        private void checkNotOwner() throws TemplateModelException {
+            if (enumerationOwnedBySomeone) {
+                throw new TemplateModelException(
+                        "This collection value wraps a java.util.Enumeration, 
thus it can be listed only once.");
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java 
b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
index 0351edd..ebc5a3d 100644
--- 
a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
+++ 
b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
@@ -19,22 +19,52 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Array;
-import java.util.ArrayList;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Set;
 
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreAPI;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._DelayedFTLTypeDescription;
+import org.apache.freemarker.core._DelayedShortClassName;
+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.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.RichObjectWrapper;
 import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
 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.impl.beans.BeansWrapper;
-import org.apache.freemarker.core.model.impl.beans.BeansWrapperConfiguration;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.WriteProtectable;
+import org.apache.freemarker.core.util._ClassUtil;
 import org.apache.freemarker.dom.NodeModel;
+import org.slf4j.Logger;
 import org.w3c.dom.Node;
 
 /**
@@ -54,46 +84,583 @@ import org.w3c.dom.Node;
  * JSR 133 and related literature). When used as part of {@link 
Configuration}, of course it's enough if that was safely
  * published and then left unmodified.
  */
-public class DefaultObjectWrapper extends 
org.apache.freemarker.core.model.impl.beans.BeansWrapper {
-    
+// TODO Get rid of unused stuff (including other clases in this package) 
coming from BeansWrapper.
+public class DefaultObjectWrapper implements RichObjectWrapper, 
WriteProtectable {
+
+    private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+
+    /**
+     * At this level of exposure, all methods and properties of the
+     * wrapped objects are exposed to the template.
+     */
+    public static final int EXPOSE_ALL = 0;
+
+    /**
+     * At this level of exposure, all methods and properties of the wrapped
+     * objects are exposed to the template except methods that are deemed
+     * not safe. The not safe methods are java.lang.Object methods wait() and
+     * notify(), java.lang.Class methods getClassLoader() and newInstance(),
+     * java.lang.reflect.Method and java.lang.reflect.Constructor invoke() and
+     * newInstance() methods, all java.lang.reflect.Field set methods, all
+     * java.lang.Thread and java.lang.ThreadGroup methods that can change its
+     * state, as well as the usual suspects in java.lang.System and
+     * java.lang.Runtime.
+     */
+    public static final int EXPOSE_SAFE = 1;
+
+    /**
+     * At this level of exposure, only property getters are exposed.
+     * Additionally, property getters that map to unsafe methods are not
+     * exposed (i.e. Class.classLoader and Thread.contextClassLoader).
+     */
+    public static final int EXPOSE_PROPERTIES_ONLY = 2;
+
+    /**
+     * At this level of exposure, no bean properties and methods are exposed.
+     * Only map items, resource bundle items, and objects retrieved through
+     * the generic get method (on objects of classes that have a generic get
+     * method) can be retrieved through the hash interface. You might want to
+     * call {@link #setMethodsShadowItems(boolean)} with <tt>false</tt> value 
to
+     * speed up map item retrieval.
+     */
+    public static final int EXPOSE_NOTHING = 3;
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+    // Introspection cache:
+
+    private final Object sharedIntrospectionLock;
+
+    /**
+     * {@link Class} to class info cache.
+     * This object is possibly shared with other {@link 
DefaultObjectWrapper}-s!
+     *
+     * <p>To write this, always use {@link 
#replaceClassIntrospector(ClassIntrospectorBuilder)}.
+     *
+     * <p>When reading this, it's good idea to synchronize on 
sharedInrospectionLock when it doesn't hurt overall
+     * performance. In theory that's not needed, but apps might fail to keep 
the rules.
+     */
+    private ClassIntrospector classIntrospector;
+
+    /**
+     * {@link String} class name to {@link StaticModel} cache.
+     * This object only belongs to a single {@link DefaultObjectWrapper}.
+     * This has to be final as {@link #getStaticModels()} might returns it any 
time and then it has to remain a good
+     * reference.
+     */
+    private final StaticModels staticModels;
+
     /**
-     * Use {@link DefaultObjectWrapperBuilder} instead if possible. Instances 
created with this constructor won't share
-     * the class introspection caches with other instances. See {@link 
BeansWrapper#BeansWrapper(Version)} (the
-     * superclass constructor) for more details.
-     * 
+     * {@link String} class name to {@link EnumerationModel} cache.
+     * This object only belongs to a single {@link DefaultObjectWrapper}.
+     * This has to be final as {@link #getStaticModels()} might returns it any 
time and then it has to remain a good
+     * reference.
+     */
+    private final ClassBasedModelFactory enumModels;
+
+    /**
+     * Object to wrapped object cache; not used by default.
+     * This object only belongs to a single {@link DefaultObjectWrapper}.
+     */
+    private final ModelCache modelCache;
+
+    private final BooleanModel falseModel;
+    private final BooleanModel trueModel;
+
+    // 
-----------------------------------------------------------------------------------------------------------------
+
+    // Why volatile: In principle it need not be volatile, but we want to 
catch modification attempts even if the
+    // object was published improperly to other threads. After all, the main 
goal of WriteProtectable is protecting
+    // things from buggy user code.
+    private volatile boolean writeProtected;
+
+    private int defaultDateType; // initialized by PropertyAssignments.apply
+    private ObjectWrapper outerIdentity = this;
+    private boolean methodsShadowItems = true;
+    private boolean strict;  // initialized by PropertyAssignments.apply
+
+    private final Version incompatibleImprovements;
+
+    /**
+     * Use {@link DefaultObjectWrapperBuilder} instead of the public 
constructors if possible.
+     * The main disadvantage of using the public constructors is that the 
instances won't share caches. So unless having
+     * a private cache is your goal, don't use them. See
+     *
      * @param incompatibleImprovements
-     *            It's the same as in {@link 
BeansWrapper#BeansWrapper(Version)}.
-     * 
+     *   Sets which of the non-backward-compatible improvements should be 
enabled. Not {@code null}. This version number
+     *   is the same as the FreeMarker version number with which the 
improvements were implemented.
+     *
+     *   <p>For new projects, it's recommended to set this to the FreeMarker 
version that's used during the development.
+     *   For released products that are still actively developed it's a low 
risk change to increase the 3rd
+     *   version number further as FreeMarker is updated, but of course you 
should always check the list of effects
+     *   below. Increasing the 2nd or 1st version number possibly mean 
substantial changes with higher risk of breaking
+     *   the application, but again, see the list of effects below.
+     *
+     *   <p>The reason it's separate from {@link 
Configuration#setIncompatibleImprovements(Version)} is that
+     *   {@link ObjectWrapper} objects are often shared among multiple {@link 
Configuration}-s, so the two version
+     *   numbers are technically independent. But it's recommended to keep 
those two version numbers the same.
+     *
+     *   <p>The changes enabled by {@code incompatibleImprovements} are:
+     *   <ul>
+     *     <li>
+     *       <p>2.3.0: No changes; this is the starting point, the version 
used in older projects.
+     *     </li>
+     *     <li>
+     *       <p>2.3.21 (or higher):
+     *       Several glitches were oms in <em>overloaded</em> method 
selection. This usually just gets
+     *       rid of errors (like ambiguity exceptions and numerical precision 
loses due to bad overloaded method
+     *       choices), still, as in some cases the method chosen can be a 
different one now (that was the point of
+     *       the reworking after all), it can mean a change in the behavior of 
the application. The most important
+     *       change is that the treatment of {@code null} arguments were oms, 
as earlier they were only seen
+     *       applicable to parameters of type {@code Object}. Now {@code 
null}-s are seen to be applicable to any
+     *       non-primitive parameters, and among those the one with the most 
specific type will be preferred (just
+     *       like in Java), which is hence never the one with the {@code 
Object} parameter type. For more details
+     *       about overloaded method selection changes see the version history 
in the FreeMarker Manual.
+     *     </li>
+     *     <li>
+     *       <p>2.3.24 (or higher):
+     *       {@link Iterator}-s were always said to be non-empty when using 
{@code ?has_content} and such (i.e.,
+     *       operators that check emptiness without reading any elements). Now 
an {@link Iterator} counts as
+     *       empty exactly if it has no elements left. (Note that this bug has 
never affected basic functionality, like
+     *       {@code <#list ...>}.)
+     *     </li>
+     *   </ul>
+     *
+     *   <p>Note that the version will be normalized to the lowest version 
where the same incompatible
+     *   {@link DefaultObjectWrapper} improvements were already present, so 
{@link #getIncompatibleImprovements()} might returns
+     *   a lower version than what you have specified.
+     *
      * @since 2.3.21
      */
     public DefaultObjectWrapper(Version incompatibleImprovements) {
-        this(new DefaultObjectWrapperConfiguration(incompatibleImprovements) { 
}, false);
+        this(new DefaultObjectWrapperConfiguration(incompatibleImprovements) 
{}, false);
+        // Attention! Don't don anything here, as the instance is possibly 
already visible to other threads through the
+        // model factory callbacks.
     }
 
     /**
-     * Use {@link #DefaultObjectWrapper(DefaultObjectWrapperConfiguration, 
boolean)} instead if possible;
-     * it does the same, except that it tolerates a non-{@link 
DefaultObjectWrapperConfiguration} configuration too.
-     * 
+     * Same as {@link #DefaultObjectWrapper(DefaultObjectWrapperConfiguration, 
boolean, boolean)} with {@code true}
+     * {@code finalizeConstruction} argument.
+     *
      * @since 2.3.21
      */
-    protected DefaultObjectWrapper(BeansWrapperConfiguration bwCfg, boolean 
writeProtected) {
-        super(bwCfg, writeProtected, false);
-        DefaultObjectWrapperConfiguration dowDowCfg = bwCfg instanceof 
DefaultObjectWrapperConfiguration
-                ? (DefaultObjectWrapperConfiguration) bwCfg
-                : new 
DefaultObjectWrapperConfiguration(bwCfg.getIncompatibleImprovements()) { }; 
+    protected DefaultObjectWrapper(DefaultObjectWrapperConfiguration bwConf, 
boolean writeProtected) {
+        this(bwConf, writeProtected, true);
+    }
+
+    /**
+     * Initializes the instance based on the the {@link 
DefaultObjectWrapperConfiguration} specified.
+     *
+     * @param writeProtected Makes the instance's configuration settings 
read-only via
+     *     {@link WriteProtectable#writeProtect()}; this way it can use the 
shared class introspection cache.
+     *
+     * @param finalizeConstruction Decides if the construction is finalized 
now, or the caller will do some more
+     *     adjustments on the instance and then call {@link 
#finalizeConstruction(boolean)} itself.
+     *
+     * @since 2.3.22
+     */
+    protected DefaultObjectWrapper(DefaultObjectWrapperConfiguration bwConf, 
boolean writeProtected, boolean finalizeConstruction) {
+        incompatibleImprovements = bwConf.getIncompatibleImprovements();  // 
normalized
+
+        defaultDateType = bwConf.getDefaultDateType();
+        outerIdentity = bwConf.getOuterIdentity() != null ? 
bwConf.getOuterIdentity() : this;
+        strict = bwConf.isStrict();
+
+        if (!writeProtected) {
+            // As this is not a read-only DefaultObjectWrapper, the 
classIntrospector will be possibly replaced for a few times,
+            // but we need to use the same sharedInrospectionLock forever, 
because that's what the model factories
+            // synchronize on, even during the classIntrospector is being 
replaced.
+            sharedIntrospectionLock = new Object();
+            classIntrospector = new 
ClassIntrospector(bwConf.classIntrospectorFactory, sharedIntrospectionLock);
+        } else {
+            // As this is a read-only DefaultObjectWrapper, the 
classIntrospector is never replaced, and since it's shared by
+            // other DefaultObjectWrapper instances, we use the lock belonging 
to the shared ClassIntrospector.
+            classIntrospector = bwConf.classIntrospectorFactory.build();
+            sharedIntrospectionLock = classIntrospector.getSharedLock();
+        }
+
+        falseModel = new BooleanModel(Boolean.FALSE, this);
+        trueModel = new BooleanModel(Boolean.TRUE, this);
+
+        staticModels = new StaticModels(this);
+        enumModels = new _EnumModels(this);
+        modelCache = new BeansModelCache(this);
+        setUseModelCache(bwConf.getUseModelCache());
+
         finalizeConstruction(writeProtected);
     }
 
     /**
-     * Calls {@link BeansWrapper#BeansWrapper(BeansWrapperConfiguration, 
boolean)} and sets up
-     * {@link DefaultObjectWrapper}-specific fields.
-     * 
+     * Meant to be called after {@link 
DefaultObjectWrapper#DefaultObjectWrapper(DefaultObjectWrapperConfiguration, 
boolean, boolean)} when
+     * its last argument was {@code false}; makes the instance read-only if 
necessary, then registers the model
+     * factories in the class introspector. No further changes should be done 
after calling this, if
+     * {@code writeProtected} was {@code true}.
+     *
      * @since 2.3.22
      */
-    protected DefaultObjectWrapper(DefaultObjectWrapperConfiguration dowCfg, 
boolean writeProtected) {
-        this((BeansWrapperConfiguration) dowCfg, writeProtected);
+    protected void finalizeConstruction(boolean writeProtected) {
+        if (writeProtected) {
+            writeProtect();
+        }
+
+        // Attention! At this point, the DefaultObjectWrapper must be fully 
initialized, as when the model factories are
+        // registered below, the DefaultObjectWrapper can immediately get 
concurrent callbacks. That those other threads will
+        // see consistent image of the DefaultObjectWrapper is ensured that 
callbacks are always sync-ed on
+        // classIntrospector.sharedLock, and so is 
classIntrospector.registerModelFactory(...).
+
+        registerModelFactories();
+    }
+
+    /**
+     * Makes the configuration properties (settings) of this {@link 
DefaultObjectWrapper} object read-only. As changing them
+     * after the object has become visible to multiple threads leads to 
undefined behavior, it's recommended to call
+     * this when you have finished configuring the object.
+     *
+     * <p>Consider using {@link DefaultObjectWrapperBuilder} instead, which 
gives an instance that's already
+     * write protected and also uses some shared caches/pools.
+     *
+     * @since 2.3.21
+     */
+    @Override
+    public void writeProtect() {
+        writeProtected = true;
+    }
+
+    /**
+     * @since 2.3.21
+     */
+    @Override
+    public boolean isWriteProtected() {
+        return writeProtected;
+    }
+
+    Object getSharedIntrospectionLock() {
+        return sharedIntrospectionLock;
+    }
+
+    /**
+     * If this object is already read-only according to {@link 
WriteProtectable}, throws {@link IllegalStateException},
+     * otherwise does nothing.
+     *
+     * @since 2.3.21
+     */
+    protected void checkModifiable() {
+        if (writeProtected) throw new IllegalStateException(
+                "Can't modify the " + getClass().getName() + " object, as it 
was write protected.");
+    }
+
+    /**
+     * @see #setStrict(boolean)
+     */
+    public boolean isStrict() {
+        return strict;
     }
-    
+
+    /**
+     * Specifies if an attempt to read a bean property that doesn't exist in 
the
+     * wrapped object should throw an {@link InvalidPropertyException}.
+     *
+     * <p>If this property is <tt>false</tt> (the default) then an attempt to 
read
+     * a missing bean property is the same as reading an existing bean 
property whose
+     * value is <tt>null</tt>. The template can't tell the difference, and 
thus always
+     * can use <tt>?default('something')</tt> and <tt>?exists</tt> and similar 
built-ins
+     * to handle the situation.
+     *
+     * <p>If this property is <tt>true</tt> then an attempt to read a bean 
propertly in
+     * the template (like <tt>myBean.aProperty</tt>) that doesn't exist in the 
bean
+     * object (as opposed to just holding <tt>null</tt> value) will cause
+     * {@link InvalidPropertyException}, which can't be suppressed in the 
template
+     * (not even with <tt>myBean.noSuchProperty?default('something')</tt>). 
This way
+     * <tt>?default('something')</tt> and <tt>?exists</tt> and similar 
built-ins can be used to
+     * handle existing properties whose value is <tt>null</tt>, without the 
risk of
+     * hiding typos in the property names. Typos will always cause error. But 
mind you, it
+     * goes against the basic approach of FreeMarker, so use this feature only 
if you really
+     * know what you are doing.
+     */
+    public void setStrict(boolean strict) {
+        checkModifiable();
+        this.strict = strict;
+    }
+
+    /**
+     * When wrapping an object, the DefaultObjectWrapper commonly needs to wrap
+     * "sub-objects", for example each element in a wrapped collection.
+     * Normally it wraps these objects using itself. However, this makes
+     * it difficult to delegate to a DefaultObjectWrapper as part of a custom
+     * aggregate ObjectWrapper. This method lets you set the ObjectWrapper
+     * which will be used to wrap the sub-objects.
+     * @param outerIdentity the aggregate ObjectWrapper
+     */
+    public void setOuterIdentity(ObjectWrapper outerIdentity) {
+        checkModifiable();
+        this.outerIdentity = outerIdentity;
+    }
+
+    /**
+     * By default returns <tt>this</tt>.
+     * @see #setOuterIdentity(ObjectWrapper)
+     */
+    public ObjectWrapper getOuterIdentity() {
+        return outerIdentity;
+    }
+
+    // I have commented this out, as it won't be in 2.3.20 yet.
+    /*
+    /**
+     * Tells which non-backward-compatible overloaded method selection fixes 
to apply;
+     * see {@link #setOverloadedMethodSelection(Version)}.
+     * /
+    public Version getOverloadedMethodSelection() {
+        return overloadedMethodSelection;
+    }
+
+    /**
+     * Sets which non-backward-compatible overloaded method selection fixes to 
apply.
+     * This has similar logic as {@link 
Configuration#setIncompatibleImprovements(Version)},
+     * but only applies to this aspect.
+     *
+     * Currently significant values:
+     * <ul>
+     *   <li>2.3.21: Completetlly rewritten overloaded method selection, fixes 
several issues with the old one.</li>
+     * </ul>
+     * /
+    public void setOverloadedMethodSelection(Version version) {
+        overloadedMethodSelection = version;
+    }
+    */
+
+    /**
+     * Sets the method exposure level. By default, set to 
<code>EXPOSE_SAFE</code>.
+     * @param exposureLevel can be any of the <code>EXPOSE_xxx</code>
+     * constants.
+     */
+    public void setExposureLevel(int exposureLevel) {
+        checkModifiable();
+
+        if (classIntrospector.getExposureLevel() != exposureLevel) {
+            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
+            pa.setExposureLevel(exposureLevel);
+            replaceClassIntrospector(pa);
+        }
+    }
+
+    /**
+     * @since 2.3.21
+     */
+    public int getExposureLevel() {
+        return classIntrospector.getExposureLevel();
+    }
+
+    /**
+     * Controls whether public instance fields of classes are exposed to
+     * templates.
+     * @param exposeFields if set to true, public instance fields of classes
+     * that do not have a property getter defined can be accessed directly by
+     * their name. If there is a property getter for a property of the same
+     * name as the field (i.e. getter "getFoo()" and field "foo"), then
+     * referring to "foo" in template invokes the getter. If set to false, no
+     * access to public instance fields of classes is given. Default is false.
+     */
+    public void setExposeFields(boolean exposeFields) {
+        checkModifiable();
+
+        if (classIntrospector.getExposeFields() != exposeFields) {
+            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
+            pa.setExposeFields(exposeFields);
+            replaceClassIntrospector(pa);
+        }
+    }
+
+    /**
+     * Returns whether exposure of public instance fields of classes is
+     * enabled. See {@link #setExposeFields(boolean)} for details.
+     * @return true if public instance fields are exposed, false otherwise.
+     */
+    public boolean isExposeFields() {
+        return classIntrospector.getExposeFields();
+    }
+
+    public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+        return classIntrospector.getMethodAppearanceFineTuner();
+    }
+
+    /**
+     * Used to tweak certain aspects of how methods appear in the data-model;
+     * see {@link MethodAppearanceFineTuner} for more.
+     */
+    public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner 
methodAppearanceFineTuner) {
+        checkModifiable();
+
+        if (classIntrospector.getMethodAppearanceFineTuner() != 
methodAppearanceFineTuner) {
+            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
+            pa.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+            replaceClassIntrospector(pa);
+        }
+    }
+
+    MethodSorter getMethodSorter() {
+        return classIntrospector.getMethodSorter();
+    }
+
+    void setMethodSorter(MethodSorter methodSorter) {
+        checkModifiable();
+
+        if (classIntrospector.getMethodSorter() != methodSorter) {
+            ClassIntrospectorBuilder pa = 
classIntrospector.getPropertyAssignments();
+            pa.setMethodSorter(methodSorter);
+            replaceClassIntrospector(pa);
+        }
+    }
+
+    /**
+     * Tells if this instance acts like if its class introspection cache is 
sharable with other {@link DefaultObjectWrapper}-s.
+     * A restricted cache denies certain too "antisocial" operations, like 
{@link #clearClassIntrospecitonCache()}.
+     * The value depends on how the instance
+     * was created; with a public constructor (then this is {@code false}), or 
with {@link DefaultObjectWrapperBuilder}
+     * (then it's {@code true}). Note that in the last case it's possible that 
the introspection cache
+     * will not be actually shared because there's no one to share with, but 
this will {@code true} even then.
+     *
+     * @since 2.3.21
+     */
+    public boolean isClassIntrospectionCacheRestricted() {
+        return classIntrospector.getHasSharedInstanceRestrictons();
+    }
+
+    /**
+     * Replaces the value of {@link #classIntrospector}, but first it 
unregisters
+     * the model factories in the old {@link #classIntrospector}.
+     */
+    private void replaceClassIntrospector(ClassIntrospectorBuilder pa) {
+        checkModifiable();
+
+        final ClassIntrospector newCI = new ClassIntrospector(pa, 
sharedIntrospectionLock);
+        final ClassIntrospector oldCI;
+
+        // In principle this need not be synchronized, but as apps might 
publish the configuration improperly, or
+        // even modify the wrapper after publishing. This doesn't give 100% 
protection from those violations,
+        // as classIntrospector reading aren't everywhere synchronized for 
performance reasons. It still decreases the
+        // chance of accidents, because some ops on classIntrospector are 
synchronized, and because it will at least
+        // push the new value into the common shared memory.
+        synchronized (sharedIntrospectionLock) {
+            oldCI = classIntrospector;
+            if (oldCI != null) {
+                // Note that after unregistering the model factory might still 
gets some callback from the old
+                // classIntrospector
+                if (staticModels != null) {
+                    oldCI.unregisterModelFactory(staticModels);
+                    staticModels.clearCache();
+                }
+                if (enumModels != null) {
+                    oldCI.unregisterModelFactory(enumModels);
+                    enumModels.clearCache();
+                }
+                if (modelCache != null) {
+                    oldCI.unregisterModelFactory(modelCache);
+                    modelCache.clearCache();
+                }
+                if (trueModel != null) {
+                    trueModel.clearMemberCache();
+                }
+                if (falseModel != null) {
+                    falseModel.clearMemberCache();
+                }
+            }
+
+            classIntrospector = newCI;
+
+            registerModelFactories();
+        }
+    }
+
+    private void registerModelFactories() {
+        if (staticModels != null) {
+            classIntrospector.registerModelFactory(staticModels);
+        }
+        if (enumModels != null) {
+            classIntrospector.registerModelFactory(enumModels);
+        }
+        if (modelCache != null) {
+            classIntrospector.registerModelFactory(modelCache);
+        }
+    }
+
+    /**
+     * Sets whether methods shadow items in beans. When true (this is the
+     * default value), <code>${object.name}</code> will first try to locate
+     * a bean method or property with the specified name on the object, and
+     * only if it doesn't find it will it try to call
+     * <code>object.get(name)</code>, the so-called "generic get method" that
+     * is usually used to access items of a container (i.e. elements of a map).
+     * When set to false, the lookup order is reversed and generic get method
+     * is called first, and only if it returns null is method lookup attempted.
+     */
+    public void setMethodsShadowItems(boolean methodsShadowItems) {
+        // This sync is here as this method was originally synchronized, but 
was never truly thread-safe, so I don't
+        // want to advertise it in the javadoc, nor I wanted to break any apps 
that work because of this accidentally.
+        synchronized (this) {
+            checkModifiable();
+            this.methodsShadowItems = methodsShadowItems;
+        }
+    }
+
+    boolean isMethodsShadowItems() {
+        return methodsShadowItems;
+    }
+
+    /**
+     * Sets the default date type to use for date models that result from
+     * a plain <tt>java.util.Date</tt> instead of <tt>java.sql.Date</tt> or
+     * <tt>java.sql.Time</tt> or <tt>java.sql.Timestamp</tt>. Default value is
+     * {@link TemplateDateModel#UNKNOWN}.
+     * @param defaultDateType the new default date type.
+     */
+    public void setDefaultDateType(int defaultDateType) {
+        // This sync is here as this method was originally synchronized, but 
was never truly thread-safe, so I don't
+        // want to advertise it in the javadoc, nor I wanted to break any apps 
that work because of this accidentally.
+        synchronized (this) {
+            checkModifiable();
+
+            this.defaultDateType = defaultDateType;
+        }
+    }
+
+    /**
+     * Returns the default date type. See {@link #setDefaultDateType(int)} for
+     * details.
+     * @return the default date type
+     */
+    public int getDefaultDateType() {
+        return defaultDateType;
+    }
+
+    /**
+     * Sets whether this wrapper caches the {@link TemplateModel}-s created 
for the Java objects that has wrapped with
+     * this object wrapper. Default is {@code false}.
+     * When set to {@code true}, calling {@link #wrap(Object)} multiple times 
for
+     * the same object will likely return the same model (although there is
+     * no guarantee as the cache items can be cleared any time).
+     */
+    public void setUseModelCache(boolean useCache) {
+        checkModifiable();
+        modelCache.setUseCache(useCache);
+    }
+
+    /**
+     * @since 2.3.21
+     */
+    public boolean getUseModelCache() {
+        return modelCache.getUseCache();
+    }
+
+    /**
+     * Returns the version given with {@link #DefaultObjectWrapper(Version)}, 
normalized to the lowest version where a change
+     * has occurred. Thus, this is not necessarily the same version than that 
was given to the constructor.
+     *
+     * @since 2.3.21
+     */
+    public Version getIncompatibleImprovements() {
+        return incompatibleImprovements;
+    }
+
     /**
      * Wraps the parameter object to {@link TemplateModel} interface(s). 
Simple types like numbers, strings, booleans
      * and dates will be wrapped into the corresponding {@code SimpleXxx} 
classes (like {@link SimpleNumber}).
@@ -104,11 +671,15 @@ public class DefaultObjectWrapper extends 
org.apache.freemarker.core.model.impl.
     @Override
     public TemplateModel wrap(Object obj) throws TemplateModelException {
         if (obj == null) {
-            return super.wrap(null);
+            return null;
         }
         if (obj instanceof TemplateModel) {
             return (TemplateModel) obj;
         }
+        if (obj instanceof TemplateModelAdapter) {
+            return ((TemplateModelAdapter) obj).getTemplateModel();
+        }
+
         if (obj instanceof String) {
             return new SimpleScalar((String) obj);
         }
@@ -148,68 +719,847 @@ public class DefaultObjectWrapper extends 
org.apache.freemarker.core.model.impl.
         if (obj instanceof Iterable) {
             return DefaultIterableAdapter.adapt((Iterable<?>) obj, this);
         }
+        if (obj instanceof Enumeration) {
+            return DefaultEnumerationAdapter.adapt((Enumeration<?>) obj, this);
+        }
+
+        // [FM3] Via plugin mechanism, not by default anymore
+        if (obj instanceof Node) {
+            return handW3CNode((Node) obj);
+        }
+
         return handleUnknownType(obj);
     }
-    
+
+    protected TemplateModel handW3CNode(Node node) throws 
TemplateModelException {
+        return NodeModel.wrap(node);
+    }
+
     /**
-     * Called for an object that isn't considered to be of a "basic" Java 
type, like for an application specific type,
-     * or for a W3C DOM_WRAPPER node. In its default implementation, W3C 
{@link Node}-s will be wrapped as {@link NodeModel}-s
-     * (allows DOM_WRAPPER tree traversal), others will be wrapped using 
{@link BeansWrapper#wrap(Object)}.
-     * 
+     * Called for an object that isn't considered to be of a "basic" Java 
type, like for an application specific type.
+     * In its default implementation, the object will be wrapped as a generic 
JavaBean.
+     *
      * <p>
      * When you override this method, you should first decide if you want to 
wrap the object in a custom way (and if so
      * then do it and return with the result), and if not, then you should 
call the super method (assuming the default
      * behavior is fine with you).
      */
     protected TemplateModel handleUnknownType(Object obj) throws 
TemplateModelException {
-        if (obj instanceof Node) {
-            return wrapDomNode(obj);
+        return modelCache.getInstance(obj);
+    }
+
+    /**
+     * Wraps a Java method so that it can be called from templates, without 
wrapping its parent ("this") object. The
+     * result is almost the same as that you would get by wrapping the parent 
object then getting the method from the
+     * resulting {@link TemplateHashModel} by name. Except, if the wrapped 
method is overloaded, with this method you
+     * explicitly select a an overload, while otherwise you would get a {@link 
TemplateMethodModelEx} that selects an
+     * overload each time it's called based on the argument values.
+     *
+     * @param object The object whose method will be called, or {@code null} 
if {@code method} is a static method.
+     *          This object will be used "as is", like without unwrapping it 
if it's a {@link TemplateModelAdapter}.
+     * @param method The method to call, which must be an (inherited) member 
of the class of {@code object}, as
+     *          described by {@link Method#invoke(Object, Object...)}
+     *
+     * @since 2.3.22
+     */
+    public TemplateMethodModelEx wrap(Object object, Method method) {
+        return new SimpleMethodModel(object, method, 
method.getParameterTypes(), this);
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    @Override
+    public TemplateHashModel wrapAsAPI(Object obj) throws 
TemplateModelException {
+        return new APIModel(obj, this);
+    }
+
+    private final ModelFactory BOOLEAN_FACTORY = new ModelFactory() {
+        @Override
+        public TemplateModel create(Object object, ObjectWrapper wrapper) {
+            return ((Boolean) object).booleanValue() ? trueModel : falseModel;
+        }
+    };
+
+    private static final ModelFactory ITERATOR_FACTORY = new ModelFactory() {
+        @Override
+        public TemplateModel create(Object object, ObjectWrapper wrapper) {
+            return new IteratorModel((Iterator<?>) object, 
(DefaultObjectWrapper) wrapper);
+        }
+    };
+
+    private static final ModelFactory ENUMERATION_FACTORY = new ModelFactory() 
{
+        @Override
+        public TemplateModel create(Object object, ObjectWrapper wrapper) {
+            return new EnumerationModel((Enumeration<?>) object, 
(DefaultObjectWrapper) wrapper);
+        }
+    };
+
+    protected ModelFactory getModelFactory(Class<?> clazz) {
+        if (Map.class.isAssignableFrom(clazz)) {
+            return SimpleMapModel.FACTORY;
+        }
+        if (Collection.class.isAssignableFrom(clazz)) {
+            return CollectionModel.FACTORY;
+        }
+        if (Number.class.isAssignableFrom(clazz)) {
+            return NumberModel.FACTORY;
+        }
+        if (Date.class.isAssignableFrom(clazz)) {
+            return DateModel.FACTORY;
+        }
+        if (Boolean.class == clazz) { // Boolean is final
+            return BOOLEAN_FACTORY;
+        }
+        if (ResourceBundle.class.isAssignableFrom(clazz)) {
+            return ResourceBundleModel.FACTORY;
+        }
+        if (Iterator.class.isAssignableFrom(clazz)) {
+            return ITERATOR_FACTORY;
+        }
+        if (Enumeration.class.isAssignableFrom(clazz)) {
+            return ENUMERATION_FACTORY;
+        }
+        if (clazz.isArray()) {
+            return ArrayModel.FACTORY;
+        }
+        return StringModel.FACTORY;
+    }
+
+    /**
+     * Attempts to unwrap a model into underlying object. Generally, this
+     * method is the inverse of the {@link #wrap(Object)} method. In addition
+     * it will unwrap arbitrary {@link TemplateNumberModel} instances into
+     * a number, arbitrary {@link TemplateDateModel} instances into a date,
+     * {@link TemplateScalarModel} instances into a String, arbitrary
+     * {@link TemplateBooleanModel} instances into a Boolean, arbitrary
+     * {@link TemplateHashModel} instances into a Map, arbitrary
+     * {@link TemplateSequenceModel} into a List, and arbitrary
+     * {@link TemplateCollectionModel} into a Set. All other objects are
+     * returned unchanged.
+     * @throws TemplateModelException if an attempted unwrapping fails.
+     */
+    @Override
+    public Object unwrap(TemplateModel model) throws TemplateModelException {
+        return unwrap(model, Object.class);
+    }
+
+    /**
+     * Attempts to unwrap a model into an object of the desired class.
+     * Generally, this method is the inverse of the {@link #wrap(Object)}
+     * method. It recognizes a wide range of target classes - all Java built-in
+     * primitives, primitive wrappers, numbers, dates, sets, lists, maps, and
+     * native arrays.
+     * @param model the model to unwrap
+     * @param targetClass the class of the unwrapped result; {@code 
Object.class} of we don't know what the expected type is.
+     * @return the unwrapped result of the desired class
+     * @throws TemplateModelException if an attempted unwrapping fails.
+     *
+     * @see #tryUnwrapTo(TemplateModel, Class)
+     */
+    public Object unwrap(TemplateModel model, Class<?> targetClass)
+            throws TemplateModelException {
+        final Object obj = tryUnwrapTo(model, targetClass);
+        if (obj == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+            throw new TemplateModelException("Can not unwrap model of type " +
+                    model.getClass().getName() + " to type " + 
targetClass.getName());
+        }
+        return obj;
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    @Override
+    public Object tryUnwrapTo(TemplateModel model, Class<?> targetClass) 
throws TemplateModelException {
+        return tryUnwrapTo(model, targetClass, 0);
+    }
+
+    /**
+     * @param typeFlags
+     *            Used when unwrapping for overloaded methods and so the 
{@code targetClass} is possibly too generic
+     *            (as it's the most specific common superclass). Must be 0 
when unwrapping parameter values for
+     *            non-overloaded methods.
+     * @return {@link ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} 
or the unwrapped object.
+     */
+    Object tryUnwrapTo(TemplateModel model, Class<?> targetClass, int 
typeFlags) throws TemplateModelException {
+        Object res = tryUnwrapTo(model, targetClass, typeFlags, null);
+        if ((typeFlags & TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT) != 0
+                && res instanceof Number) {
+            return OverloadedNumberUtil.addFallbackType((Number) res, 
typeFlags);
+        } else {
+            return res;
+        }
+    }
+
+    /**
+     * See {@link #tryUnwrapTo(TemplateModel, Class, int)}.
+     */
+    private Object tryUnwrapTo(final TemplateModel model, Class<?> 
targetClass, final int typeFlags,
+                               final Map<Object, Object> recursionStops)
+            throws TemplateModelException {
+        if (model == null) {
+            return null;
+        }
+
+        if (targetClass.isPrimitive()) {
+            targetClass = _ClassUtil.primitiveClassToBoxingClass(targetClass);
+        }
+
+        // This is for transparent interop with other wrappers (and ourselves)
+        // Passing the targetClass allows e.g. a Jython-aware method that 
declares a
+        // PyObject as its argument to receive a PyObject from a Jython-aware 
TemplateModel
+        // passed as an argument to TemplateMethodModelEx etc.
+        if (model instanceof AdapterTemplateModel) {
+            Object wrapped = ((AdapterTemplateModel) 
model).getAdaptedObject(targetClass);
+            if (targetClass == Object.class || 
targetClass.isInstance(wrapped)) {
+                return wrapped;
+            }
+
+            // Attempt numeric conversion:
+            if (targetClass != Object.class && (wrapped instanceof Number && 
_ClassUtil.isNumerical(targetClass))) {
+                Number number = forceUnwrappedNumberToType((Number) wrapped, 
targetClass);
+                if (number != null) return number;
+            }
+        }
+
+        if (model instanceof WrapperTemplateModel) {
+            Object wrapped = ((WrapperTemplateModel) model).getWrappedObject();
+            if (targetClass == Object.class || 
targetClass.isInstance(wrapped)) {
+                return wrapped;
+            }
+
+            // Attempt numeric conversion:
+            if (targetClass != Object.class && (wrapped instanceof Number && 
_ClassUtil.isNumerical(targetClass))) {
+                Number number = forceUnwrappedNumberToType((Number) wrapped, 
targetClass);
+                if (number != null) {
+                    return number;
+                }
+            }
+        }
+
+        // Translation of generic template models to POJOs. First give priority
+        // to various model interfaces based on the targetClass. This helps us
+        // select the appropriate interface in multi-interface models when we
+        // know what is expected as the return type.
+        if (targetClass != Object.class) {
+
+            // [2.4][IcI]: Should also check for CharSequence at the end
+            if (String.class == targetClass) {
+                if (model instanceof TemplateScalarModel) {
+                    return ((TemplateScalarModel) model).getAsString();
+                }
+                // String is final, so no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            // Primitive numeric types & Number.class and its subclasses
+            if (_ClassUtil.isNumerical(targetClass)) {
+                if (model instanceof TemplateNumberModel) {
+                    Number number = forceUnwrappedNumberToType(
+                            ((TemplateNumberModel) model).getAsNumber(), 
targetClass);
+                    if (number != null) {
+                        return number;
+                    }
+                }
+            }
+
+            if (boolean.class == targetClass || Boolean.class == targetClass) {
+                if (model instanceof TemplateBooleanModel) {
+                    return Boolean.valueOf(((TemplateBooleanModel) 
model).getAsBoolean());
+                }
+                // Boolean is final, no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            if (Map.class == targetClass) {
+                if (model instanceof TemplateHashModel) {
+                    return new HashAdapter((TemplateHashModel) model, this);
+                }
+            }
+
+            if (List.class == targetClass) {
+                if (model instanceof TemplateSequenceModel) {
+                    return new SequenceAdapter((TemplateSequenceModel) model, 
this);
+                }
+            }
+
+            if (Set.class == targetClass) {
+                if (model instanceof TemplateCollectionModel) {
+                    return new SetAdapter((TemplateCollectionModel) model, 
this);
+                }
+            }
+
+            if (Collection.class == targetClass || Iterable.class == 
targetClass) {
+                if (model instanceof TemplateCollectionModel) {
+                    return new CollectionAdapter((TemplateCollectionModel) 
model,
+                            this);
+                }
+                if (model instanceof TemplateSequenceModel) {
+                    return new SequenceAdapter((TemplateSequenceModel) model, 
this);
+                }
+            }
+
+            // TemplateSequenceModels can be converted to arrays
+            if (targetClass.isArray()) {
+                if (model instanceof TemplateSequenceModel) {
+                    return unwrapSequenceToArray((TemplateSequenceModel) 
model, targetClass, true, recursionStops);
+                }
+                // array classes are final, no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            // Allow one-char strings to be coerced to characters
+            if (char.class == targetClass || targetClass == Character.class) {
+                if (model instanceof TemplateScalarModel) {
+                    String s = ((TemplateScalarModel) model).getAsString();
+                    if (s.length() == 1) {
+                        return Character.valueOf(s.charAt(0));
+                    }
+                }
+                // Character is final, no other conversion will work
+                return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+            }
+
+            if (Date.class.isAssignableFrom(targetClass) && model instanceof 
TemplateDateModel) {
+                Date date = ((TemplateDateModel) model).getAsDate();
+                if (targetClass.isInstance(date)) {
+                    return date;
+                }
+            }
+        }  //  End: if (targetClass != Object.class)
+
+        // Since the targetClass was of no help initially, now we use
+        // a quite arbitrary order in which we walk through the TemplateModel 
subinterfaces, and unwrapp them to
+        // their "natural" Java correspondent. We still try exclude 
unwrappings that won't fit the target parameter
+        // type(s). This is mostly important because of multi-typed FTL values 
that could be unwrapped on multiple ways.
+        int itf = typeFlags; // Iteration's Type Flags. Should be always 0 for 
non-overloaded and when !is2321Bugfixed.
+        // If itf != 0, we possibly execute the following loop body at twice: 
once with utilizing itf, and if it has not
+        // returned, once more with itf == 0. Otherwise we execute this once 
with itf == 0.
+        do {
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_NUMBER) != 0)
+                    && model instanceof TemplateNumberModel) {
+                Number number = ((TemplateNumberModel) model).getAsNumber();
+                if (itf != 0 || targetClass.isInstance(number)) {
+                    return number;
+                }
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_DATE) != 0)
+                    && model instanceof TemplateDateModel) {
+                Date date = ((TemplateDateModel) model).getAsDate();
+                if (itf != 0 || targetClass.isInstance(date)) {
+                    return date;
+                }
+            }
+            if ((itf == 0 || (itf & (TypeFlags.ACCEPTS_STRING | 
TypeFlags.CHARACTER)) != 0)
+                    && model instanceof TemplateScalarModel
+                    && (itf != 0 || 
targetClass.isAssignableFrom(String.class))) {
+                String strVal = ((TemplateScalarModel) model).getAsString();
+                if (itf == 0 || (itf & TypeFlags.CHARACTER) == 0) {
+                    return strVal;
+                } else { // TypeFlags.CHAR == 1
+                    if (strVal.length() == 1) {
+                        if ((itf & TypeFlags.ACCEPTS_STRING) != 0) {
+                            return new CharacterOrString(strVal);
+                        } else {
+                            return Character.valueOf(strVal.charAt(0));
+                        }
+                    } else if ((itf & TypeFlags.ACCEPTS_STRING) != 0) {
+                        return strVal;
+                    }
+                    // It had to be unwrapped to Character, but the string 
length wasn't 1 => Fall through
+                }
+            }
+            // Should be earlier than TemplateScalarModel, but we keep it here 
until FM 2.4 or such
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_BOOLEAN) != 0)
+                    && model instanceof TemplateBooleanModel
+                    && (itf != 0 || 
targetClass.isAssignableFrom(Boolean.class))) {
+                return Boolean.valueOf(((TemplateBooleanModel) 
model).getAsBoolean());
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_MAP) != 0)
+                    && model instanceof TemplateHashModel
+                    && (itf != 0 || 
targetClass.isAssignableFrom(HashAdapter.class))) {
+                return new HashAdapter((TemplateHashModel) model, this);
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_LIST) != 0)
+                    && model instanceof TemplateSequenceModel
+                    && (itf != 0 || 
targetClass.isAssignableFrom(SequenceAdapter.class))) {
+                return new SequenceAdapter((TemplateSequenceModel) model, 
this);
+            }
+            if ((itf == 0 || (itf & TypeFlags.ACCEPTS_SET) != 0)
+                    && model instanceof TemplateCollectionModel
+                    && (itf != 0 || 
targetClass.isAssignableFrom(SetAdapter.class))) {
+                return new SetAdapter((TemplateCollectionModel) model, this);
+            }
+
+            if ((itf & TypeFlags.ACCEPTS_ARRAY) != 0
+                    && model instanceof TemplateSequenceModel) {
+                return new SequenceAdapter((TemplateSequenceModel) model, 
this);
+            }
+
+            if (itf == 0) {
+                break;
+            }
+            itf = 0; // start 2nd iteration
+        } while (true);
+
+        // Last ditch effort - is maybe the model itself is an instance of the 
required type?
+        // Note that this will be always true for Object.class targetClass.
+        if (targetClass.isInstance(model)) {
+            return model;
+        }
+
+        return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+    }
+
+    /**
+     * @param tryOnly
+     *            If {@code true}, if the conversion of an item to the 
component type isn't possible, the method returns
+     *            {@link 
ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} instead of throwing a
+     *            {@link TemplateModelException}.
+     */
+    Object unwrapSequenceToArray(
+            TemplateSequenceModel seq, Class<?> arrayClass, boolean tryOnly, 
Map<Object, Object> recursionStops)
+            throws TemplateModelException {
+        if (recursionStops != null) {
+            Object retval = recursionStops.get(seq);
+            if (retval != null) {
+                return retval;
+            }
+        } else {
+            recursionStops = new IdentityHashMap<>();
+        }
+        Class<?> componentType = arrayClass.getComponentType();
+        Object array = Array.newInstance(componentType, seq.size());
+        recursionStops.put(seq, array);
+        try {
+            final int size = seq.size();
+            for (int i = 0; i < size; i++) {
+                final TemplateModel seqItem = seq.get(i);
+                Object val = tryUnwrapTo(seqItem, componentType, 0, 
recursionStops);
+                if (val == 
ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
+                    if (tryOnly) {
+                        return 
ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
+                    } else {
+                        throw new _TemplateModelException(
+                                "Failed to convert ",  new 
_DelayedFTLTypeDescription(seq),
+                                " object to ", new 
_DelayedShortClassName(array.getClass()),
+                                ": Problematic sequence item at index ", 
Integer.valueOf(i) ," with value type: ",
+                                new _DelayedFTLTypeDescription(seqItem));
+                    }
+
+                }
+                Array.set(array, i, val);
+            }
+        } finally {
+            recursionStops.remove(seq);
         }
-        return super.wrap(obj); 
+        return array;
     }
-    
-    public TemplateModel wrapDomNode(Object obj) {
-        return NodeModel.wrap((Node) obj);
+
+    Object listToArray(List<?> list, Class<?> arrayClass, Map<Object, Object> 
recursionStops)
+            throws TemplateModelException {
+        if (list instanceof SequenceAdapter) {
+            return unwrapSequenceToArray(
+                    ((SequenceAdapter) list).getTemplateSequenceModel(),
+                    arrayClass, false,
+                    recursionStops);
+        }
+
+        if (recursionStops != null) {
+            Object retval = recursionStops.get(list);
+            if (retval != null) {
+                return retval;
+            }
+        } else {
+            recursionStops = new IdentityHashMap<>();
+        }
+        Class<?> componentType = arrayClass.getComponentType();
+        Object array = Array.newInstance(componentType, list.size());
+        recursionStops.put(list, array);
+        try {
+            boolean isComponentTypeExamined = false;
+            boolean isComponentTypeNumerical = false;  // will be filled on 
demand
+            boolean isComponentTypeList = false;  // will be filled on demand
+            int i = 0;
+            for (Object listItem : list) {
+                if (listItem != null && !componentType.isInstance(listItem)) {
+                    // Type conversion is needed. If we can't do it, we just 
let it fail at Array.set later.
+                    if (!isComponentTypeExamined) {
+                        isComponentTypeNumerical = 
_ClassUtil.isNumerical(componentType);
+                        isComponentTypeList = 
List.class.isAssignableFrom(componentType);
+                        isComponentTypeExamined = true;
+                    }
+                    if (isComponentTypeNumerical && listItem instanceof 
Number) {
+                        listItem = forceUnwrappedNumberToType((Number) 
listItem, componentType);
+                    } else if (componentType == String.class && listItem 
instanceof Character) {
+                        listItem = String.valueOf(((Character) 
listItem).charValue());
+                    } else if ((componentType == Character.class || 
componentType == char.class)
+                            && listItem instanceof String) {
+                        String listItemStr = (String) listItem;
+                        if (listItemStr.length() == 1) {
+                            listItem = 
Character.valueOf(listItemStr.charAt(0));
+                        }
+                    } else if (componentType.isArray()) {
+                        if (listItem instanceof List) {
+                            listItem = listToArray((List<?>) listItem, 
componentType, recursionStops);
+                        } else if (listItem instanceof TemplateSequenceModel) {
+                            listItem = 
unwrapSequenceToArray((TemplateSequenceModel) listItem, componentType, false, 
recursionStops);
+                        }
+                    } else if (isComponentTypeList && 
listItem.getClass().isArray()) {
+                        listItem = arrayToList(listItem);
+                    }
+                }
+                try {
+                    Array.set(array, i, listItem);
+                } catch (IllegalArgumentException e) {
+                    throw new TemplateModelException(
+                            "Failed to convert " + 
_ClassUtil.getShortClassNameOfObject(list)
+                                    + " object to " + 
_ClassUtil.getShortClassNameOfObject(array)
+                                    + ": Problematic List item at index " + i 
+ " with value type: "
+                                    + 
_ClassUtil.getShortClassNameOfObject(listItem), e);
+                }
+                i++;
+            }
+        } finally {
+            recursionStops.remove(list);
+        }
+        return array;
     }
 
     /**
-     * Converts an array to a java.util.List.
+     * @param array Must be an array (of either a reference or primitive type)
      */
-    protected Object convertArray(Object arr) {
-        // FM 2.4: Use Arrays.asList instead
-        final int size = Array.getLength(arr);
-        ArrayList list = new ArrayList(size);
-        for (int i = 0; i < size; i++) {
-            list.add(Array.get(arr, i));
+    List<?> arrayToList(Object array) throws TemplateModelException {
+        if (array instanceof Object[]) {
+            // Array of any non-primitive type.
+            // Note that an array of non-primitive type is always instanceof 
Object[].
+            Object[] objArray = (Object[]) array;
+            return objArray.length == 0 ? Collections.EMPTY_LIST : new 
NonPrimitiveArrayBackedReadOnlyList(objArray);
+        } else {
+            // Array of any primitive type
+            return Array.getLength(array) == 0 ? Collections.EMPTY_LIST : new 
PrimtiveArrayBackedReadOnlyList(array);
+        }
+    }
+
+    /**
+     * Converts a number to the target type aggressively (possibly with 
overflow or significant loss of precision).
+     * @param n Non-{@code null}
+     * @return {@code null} if the conversion has failed.
+     */
+    static Number forceUnwrappedNumberToType(final Number n, final Class<?> 
targetType) {
+        // We try to order the conditions by decreasing probability.
+        if (targetType == n.getClass()) {
+            return n;
+        } else if (targetType == int.class || targetType == Integer.class) {
+            return n instanceof Integer ? (Integer) n : 
Integer.valueOf(n.intValue());
+        } else if (targetType == long.class || targetType == Long.class) {
+            return n instanceof Long ? (Long) n : Long.valueOf(n.longValue());
+        } else if (targetType == double.class || targetType == Double.class) {
+            return n instanceof Double ? (Double) n : 
Double.valueOf(n.doubleValue());
+        } else if (targetType == BigDecimal.class) {
+            if (n instanceof BigDecimal) {
+                return n;
+            } else if (n instanceof BigInteger) {
+                return new BigDecimal((BigInteger) n);
+            } else if (n instanceof Long) {
+                // Because we can't represent long accurately as double
+                return BigDecimal.valueOf(n.longValue());
+            } else {
+                return new BigDecimal(n.doubleValue());
+            }
+        } else if (targetType == float.class || targetType == Float.class) {
+            return n instanceof Float ? (Float) n : 
Float.valueOf(n.floatValue());
+        } else if (targetType == byte.class || targetType == Byte.class) {
+            return n instanceof Byte ? (Byte) n : Byte.valueOf(n.byteValue());
+        } else if (targetType == short.class || targetType == Short.class) {
+            return n instanceof Short ? (Short) n : 
Short.valueOf(n.shortValue());
+        } else if (targetType == BigInteger.class) {
+            if (n instanceof BigInteger) {
+                return n;
+            } else {
+                if (n instanceof OverloadedNumberUtil.IntegerBigDecimal) {
+                    return ((OverloadedNumberUtil.IntegerBigDecimal) 
n).bigIntegerValue();
+                } else if (n instanceof BigDecimal) {
+                    return ((BigDecimal) n).toBigInteger();
+                } else {
+                    return BigInteger.valueOf(n.longValue());
+                }
+            }
+        } else {
+            final Number oriN = n instanceof 
OverloadedNumberUtil.NumberWithFallbackType
+                    ? ((OverloadedNumberUtil.NumberWithFallbackType) 
n).getSourceNumber() : n;
+            if (targetType.isInstance(oriN)) {
+                // Handle nonstandard Number subclasses as well as directly 
java.lang.Number.
+                return oriN;
+            } else {
+                // Fails
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Invokes the specified method, wrapping the return value. The specialty
+     * of this method is that if the return value is null, and the return type
+     * of the invoked method is void, {@link TemplateModel#NOTHING} is 
returned.
+     * @param object the object to invoke the method on
+     * @param method the method to invoke
+     * @param args the arguments to the method
+     * @return the wrapped return value of the method.
+     * @throws InvocationTargetException if the invoked method threw an 
exception
+     * @throws IllegalAccessException if the method can't be invoked due to an
+     * access restriction.
+     * @throws TemplateModelException if the return value couldn't be wrapped
+     * (this can happen if the wrapper has an outer identity or is subclassed,
+     * and the outer identity or the subclass throws an exception. Plain
+     * DefaultObjectWrapper never throws TemplateModelException).
+     */
+    TemplateModel invokeMethod(Object object, Method method, Object[] args)
+            throws InvocationTargetException,
+            IllegalAccessException,
+            TemplateModelException {
+        // [2.4]: Java's Method.invoke truncates numbers if the target type 
has not enough bits to hold the value.
+        // There should at least be an option to check this.
+        Object retval = method.invoke(object, args);
+        return
+                method.getReturnType() == void.class
+                        ? TemplateModel.NOTHING
+                        : getOuterIdentity().wrap(retval);
+    }
+
+    /**
+     * Returns a hash model that represents the so-called class static models.
+     * Every class static model is itself a hash through which you can call
+     * static methods on the specified class. To obtain a static model for a
+     * class, get the element of this hash with the fully qualified class name.
+     * For example, if you place this hash model inside the root data model
+     * under name "statics", you can use i.e. <code>statics["java.lang.
+     * System"]. currentTimeMillis()</code> to call the {@link
+     * java.lang.System#currentTimeMillis()} method.
+     * @return a hash model whose keys are fully qualified class names, and
+     * that returns hash models whose elements are the static models of the
+     * classes.
+     */
+    public TemplateHashModel getStaticModels() {
+        return staticModels;
+    }
+
+    /**
+     * Returns a hash model that represents the so-called class enum models.
+     * Every class' enum model is itself a hash through which you can access
+     * enum value declared by the specified class, assuming that class is an
+     * enumeration. To obtain an enum model for a class, get the element of 
this
+     * hash with the fully qualified class name. For example, if you place this
+     * hash model inside the root data model under name "enums", you can use
+     * i.e. <code>statics["java.math.RoundingMode"].UP</code> to access the
+     * {@link java.math.RoundingMode#UP} value.
+     * @return a hash model whose keys are fully qualified class names, and
+     * that returns hash models whose elements are the enum models of the
+     * classes.
+     */
+    public TemplateHashModel getEnumModels() {
+        return enumModels;
+    }
+
+    /** For Unit tests only */
+    ModelCache getModelCache() {
+        return modelCache;
+    }
+
+    /**
+     * Creates a new instance of the specified class using the method call 
logic of this object wrapper for calling the
+     * constructor. Overloaded constructors and varargs are supported. Only 
public constructors will be called.
+     *
+     * @param clazz The class whose constructor we will call.
+     * @param arguments The list of {@link TemplateModel}-s to pass to the 
constructor after unwrapping them
+     * @return The instance created; it's not wrapped into {@link 
TemplateModel}.
+     */
+    public Object newInstance(Class<?> clazz, List/*<? extends 
TemplateModel>*/ arguments)
+            throws TemplateModelException {
+        try {
+            Object ctors = 
classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY);
+            if (ctors == null) {
+                throw new TemplateModelException("Class " + clazz.getName() +
+                        " has no public constructors.");
+            }
+            Constructor<?> ctor = null;
+            Object[] objargs;
+            if (ctors instanceof SimpleMethod) {
+                SimpleMethod sm = (SimpleMethod) ctors;
+                ctor = (Constructor<?>) sm.getMember();
+                objargs = sm.unwrapArguments(arguments, this);
+                try {
+                    return ctor.newInstance(objargs);
+                } catch (Exception e) {
+                    if (e instanceof TemplateModelException) throw 
(TemplateModelException) e;
+                    throw 
_MethodUtil.newInvocationTemplateModelException(null, ctor, e);
+                }
+            } else if (ctors instanceof OverloadedMethods) {
+                final MemberAndArguments mma = ((OverloadedMethods) 
ctors).getMemberAndArguments(arguments, this);
+                try {
+                    return mma.invokeConstructor(this);
+                } catch (Exception e) {
+                    if (e instanceof TemplateModelException) throw 
(TemplateModelException) e;
+
+                    throw 
_MethodUtil.newInvocationTemplateModelException(null, 
mma.getCallableMemberDescriptor(), e);
+                }
+            } else {
+                // Cannot happen
+                throw new BugException();
+            }
+        } catch (TemplateModelException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new TemplateModelException(
+                    "Error while creating new instance of class " + 
clazz.getName() + "; see cause exception", e);
+        }
+    }
+
+    /**
+     * Removes the introspection data for a class from the cache.
+     * Use this if you know that a class is not used anymore in templates.
+     * If the class will be still used, the cache entry will be silently
+     * re-created, so this isn't a dangerous operation.
+     *
+     * @since 2.3.20
+     */
+    public void removeFromClassIntrospectionCache(Class<?> clazz) {
+        classIntrospector.remove(clazz);
+    }
+
+    /**
+     * Removes all class introspection data from the cache.
+     *
+     * <p>Use this if you want to free up memory on the expense of recreating
+     * the cache entries for the classes that will be used later in templates.
+     *
+     * @throws IllegalStateException if {@link 
#isClassIntrospectionCacheRestricted()} is {@code true}.
+     *
+     * @since 2.3.20
+     */
+    public void clearClassIntrospecitonCache() {
+        classIntrospector.clearCache();
+    }
+
+    ClassIntrospector getClassIntrospector() {
+        return classIntrospector;
+    }
+
+    /**
+     * Converts any {@link BigDecimal}s in the passed array to the type of
+     * the corresponding formal argument of the method.
+     */
+    // Unused?
+    public static void coerceBigDecimals(AccessibleObject callable, Object[] 
args) {
+        Class<?>[] formalTypes = null;
+        for (int i = 0; i < args.length; ++i) {
+            Object arg = args[i];
+            if (arg instanceof BigDecimal) {
+                if (formalTypes == null) {
+                    if (callable instanceof Method) {
+                        formalTypes = ((Method) callable).getParameterTypes();
+                    } else if (callable instanceof Constructor) {
+                        formalTypes = ((Constructor<?>) 
callable).getParameterTypes();
+                    } else {
+                        throw new IllegalArgumentException("Expected method or 
"
+                                + " constructor; callable is " +
+                                callable.getClass().getName());
+                    }
+                }
+                args[i] = coerceBigDecimal((BigDecimal) arg, formalTypes[i]);
+            }
+        }
+    }
+
+    /**
+     * Converts any {@link BigDecimal}-s in the passed array to the type of
+     * the corresponding formal argument of the method via {@link 
#coerceBigDecimal(BigDecimal, Class)}.
+     */
+    public static void coerceBigDecimals(Class<?>[] formalTypes, Object[] 
args) {
+        int typeLen = formalTypes.length;
+        int argsLen = args.length;
+        int min = Math.min(typeLen, argsLen);
+        for (int i = 0; i < min; ++i) {
+            Object arg = args[i];
+            if (arg instanceof BigDecimal) {
+                args[i] = coerceBigDecimal((BigDecimal) arg, formalTypes[i]);
+            }
+        }
+        if (argsLen > typeLen) {
+            Class<?> varArgType = formalTypes[typeLen - 1];
+            for (int i = typeLen; i < argsLen; ++i) {
+                Object arg = args[i];
+                if (arg instanceof BigDecimal) {
+                    args[i] = coerceBigDecimal((BigDecimal) arg, varArgType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Converts {@link BigDecimal} to the class given in the {@code 
formalType} argument if that's a known numerical
+     * type, returns the {@link BigDecimal} as is otherwise. Overflow and 
precision loss are possible, similarly as
+     * with casting in Java.
+     */
+    public static Object coerceBigDecimal(BigDecimal bd, Class<?> formalType) {
+        // int is expected in most situations, so we check it first
+        if (formalType == int.class || formalType == Integer.class) {
+            return Integer.valueOf(bd.intValue());
+        } else if (formalType == double.class || formalType == Double.class) {
+            return Double.valueOf(bd.doubleValue());
+        } else if (formalType == long.class || formalType == Long.class) {
+            return Long.valueOf(bd.longValue());
+        } else if (formalType == float.class || formalType == Float.class) {
+            return Float.valueOf(bd.floatValue());
+        } else if (formalType == short.class || formalType == Short.class) {
+            return Short.valueOf(bd.shortValue());
+        } else if (formalType == byte.class || formalType == Byte.class) {
+            return Byte.valueOf(bd.byteValue());
+        } else if (java.math.BigInteger.class.isAssignableFrom(formalType)) {
+            return bd.toBigInteger();
+        } else {
+            return bd;
         }
-        return list;
     }
 
     /**
      * Returns the lowest version number that is equivalent with the parameter 
version.
-     * 
+     *
      * @since 2.3.22
      */
     protected static Version normalizeIncompatibleImprovementsVersion(Version 
incompatibleImprovements) {
-        return 
BeansWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
+        _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+        return Configuration.VERSION_3_0_0;
     }
 
+
     /**
-     * @since 2.3.22
+     * Returns the name-value pairs that describe the configuration of this 
{@link DefaultObjectWrapper}; called from
+     * {@link #toString()}. The expected format is like {@code "foo=bar, 
baaz=wombat"}. When overriding this, you should
+     * call the super method, and then insert the content before it with a 
following {@code ", "}, or after it with a
+     * preceding {@code ", "}.
      */
-    @Override
     protected String toPropertiesString() {
-        String bwProps = super.toPropertiesString();
-        
-        // Remove simpleMapWrapper, as its irrelevant for this wrapper:
-        if (bwProps.startsWith("simpleMapWrapper")) {
-            int smwEnd = bwProps.indexOf(',');
-            if (smwEnd != -1) {
-                bwProps = bwProps.substring(smwEnd + 1).trim();
-            }
-        }
-        
-        return "";
+        // Start with "simpleMapWrapper", because the override in 
DefaultObjectWrapper expects it to be there!
+        return "exposureLevel=" + classIntrospector.getExposureLevel() + ", "
+                + "exposeFields=" + classIntrospector.getExposeFields() + ", "
+                + "sharedClassIntrospCache="
+                + (classIntrospector.isShared() ? "@" + 
System.identityHashCode(classIntrospector) : "none");
     }
-    
+
+    /**
+     * Returns the exact class name and the identity hash, also the values of 
the most often used
+     * {@link DefaultObjectWrapper} configuration properties, also if which 
(if any) shared class introspection
+     * cache it uses.
+     */
+    @Override
+    public String toString() {
+        final String propsStr = toPropertiesString();
+        return _ClassUtil.getShortClassNameOfObject(this) + "@" + 
System.identityHashCode(this)
+                + "(" + incompatibleImprovements + ", "
+                + (propsStr.length() != 0 ? propsStr + ", ..." : "")
+                + ")";
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java
 
b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java
index 40768f8..89af870 100644
--- 
a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java
+++ 
b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java
@@ -25,17 +25,85 @@ import java.util.Map;
 import java.util.WeakHashMap;
 
 import org.apache.freemarker.core.Version;
-import org.apache.freemarker.core.model.impl.beans.BeansWrapperBuilder;
-import org.apache.freemarker.core.model.impl.beans._BeansAPI;
+import org.apache.freemarker.core.model.TemplateModel;
 
 /**
- * Gets/creates a {@link DefaultObjectWrapper} singleton instance that's 
already configured as specified in the
- * properties of this object; this is recommended over using the {@link 
DefaultObjectWrapper} constructors. The returned
- * instance can't be further configured (it's write protected).
- * 
- * <p>See {@link BeansWrapperBuilder} for more info, as that works 
identically. 
- * 
- * @since 2.3.21
+ * Gets/creates a {@link DefaultObjectWrapper} singleton instance that's 
already configured as specified in the properties of
+ * this object; this is recommended over using the {@link 
DefaultObjectWrapper} constructors. The returned instance can't be
+ * further configured (it's write protected).
+ *
+ * <p>The builder meant to be used as a drop-away object (not stored in a 
field), like in this example:
+ * <pre>
+ *    DefaultObjectWrapper dow = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21).build();
+ * </pre>
+ *
+ * <p>Or, a more complex example:</p>
+ * <pre>
+ *    // Create the builder:
+ *    DefaultObjectWrapperBuilder builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21);
+ *    // Set desired DefaultObjectWrapper configuration properties:
+ *    builder.setUseModelCache(true);
+ *    builder.setExposeFields(true);
+ *
+ *    // Get the singleton:
+ *    DefaultObjectWrapper dow = builder.build();
+ *    // You don't need the builder anymore.
+ * </pre>
+ *
+ * <p>Despite that builders aren't meant to be used as long-lived objects 
(singletons), the builder is thread-safe after
+ * you have stopped calling its setters and it was safely published (see JSR 
133) to other threads. This can be useful
+ * if you have to put the builder into an IoC container, rather than the 
singleton it produces.
+ *
+ * <p>The main benefit of using a builder instead of a {@link 
DefaultObjectWrapper} constructor is that this way the
+ * internal object wrapping-related caches (most notably the class 
introspection cache) will come from a global,
+ * JVM-level (more precisely, {@code freemarker-core.jar}-class-loader-level) 
cache. Also the
+ * {@link DefaultObjectWrapper} singletons
+ * themselves are stored in this global cache. Some of the wrapping-related 
caches are expensive to build and can take
+ * significant amount of memory. Using builders, components that use 
FreeMarker will share {@link DefaultObjectWrapper}
+ * instances and said caches even if they use separate FreeMarker {@link 
org.apache.freemarker.core.Configuration}-s. (Many Java libraries use
+ * FreeMarker internally, so {@link org.apache.freemarker.core.Configuration} 
sharing is not an option.)
+ *
+ * <p>Note that the returned {@link DefaultObjectWrapper} instances are only 
weak-referenced from inside the builder mechanism,
+ * so singletons are garbage collected when they go out of usage, just like 
non-singletons.
+ *
+ * <p>About the object wrapping-related caches:
+ * <ul>
+ *   <li><p>Class introspection cache: Stores information about classes that 
once had to be wrapped. The cache is
+ *     stored in the static fields of certain FreeMarker classes. Thus, if you 
have two {@link DefaultObjectWrapper}
+ *     instances, they might share the same class introspection cache. But if 
you have two
+ *     {@code freemarker.jar}-s (typically, in two Web Application's {@code 
WEB-INF/lib} directories), those won't
+ *     share their caches (as they don't share the same FreeMarker classes).
+ *     Also, currently there's a separate cache for each permutation of the 
property values that influence class
+ *     introspection: {@link 
DefaultObjectWrapperBuilder#setExposeFields(boolean) expose_fields} and
+ *     {@link DefaultObjectWrapperBuilder#setExposureLevel(int) 
exposure_level}. So only {@link DefaultObjectWrapper} where those
+ *     properties are the same may share class introspection caches among each 
other.
+ *   </li>
+ *   <li><p>Model caches: These are local to a {@link DefaultObjectWrapper}. 
{@link DefaultObjectWrapperBuilder} returns the same
+ *     {@link DefaultObjectWrapper} instance for equivalent properties (unless 
the existing instance was garbage collected
+ *     and thus a new one had to be created), hence these caches will be 
re-used too. {@link DefaultObjectWrapper} instances
+ *     are cached in the static fields of FreeMarker too, but there's a 
separate cache for each
+ *     Thread Context Class Loader, which in a servlet container practically 
means a separate cache for each Web
+ *     Application (each servlet context). (This is like so because for 
resolving class names to classes FreeMarker
+ *     uses the Thread Context Class Loader, so the result of the resolution 
can be different for different
+ *     Thread Context Class Loaders.) The model caches are:
+ *     <ul>
+ *       <li><p>
+ *         Static model caches: These are used by the hash returned by {@link 
DefaultObjectWrapper#getEnumModels()} and
+ *         {@link DefaultObjectWrapper#getStaticModels()}, for caching {@link 
TemplateModel}-s for the static methods/fields
+ *         and Java enums that were accessed through them. To use said hashes, 
you have to put them
+ *         explicitly into the data-model or expose them to the template 
explicitly otherwise, so in most applications
+ *         these caches aren't unused.
+ *       </li>
+ *       <li><p>
+ *         Instance model cache: By default off (see {@link 
DefaultObjectWrapper#setUseModelCache(boolean)}). Caches the
+ *         {@link TemplateModel}-s for all Java objects that were accessed 
from templates.
+ *       </li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * <p>Note that what this method documentation says about {@link 
DefaultObjectWrapper} also applies to
+ * {@link DefaultObjectWrapperBuilder}.
  */
 public class DefaultObjectWrapperBuilder extends 
DefaultObjectWrapperConfiguration {
 
@@ -65,12 +133,20 @@ public class DefaultObjectWrapperBuilder extends 
DefaultObjectWrapperConfigurati
      * a singleton that is also in use elsewhere. 
      */
     public DefaultObjectWrapper build() {
-        return _BeansAPI.getBeansWrapperSubclassSingleton(
+        return _ModelAPI.getDefaultObjectWrapperSubclassSingleton(
                 this, INSTANCE_CACHE, INSTANCE_CACHE_REF_QUEUE, 
DefaultObjectWrapperFactory.INSTANCE);
     }
-    
+
+    /**
+     * For unit testing only
+     */
+    static Map<ClassLoader, Map<DefaultObjectWrapperConfiguration, 
WeakReference<DefaultObjectWrapper>>>
+            getInstanceCache() {
+        return INSTANCE_CACHE;
+    }
+
     private static class DefaultObjectWrapperFactory
-        implements 
_BeansAPI._BeansWrapperSubclassFactory<DefaultObjectWrapper, 
DefaultObjectWrapperConfiguration> {
+        implements 
_ModelAPI._DefaultObjectWrapperSubclassFactory<DefaultObjectWrapper, 
DefaultObjectWrapperConfiguration> {
     
         private static final DefaultObjectWrapperFactory INSTANCE = new 
DefaultObjectWrapperFactory(); 
         

Reply via email to