http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
new file mode 100644
index 0000000..f98f0f8
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
@@ -0,0 +1,689 @@
+/*
+ * 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 static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.lang.ref.Reference;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.test.util.TestUtil;
+
+import junit.framework.TestCase;
+
+public class DefaultObjectWrapperSingletonsTest extends TestCase {
+
+    public DefaultObjectWrapperSingletonsTest(String name) {
+        super(name);
+    }
+    
+    @Override
+    protected void setUp() throws Exception {
+        DefaultObjectWrapperBuilder.clearInstanceCache();
+    }
+
+    public void testBuilderEqualsAndHash() throws Exception {
+        assertEquals(Configuration.VERSION_3_0_0, new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).getIncompatibleImprovements());
+        try {
+            new 
DefaultObjectWrapperBuilder(TestUtil.getClosestFutureVersion());
+            fail("Maybe you need to update this test for the new FreeMarker 
version");
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("upgrade"));
+        }
+
+        DefaultObjectWrapperBuilder builder1;
+        DefaultObjectWrapperBuilder builder2;
+        
+        builder1 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+        builder2 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+        assertEquals(builder1, builder2);
+        
+        builder1.setExposeFields(true);
+        assertNotEquals(builder1, builder2);
+        assertFalse(builder1.hashCode() == builder2.hashCode());
+        builder2.setExposeFields(true);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+        
+        builder1.setExposureLevel(0);
+        assertNotEquals(builder1, builder2);
+        assertFalse(builder1.hashCode() == builder2.hashCode());
+        builder2.setExposureLevel(0);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+        
+        builder1.setExposureLevel(1);
+        assertNotEquals(builder1, builder2);
+        assertFalse(builder1.hashCode() == builder2.hashCode());
+        builder2.setExposureLevel(1);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+        
+        builder1.setDefaultDateType(TemplateDateModel.DATE);
+        assertNotEquals(builder1, builder2);
+        builder2.setDefaultDateType(TemplateDateModel.DATE);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+        
+        builder1.setStrict(true);
+        assertNotEquals(builder1, builder2);
+        assertFalse(builder1.hashCode() == builder2.hashCode());
+        builder2.setStrict(true);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+
+        builder1.setUseModelCache(true);
+        assertNotEquals(builder1, builder2);
+        assertFalse(builder1.hashCode() == builder2.hashCode());
+        builder2.setUseModelCache(true);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+        
+        AlphabeticalMethodSorter ms = new AlphabeticalMethodSorter(true);
+        builder1.setMethodSorter(ms);
+        assertNotEquals(builder1, builder2);
+        builder2.setMethodSorter(ms);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+
+        MethodAppearanceFineTuner maft = new MethodAppearanceFineTuner() {
+            @Override
+            public void process(DecisionInput in, Decision out) { }
+        };
+        builder1.setMethodAppearanceFineTuner(maft);
+        assertNotEquals(builder1, builder2);
+        builder2.setMethodAppearanceFineTuner(maft);
+        assertEquals(builder1, builder2);
+        assertTrue(builder1.hashCode() == builder2.hashCode());
+    }
+    
+    public void testDefaultObjectWrapperBuilderProducts() throws Exception {
+        List<DefaultObjectWrapper> hardReferences = new LinkedList<>();
+        
+        assertEquals(0, getDefaultObjectWrapperInstanceCacheSize());
+        
+        {
+            DefaultObjectWrapper ow = 
getDefaultObjectWrapperWithSetting(Configuration.VERSION_3_0_0, true);
+            assertEquals(1, getDefaultObjectWrapperInstanceCacheSize());
+            assertSame(ow.getClass(), DefaultObjectWrapper.class);
+            assertEquals(Configuration.VERSION_3_0_0, 
ow.getIncompatibleImprovements());
+            assertTrue(ow.isWriteProtected());
+            assertFalse(ow.isStrict());
+            assertTrue(ow.getUseModelCache());
+            assertEquals(TemplateDateModel.UNKNOWN, ow.getDefaultDateType());
+            assertSame(ow, ow.getOuterIdentity());
+            assertTrue(ow.isClassIntrospectionCacheRestricted());
+            assertNull(ow.getMethodAppearanceFineTuner());
+            assertNull(ow.getMethodSorter());
+            
+            try {
+                ow.setExposeFields(true);  // can't modify the settings of a 
(potential) singleton
+                fail();
+            } catch (IllegalStateException e) {
+                assertThat(e.getMessage(), containsString("modify"));
+            }
+            
+            assertSame(ow, 
getDefaultObjectWrapperWithSetting(Configuration.VERSION_3_0_0, true));
+            assertEquals(1, getDefaultObjectWrapperInstanceCacheSize());
+            
+            hardReferences.add(ow);
+        }
+        
+        {
+            DefaultObjectWrapper ow = 
getDefaultObjectWrapperWithSetting(Configuration.VERSION_3_0_0, false);
+            assertEquals(2, getDefaultObjectWrapperInstanceCacheSize());
+            assertSame(ow.getClass(), DefaultObjectWrapper.class);
+            assertEquals(Configuration.VERSION_3_0_0, 
ow.getIncompatibleImprovements());
+            assertTrue(ow.isWriteProtected());
+            assertFalse(ow.getUseModelCache());
+
+            assertSame(ow, 
getDefaultObjectWrapperWithSetting(Configuration.VERSION_3_0_0, false));
+            
+            hardReferences.add(ow);
+        }
+        
+        {
+            DefaultObjectWrapperBuilder factory = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            
factory.setExposureLevel(DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY);
+            DefaultObjectWrapper ow1 = factory.build();
+            DefaultObjectWrapper ow2 = factory.build();
+            assertEquals(3, getDefaultObjectWrapperInstanceCacheSize());
+            assertSame(ow1, ow2);
+            
+            assertSame(ow1.getClass(), DefaultObjectWrapper.class);
+            assertEquals(Configuration.VERSION_3_0_0, 
ow1.getIncompatibleImprovements());
+            assertTrue(ow1.isWriteProtected());
+            assertEquals(DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY, 
ow1.getExposureLevel());
+            assertFalse(ow1.isStrict());
+            assertEquals(TemplateDateModel.UNKNOWN, ow1.getDefaultDateType());
+            assertSame(ow1, ow1.getOuterIdentity());
+            
+            hardReferences.add(ow1);
+        }
+        
+        {
+            DefaultObjectWrapperBuilder factory = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            factory.setExposeFields(true);
+            DefaultObjectWrapper ow1 = factory.build();
+            DefaultObjectWrapper ow2 = factory.build();
+            assertEquals(4, getDefaultObjectWrapperInstanceCacheSize());
+            assertSame(ow1, ow2);
+            
+            assertSame(ow1.getClass(), DefaultObjectWrapper.class);
+            assertEquals(Configuration.VERSION_3_0_0, 
ow1.getIncompatibleImprovements());
+            assertTrue(ow1.isWriteProtected());
+            assertTrue(ow1.isExposeFields());
+            
+            hardReferences.add(ow1);
+        }
+        
+        {
+            DefaultObjectWrapperBuilder factory = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            factory.setStrict(true);
+            factory.setDefaultDateType(TemplateDateModel.DATETIME);
+            factory.setOuterIdentity(new 
SimpleObjectWrapper(Configuration.VERSION_3_0_0));
+            DefaultObjectWrapper ow = factory.build();
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertTrue(ow.isStrict());
+            assertEquals(TemplateDateModel.DATETIME, ow.getDefaultDateType());
+            assertSame(SimpleObjectWrapper.class, 
ow.getOuterIdentity().getClass());
+            
+            hardReferences.add(ow);
+        }
+        
+        // Effect of reference and cache clearings:
+        {
+            DefaultObjectWrapper bw1 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(5, 
getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            clearDefaultObjectWrapperInstanceCacheReferences(false);
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(0, 
getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            DefaultObjectWrapper bw2 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+            assertNotSame(bw1, bw2);
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(1, 
getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            assertSame(bw2, new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build());
+            assertEquals(1, 
getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            clearDefaultObjectWrapperInstanceCacheReferences(true);
+            DefaultObjectWrapper bw3 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+            assertNotSame(bw2, bw3);
+            assertEquals(1, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(1, 
getDefaultObjectWrapperNonClearedInstanceCacheSize());
+        }
+
+        {
+            DefaultObjectWrapperBuilder factory = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            factory.setUseModelCache(true);
+            DefaultObjectWrapper ow = factory.build();
+            assertTrue(ow.getUseModelCache());
+            assertEquals(2, getDefaultObjectWrapperInstanceCacheSize());
+            
+            hardReferences.add(ow);
+        }
+        
+        assertTrue(hardReferences.size() != 0);  // just to save it from GC 
until this line        
+    }
+    
+    private DefaultObjectWrapper getDefaultObjectWrapperWithSetting(Version 
ici, boolean useModelCache) {
+        DefaultObjectWrapperBuilder f = new DefaultObjectWrapperBuilder(ici);
+        f.setUseModelCache(useModelCache);
+        return f.build();
+    }
+
+    public void testMultipleTCCLs() {
+        List<DefaultObjectWrapper> hardReferences = new LinkedList<>();
+        
+        assertEquals(0, getDefaultObjectWrapperInstanceCacheSize());
+        
+        DefaultObjectWrapper bw1 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+        assertEquals(1, getDefaultObjectWrapperInstanceCacheSize());
+        hardReferences.add(bw1);
+        
+        ClassLoader oldTCCL = Thread.currentThread().getContextClassLoader();
+        // Doesn't mater what, just be different from oldTCCL: 
+        ClassLoader newTCCL = oldTCCL == null ? getClass().getClassLoader() : 
null;
+        
+        DefaultObjectWrapper bw2;
+        Thread.currentThread().setContextClassLoader(newTCCL);
+        try {
+            bw2 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+            assertEquals(2, getDefaultObjectWrapperInstanceCacheSize());
+            hardReferences.add(bw2);
+            
+            assertNotSame(bw1, bw2);
+            assertSame(bw2, new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build());
+        } finally {
+            Thread.currentThread().setContextClassLoader(oldTCCL);
+        }
+        
+        assertSame(bw1, new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build());
+        assertEquals(2, getDefaultObjectWrapperInstanceCacheSize());
+
+        DefaultObjectWrapper bw3;
+        Thread.currentThread().setContextClassLoader(newTCCL);
+        try {
+            assertSame(bw2, new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build());
+            
+            DefaultObjectWrapperBuilder bwb = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            bwb.setExposeFields(true);
+            bw3 = bwb.build();
+            assertEquals(3, getDefaultObjectWrapperInstanceCacheSize());
+            hardReferences.add(bw3);
+        } finally {
+            Thread.currentThread().setContextClassLoader(oldTCCL);
+        }
+        
+        {
+            DefaultObjectWrapperBuilder bwb = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            bwb.setExposeFields(true);
+            DefaultObjectWrapper bw4 = bwb.build();
+            assertEquals(4, getDefaultObjectWrapperInstanceCacheSize());
+            assertNotSame(bw3, bw4);
+            hardReferences.add(bw4);
+        }
+        
+        assertTrue(hardReferences.size() != 0);  // just to save it from GC 
until this line        
+    }
+    
+    public void testClassInrospectorCache() throws TemplateModelException {
+        assertFalse(new 
DefaultObjectWrapper(Configuration.VERSION_3_0_0).isClassIntrospectionCacheRestricted());
+        assertTrue(new DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0)
+                .build().isClassIntrospectionCacheRestricted());
+        
+        ClassIntrospectorBuilder.clearInstanceCache();
+        DefaultObjectWrapperBuilder.clearInstanceCache();
+        checkClassIntrospectorCacheSize(0);
+        
+        List<DefaultObjectWrapper> hardReferences = new LinkedList<>();
+        DefaultObjectWrapperBuilder builder;
+
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            
+            DefaultObjectWrapper bw1 = builder.build();
+            checkClassIntrospectorCacheSize(1);
+            
+            builder.setExposureLevel(DefaultObjectWrapper.EXPOSE_SAFE);  // 
this was already set to this
+            builder.setUseModelCache(true);  // this shouldn't matter for the 
introspection cache
+            DefaultObjectWrapper bw2 = builder.build();
+            checkClassIntrospectorCacheSize(1);
+            
+            assertSame(bw2.getClassIntrospector(), bw1.getClassIntrospector());
+            assertNotSame(bw1, bw2);
+            
+            // Wrapping tests:
+            assertFalse(exposesFields(bw1));
+            assertTrue(exposesProperties(bw1));
+            assertTrue(exposesMethods(bw1));
+            assertFalse(exposesUnsafe(bw1));
+            assertTrue(bw1.isClassIntrospectionCacheRestricted());
+            // Prevent introspection cache GC:
+            hardReferences.add(bw1);
+            hardReferences.add(bw2);
+        }
+        
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            builder.setExposeFields(true);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(2);
+            // Wrapping tests:
+            assertTrue(exposesFields(ow));
+            assertTrue(exposesProperties(ow));
+            assertTrue(exposesMethods(ow));
+            assertFalse(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+
+        {
+            builder.setExposureLevel(DefaultObjectWrapper.EXPOSE_ALL);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(3);
+            // Wrapping tests:
+            assertTrue(exposesFields(ow));
+            assertTrue(exposesProperties(ow));
+            assertTrue(exposesMethods(ow));
+            assertTrue(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+        
+        {
+            builder.setExposeFields(false);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(4);
+            // Wrapping tests:
+            assertFalse(exposesFields(ow));
+            assertTrue(exposesProperties(ow));
+            assertTrue(exposesMethods(ow));
+            assertTrue(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+        
+        {
+            builder.setExposureLevel(DefaultObjectWrapper.EXPOSE_NOTHING);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(5);
+            // Wrapping tests:
+            assertFalse(exposesFields(ow));
+            assertFalse(exposesProperties(ow));
+            assertFalse(exposesMethods(ow));
+            assertFalse(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+
+        {
+            builder.setExposeFields(true);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(6);
+            // Wrapping tests:
+            assertTrue(exposesFields(ow));
+            assertFalse(exposesProperties(ow));
+            assertFalse(exposesMethods(ow));
+            assertFalse(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+
+        {
+            
builder.setExposureLevel(DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(7);
+            // Wrapping tests:
+            assertTrue(exposesFields(ow));
+            assertTrue(exposesProperties(ow));
+            assertFalse(exposesMethods(ow));
+            assertFalse(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+        
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            builder.setUseModelCache(true);
+            builder.setExposeFields(false);
+            
builder.setExposureLevel(DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY);
+            
+            DefaultObjectWrapper bw1 = builder.build();
+            checkClassIntrospectorCacheSize(8);
+            ClassIntrospector ci1 = bw1.getClassIntrospector();
+            
+            builder.setUseModelCache(false);  // Shouldn't mater for the 
ClassIntrospector
+            DefaultObjectWrapper bw2 = builder.build();
+            ClassIntrospector ci2 = bw2.getClassIntrospector();
+            checkClassIntrospectorCacheSize(8);
+            
+            assertSame(ci1, ci2);
+            assertNotSame(bw1, bw2);
+            
+            // Wrapping tests:
+            assertFalse(exposesFields(bw1));
+            assertTrue(exposesProperties(bw1));
+            assertFalse(exposesMethods(bw1));
+            assertFalse(exposesUnsafe(bw1));
+
+            // Prevent introspection cache GC:
+            hardReferences.add(bw1);
+            hardReferences.add(bw2);
+        }
+
+        // The ClassInrospector cache couldn't become cleared in reality 
otherwise:
+        DefaultObjectWrapperBuilder.clearInstanceCache();
+
+        clearClassIntrospectorInstanceCacheReferences(false);
+        checkClassIntrospectorCacheSize(8);
+        assertEquals(0, getClassIntrospectorNonClearedInstanceCacheSize());
+
+        {
+            builder.setExposeFields(false);
+            
+            DefaultObjectWrapper bw1 = builder.build();
+            checkClassIntrospectorCacheSize(8);
+            assertEquals(1, getClassIntrospectorNonClearedInstanceCacheSize());
+            ClassIntrospector ci1 = bw1.getClassIntrospector();
+            
+            builder.setUseModelCache(true);  // Shouldn't mater
+            DefaultObjectWrapper bw2 = builder.build();
+            ClassIntrospector ci2 = bw2.getClassIntrospector();
+            
+            assertSame(ci1, ci2);
+            assertNotSame(bw1, bw2);
+            
+            // Wrapping tests:
+            assertFalse(exposesFields(bw1));
+            assertTrue(exposesProperties(bw1));
+            assertFalse(exposesMethods(bw1));
+            assertFalse(exposesUnsafe(bw1));
+
+            // Prevent introspection cache GC:
+            hardReferences.add(bw1);
+            hardReferences.add(bw2);
+        }
+        
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(8);
+            assertEquals(2, getClassIntrospectorNonClearedInstanceCacheSize());
+            // Wrapping tests:
+            assertFalse(exposesFields(ow));
+            assertTrue(exposesProperties(ow));
+            assertTrue(exposesMethods(ow));
+            assertFalse(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+
+        clearClassIntrospectorInstanceCacheReferences(true);
+        checkClassIntrospectorCacheSize(8);
+        assertEquals(0, getClassIntrospectorNonClearedInstanceCacheSize());
+        
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            builder.setExposeFields(true);
+            DefaultObjectWrapper ow = builder.build();
+            checkClassIntrospectorCacheSize(1);
+            // Wrapping tests:
+            assertTrue(exposesFields(ow));
+            assertTrue(exposesProperties(ow));
+            assertTrue(exposesMethods(ow));
+            assertFalse(exposesUnsafe(ow));
+            // Prevent introspection cache GC:
+            hardReferences.add(ow);
+        }
+        
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            builder.setMethodAppearanceFineTuner(new 
MethodAppearanceFineTuner() {
+                @Override
+                public void process(DecisionInput in, Decision out) {
+                }
+            });  // spoils ClassIntrospector() sharing
+
+            builder.setUseModelCache(false);
+            DefaultObjectWrapper bw1 = builder.build();
+            assertSame(bw1, builder.build());
+
+            builder.setUseModelCache(true);
+            DefaultObjectWrapper bw2 = builder.build();
+            checkClassIntrospectorCacheSize(1);
+            assertNotSame(bw1, bw2);
+            assertNotSame(bw1.getClassIntrospector(), 
bw2.getClassIntrospector());
+            assertTrue(bw1.isClassIntrospectionCacheRestricted());
+            assertTrue(bw2.isClassIntrospectionCacheRestricted());
+        }
+
+        {
+            builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+            builder.setMethodAppearanceFineTuner(
+                    GetlessMethodsAsPropertyGettersRule.INSTANCE);  // doesn't 
spoils sharing
+
+            builder.setUseModelCache(false);
+            DefaultObjectWrapper bw1 = builder.build();
+            assertSame(bw1, builder.build());
+            checkClassIntrospectorCacheSize(2);
+            
+            builder.setUseModelCache(true);
+            DefaultObjectWrapper bw2 = builder.build();
+            checkClassIntrospectorCacheSize(2);
+            
+            assertNotSame(bw1, bw2);
+            assertSame(bw1.getClassIntrospector(), 
bw2.getClassIntrospector());  // !
+            assertTrue(bw2.isClassIntrospectionCacheRestricted());
+        }
+        
+        assertTrue(hardReferences.size() != 0);  // just to save it from GC 
until this line        
+    }
+    
+    private void checkClassIntrospectorCacheSize(int expectedSize) {
+        assertEquals(expectedSize, getClassIntrospectorInstanceCacheSize());
+    }
+
+    private void assertNotEquals(Object o1, Object o2) {
+        assertFalse(o1.equals(o2));
+    }
+    
+    public class C {
+        
+        public String foo = "FOO";
+        
+        public String getBar() {
+            return "BAR";
+        }
+        
+    }
+
+    private boolean exposesFields(DefaultObjectWrapper ow) throws 
TemplateModelException {
+        TemplateHashModel thm = (TemplateHashModel) ow.wrap(new C());
+        TemplateScalarModel r = (TemplateScalarModel) thm.get("foo");
+        if (r == null) return false;
+        assertEquals("FOO", r.getAsString());
+        return true;
+    }
+
+    private boolean exposesProperties(DefaultObjectWrapper ow) throws 
TemplateModelException {
+        TemplateHashModel thm = (TemplateHashModel) ow.wrap(new C());
+        TemplateScalarModel r = (TemplateScalarModel) thm.get("bar");
+        if (r == null) return false;
+        assertEquals("BAR", r.getAsString());
+        return true;
+    }
+
+    private boolean exposesMethods(DefaultObjectWrapper ow) throws 
TemplateModelException {
+        TemplateHashModel thm = (TemplateHashModel) ow.wrap(new C());
+        return thm.get("getBar") != null;
+    }
+
+    private boolean exposesUnsafe(DefaultObjectWrapper ow) throws 
TemplateModelException {
+        TemplateHashModel thm = (TemplateHashModel) ow.wrap(new C());
+        return thm.get("wait") != null;
+    }
+    
+    static int getClassIntrospectorInstanceCacheSize() {
+        Map instanceCache = ClassIntrospectorBuilder.getInstanceCache();
+        synchronized (instanceCache) {
+            return instanceCache.size();
+        }
+    }
+
+    static int getClassIntrospectorNonClearedInstanceCacheSize() {
+        Map instanceCache = ClassIntrospectorBuilder.getInstanceCache();
+        synchronized (instanceCache) {
+            int cnt = 0;
+            for (Iterator it = instanceCache.values().iterator(); 
it.hasNext(); ) {
+                if (((Reference) it.next()).get() != null) cnt++;
+            }
+            return cnt;
+        }
+    }
+    
+    static void clearClassIntrospectorInstanceCacheReferences(boolean enqueue) 
{
+        Map instanceCache = ClassIntrospectorBuilder.getInstanceCache();
+        synchronized (instanceCache) {
+            for (Iterator it = instanceCache.values().iterator(); 
it.hasNext(); ) {
+                Reference ref = ((Reference) it.next());
+                ref.clear();
+                if (enqueue) {
+                    ref.enqueue();
+                }
+            }
+        }
+    }
+
+    static int getDefaultObjectWrapperInstanceCacheSize() {
+        Map instanceCache = DefaultObjectWrapperBuilder.getInstanceCache();
+        synchronized (instanceCache) {
+            int size = 0; 
+            for (Iterator it1 = instanceCache.values().iterator(); 
it1.hasNext(); ) {
+                size += ((Map) it1.next()).size();
+            }
+            return size;
+        }
+    }
+
+    static int getDefaultObjectWrapperNonClearedInstanceCacheSize() {
+        Map instanceCache = DefaultObjectWrapperBuilder.getInstanceCache();
+        synchronized (instanceCache) {
+            int cnt = 0;
+            for (Iterator it1 = instanceCache.values().iterator(); 
it1.hasNext(); ) {
+                Map tcclScope = (Map) it1.next();
+                for (Iterator it2 = tcclScope.values().iterator(); 
it2.hasNext(); ) {
+                    if (((Reference) it2.next()).get() != null) cnt++;
+                }
+            }
+            return cnt;
+        }
+    }
+    
+    static void clearDefaultObjectWrapperInstanceCacheReferences(boolean 
enqueue) {
+        Map instanceCache = DefaultObjectWrapperBuilder.getInstanceCache();
+        synchronized (instanceCache) {
+            for (Iterator it1 = instanceCache.values().iterator(); 
it1.hasNext(); ) {
+                Map tcclScope = (Map) it1.next();
+                for (Iterator it2 = tcclScope.values().iterator(); 
it2.hasNext(); ) {
+                    Reference ref = ((Reference) it2.next());
+                    ref.clear();
+                    if (enqueue) {
+                        ref.enqueue();
+                    }
+                }
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
index 48099a7..5b62a9b 100644
--- 
a/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
@@ -39,6 +39,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.Vector;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -77,9 +78,10 @@ import com.google.common.collect.ImmutableMap;
 
 public class DefaultObjectWrapperTest {
 
-    private final static DefaultObjectWrapper OW300 = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0)
+    private final static DefaultObjectWrapper OW = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0)
             .build();
 
+    // This will make sense if we will have multipe incompatibleImprovement 
versions.
     @Test
     public void testIncompatibleImprovementsVersionBreakPoints() throws 
Exception {
         List<Version> expected = new ArrayList<>();
@@ -135,22 +137,26 @@ public class DefaultObjectWrapperTest {
     @SuppressWarnings("boxing")
     @Test
     public void testBuilder() throws Exception {
-        for (boolean simpleMapWrapper : new boolean[] { true, false }) {
-            DefaultObjectWrapperBuilder builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
-            builder.setSimpleMapWrapper(simpleMapWrapper); // Shouldn't mater
-            DefaultObjectWrapper bw = builder.build();
-            assertSame(bw, builder.build());
-            assertSame(bw.getClass(), DefaultObjectWrapper.class);
-            assertEquals(Configuration.VERSION_3_0_0, 
bw.getIncompatibleImprovements());
-            assertTrue(bw.isWriteProtected());
-            assertEquals(simpleMapWrapper, bw.isSimpleMapWrapper());
-
-            assertThat(bw.wrap(new HashMap()), 
instanceOf(DefaultMapAdapter.class));
-            assertThat(bw.wrap(new ArrayList()), 
instanceOf(DefaultListAdapter.class));
-            assertThat(bw.wrap(new String[] {}), 
instanceOf(DefaultArrayAdapter.class));
-            assertThat(bw.wrap(new HashSet()), 
instanceOf(DefaultNonListCollectionAdapter.class));
-            assertThat(bw.wrap(new PureIterable()), 
instanceOf(DefaultIterableAdapter.class));
-        }
+        DefaultObjectWrapperBuilder builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+        DefaultObjectWrapper ow = builder.build();
+        assertSame(ow, builder.build());
+        assertSame(ow.getClass(), DefaultObjectWrapper.class);
+        assertEquals(Configuration.VERSION_3_0_0, 
ow.getIncompatibleImprovements());
+        assertTrue(ow.isWriteProtected());
+    }
+
+    @Test
+    public void testWrappedTypes() throws Exception {
+        DefaultObjectWrapperBuilder builder = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0);
+        DefaultObjectWrapper ow = builder.build();
+
+        assertThat(ow.wrap(new HashMap()), 
instanceOf(DefaultMapAdapter.class));
+        assertThat(ow.wrap(new ArrayList()), 
instanceOf(DefaultListAdapter.class));
+        assertThat(ow.wrap(new String[] {}), 
instanceOf(DefaultArrayAdapter.class));
+        assertThat(ow.wrap(new HashSet()), 
instanceOf(DefaultNonListCollectionAdapter.class));
+        assertThat(ow.wrap(new PureIterable()), 
instanceOf(DefaultIterableAdapter.class));
+        assertThat(ow.wrap(new Vector<>().iterator()), 
instanceOf(DefaultIteratorAdapter.class));
+        assertThat(ow.wrap(new Vector<>().elements()), 
instanceOf(DefaultEnumerationAdapter.class));
     }
     
     @Test
@@ -218,8 +224,8 @@ public class DefaultObjectWrapperTest {
 
     @SuppressWarnings("boxing")
     @Test
-    public void testRoundtripping() throws TemplateModelException, 
ClassNotFoundException {
-        DefaultObjectWrapper dow22 = new 
DefaultObjectWrapper(Configuration.VERSION_3_0_0);
+    public void testCompositeValueWrapping() throws TemplateModelException, 
ClassNotFoundException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper(Configuration.VERSION_3_0_0);
 
         final Map hashMap = new HashMap();
         inintTestMap(hashMap);
@@ -234,13 +240,16 @@ public class DefaultObjectWrapperTest {
         linkedList.add("c");
         final int[] intArray = new int[] { 1, 2, 3 };
         final String[] stringArray = new String[] { "a", "b", "c" };
+        final PureIterable pureIterable = new PureIterable();
+        final HashSet hashSet = new HashSet();
 
-        assertRoundtrip(dow22, linkedHashMap, DefaultMapAdapter.class, 
LinkedHashMap.class, linkedHashMap.toString());
-        assertRoundtrip(dow22, treeMap, DefaultMapAdapter.class, 
TreeMap.class, treeMap.toString());
-        assertRoundtrip(dow22, gMap, DefaultMapAdapter.class, 
ImmutableMap.class, gMap.toString());
-        assertRoundtrip(dow22, linkedList, DefaultListAdapter.class, 
LinkedList.class, linkedList.toString());
-        assertRoundtrip(dow22, intArray, DefaultArrayAdapter.class, 
int[].class, null);
-        assertRoundtrip(dow22, stringArray, DefaultArrayAdapter.class, 
String[].class, null);
+        assertRoundtrip(ow, linkedHashMap, DefaultMapAdapter.class, 
LinkedHashMap.class, linkedHashMap.toString());
+        assertRoundtrip(ow, treeMap, DefaultMapAdapter.class, TreeMap.class, 
treeMap.toString());
+        assertRoundtrip(ow, gMap, DefaultMapAdapter.class, ImmutableMap.class, 
gMap.toString());
+        assertRoundtrip(ow, linkedList, DefaultListAdapter.class, 
LinkedList.class, linkedList.toString());
+        assertRoundtrip(ow, intArray, DefaultArrayAdapter.class, int[].class, 
null);
+        assertRoundtrip(ow, stringArray, DefaultArrayAdapter.class, 
String[].class, null);
+        assertRoundtrip(ow, hashSet, DefaultNonListCollectionAdapter.class, 
HashSet.class, hashSet.toString());
     }
 
     @SuppressWarnings("boxing")
@@ -260,7 +269,7 @@ public class DefaultObjectWrapperTest {
         testMap.put("d", Collections.singletonList("x"));
 
         {
-            TemplateHashModelEx hash = (TemplateHashModelEx) 
OW300.wrap(testMap);
+            TemplateHashModelEx hash = (TemplateHashModelEx) OW.wrap(testMap);
             assertEquals(4, hash.size());
             assertFalse(hash.isEmpty());
             assertNull(hash.get("e"));
@@ -276,7 +285,7 @@ public class DefaultObjectWrapperTest {
         }
 
         {
-            assertTrue(((TemplateHashModel) 
OW300.wrap(Collections.emptyMap())).isEmpty());
+            assertTrue(((TemplateHashModel) 
OW.wrap(Collections.emptyMap())).isEmpty());
         }
     }
 
@@ -290,14 +299,14 @@ public class DefaultObjectWrapperTest {
                 if (idx >= expectedItems.length) {
                     fail("Number of items is more than the expected " + 
expectedItems.length);
                 }
-                assertEquals(expectedItems[idx], OW300.unwrap(actualItem));
+                assertEquals(expectedItems[idx], OW.unwrap(actualItem));
                 if (i == 1) {
                     // In the 2nd round we also test with two iterators in 
parallel.
                     // This 2nd iterator is also special in that its hasNext() 
is never called.
                     if (it2 == null) {
                         it2 = coll.iterator();
                     }
-                    assertEquals(expectedItems[idx], OW300.unwrap(it2.next()));
+                    assertEquals(expectedItems[idx], OW.unwrap(it2.next()));
                 }
                 idx++;
             }
@@ -317,7 +326,7 @@ public class DefaultObjectWrapperTest {
             testList.add("c");
             testList.add(new String[] { "x" });
 
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testList);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testList);
             assertTrue(seq instanceof DefaultListAdapter);
             assertFalse(seq instanceof TemplateCollectionModel); // Maybe 
changes at 2.4.0
             assertEquals(4, seq.size());
@@ -337,7 +346,7 @@ public class DefaultObjectWrapperTest {
             testList.add(null);
             testList.add("c");
 
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testList);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testList);
             assertTrue(seq instanceof DefaultListAdapter);
             assertTrue(seq instanceof TemplateCollectionModel); // Maybe 
changes at 2.4.0
             assertEquals(3, seq.size());
@@ -364,16 +373,16 @@ public class DefaultObjectWrapperTest {
 
     @Test
     public void testArrayAdapterTypes() throws TemplateModelException {
-        assertArrayAdapterClass("Object", OW300.wrap(new Object[] {}));
-        assertArrayAdapterClass("Object", OW300.wrap(new String[] {}));
-        assertArrayAdapterClass("byte", OW300.wrap(new byte[] {}));
-        assertArrayAdapterClass("short", OW300.wrap(new short[] {}));
-        assertArrayAdapterClass("int", OW300.wrap(new int[] {}));
-        assertArrayAdapterClass("long", OW300.wrap(new long[] {}));
-        assertArrayAdapterClass("float", OW300.wrap(new float[] {}));
-        assertArrayAdapterClass("double", OW300.wrap(new double[] {}));
-        assertArrayAdapterClass("boolean", OW300.wrap(new boolean[] {}));
-        assertArrayAdapterClass("char", OW300.wrap(new char[] {}));
+        assertArrayAdapterClass("Object", OW.wrap(new Object[] {}));
+        assertArrayAdapterClass("Object", OW.wrap(new String[] {}));
+        assertArrayAdapterClass("byte", OW.wrap(new byte[] {}));
+        assertArrayAdapterClass("short", OW.wrap(new short[] {}));
+        assertArrayAdapterClass("int", OW.wrap(new int[] {}));
+        assertArrayAdapterClass("long", OW.wrap(new long[] {}));
+        assertArrayAdapterClass("float", OW.wrap(new float[] {}));
+        assertArrayAdapterClass("double", OW.wrap(new double[] {}));
+        assertArrayAdapterClass("boolean", OW.wrap(new boolean[] {}));
+        assertArrayAdapterClass("char", OW.wrap(new char[] {}));
     }
 
     private void assertArrayAdapterClass(String adapterCompType, TemplateModel 
adaptedArray) {
@@ -388,7 +397,7 @@ public class DefaultObjectWrapperTest {
         {
             final String[] testArray = new String[] { "a", null, "c" };
 
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testArray);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testArray);
             assertEquals(3, seq.size());
             assertNull(seq.get(-1));
             assertEquals("a", ((TemplateScalarModel) 
seq.get(0)).getAsString());
@@ -399,7 +408,7 @@ public class DefaultObjectWrapperTest {
 
         {
             final int[] testArray = new int[] { 11, 22 };
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testArray);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testArray);
             assertEquals(2, seq.size());
             assertNull(seq.get(-1));
             assertEqualsAndSameClass(Integer.valueOf(11), 
((TemplateNumberModel) seq.get(0)).getAsNumber());
@@ -409,7 +418,7 @@ public class DefaultObjectWrapperTest {
 
         {
             final double[] testArray = new double[] { 11, 22 };
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testArray);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testArray);
             assertEquals(2, seq.size());
             assertNull(seq.get(-1));
             assertEqualsAndSameClass(Double.valueOf(11), 
((TemplateNumberModel) seq.get(0)).getAsNumber());
@@ -419,7 +428,7 @@ public class DefaultObjectWrapperTest {
 
         {
             final boolean[] testArray = new boolean[] { true, false };
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testArray);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testArray);
             assertEquals(2, seq.size());
             assertNull(seq.get(-1));
             assertEqualsAndSameClass(Boolean.valueOf(true), 
((TemplateBooleanModel) seq.get(0)).getAsBoolean());
@@ -429,7 +438,7 @@ public class DefaultObjectWrapperTest {
 
         {
             final char[] testArray = new char[] { 'a', 'b' };
-            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW300.wrap(testArray);
+            TemplateSequenceModel seq = (TemplateSequenceModel) 
OW.wrap(testArray);
             assertEquals(2, seq.size());
             assertNull(seq.get(-1));
             assertEquals("a", ((TemplateScalarModel) 
seq.get(0)).getAsString());
@@ -476,25 +485,25 @@ public class DefaultObjectWrapperTest {
             set.add("a");
             set.add("b");
             set.add("c");
-            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW300.wrap(set);
+            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW.wrap(set);
             assertTrue(coll instanceof DefaultNonListCollectionAdapter);
             assertEquals(3, coll.size());
             assertFalse(coll.isEmpty());
             assertCollectionTMEquals(coll, "a", "b", "c");
 
-            assertTrue(coll.contains(OW300.wrap("a")));
-            assertTrue(coll.contains(OW300.wrap("b")));
-            assertTrue(coll.contains(OW300.wrap("c")));
-            assertTrue(coll.contains(OW300.wrap("c")));
-            assertFalse(coll.contains(OW300.wrap("d")));
+            assertTrue(coll.contains(OW.wrap("a")));
+            assertTrue(coll.contains(OW.wrap("b")));
+            assertTrue(coll.contains(OW.wrap("c")));
+            assertTrue(coll.contains(OW.wrap("c")));
+            assertFalse(coll.contains(OW.wrap("d")));
             try {
-                assertFalse(coll.contains(OW300.wrap(1)));
+                assertFalse(coll.contains(OW.wrap(1)));
                 fail();
             } catch (TemplateModelException e) {
                 assertThat(e.getMessage(), containsString("Integer"));
             }
 
-            assertRoundtrip(OW300, set, DefaultNonListCollectionAdapter.class, 
TreeSet.class, "[a, b, c]");
+            assertRoundtrip(OW, set, DefaultNonListCollectionAdapter.class, 
TreeSet.class, "[a, b, c]");
             
             assertSizeThroughAPIModel(3, coll);
         }
@@ -504,24 +513,24 @@ public class DefaultObjectWrapperTest {
             final List<String> list = Collections.singletonList("b");
             set.add(list);
             set.add(null);
-            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW300.wrap(set);
+            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW.wrap(set);
             TemplateModelIterator it = coll.iterator();
             final TemplateModel tm1 = it.next();
-            Object obj1 = OW300.unwrap(tm1);
+            Object obj1 = OW.unwrap(tm1);
             final TemplateModel tm2 = it.next();
-            Object obj2 = OW300.unwrap(tm2);
+            Object obj2 = OW.unwrap(tm2);
             assertTrue(obj1 == null || obj2 == null);
             assertTrue(obj1 != null && obj1.equals(list) || obj2 != null && 
obj2.equals(list));
             assertTrue(tm1 instanceof DefaultListAdapter || tm2 instanceof 
DefaultListAdapter);
 
             List similarList = new ArrayList();
             similarList.add("b");
-            assertTrue(coll.contains(OW300.wrap(similarList)));
-            assertTrue(coll.contains(OW300.wrap(null)));
-            assertFalse(coll.contains(OW300.wrap("a")));
-            assertFalse(coll.contains(OW300.wrap(1)));
+            assertTrue(coll.contains(OW.wrap(similarList)));
+            assertTrue(coll.contains(OW.wrap(null)));
+            assertFalse(coll.contains(OW.wrap("a")));
+            assertFalse(coll.contains(OW.wrap(1)));
 
-            assertRoundtrip(OW300, set, DefaultNonListCollectionAdapter.class, 
HashSet.class, "[" + obj1 + ", "
+            assertRoundtrip(OW, set, DefaultNonListCollectionAdapter.class, 
HashSet.class, "[" + obj1 + ", "
                     + obj2 + "]");
         }
     }
@@ -531,14 +540,14 @@ public class DefaultObjectWrapperTest {
     public void testCollectionAdapterOutOfBounds() throws 
TemplateModelException {
         Set set = Collections.singleton(123);
 
-        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW300.wrap(set);
+        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW.wrap(set);
         TemplateModelIterator it = coll.iterator();
 
         for (int i = 0; i < 3; i++) {
             assertTrue(it.hasNext());
         }
 
-        assertEquals(123, OW300.unwrap(it.next()));
+        assertEquals(123, OW.unwrap(it.next()));
 
         for (int i = 0; i < 3; i++) {
             assertFalse(it.hasNext());
@@ -556,7 +565,7 @@ public class DefaultObjectWrapperTest {
         Set set = new HashSet();
         set.add(null);
 
-        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW300.wrap(set);
+        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) 
OW.wrap(set);
         assertEquals(1, coll.size());
         assertFalse(coll.isEmpty());
         assertNull(coll.iterator().next());
@@ -566,18 +575,18 @@ public class DefaultObjectWrapperTest {
     public void testIteratorWrapping() throws TemplateModelException, 
ClassNotFoundException {
         final List<String> list = ImmutableList.of("a", "b", "c");
         Iterator<String> it = list.iterator();
-        TemplateCollectionModel coll = (TemplateCollectionModel) 
OW300.wrap(it);
+        TemplateCollectionModel coll = (TemplateCollectionModel) OW.wrap(it);
 
-        assertRoundtrip(OW300, coll, DefaultIteratorAdapter.class, 
Iterator.class, null);
+        assertRoundtrip(OW, coll, DefaultIteratorAdapter.class, 
Iterator.class, null);
 
         TemplateModelIterator itIt = coll.iterator();
         TemplateModelIterator itIt2 = coll.iterator(); // used later
         assertTrue(itIt.hasNext());
-        assertEquals("a", OW300.unwrap(itIt.next()));
+        assertEquals("a", OW.unwrap(itIt.next()));
         assertTrue(itIt.hasNext());
-        assertEquals("b", OW300.unwrap(itIt.next()));
+        assertEquals("b", OW.unwrap(itIt.next()));
         assertTrue(itIt.hasNext());
-        assertEquals("c", OW300.unwrap(itIt.next()));
+        assertEquals("c", OW.unwrap(itIt.next()));
         assertFalse(itIt.hasNext());
         try {
             itIt.next();
@@ -614,20 +623,20 @@ public class DefaultObjectWrapperTest {
         Map sortedMapC = new TreeMap<>();
         sortedMapC.put('a', 1);
         
-        assertEquals(1, OW300.unwrap(((TemplateHashModel) 
OW300.wrap(hashMapS)).get("a")));
-        assertEquals(1, OW300.unwrap(((TemplateHashModel) 
OW300.wrap(hashMapC)).get("a")));
-        assertEquals(1, OW300.unwrap(((TemplateHashModel) 
OW300.wrap(sortedMapS)).get("a")));
+        assertEquals(1, OW.unwrap(((TemplateHashModel) 
OW.wrap(hashMapS)).get("a")));
+        assertEquals(1, OW.unwrap(((TemplateHashModel) 
OW.wrap(hashMapC)).get("a")));
+        assertEquals(1, OW.unwrap(((TemplateHashModel) 
OW.wrap(sortedMapS)).get("a")));
         try {
-            ((TemplateHashModel) OW300.wrap(sortedMapC)).get("a");
+            ((TemplateHashModel) OW.wrap(sortedMapC)).get("a");
         } catch (TemplateModelException e) {
             assertThat(e.getMessage(), containsStringIgnoringCase("String 
key"));
         }
         
-        assertNull(((TemplateHashModel) OW300.wrap(hashMapS)).get("b"));
-        assertNull(((TemplateHashModel) OW300.wrap(hashMapC)).get("b"));
-        assertNull(((TemplateHashModel) OW300.wrap(sortedMapS)).get("b"));
+        assertNull(((TemplateHashModel) OW.wrap(hashMapS)).get("b"));
+        assertNull(((TemplateHashModel) OW.wrap(hashMapC)).get("b"));
+        assertNull(((TemplateHashModel) OW.wrap(sortedMapS)).get("b"));
         try {
-            ((TemplateHashModel) OW300.wrap(sortedMapC)).get("b");
+            ((TemplateHashModel) OW.wrap(sortedMapC)).get("b");
         } catch (TemplateModelException e) {
             assertThat(e.getMessage(), containsStringIgnoringCase("String 
key"));
         }
@@ -639,7 +648,7 @@ public class DefaultObjectWrapperTest {
         
         String listingFTL = "<#list value as x>${x}<#sep>, </#list>";
         
-        DefaultObjectWrapper ow = OW300;
+        DefaultObjectWrapper ow = OW;
         TemplateModel tm = ow.wrap(iterable);
         assertThat(tm, instanceOf(TemplateCollectionModel.class));
         TemplateCollectionModel iterableTM = (TemplateCollectionModel) tm;
@@ -661,7 +670,7 @@ public class DefaultObjectWrapperTest {
             }
         }
 
-        assertTemplateOutput(OW300, iterable, listingFTL, "a, b, c");
+        assertTemplateOutput(OW, iterable, listingFTL, "a, b, c");
     }
     
     @Test
@@ -671,9 +680,27 @@ public class DefaultObjectWrapperTest {
         InputSource is = new InputSource();
         is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
         Document doc = db.parse(is);        
-        assertTrue(OW300.wrap(doc) instanceof TemplateNodeModel);
+        assertTrue(OW.wrap(doc) instanceof TemplateNodeModel);
     }
-    
+
+    @Test
+    public void testExposureLevel() throws Exception {
+        final DefaultObjectWrapper ow = new 
DefaultObjectWrapper(Configuration.VERSION_3_0_0);
+
+        TemplateHashModel tm = (TemplateHashModel) ow.wrap(new TestBean());
+        assertNotNull(tm.get("hashCode"));
+        assertNotNull(tm.get("class"));
+        ow.setExposureLevel(DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY);
+        assertNull(tm.get("hashCode"));
+        assertNotNull(tm.get("class"));
+        ow.setExposureLevel(DefaultObjectWrapper.EXPOSE_NOTHING);
+        assertNull(tm.get("hashCode"));
+        assertNull(tm.get("class"));
+        ow.setExposureLevel(DefaultObjectWrapper.EXPOSE_ALL);
+        assertNotNull(tm.get("hashCode"));
+        assertNotNull(tm.get("class"));
+    }
+
     private void assertSizeThroughAPIModel(int expectedSize, TemplateModel 
normalModel) throws TemplateModelException {
         if (!(normalModel instanceof TemplateModelWithAPISupport)) {
             fail(); 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithShortedMethods.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithShortedMethods.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithShortedMethods.java
new file mode 100644
index 0000000..ec16f54
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithShortedMethods.java
@@ -0,0 +1,41 @@
+/*
+ * 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.Configuration;
+import org.apache.freemarker.core.Version;
+
+/**
+ * Used so that the order in which the methods are added to the introspection 
cache is deterministic. 
+ */
+public abstract class DefaultObjectWrapperWithShortedMethods extends 
DefaultObjectWrapper {
+    
+    public DefaultObjectWrapperWithShortedMethods(boolean desc) {
+        super(Configuration.VERSION_3_0_0);
+        setMethodSorter(new AlphabeticalMethodSorter(desc));
+    }
+
+    public DefaultObjectWrapperWithShortedMethods(Version 
incompatibleImprovements, boolean desc) {
+        super(incompatibleImprovements);
+        setMethodSorter(new AlphabeticalMethodSorter(desc));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithSortedMethods.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithSortedMethods.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithSortedMethods.java
new file mode 100644
index 0000000..a10247d
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperWithSortedMethods.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Version;
+
+public class DefaultObjectWrapperWithSortedMethods extends 
DefaultObjectWrapper {
+    
+    public DefaultObjectWrapperWithSortedMethods(boolean desc) {
+        this(Configuration.VERSION_3_0_0, desc);
+    }
+
+    public DefaultObjectWrapperWithSortedMethods(Version 
incompatibleImprovements, boolean desc) {
+        super(incompatibleImprovements);
+        setMethodSorter(this, desc);
+    }
+    
+    static void setMethodSorter(DefaultObjectWrapper ow, boolean desc) {
+        ow.setMethodSorter(new AlphabeticalMethodSorter(desc));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java 
b/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java
new file mode 100644
index 0000000..b608cda
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 static org.junit.Assert.*;
+
+import java.util.ArrayList;
+
+import org.apache.freemarker.core.Configuration;
+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.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class EnumModelsTest {
+    
+    @Test
+    public void modelCaching() throws Exception {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper(Configuration.VERSION_3_0_0);
+        TemplateHashModel enums = ow.getEnumModels();
+        TemplateHashModel e = (TemplateHashModel) enums.get(E.class.getName());
+        assertNotNull(e);
+        assertNotNull(e.get("A"));
+        assertNotNull(e.get("B"));
+        assertNull(e.get("C"));
+
+        try {
+            enums.get("no.such.ClassExists");
+            fail();
+        } catch (TemplateModelException ex) {
+            assertTrue(ex.getCause() instanceof ClassNotFoundException);
+        }
+        
+        TemplateModel a = e.get("A");
+        assertTrue(a instanceof TemplateScalarModel);
+        assertTrue(a instanceof TemplateHashModel);
+        assertEquals(((TemplateScalarModel) a).getAsString(), "ts:A");
+        TemplateMethodModelEx nameMethod = (TemplateMethodModelEx) 
((TemplateHashModel) a).get("name");
+        assertEquals(((TemplateScalarModel) nameMethod.exec(new 
ArrayList())).getAsString(), "A");
+        
+        assertSame(e, enums.get(E.class.getName()));
+        
+        ow.clearClassIntrospecitonCache();
+        TemplateHashModel eAfterClean = (TemplateHashModel) 
enums.get(E.class.getName());
+        assertNotSame(e, eAfterClean);
+        assertSame(eAfterClean, enums.get(E.class.getName()));
+        assertNotNull(eAfterClean.get("A"));
+        assertNotNull(eAfterClean.get("B"));
+        assertNull(eAfterClean.get("C"));
+    }
+    
+    public enum E {
+        A, B;
+
+        @Override
+        public String toString() {
+            return "ts:" + super.toString();
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/ErrorMessagesTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/ErrorMessagesTest.java 
b/src/test/java/org/apache/freemarker/core/model/impl/ErrorMessagesTest.java
new file mode 100644
index 0000000..3bd0363
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/ErrorMessagesTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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 static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+import java.util.Date;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.TemplateHTMLOutputModel;
+import org.junit.Test;
+
+public class ErrorMessagesTest {
+
+    @Test
+    public void getterMessage() throws TemplateModelException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+        TemplateHashModel thm= (TemplateHashModel) ow.wrap(new TestBean());
+        
+        try {
+            thm.get("foo");
+        } catch (TemplateModelException e) {
+            e.printStackTrace();
+            final String msg = e.getMessage();
+            assertThat(msg, containsString("\"foo\""));
+            assertThat(msg, containsString("existing sub-variable"));
+        }
+        assertNull(thm.get("bar"));
+    }
+    
+    @Test
+    public void markupOutputParameter() throws Exception {
+        TemplateHTMLOutputModel html = 
HTMLOutputFormat.INSTANCE.fromMarkup("<p>a");
+
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapperBuilder(Configuration.VERSION_3_0_0).build();
+        TemplateHashModel thm = (TemplateHashModel) ow.wrap(new TestBean());
+        
+        {
+            TemplateMethodModelEx m = (TemplateMethodModelEx) thm.get("m1");
+            try {
+                m.exec(Collections.singletonList(html));
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), allOf(
+                        containsString("String"), containsString("convert"), 
containsString("markup_output"),
+                        containsString("Tip:"), 
containsString("?markup_string")));
+            }
+        }
+        
+        {
+            TemplateMethodModelEx m = (TemplateMethodModelEx) thm.get("m2");
+            try {
+                m.exec(Collections.singletonList(html));
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), allOf(
+                        containsString("Date"), containsString("convert"), 
containsString("markup_output"),
+                        not(containsString("?markup_string"))));
+            }
+        }
+        
+        for (String methodName : new String[] { "mOverloaded", "mOverloaded3" 
}) {
+            TemplateMethodModelEx m = (TemplateMethodModelEx) 
thm.get(methodName);
+            try {
+                m.exec(Collections.singletonList(html));
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), allOf(
+                        containsString("No compatible overloaded"),
+                        containsString("String"), 
containsString("markup_output"),
+                        containsString("Tip:"), 
containsString("?markup_string")));
+            }
+        }
+        
+        {
+            TemplateMethodModelEx m = (TemplateMethodModelEx) 
thm.get("mOverloaded2");
+            try {
+                m.exec(Collections.singletonList(html));
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), allOf(
+                        containsString("No compatible overloaded"),
+                        containsString("Integer"), 
containsString("markup_output"),
+                        not(containsString("?markup_string"))));
+            }
+        }
+        
+        {
+            TemplateMethodModelEx m = (TemplateMethodModelEx) 
thm.get("mOverloaded4");
+            Object r = m.exec(Collections.singletonList(html));
+            if (r instanceof TemplateScalarModel) {
+                r = ((TemplateScalarModel) r).getAsString();
+            }
+            assertEquals("<p>a", r);
+        }
+    }
+    
+    public static class TestBean {
+        
+        public String getFoo() {
+            throw new RuntimeException("Dummy");
+        }
+        
+        public void m1(String s) {
+            // nop
+        }
+
+        public void m2(Date s) {
+            // nop
+        }
+
+        public void mOverloaded(String s) {
+            // nop
+        }
+
+        public void mOverloaded(Date d) {
+            // nop
+        }
+
+        public void mOverloaded2(Integer n) {
+            // nop
+        }
+
+        public void mOverloaded2(Date d) {
+            // nop
+        }
+
+        public void mOverloaded3(String... s) {
+            // nop
+        }
+
+        public void mOverloaded3(Date d) {
+            // nop
+        }
+        
+        public String mOverloaded4(String s) {
+            return s;
+        }
+
+        public String mOverloaded4(TemplateHTMLOutputModel s) throws 
TemplateModelException {
+            return s.getOutputFormat().getMarkupString(s);
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/FineTuneMethodAppearanceTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/FineTuneMethodAppearanceTest.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/FineTuneMethodAppearanceTest.java
new file mode 100644
index 0000000..dfd554d
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/FineTuneMethodAppearanceTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import static org.junit.Assert.*;
+
+import org.apache.freemarker.core.Configuration;
+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.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FineTuneMethodAppearanceTest {
+
+    @Test
+    public void newWayOfConfiguring() throws TemplateModelException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper(Configuration.VERSION_3_0_0);
+        
ow.setMethodAppearanceFineTuner(GetlessMethodsAsPropertyGettersRule.INSTANCE);
+        ow.setExposeFields(true);
+        checkIfProperlyWrapped(ow.wrap(new C()));
+    }
+    
+    private void checkIfProperlyWrapped(TemplateModel tm) throws 
TemplateModelException {
+        TemplateHashModel thm = (TemplateHashModel) tm;
+        assertEquals("v1", ((TemplateScalarModel) 
thm.get("v1")).getAsString());
+        assertEquals("v2()", ((TemplateScalarModel) 
thm.get("v2")).getAsString());
+        assertEquals("getV3()", ((TemplateScalarModel) 
thm.get("v3")).getAsString());
+        assertTrue(thm.get("getV3") instanceof TemplateMethodModelEx);
+    }
+    
+    static public class C {
+        
+        public String v1 = "v1";
+
+        public String v2 = "v2";
+        public String v2() { return "v2()"; }
+        
+        public String v3() { return "v3()"; }
+        public String getV3() { return "getV3()"; }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/GetlessMethodsAsPropertyGettersRule.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/GetlessMethodsAsPropertyGettersRule.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/GetlessMethodsAsPropertyGettersRule.java
new file mode 100644
index 0000000..d62d355
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/GetlessMethodsAsPropertyGettersRule.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+
+class GetlessMethodsAsPropertyGettersRule implements 
MethodAppearanceFineTuner, SingletonCustomizer {
+    
+    static final GetlessMethodsAsPropertyGettersRule INSTANCE = new 
GetlessMethodsAsPropertyGettersRule();
+    
+    // Can't be constructed from outside
+    private GetlessMethodsAsPropertyGettersRule() { }
+
+    @Override
+    public void process(
+            DecisionInput in, Decision out) {
+        legacyProcess(in.getContainingClass(), in.getMethod(), out);
+    }
+
+    /** This only exists as the tests need to call this through the deprecated 
method too. */
+    public void legacyProcess(
+            Class clazz, Method m, Decision decision) {
+        if (m.getDeclaringClass() != Object.class
+                && m.getReturnType() != void.class
+                && m.getParameterTypes().length == 0) {
+            String mName = m.getName();
+            if (!looksLikePropertyReadMethod(mName)) {
+                decision.setExposeMethodAs(null);
+                try {
+                    decision.setExposeAsProperty(new PropertyDescriptor(
+                            mName, clazz, mName, null));
+                } catch (IntrospectionException e) {  // Won't happen...
+                    throw new RuntimeException(e); 
+                }
+            }
+        }
+    }
+    
+    private static boolean looksLikePropertyReadMethod(String name) {
+        final int verbEnd;
+        if (name.startsWith("get")) verbEnd = 3;
+        else if (name.startsWith("is")) verbEnd = 2;
+        else return false;
+        
+        return name.length() == verbEnd || 
Character.isUpperCase(name.charAt(verbEnd));
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/IsApplicableTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/IsApplicableTest.java 
b/src/test/java/org/apache/freemarker/core/model/impl/IsApplicableTest.java
new file mode 100644
index 0000000..e7c473b
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/model/impl/IsApplicableTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class IsApplicableTest extends TestCase {
+
+    public IsApplicableTest(String name) {
+        super(name);
+    }
+    
+    public void testSingle() {
+        ArgumentTypes ats = new ArgumentTypes(new Object[] { new Object() });
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, String.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+        
+        ats = new ArgumentTypes(new Object[] { "" });
+        assertApplicable(ats, Object.class);
+        assertApplicable(ats, CharSequence.class);
+        assertApplicable(ats, String.class);
+        assertNotApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+
+        ats = new ArgumentTypes(new Object[] { 1 });
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, String.class);
+        assertNotApplicable(ats, Short.class);
+        assertNotApplicable(ats, Short.TYPE);
+        assertApplicable(ats, Integer.class);
+        assertApplicable(ats, Integer.TYPE);
+        assertApplicable(ats, Float.class);
+        assertApplicable(ats, Float.TYPE);
+        assertApplicable(ats, Double.class);
+        assertApplicable(ats, Double.TYPE);
+        assertApplicable(ats, BigDecimal.class);
+        assertApplicable(ats, BigInteger.class);
+
+        ats = new ArgumentTypes(new Object[] { new 
OverloadedNumberUtil.IntegerOrByte(1, (byte) 1) });
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, String.class);
+        assertApplicable(ats, Short.class);
+        assertApplicable(ats, Short.TYPE);
+        assertApplicable(ats, Integer.class);
+        assertApplicable(ats, Integer.TYPE);
+        assertApplicable(ats, Float.class);
+        assertApplicable(ats, Float.TYPE);
+        assertApplicable(ats, Double.class);
+        assertApplicable(ats, Double.TYPE);
+        assertApplicable(ats, BigDecimal.class);
+        assertApplicable(ats, BigInteger.class);
+        
+        ats = new ArgumentTypes(new Object[] { 1.0f });
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, String.class);
+        assertNotApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+        assertApplicable(ats, Float.class);
+        assertApplicable(ats, Float.TYPE);
+        assertApplicable(ats, Double.class);
+        assertApplicable(ats, Double.TYPE);
+        assertApplicable(ats, BigDecimal.class);
+        assertNotApplicable(ats, BigInteger.class);
+        
+        ats = new ArgumentTypes(new Object[] { null });
+        assertApplicable(ats, Object.class);
+        assertApplicable(ats, String.class);
+        assertApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+        assertNotApplicable(ats, Boolean.TYPE);
+        assertNotApplicable(ats, Object.class, Object.class);
+        assertNotApplicable(ats);
+    }
+    
+    public void testMulti() {
+        ArgumentTypes ats = new ArgumentTypes(new Object[] { new Object(), "", 
1, true });
+        assertApplicable(ats, Object.class, Object.class, Object.class, 
Object.class);
+        assertApplicable(ats, Object.class, String.class, Number.class, 
Boolean.class);
+        assertApplicable(ats, Object.class, CharSequence.class, Integer.class, 
Serializable.class);
+        assertApplicable(ats, Object.class, Comparable.class, Integer.TYPE, 
Serializable.class);
+        assertNotApplicable(ats, Object.class, String.class, Number.class, 
Number.class);
+        assertNotApplicable(ats, Object.class, StringBuilder.class, 
Number.class, Boolean.class);
+        assertNotApplicable(ats, int.class, Object.class, Object.class, 
Object.class);
+        assertNotApplicable(ats, Object.class, Object.class, Object.class);
+        assertNotApplicable(ats, Object.class, Object.class, Object.class, 
Object.class, Object.class);
+    }    
+
+    public void testNoParam() {
+        ArgumentTypes ats = new ArgumentTypes(new Object[] { });
+        assertApplicable(ats);
+        assertNotApplicable(ats, Object.class);
+    }
+
+    public void testVarags() {
+        Object[][] argLists = new Object[][] {
+            new Object[] { "", 1, 2, 3 },
+            new Object[] { "", 1, (byte) 2, 3 },
+            new Object[] { "", 1},
+            new Object[] { "" },
+        };
+        for (Object[] args : argLists) {
+            ArgumentTypes ats = new ArgumentTypes(args);
+            assertApplicable(ats, true, String.class, int[].class);
+            assertApplicable(ats, true, String.class, Integer[].class);
+            assertApplicable(ats, true, Object.class, Comparable[].class);
+            assertApplicable(ats, true, Object.class, Object[].class);
+            assertNotApplicable(ats, true, StringBuilder.class, int[].class);
+            if (args.length > 1) {
+                assertNotApplicable(ats, true, String.class, String[].class);
+            } else {
+                assertApplicable(ats, true, String.class, String[].class);
+            }
+        }
+    }
+    
+    private void assertNotApplicable(ArgumentTypes ats, Class... paramTypes) {
+        assertNotApplicable(ats, false, paramTypes);
+    }
+    
+    private void assertNotApplicable(ArgumentTypes ats, boolean varargs, 
Class... paramTypes) {
+        List tested = new ArrayList();
+        tested.add(new ReflectionCallableMemberDescriptor((Method) null, 
paramTypes));
+        if (ats.getApplicables(tested, varargs).size() != 0) {
+            fail("Parameter types were applicable");
+        }
+    }
+
+    private void assertApplicable(ArgumentTypes ats, Class<?>... paramTypes) {
+        assertApplicable(ats, false, paramTypes);
+    }
+    
+    private void assertApplicable(ArgumentTypes ats, boolean varargs, 
Class<?>... paramTypes) {
+        List tested = new ArrayList();
+        tested.add(new ReflectionCallableMemberDescriptor((Method) null, 
paramTypes));
+        if (ats.getApplicables(tested, varargs).size() != 1) {
+            fail("Parameter types weren't applicable");
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/test/java/org/apache/freemarker/core/model/impl/IsMoreSpecificParameterTypeTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/IsMoreSpecificParameterTypeTest.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/IsMoreSpecificParameterTypeTest.java
new file mode 100644
index 0000000..fc45156
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/core/model/impl/IsMoreSpecificParameterTypeTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+public class IsMoreSpecificParameterTypeTest extends TestCase {
+
+    public IsMoreSpecificParameterTypeTest(String name) {
+        super(name);
+    }
+    
+    public void testFixed() {
+        assertEquals(1, 
_MethodUtil.isMoreOrSameSpecificParameterType(String.class, String.class, true, 
0));
+        assertEquals(1, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, int.class, true, 0));
+        
+        assertEquals(2, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Integer.class, true, 
0));
+        assertEquals(2, 
_MethodUtil.isMoreOrSameSpecificParameterType(boolean.class, Boolean.class, 
true, 0));
+        
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, long.class, true, 0));
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, double.class, true, 
0));
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Long.class, true, 0));
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Double.class, true, 
0));
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, Long.class, true, 
0));
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, Double.class, 
true, 0));
+
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(HashMap.class, Map.class, true, 
0));
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(String.class, CharSequence.class, 
true, 0));
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, Number.class, 
true, 0));
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Number.class, true, 
0));
+        
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Map.class, String.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, int.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Boolean.class, boolean.class, 
true, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, boolean.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Boolean.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, String.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, BigDecimal.class, 
true, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Long.class, Integer.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(long.class, Integer.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Long.class, int.class, true, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, BigDecimal.class, 
true, 0));
+    }
+    
+    public void testBuggy() {
+        assertEquals(1, 
_MethodUtil.isMoreOrSameSpecificParameterType(String.class, String.class, 
false, 0));
+        assertEquals(1, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, int.class, false, 0));
+        
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Integer.class, false, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(boolean.class, Boolean.class, 
false, 0));
+        
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, long.class, false, 0));
+        assertEquals(3, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, double.class, false, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Long.class, false, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Double.class, false, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, Long.class, false, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, Double.class, 
false, 0));
+
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(HashMap.class, Map.class, false, 
0));
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(String.class, CharSequence.class, 
false, 0));
+        assertEquals(4, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, Number.class, 
false, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Number.class, false, 
0));
+        
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Map.class, String.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, int.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Boolean.class, boolean.class, 
true, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, boolean.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, Boolean.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, String.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(int.class, BigDecimal.class, 
true, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Long.class, Integer.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(long.class, Integer.class, true, 
0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Long.class, int.class, true, 0));
+        assertEquals(0, 
_MethodUtil.isMoreOrSameSpecificParameterType(Integer.class, BigDecimal.class, 
true, 0));
+    }
+
+}


Reply via email to