You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/03/17 21:19:51 UTC

[3/4] incubator-freemarker git commit: Made DefaultObjectWrapper immtutable (no more setters), also removed public constructors. Instead, instances are created with DefaultObjectWrapper.Builder.build(). Reworked DefaultObjectWrapper and ClassIntrospector

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/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 f5c9a2c..da6c675 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,6 +19,8 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
@@ -36,6 +38,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
 import java.util.Set;
+import java.util.WeakHashMap;
 
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.Version;
@@ -61,30 +64,28 @@ 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.BuilderBase;
 import org.apache.freemarker.core.util._ClassUtil;
 import org.apache.freemarker.dom.NodeModel;
 import org.slf4j.Logger;
 import org.w3c.dom.Node;
 
 /**
- * The default implementation of the {@link ObjectWrapper} interface. Usually, you don't need to create instances of
+ * The default implementation of the {@link ObjectWrapper} interface. Usually, you don't need to invoke instances of
  * this, as an instance of this is already the default value of the
  * {@link Configuration#setObjectWrapper(ObjectWrapper) object_wrapper setting}. Then the
- * {@link #DefaultObjectWrapper(Version) incompatibleImprovements} of the {@link DefaultObjectWrapper} will be the
- * same that you have set for the {@link Configuration} itself.
+ * {@link ExtendableBuilder#ExtendableBuilder(Version, boolean) incompatibleImprovements} of the
+ * {@link DefaultObjectWrapper} will be the same that you have set for the {@link Configuration} itself.
  * 
  * <p>
- * If you still need to create an instance, that should be done with an {@link DefaultObjectWrapperBuilder} (or
- * with {@link Configuration#setSetting(String, String)} with {@code "object_wrapper"} key), not with
- * its constructor, as that allows FreeMarker to reuse singletons.
+ * If you still need to invoke an instance, that should be done with {@link Builder#build()} (or
+ * with {@link Configuration#setSetting(String, String)} with {@code "objectWrapper"} key); the constructor isn't
+ * public.
  *
  * <p>
- * This class is only thread-safe after you have finished calling its setter methods, and then safely published it (see
- * 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.
+ * This class is thread-safe.
  */
-public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable {
+public class DefaultObjectWrapper implements RichObjectWrapper {
 
     private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
 
@@ -118,9 +119,7 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
      * 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.
+     * method) can be retrieved through the hash interface.
      */
     public static final int EXPOSE_NOTHING = 3;
 
@@ -133,12 +132,10 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
      * {@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;
+    private final ClassIntrospector classIntrospector;
 
     /**
      * {@link String} class name to {@link StaticModel} cache.
@@ -158,138 +155,56 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
 
     // -----------------------------------------------------------------------------------------------------------------
 
-    // 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 int defaultDateType;
+    private final ObjectWrapper outerIdentity;
+    private final boolean strict;
     @Deprecated // Only exists to keep some JUnit tests working... [FM3]
-    private boolean useModelCache;
+    private final boolean useModelCache;
 
     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
-     *   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);
-        // Attention! Don't don anything here, as the instance is possibly already visible to other threads through the
-        // model factory callbacks.
-    }
-
-    /**
-     * Same as {@link #DefaultObjectWrapper(DefaultObjectWrapperConfiguration, boolean, boolean)} with {@code true}
-     * {@code finalizeConstruction} argument.
-     *
-     * @since 2.3.21
-     */
-    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.
+     * Initializes the instance based on the the {@link ExtendableBuilder} specified.
      *
      * @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
+     *     adjustments on the instance and then call {@link #finalizeConstruction()} itself.
      */
-    protected DefaultObjectWrapper(DefaultObjectWrapperConfiguration bwConf, boolean writeProtected, boolean finalizeConstruction) {
-        incompatibleImprovements = bwConf.getIncompatibleImprovements();  // normalized
+    protected DefaultObjectWrapper(ExtendableBuilder builder, boolean finalizeConstruction) {
+        incompatibleImprovements = builder.getIncompatibleImprovements();  // normalized
 
-        defaultDateType = bwConf.getDefaultDateType();
-        outerIdentity = bwConf.getOuterIdentity() != null ? bwConf.getOuterIdentity() : this;
-        strict = bwConf.isStrict();
+        defaultDateType = builder.getDefaultDateType();
+        outerIdentity = builder.getOuterIdentity() != null ? builder.getOuterIdentity() : this;
+        strict = builder.isStrict();
 
-        if (!writeProtected) {
+        if (builder.getUsePrivateCaches()) {
             // 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.classIntrospectorBuilder, sharedIntrospectionLock);
+            classIntrospector = new ClassIntrospector(builder.classIntrospectorBuilder, 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.classIntrospectorBuilder.build();
+            classIntrospector = builder.classIntrospectorBuilder.build();
             sharedIntrospectionLock = classIntrospector.getSharedLock();
         }
 
         staticModels = new StaticModels(this);
         enumModels = new EnumModels(this);
-        setUseModelCache(bwConf.getUseModelCache());
+        useModelCache = builder.getUseModelCache();
 
-        finalizeConstruction(writeProtected);
+        finalizeConstruction();
     }
 
     /**
-     * Meant to be called after {@link DefaultObjectWrapper#DefaultObjectWrapper(DefaultObjectWrapperConfiguration, boolean, boolean)} when
+     * Meant to be called after {@link DefaultObjectWrapper#DefaultObjectWrapper(ExtendableBuilder, 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 void finalizeConstruction(boolean writeProtected) {
-        if (writeProtected) {
-            writeProtect();
-        }
-
+    protected void finalizeConstruction() {
         // 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
@@ -298,94 +213,20 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
         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)
+     * @see ExtendableBuilder#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)
+     * @see ExtendableBuilder#setOuterIdentity(ObjectWrapper)
      */
     public ObjectWrapper getOuterIdentity() {
         return outerIdentity;
@@ -417,21 +258,6 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
     */
 
     /**
-     * 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 builder = classIntrospector.createBuilder();
-            builder.setExposureLevel(exposureLevel);
-            replaceClassIntrospector(builder);
-        }
-    }
-
-    /**
      * @since 2.3.21
      */
     public int getExposureLevel() {
@@ -439,28 +265,8 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
     }
 
     /**
-     * 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 builder = classIntrospector.createBuilder();
-            builder.setExposeFields(exposeFields);
-            replaceClassIntrospector(builder);
-        }
-    }
-
-    /**
      * Returns whether exposure of public instance fields of classes is
-     * enabled. See {@link #setExposeFields(boolean)} for details.
+     * enabled. See {@link ExtendableBuilder#setExposeFields(boolean)} for details.
      * @return true if public instance fields are exposed, false otherwise.
      */
     public boolean isExposeFields() {
@@ -471,39 +277,15 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
         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 builder = classIntrospector.createBuilder();
-            builder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
-            replaceClassIntrospector(builder);
-        }
-    }
-
     MethodSorter getMethodSorter() {
         return classIntrospector.getMethodSorter();
     }
 
-    void setMethodSorter(MethodSorter methodSorter) {
-        checkModifiable();
-
-        if (classIntrospector.getMethodSorter() != methodSorter) {
-            ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
-            builder.setMethodSorter(methodSorter);
-            replaceClassIntrospector(builder);
-        }
-    }
-
     /**
      * 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}
+     * was created; with a public constructor (then this is {@code false}), or with {@link Builder}
      * (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.
      *
@@ -513,42 +295,6 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
         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 builder) {
-        checkModifiable();
-
-        final ClassIntrospector newCI = new ClassIntrospector(builder, sharedIntrospectionLock);
-        final ClassIntrospector oldCI;
-
-        // In principle this need not be synchronized, but as apps might publish the configuration improperly, or
-        // 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();
-                }
-            }
-
-            classIntrospector = newCI;
-
-            registerModelFactories();
-        }
-    }
-
     private void registerModelFactories() {
         if (staticModels != null) {
             classIntrospector.registerModelFactory(staticModels);
@@ -559,47 +305,7 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
     }
 
     /**
-     * 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
+     * Returns the default date type. See {@link ExtendableBuilder#setDefaultDateType(int)} for
      * details.
      * @return the default date type
      */
@@ -612,23 +318,14 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
      */
     // [FM3] Remove
     @Deprecated
-    public void setUseModelCache(boolean useCache) {
-        checkModifiable();
-        useModelCache = useCache;
-    }
-
-    /**
-     * @deprecated Does nothing in FreeMarker 3 - we kept it for now to postopne reworking some JUnit tests.
-     */
-    // [FM3] Remove
-    @Deprecated
     public boolean getUseModelCache() {
         return useModelCache;
     }
 
     /**
-     * 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.
+     * Returns the version given with {@link Builder (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
      */
@@ -1481,4 +1178,501 @@ public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable
                 + ")";
     }
 
+    /**
+     * 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 Builder(Configuration.VERSION_2_3_21).build();
+     * </pre>
+     *
+     * <p>Or, a more complex example:</p>
+     * <pre>
+     *    // Create the builder:
+     *    Builder builder = new Builder(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 Configuration}-s. (Many Java libraries use
+     * FreeMarker internally, so {@link 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 Builder#setExposeFields(boolean) expose_fields} and
+     *     {@link Builder#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 Builder} 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 ExtendableBuilder#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 Builder}.
+     */
+    public static final class Builder extends ExtendableBuilder<DefaultObjectWrapper, Builder> {
+
+        private final static Map<ClassLoader, Map<Builder, WeakReference<DefaultObjectWrapper>>>
+                INSTANCE_CACHE = new WeakHashMap<>();
+        private final static ReferenceQueue<DefaultObjectWrapper> INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue<>();
+
+        /**
+         * See {@link ExtendableBuilder#ExtendableBuilder(Version, boolean)}
+         */
+        public Builder(Version incompatibleImprovements) {
+            super(incompatibleImprovements, false);
+        }
+
+        /** For unit testing only */
+        static void clearInstanceCache() {
+            synchronized (INSTANCE_CACHE) {
+                INSTANCE_CACHE.clear();
+            }
+        }
+
+        /**
+         * Returns a {@link DefaultObjectWrapper} instance that matches the settings of this builder. This will be possibly
+         * a singleton that is also in use elsewhere.
+         */
+        public DefaultObjectWrapper build() {
+            return _ModelAPI.getDefaultObjectWrapperSubclassSingleton(
+                    this, INSTANCE_CACHE, INSTANCE_CACHE_REF_QUEUE, ConstructorInvoker.INSTANCE);
+        }
+
+        /**
+         * For unit testing only
+         */
+        static Map<ClassLoader, Map<Builder, WeakReference<DefaultObjectWrapper>>> getInstanceCache() {
+            return INSTANCE_CACHE;
+        }
+
+        private static class ConstructorInvoker
+            implements _ModelAPI._ConstructorInvoker<DefaultObjectWrapper, Builder> {
+
+            private static final ConstructorInvoker INSTANCE = new ConstructorInvoker();
+
+            @Override
+            public DefaultObjectWrapper invoke(Builder builder) {
+                return new DefaultObjectWrapper(builder, true);
+            }
+        }
+
+    }
+
+    /**
+     * Holds {@link DefaultObjectWrapper} configuration settings and defines their defaults.
+     * You will not use this abstract class directly, but concrete subclasses like {@link Builder}.
+     * Unless, you are developing a builder for a custom {@link DefaultObjectWrapper} subclass. In that case, note that
+     * overriding the {@link #equals} and {@link #hashCode} is important, as these objects are used as {@link ObjectWrapper}
+     * singleton lookup keys.
+     */
+    protected abstract static class ExtendableBuilder<
+            ProductT extends DefaultObjectWrapper, SelfT extends ExtendableBuilder<ProductT, SelfT>>
+            extends BuilderBase<ProductT, SelfT> implements Cloneable {
+
+        private final Version incompatibleImprovements;
+
+        ClassIntrospector.Builder classIntrospectorBuilder;
+
+        // Properties and their *defaults*:
+        private boolean simpleMapWrapper;
+        private int defaultDateType = TemplateDateModel.UNKNOWN;
+        private ObjectWrapper outerIdentity;
+        private boolean strict;
+        private boolean useModelCache;
+        private boolean usePrivateCaches;
+        // Attention!
+        // - As this object is a cache key, non-normalized field values should be avoided.
+        // - Fields with default values must be set until the end of the constructor to ensure that when the lookup happens,
+        //   there will be no unset fields.
+        // - If you add a new field, review all methods in this class
+
+        /**
+         * @param incompatibleImprovements
+         *   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>3.0.0: No changes; this is the starting point, the version used in older projects.
+         *     </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.
+         * @param isIncompImprsAlreadyNormalized
+         *         Tells if the {@code incompatibleImprovements} parameter contains an <em>already normalized</em> value.
+         *         This parameter meant to be {@code true} when the class that extends {@link DefaultObjectWrapper} needs to
+         *         add additional breaking versions over those of {@link DefaultObjectWrapper}. Thus, if this parameter is
+         *         {@code true}, the versions where {@link DefaultObjectWrapper} had breaking changes must be already
+         *         factored into the {@code incompatibleImprovements} parameter value, as no more normalization will happen.
+         *         (You can use {@link DefaultObjectWrapper#normalizeIncompatibleImprovementsVersion(Version)} to discover
+         *         those.)
+         */
+        protected ExtendableBuilder(Version incompatibleImprovements, boolean isIncompImprsAlreadyNormalized) {
+            _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+
+            incompatibleImprovements = isIncompImprsAlreadyNormalized
+                    ? incompatibleImprovements
+                    : normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
+            this.incompatibleImprovements = incompatibleImprovements;
+
+            classIntrospectorBuilder = new ClassIntrospector.Builder(incompatibleImprovements);
+        }
+
+        /**
+         * Properly implementing this method is important if the builder is used as a cache key; if you override
+         * {@link ExtendableBuilder} and add new fields, don't forget to override it!
+         */
+        // TODO Move this to Builder and a static helper method
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + incompatibleImprovements.hashCode();
+            result = prime * result + (simpleMapWrapper ? 1231 : 1237);
+            result = prime * result + defaultDateType;
+            result = prime * result + (outerIdentity != null ? outerIdentity.hashCode() : 0);
+            result = prime * result + (strict ? 1231 : 1237);
+            result = prime * result + (useModelCache ? 1231 : 1237);
+            result = prime * result + (usePrivateCaches ? 1231 : 1237);
+            result = prime * result + classIntrospectorBuilder.hashCode();
+            return result;
+        }
+
+        /**
+         * Two {@link ExtendableBuilder}-s are equal exactly if their classes are identical ({@code ==}), and their
+         * field values are equal. Properly implementing this method is important if the builder is used as a cache key;
+         * if you override {@link ExtendableBuilder} and add new fields, don't forget to override it!
+         */
+        // TODO Move this to Builder and a static helper method
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            ExtendableBuilder other = (ExtendableBuilder) obj;
+
+            if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false;
+            if (simpleMapWrapper != other.simpleMapWrapper) return false;
+            if (defaultDateType != other.defaultDateType) return false;
+            if (outerIdentity != other.outerIdentity) return false;
+            if (strict != other.strict) return false;
+            if (useModelCache != other.useModelCache) return false;
+            if (usePrivateCaches != other.usePrivateCaches) return false;
+            return classIntrospectorBuilder.equals(other.classIntrospectorBuilder);
+        }
+
+        /**
+         * In case the builder is used as a cache key, this is used to clone it before it's actually used as a key; if
+         * you override {@link ExtendableBuilder} and add new fields that needs deep cloning, don't forget to
+         * override it! Calls {@link Object#clone()} internally (among others), so newly added fields are automatically
+         * copied, but again, that's not enough if the field value is mutable.
+         */
+        // TODO Move this to Builder and DeepCloneableBuilder
+        protected SelfT deepClone() {
+            try {
+                @SuppressWarnings("unchecked") SelfT clone = (SelfT) super.clone();
+                clone.classIntrospectorBuilder = (ClassIntrospector.Builder) classIntrospectorBuilder.clone();
+                return clone;
+            } catch (CloneNotSupportedException e) {
+                throw new RuntimeException("Failed to deepClone ExtendableBuilder", e);
+            }
+        }
+
+        public Version getIncompatibleImprovements() {
+            return incompatibleImprovements;
+        }
+
+        /**
+         * Getter pair of {@link #setDefaultDateType(int)}
+         */
+        public int getDefaultDateType() {
+            return defaultDateType;
+        }
+
+        /**
+         * 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.defaultDateType = defaultDateType;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setDefaultDateType(int)}.
+         */
+        public SelfT defaultDateType(int defaultDateType) {
+            setDefaultDateType(defaultDateType);
+            return self();
+        }
+
+        /**
+         * Getter pair of {@link #setOuterIdentity(ObjectWrapper)}.
+         */
+        public ObjectWrapper getOuterIdentity() {
+            return outerIdentity;
+        }
+
+        /**
+         * 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, or {@code null} if we will use the object created by this builder.
+         */
+        public void setOuterIdentity(ObjectWrapper outerIdentity) {
+            this.outerIdentity = outerIdentity;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setOuterIdentity(ObjectWrapper)}.
+         */
+        public SelfT outerIdentity(ObjectWrapper outerIdentity) {
+            setOuterIdentity(outerIdentity);
+            return self();
+        }
+
+        /**
+         * Getter pair of {@link #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) {
+            this.strict = strict;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setStrict(boolean)}.
+         */
+        public SelfT strict(boolean strict) {
+            setStrict(strict);
+            return self();
+        }
+
+        public boolean getUseModelCache() {
+            return useModelCache;
+        }
+
+        /**
+         * @deprecated Does nothing in FreeMarker 3 - we kept it for now to postopne reworking some JUnit tests.
+         */
+        // [FM3] Remove
+        public void setUseModelCache(boolean useModelCache) {
+            this.useModelCache = useModelCache;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setUseModelCache(boolean)}.
+         * @deprecated Does nothing in FreeMarker 3 - we kept it for now to postopne reworking some JUnit tests.
+         */
+        public SelfT useModelCache(boolean useModelCache) {
+            setUseModelCache(useModelCache);
+            return self();
+        }
+
+        /**
+         * Getter pair of {@link #setUsePrivateCaches(boolean)}.
+         */
+        public boolean getUsePrivateCaches() {
+            return usePrivateCaches;
+        }
+
+        /**
+         * Tells if the instance cerates should try to caches with other {@link DefaultObjectWrapper} instances (where
+         * possible), or it should always invoke its own caches and not share that with anyone else.
+         * */
+        public void setUsePrivateCaches(boolean usePrivateCaches) {
+            this.usePrivateCaches = usePrivateCaches;
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setUsePrivateCaches(boolean)}
+         */
+        public SelfT usePrivateCaches(boolean usePrivateCaches) {
+            setUsePrivateCaches(usePrivateCaches);
+            return self();
+        }
+
+        public int getExposureLevel() {
+            return classIntrospectorBuilder.getExposureLevel();
+        }
+
+        /**
+         * 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) {
+            classIntrospectorBuilder.setExposureLevel(exposureLevel);
+        }
+
+        public SelfT exposureLevel(int exposureLevel) {
+            setExposureLevel(exposureLevel);
+            return self();
+        }
+
+        /**
+         * Getter pair of {@link #setExposeFields(boolean)}
+         */
+        public boolean getExposeFields() {
+            return classIntrospectorBuilder.getExposeFields();
+        }
+
+        /**
+         * 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) {
+            classIntrospectorBuilder.setExposeFields(exposeFields);
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setExposeFields(boolean)}
+         */
+        public SelfT exposeFields(boolean exposeFields) {
+            setExposeFields(exposeFields);
+            return self();
+        }
+
+        /**
+         * Getter pair of {@link #setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}
+         */
+        public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+            return classIntrospectorBuilder.getMethodAppearanceFineTuner();
+        }
+
+        /**
+         * Used to tweak certain aspects of how methods appear in the data-model;
+         * see {@link MethodAppearanceFineTuner} for more.
+         * Setting this to non-{@code null} will disable class introspection cache sharing, unless
+         * the value implements {@link SingletonCustomizer}.
+         */
+        public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+            classIntrospectorBuilder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+        }
+
+        /**
+         * Fluent API equivalent of {@link #setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}
+         */
+        public SelfT methodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+            setMethodAppearanceFineTuner(methodAppearanceFineTuner);
+            return self();
+        }
+
+        /**
+         * Used internally for testing.
+         */
+        MethodSorter getMethodSorter() {
+            return classIntrospectorBuilder.getMethodSorter();
+        }
+
+        /**
+         * Used internally for testing.
+         */
+        void setMethodSorter(MethodSorter methodSorter) {
+            classIntrospectorBuilder.setMethodSorter(methodSorter);
+        }
+
+        /**
+         * Used internally for testing.
+         */
+        SelfT methodSorter(MethodSorter methodSorter) {
+            setMethodSorter(methodSorter);
+            return self();
+        }
+
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/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
deleted file mode 100644
index c8d1f4f..0000000
--- a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core.model.impl;
-
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
-import java.util.Map;
-import java.util.WeakHashMap;
-
-import org.apache.freemarker.core.Version;
-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>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 {
-
-    private final static Map<ClassLoader, Map<DefaultObjectWrapperConfiguration, WeakReference<DefaultObjectWrapper>>>
-            INSTANCE_CACHE = new WeakHashMap<>();
-    private final static ReferenceQueue<DefaultObjectWrapper> INSTANCE_CACHE_REF_QUEUE
-            = new ReferenceQueue<>();
-    
-    /**
-     * Creates a builder that creates a {@link DefaultObjectWrapper} with the given {@code incompatibleImprovements};
-     * using at least 2.3.22 is highly recommended. See {@link DefaultObjectWrapper#DefaultObjectWrapper(Version)} for
-     * more information about the impact of {@code incompatibleImprovements} values.
-     */
-    public DefaultObjectWrapperBuilder(Version incompatibleImprovements) {
-        super(incompatibleImprovements);
-    }
-
-    /** For unit testing only */
-    static void clearInstanceCache() {
-        synchronized (INSTANCE_CACHE) {
-            INSTANCE_CACHE.clear();
-        }
-    }
-    
-    /**
-     * Returns a {@link DefaultObjectWrapper} instance that matches the settings of this builder. This will be possibly
-     * a singleton that is also in use elsewhere. 
-     */
-    public DefaultObjectWrapper build() {
-        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 _ModelAPI._DefaultObjectWrapperSubclassFactory<DefaultObjectWrapper, DefaultObjectWrapperConfiguration> {
-    
-        private static final DefaultObjectWrapperFactory INSTANCE = new DefaultObjectWrapperFactory(); 
-        
-        @Override
-        public DefaultObjectWrapper create(DefaultObjectWrapperConfiguration bwConf) {
-            return new DefaultObjectWrapper(bwConf, true);
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
deleted file mode 100644
index 28111d4..0000000
--- a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperConfiguration.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * 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.Version;
-import org.apache.freemarker.core._CoreAPI;
-import org.apache.freemarker.core.model.ObjectWrapper;
-import org.apache.freemarker.core.model.TemplateDateModel;
-
-/**
- * Holds {@link DefaultObjectWrapper} configuration settings and defines their defaults.
- * You will not use this abstract class directly, but concrete subclasses like {@link DefaultObjectWrapperBuilder}.
- * Unless, you are developing a builder for a custom {@link DefaultObjectWrapper} subclass. In that case, note that
- * overriding the {@link #equals} and {@link #hashCode} is important, as these objects are used as {@link ObjectWrapper}
- * singleton lookup keys.
- */
-public abstract class DefaultObjectWrapperConfiguration implements Cloneable {
-
-    private final Version incompatibleImprovements;
-
-    ClassIntrospectorBuilder classIntrospectorBuilder;
-
-    // Properties and their *defaults*:
-    private boolean simpleMapWrapper = false;
-    private int defaultDateType = TemplateDateModel.UNKNOWN;
-    private ObjectWrapper outerIdentity = null;
-    private boolean strict = false;
-    private boolean useModelCache = false;
-    // Attention!
-    // - As this object is a cache key, non-normalized field values should be avoided.
-    // - Fields with default values must be set until the end of the constructor to ensure that when the lookup happens,
-    //   there will be no unset fields.
-    // - If you add a new field, review all methods in this class
-
-    /**
-     * @param incompatibleImprovements
-     *            See the corresponding parameter of {@link DefaultObjectWrapper#DefaultObjectWrapper(Version)}. Not {@code null}. Note
-     *            that the version will be normalized to the lowest version where the same incompatible
-     *            {@link DefaultObjectWrapper} improvements were already present, so for the returned instance
-     *            {@link #getIncompatibleImprovements()} might returns a lower version than what you have specified
-     *            here.
-     * @param isIncompImprsAlreadyNormalized
-     *            Tells if the {@code incompatibleImprovements} parameter contains an <em>already normalized</em> value.
-     *            This parameter meant to be {@code true} when the class that extends {@link DefaultObjectWrapper} needs to add
-     *            additional breaking versions over those of {@link DefaultObjectWrapper}. Thus, if this parameter is
-     *            {@code true}, the versions where {@link DefaultObjectWrapper} had breaking changes must be already factored
-     *            into the {@code incompatibleImprovements} parameter value, as no more normalization will happen. (You
-     *            can use {@link DefaultObjectWrapper#normalizeIncompatibleImprovementsVersion(Version)} to discover those.)
-     *
-     * @since 2.3.22
-     */
-    protected DefaultObjectWrapperConfiguration(Version incompatibleImprovements, boolean isIncompImprsAlreadyNormalized) {
-        _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
-
-        incompatibleImprovements = isIncompImprsAlreadyNormalized
-                ? incompatibleImprovements
-                : DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
-        this.incompatibleImprovements = incompatibleImprovements;
-
-        classIntrospectorBuilder = new ClassIntrospectorBuilder(incompatibleImprovements);
-    }
-
-    /**
-     * Same as {@link #DefaultObjectWrapperConfiguration(Version, boolean) DefaultObjectWrapperConfiguration(Version, false)}.
-     */
-    protected DefaultObjectWrapperConfiguration(Version incompatibleImprovements) {
-        this(incompatibleImprovements, false);
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + incompatibleImprovements.hashCode();
-        result = prime * result + (simpleMapWrapper ? 1231 : 1237);
-        result = prime * result + defaultDateType;
-        result = prime * result + (outerIdentity != null ? outerIdentity.hashCode() : 0);
-        result = prime * result + (strict ? 1231 : 1237);
-        result = prime * result + (useModelCache ? 1231 : 1237);
-        result = prime * result + classIntrospectorBuilder.hashCode();
-        return result;
-    }
-
-    /**
-     * Two {@link DefaultObjectWrapperConfiguration}-s are equal exactly if their classes are identical ({@code ==}), and their
-     * field values are equal.
-     */
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) return true;
-        if (obj == null) return false;
-        if (getClass() != obj.getClass()) return false;
-        DefaultObjectWrapperConfiguration other = (DefaultObjectWrapperConfiguration) obj;
-
-        if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false;
-        if (simpleMapWrapper != other.simpleMapWrapper) return false;
-        if (defaultDateType != other.defaultDateType) return false;
-        if (outerIdentity != other.outerIdentity) return false;
-        if (strict != other.strict) return false;
-        if (useModelCache != other.useModelCache) return false;
-        return classIntrospectorBuilder.equals(other.classIntrospectorBuilder);
-    }
-
-    protected Object clone(boolean deepCloneKey) {
-        try {
-            DefaultObjectWrapperConfiguration clone = (DefaultObjectWrapperConfiguration) super.clone();
-            if (deepCloneKey) {
-                clone.classIntrospectorBuilder
-                        = (ClassIntrospectorBuilder) classIntrospectorBuilder.clone();
-            }
-            return clone;
-        } catch (CloneNotSupportedException e) {
-            throw new RuntimeException("Failed to clone DefaultObjectWrapperConfiguration", e);
-        }
-    }
-
-    public int getDefaultDateType() {
-        return defaultDateType;
-    }
-
-    /** See {@link DefaultObjectWrapper#setDefaultDateType(int)}. */
-    public void setDefaultDateType(int defaultDateType) {
-        this.defaultDateType = defaultDateType;
-    }
-
-    public ObjectWrapper getOuterIdentity() {
-        return outerIdentity;
-    }
-
-    /**
-     * See {@link DefaultObjectWrapper#setOuterIdentity(ObjectWrapper)}, except here the default is {@code null} that means
-     * the {@link ObjectWrapper} that you will set up with this {@link DefaultObjectWrapperBuilder} object.
-     */
-    public void setOuterIdentity(ObjectWrapper outerIdentity) {
-        this.outerIdentity = outerIdentity;
-    }
-
-    public boolean isStrict() {
-        return strict;
-    }
-
-    /** See {@link DefaultObjectWrapper#setStrict(boolean)}. */
-    public void setStrict(boolean strict) {
-        this.strict = strict;
-    }
-
-    public boolean getUseModelCache() {
-        return useModelCache;
-    }
-
-    /** See {@link DefaultObjectWrapper#setUseModelCache(boolean)} (it means the same). */
-    public void setUseModelCache(boolean useModelCache) {
-        this.useModelCache = useModelCache;
-    }
-
-    public Version getIncompatibleImprovements() {
-        return incompatibleImprovements;
-    }
-
-    public int getExposureLevel() {
-        return classIntrospectorBuilder.getExposureLevel();
-    }
-
-    /** See {@link DefaultObjectWrapper#setExposureLevel(int)}. */
-    public void setExposureLevel(int exposureLevel) {
-        classIntrospectorBuilder.setExposureLevel(exposureLevel);
-    }
-
-    public boolean getExposeFields() {
-        return classIntrospectorBuilder.getExposeFields();
-    }
-
-    /** See {@link DefaultObjectWrapper#setExposeFields(boolean)}. */
-    public void setExposeFields(boolean exposeFields) {
-        classIntrospectorBuilder.setExposeFields(exposeFields);
-    }
-
-    public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
-        return classIntrospectorBuilder.getMethodAppearanceFineTuner();
-    }
-
-    /**
-     * See {@link DefaultObjectWrapper#setMethodAppearanceFineTuner(MethodAppearanceFineTuner)}; additionally,
-     * note that currently setting this to non-{@code null} will disable class introspection cache sharing, unless
-     * the value implements {@link SingletonCustomizer}.
-     */
-    public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
-        classIntrospectorBuilder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
-    }
-
-    MethodSorter getMethodSorter() {
-        return classIntrospectorBuilder.getMethodSorter();
-    }
-
-    void setMethodSorter(MethodSorter methodSorter) {
-        classIntrospectorBuilder.setMethodSorter(methodSorter);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java b/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
index 773e8f3..9218bdf 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/MethodSorter.java
@@ -24,7 +24,7 @@ import java.util.List;
 
 /**
  * Used for JUnit testing method-order dependence bugs via
- * {@link DefaultObjectWrapper#setMethodSorter(MethodSorter)}.
+ * {@link DefaultObjectWrapper.Builder#setMethodSorter(MethodSorter)}.
  */
 interface MethodSorter {
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
index 52a7744..e783af8 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedMethodsSubset.java
@@ -134,7 +134,7 @@ abstract class OverloadedMethodsSubset {
         MaybeEmptyCallableMemberDescriptor memberDesc
                 = (MaybeEmptyCallableMemberDescriptor) argTypesToMemberDescCache.get(argTypes);
         if (memberDesc == null) {
-            // Synchronized so that we won't unnecessarily create the same member desc. for multiple times in parallel.
+            // Synchronized so that we won't unnecessarily invoke the same member desc. for multiple times in parallel.
             synchronized (argTypesToMemberDescCache) {
                 memberDesc = (MaybeEmptyCallableMemberDescriptor) argTypesToMemberDescCache.get(argTypes);
                 if (memberDesc == null) {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
index f858cc8..6547923 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/OverloadedVarArgsMethods.java
@@ -63,7 +63,7 @@ class OverloadedVarArgsMethods extends OverloadedMethodsSubset {
         // - m(t1), because a varargs array can be 0 long
         //
         // But we can't do that for real, because we had to add infinite number of methods. Also, for efficiency we
-        // don't want to create unwrappingHintsByParamCount entries at the indices which are still unused.
+        // don't want to invoke unwrappingHintsByParamCount entries at the indices which are still unused.
         // So we only update the already existing hints. Remember that we already have m(t1, t2) there.
         
         final int paramCount = paramTypes.length;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java b/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
index 2e738a0..eeec18f 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/RestrictedObjectWrapper.java
@@ -31,13 +31,8 @@ import org.apache.freemarker.core.model.TemplateModelException;
  */
 public class RestrictedObjectWrapper extends DefaultObjectWrapper {
 
-    /**
-     * @param incompatibleImprovements see in {@link DefaultObjectWrapper#DefaultObjectWrapper(Version)}.
-     * 
-     * @since 2.3.21
-     */
-    public RestrictedObjectWrapper(Version incompatibleImprovements) {
-        super(incompatibleImprovements);
+    protected RestrictedObjectWrapper(Builder builder) {
+        super(builder, true);
     }
 
     /**
@@ -55,4 +50,26 @@ public class RestrictedObjectWrapper extends DefaultObjectWrapper {
         throw new TemplateModelException("RestrictedObjectWrapper deliberately doesn't allow ?api.");
     }
 
+    protected static abstract class ExtendableBuilder<
+            ProductT extends RestrictedObjectWrapper, SelfT extends ExtendableBuilder<ProductT,
+            SelfT>> extends DefaultObjectWrapper.ExtendableBuilder<ProductT, SelfT> {
+
+        protected ExtendableBuilder(Version incompatibleImprovements, boolean isIncompImprsAlreadyNormalized) {
+            super(incompatibleImprovements, isIncompImprsAlreadyNormalized);
+        }
+
+    }
+
+    public static final class Builder extends ExtendableBuilder<RestrictedObjectWrapper, Builder> {
+
+        public Builder(Version incompatibleImprovements) {
+            super(incompatibleImprovements, false);
+        }
+
+        @Override
+        public RestrictedObjectWrapper build() {
+            return new RestrictedObjectWrapper(this);
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java b/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
index 16ec685..ef475ca 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/SimpleHash.java
@@ -65,7 +65,7 @@ import org.apache.freemarker.core.model.WrappingTemplateModel;
  * to which it had to be passed adapted to a {@link Map}).
  * 
  * <p>
- * If regardless of which of the above two cases stand, you just need to (or more convenient to) create the hash from a
+ * If regardless of which of the above two cases stand, you just need to (or more convenient to) invoke the hash from a
  * {@link Map} (via {@link SimpleHash#SimpleHash(Map, ObjectWrapper)} or
  * {@link SimpleHash#SimpleHash(Map, ObjectWrapper)}), which will be the faster depends on how many times will the
  * <em>same</em> {@link Map} entry be read from the template(s) later, on average. If, on average, you read each entry

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java b/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
index 17cd03a..8811180 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/SimpleSequence.java
@@ -59,7 +59,7 @@ import org.apache.freemarker.core.model.WrappingTemplateModel;
  * from a plain Java method to which it had to be passed adapted to a {@link List}).
  * 
  * <p>
- * If regardless of which of the above two cases stand, you just need to (or more convenient to) create the sequence
+ * If regardless of which of the above two cases stand, you just need to (or more convenient to) invoke the sequence
  * from a {@link List} (via {@link DefaultListAdapter#adapt(List, RichObjectWrapper)} or
  * {@link SimpleSequence#SimpleSequence(Collection)}), which will be the faster depends on how many times will the
  * <em>same</em> {@link List} entry be read from the template(s) later, on average. If, on average, you read each entry

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java b/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
index 554d711..e4a0e5a 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/SingletonCustomizer.java
@@ -25,7 +25,7 @@ import org.apache.freemarker.core.model.ObjectWrapper;
  * Marker interface useful when used together with {@link MethodAppearanceFineTuner} and such customizer objects, to
  * indicate that it <b>doesn't contain reference to the {@link ObjectWrapper}</b> (so beware with non-static inner
  * classes) and can be and should be used in call introspection cache keys. This also implies that you won't
- * create many instances of the class, rather just reuse the same (or same few) instances over and over. Furthermore,
+ * invoke many instances of the class, rather just reuse the same (or same few) instances over and over. Furthermore,
  * the instances must be thread-safe. The typical pattern in which this instance should be used is like this:
  * 
  * <pre>static class MyMethodAppearanceFineTuner implements MethodAppearanceFineTuner, SingletonCustomizer {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dceec32e/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java b/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
index e5c9624..6e09e9b 100644
--- a/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
+++ b/src/main/java/org/apache/freemarker/core/model/impl/_ModelAPI.java
@@ -32,7 +32,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.freemarker.core.model.TemplateModelException;
-import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.BuilderBase;
 import org.apache.freemarker.core.util._CollectionUtil;
 
 /**
@@ -129,24 +129,26 @@ public class _ModelAPI {
     /**
      * Contains the common parts of the singleton management for {@link DefaultObjectWrapper} and {@link DefaultObjectWrapper}.  
      *  
-     * @param dowSubclassFactory Creates a <em>new</em> read-only object wrapper of the desired
+     * @param dowConstructorInvoker Creates a <em>new</em> read-only object wrapper of the desired
      *     {@link DefaultObjectWrapper} subclass. 
      */
-    // [FM3] Unnecessary generalization, unless we publish this API
-    public static <OW extends DefaultObjectWrapper, OWC extends DefaultObjectWrapperConfiguration> OW
-    getDefaultObjectWrapperSubclassSingleton(
-            OWC settings,
-            Map<ClassLoader, Map<OWC, WeakReference<OW>>> instanceCache,
-            ReferenceQueue<OW> instanceCacheRefQue,
-            _DefaultObjectWrapperSubclassFactory<OW, OWC> dowSubclassFactory) {
+    // [FM3] Generalize and publish this functionality
+    public static <
+            ObjectWrapperT extends DefaultObjectWrapper,
+            BuilderT extends DefaultObjectWrapper.ExtendableBuilder<ObjectWrapperT, BuilderT>>
+    ObjectWrapperT getDefaultObjectWrapperSubclassSingleton(
+            BuilderT builder,
+            Map<ClassLoader, Map<BuilderT, WeakReference<ObjectWrapperT>>> instanceCache,
+            ReferenceQueue<ObjectWrapperT> instanceCacheRefQue,
+            _ConstructorInvoker<ObjectWrapperT, BuilderT> dowConstructorInvoker) {
         // DefaultObjectWrapper can't be cached across different Thread Context Class Loaders (TCCL), because the result of
         // a class name (String) to Class mappings depends on it, and the staticModels and enumModels need that.
         // (The ClassIntrospector doesn't have to consider the TCCL, as it only works with Class-es, not class
         // names.)
         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
         
-        Reference<OW> instanceRef;
-        Map<OWC, WeakReference<OW>> tcclScopedCache;
+        Reference<ObjectWrapperT> instanceRef;
+        Map<BuilderT, WeakReference<ObjectWrapperT>> tcclScopedCache;
         synchronized (instanceCache) {
             tcclScopedCache = instanceCache.get(tccl);
             if (tcclScopedCache == null) {
@@ -154,27 +156,24 @@ public class _ModelAPI {
                 instanceCache.put(tccl, tcclScopedCache);
                 instanceRef = null;
             } else {
-                instanceRef = tcclScopedCache.get(settings);
+                instanceRef = tcclScopedCache.get(builder);
             }
         }
 
-        OW instance = instanceRef != null ? instanceRef.get() : null;
+        ObjectWrapperT instance = instanceRef != null ? instanceRef.get() : null;
         if (instance != null) {  // cache hit
             return instance;
         }
         // cache miss
         
-        settings = clone(settings);  // prevent any aliasing issues 
-        instance = dowSubclassFactory.create(settings);
-        if (!instance.isWriteProtected()) {
-            throw new BugException();
-        }
-        
+        builder = builder.deepClone();  // prevent any aliasing issues
+        instance = dowConstructorInvoker.invoke(builder);
+
         synchronized (instanceCache) {
-            instanceRef = tcclScopedCache.get(settings);
-            OW concurrentInstance = instanceRef != null ? instanceRef.get() : null;
+            instanceRef = tcclScopedCache.get(builder);
+            ObjectWrapperT concurrentInstance = instanceRef != null ? instanceRef.get() : null;
             if (concurrentInstance == null) {
-                tcclScopedCache.put(settings, new WeakReference<>(instance, instanceCacheRefQue));
+                tcclScopedCache.put(builder, new WeakReference<>(instance, instanceCacheRefQue));
             } else {
                 instance = concurrentInstance;
             }
@@ -185,20 +184,16 @@ public class _ModelAPI {
         return instance;
     }
 
-    @SuppressWarnings("unchecked")
-    private static <BWC extends DefaultObjectWrapperConfiguration> BWC clone(BWC settings) {
-        return (BWC) settings.clone(true);
-    }
-    
-    private static <BW extends DefaultObjectWrapper, BWC extends DefaultObjectWrapperConfiguration>
+    private static <
+            ObjectWrapperT extends DefaultObjectWrapper, BuilderT extends DefaultObjectWrapper.ExtendableBuilder>
             void removeClearedReferencesFromCache(
-                    Map<ClassLoader, Map<BWC, WeakReference<BW>>> instanceCache,
-                    ReferenceQueue<BW> instanceCacheRefQue) {
-        Reference<? extends BW> clearedRef;
+                    Map<ClassLoader, Map<BuilderT, WeakReference<ObjectWrapperT>>> instanceCache,
+                    ReferenceQueue<ObjectWrapperT> instanceCacheRefQue) {
+        Reference<? extends ObjectWrapperT> clearedRef;
         while ((clearedRef = instanceCacheRefQue.poll()) != null) {
             synchronized (instanceCache) {
-                findClearedRef: for (Map<BWC, WeakReference<BW>> tcclScopedCache : instanceCache.values()) {
-                    for (Iterator<WeakReference<BW>> it2 = tcclScopedCache.values().iterator(); it2.hasNext(); ) {
+                findClearedRef: for (Map<BuilderT, WeakReference<ObjectWrapperT>> tcclScopedCache : instanceCache.values()) {
+                    for (Iterator<WeakReference<ObjectWrapperT>> it2 = tcclScopedCache.values().iterator(); it2.hasNext(); ) {
                         if (it2.next() == clearedRef) {
                             it2.remove();
                             break findClearedRef;
@@ -211,11 +206,12 @@ public class _ModelAPI {
     
     /**
      * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+     * Used when the builder delegates the product creation to something else (typically, an instance cache). Calling
+     * {@link BuilderBase#build()} would be infinite recursion in such cases.
      */
-    public interface _DefaultObjectWrapperSubclassFactory<BW extends DefaultObjectWrapper, BWC extends DefaultObjectWrapperConfiguration> {
+    public interface _ConstructorInvoker<ProductT, BuilderT> {
         
-        /** Creates a new read-only {@link DefaultObjectWrapper}; used for {@link DefaultObjectWrapperBuilder} and such. */
-        BW create(BWC sa);
+        ProductT invoke(BuilderT builder);
     }
     
 }