freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [41/51] [abbrv] [partial] incubator-freemarker git commit: Restructured project so that freemarker-test-utils depends on freemarker-core (and hence can provide common classes for testing templates, and can use utility classes defined in the core). As a c
Date Mon, 15 May 2017 21:23:57 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/CommonSupertypeForUnwrappingHintTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/CommonSupertypeForUnwrappingHintTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/CommonSupertypeForUnwrappingHintTest.java
new file mode 100644
index 0000000..ef15dae
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/CommonSupertypeForUnwrappingHintTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+
+import junit.framework.TestCase;
+
+public class CommonSupertypeForUnwrappingHintTest extends TestCase {
+    
+    final OverloadedMethodsSubset oms = new DummyOverloadedMethodsSubset();
+
+    public CommonSupertypeForUnwrappingHintTest(String name) {
+        super(name);
+    }
+
+    public void testInterfaces() {
+        assertEquals(Serializable.class, oms.getCommonSupertypeForUnwrappingHint(String.class, Number.class));
+        assertEquals(C1I1.class, oms.getCommonSupertypeForUnwrappingHint(C2ExtC1I1.class, C3ExtC1I1.class));
+        assertEquals(Object.class, oms.getCommonSupertypeForUnwrappingHint(C3I1I2.class, C4I1I2.class));
+        assertEquals(I1.class, oms.getCommonSupertypeForUnwrappingHint(C3I1I2.class, C5I1.class));
+        assertEquals(I1.class, oms.getCommonSupertypeForUnwrappingHint(C3I1I2.class, I1.class));
+        assertEquals(I2.class, oms.getCommonSupertypeForUnwrappingHint(C3I1I2.class, I2.class));
+        assertEquals(I1.class, oms.getCommonSupertypeForUnwrappingHint(I1I2.class, I1.class));
+        assertEquals(I2.class, oms.getCommonSupertypeForUnwrappingHint(I1I2.class, I2.class));
+        assertEquals(CharSequence.class, oms.getCommonSupertypeForUnwrappingHint(String.class, StringBuilder.class));
+        assertEquals(C6.class, oms.getCommonSupertypeForUnwrappingHint(C7ExtC6I1.class, C8ExtC6I1.class));
+    }
+
+    public void testArrayAndOther() {
+        testArrayAndOther(oms);
+    }
+    
+    /** These will be the same with oms and buggy: */
+    private void testArrayAndOther(OverloadedMethodsSubset oms) {
+        assertEquals(Serializable.class, oms.getCommonSupertypeForUnwrappingHint(int[].class, String.class));
+        assertEquals(Serializable.class, oms.getCommonSupertypeForUnwrappingHint(Object[].class, String.class));
+        
+        assertEquals(Object.class, oms.getCommonSupertypeForUnwrappingHint(int[].class, List.class));
+        assertEquals(Object.class, oms.getCommonSupertypeForUnwrappingHint(Object[].class, List.class));
+        
+        assertEquals(int[].class, oms.getCommonSupertypeForUnwrappingHint(int[].class, int[].class));
+        assertEquals(Object[].class, oms.getCommonSupertypeForUnwrappingHint(Object[].class, Object[].class));
+    }
+    
+    public void testArrayAndDifferentArray() {
+        assertEquals(Serializable.class, oms.getCommonSupertypeForUnwrappingHint(int[].class, Object[].class));
+        assertEquals(Serializable.class, oms.getCommonSupertypeForUnwrappingHint(int[].class, long[].class));
+    }
+    
+    public void testPrimitive() {
+        assertEquals(Integer.class, oms.getCommonSupertypeForUnwrappingHint(int.class, Integer.class));
+        assertEquals(Integer.class, oms.getCommonSupertypeForUnwrappingHint(Integer.class, int.class));
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(int.class, Long.class));
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(Long.class, int.class));
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(Integer.class, long.class));
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(long.class, Integer.class));
+        assertEquals(Boolean.class, oms.getCommonSupertypeForUnwrappingHint(boolean.class, Boolean.class));
+        assertEquals(Boolean.class, oms.getCommonSupertypeForUnwrappingHint(Boolean.class, boolean.class));
+        assertEquals(Character.class, oms.getCommonSupertypeForUnwrappingHint(char.class, Character.class));
+        assertEquals(Character.class, oms.getCommonSupertypeForUnwrappingHint(Character.class, char.class));
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(int.class, short.class));
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(short.class, int.class));
+    }
+
+    public void testMisc() {
+        assertEquals(Number.class, oms.getCommonSupertypeForUnwrappingHint(Long.class, Integer.class));
+        assertEquals(char.class, oms.getCommonSupertypeForUnwrappingHint(char.class, char.class));
+        assertEquals(Integer.class, oms.getCommonSupertypeForUnwrappingHint(Integer.class, Integer.class));
+        assertEquals(String.class, oms.getCommonSupertypeForUnwrappingHint(String.class, String.class));
+    }
+    
+    static interface I1 { };
+    static class C1I1 implements I1 { };
+    static class C2ExtC1I1 extends C1I1 { };
+    static class C3ExtC1I1 extends C1I1 { };
+    static interface I2 { };
+    static class C3I1I2 implements I1, I2 { };
+    static class C4I1I2 implements I1, I2 { };
+    static class C5I1 implements I1 { };
+    static interface I1I2 extends I1, I2 { };
+    static class C6 { };
+    static class C7ExtC6I1 extends C6 implements I1 { };
+    static class C8ExtC6I1 extends C6 implements I1 { };
+    
+    private static class DummyOverloadedMethodsSubset extends OverloadedMethodsSubset {
+
+        DummyOverloadedMethodsSubset() {
+            super();
+        }
+
+        @Override
+        Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc) {
+            return memberDesc.getParamTypes();
+        }
+
+        @Override
+        void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
+            // Do nothing
+        }
+
+        @Override
+        MaybeEmptyMemberAndArguments getMemberAndArguments(List tmArgs, DefaultObjectWrapper w) throws TemplateModelException {
+            throw new RuntimeException("Not implemented in this dummy.");
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperDesc.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperDesc.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperDesc.java
new file mode 100644
index 0000000..0ed6e7c
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperDesc.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+public class DefaultObjectWrapperDesc extends DefaultObjectWrapper {
+
+    public DefaultObjectWrapperDesc() {
+        super(new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .methodSorter(new AlphabeticalMethodSorter(true)), true);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperInc.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperInc.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperInc.java
new file mode 100644
index 0000000..eb2cda0
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperInc.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+public class DefaultObjectWrapperInc extends DefaultObjectWrapper {
+
+    public DefaultObjectWrapperInc() {
+        super(new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .methodSorter(new AlphabeticalMethodSorter(false)), true);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperModelFactoryRegistrationTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperModelFactoryRegistrationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperModelFactoryRegistrationTest.java
new file mode 100644
index 0000000..5c2f653
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperModelFactoryRegistrationTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.lang.ref.Reference;
+
+import org.apache.freemarker.core.Configuration;
+import org.junit.Test;
+
+public class DefaultObjectWrapperModelFactoryRegistrationTest {
+
+    @Test
+    public void introspectionSettingChanges() {
+        DefaultObjectWrapper ow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).usePrivateCaches(true)
+                .build();
+        ClassIntrospector ci1 = ow.getClassIntrospector();
+        checkRegisteredModelFactories(ci1, ow.getStaticModels(), ow.getEnumModels());
+    }
+
+    private void checkRegisteredModelFactories(ClassIntrospector ci, Object... expected) {
+        Object[] actualRefs = ci.getRegisteredModelFactoriesSnapshot();
+
+        scanActuals: for (Object actaulRef : actualRefs) {
+            Object actualItem = ((Reference) actaulRef).get();
+            for (Object expectedItem : expected) {
+                if (actualItem == expectedItem) {
+                    continue scanActuals;
+                }
+            }
+            fail("Actual item " + actualItem + " is not among the expected items");
+        }
+
+        scanExpecteds: for (Object expectedItem : expected) {
+            for (Object ref : actualRefs) {
+                Object actualItem = ((Reference) ref).get();
+                if (actualItem == expectedItem) {
+                    continue scanExpecteds;
+                }
+            }
+            fail("Expected item " + expectedItem + " is not among the actual items");
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
new file mode 100644
index 0000000..d6f8177
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperSingletonsTest.java
@@ -0,0 +1,675 @@
+/*
+ * 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.TestUtil;
+
+import junit.framework.TestCase;
+
+public class DefaultObjectWrapperSingletonsTest extends TestCase {
+
+    public DefaultObjectWrapperSingletonsTest(String name) {
+        super(name);
+    }
+    
+    @Override
+    protected void setUp() throws Exception {
+        DefaultObjectWrapper.Builder.clearInstanceCache();
+    }
+
+    public void testBuilderEqualsAndHash() throws Exception {
+        assertEquals(Configuration.VERSION_3_0_0, new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).getIncompatibleImprovements());
+        try {
+            new DefaultObjectWrapper.Builder(TestUtil.getClosestFutureVersion());
+            fail("Maybe you need to update this test for the new FreeMarker version");
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("upgrade"));
+        }
+
+        DefaultObjectWrapper.Builder builder1;
+        DefaultObjectWrapper.Builder builder2;
+        
+        builder1 = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+        builder2 = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+        assertEquals(builder1, builder2);
+        
+        builder1.setExposeFields(true);
+        assertNotEquals(builder1, builder2);
+        assertNotEquals(builder1.hashCode(), builder2.hashCode());
+        builder2.setExposeFields(true);
+        assertEquals(builder1, builder2);
+        assertEquals(builder1.hashCode(), builder2.hashCode());
+        
+        builder1.setExposureLevel(0);
+        assertNotEquals(builder1, builder2);
+        assertNotEquals(builder1.hashCode(), builder2.hashCode());
+        builder2.setExposureLevel(0);
+        assertEquals(builder1, builder2);
+        assertEquals(builder1.hashCode(), builder2.hashCode());
+        
+        builder1.setExposureLevel(1);
+        assertNotEquals(builder1, builder2);
+        assertNotEquals(builder1.hashCode(), builder2.hashCode());
+        builder2.setExposureLevel(1);
+        assertEquals(builder1, builder2);
+        assertEquals(builder1.hashCode(), builder2.hashCode());
+        
+        builder1.setDefaultDateType(TemplateDateModel.DATE);
+        assertNotEquals(builder1, builder2);
+        builder2.setDefaultDateType(TemplateDateModel.DATE);
+        assertEquals(builder1, builder2);
+        assertEquals(builder1.hashCode(), builder2.hashCode());
+        
+        builder1.setStrict(true);
+        assertNotEquals(builder1, builder2);
+        assertNotEquals(builder1.hashCode(), builder2.hashCode());
+        builder2.setStrict(true);
+        assertEquals(builder1, builder2);
+        assertEquals(builder1.hashCode(), builder2.hashCode());
+
+        builder1.setUseModelCache(true);
+        assertNotEquals(builder1, builder2);
+        assertNotEquals(builder1.hashCode(), builder2.hashCode());
+        builder2.setUseModelCache(true);
+        assertEquals(builder1, builder2);
+        assertEquals(builder1.hashCode(), builder2.hashCode());
+        
+        AlphabeticalMethodSorter ms = new AlphabeticalMethodSorter(true);
+        builder1.setMethodSorter(ms);
+        assertNotEquals(builder1, builder2);
+        builder2.setMethodSorter(ms);
+        assertEquals(builder1, builder2);
+        assertEquals(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);
+        assertEquals(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());
+            assertFalse(ow.isStrict());
+            assertTrue(ow.getUseModelCache());
+            assertEquals(TemplateDateModel.UNKNOWN, ow.getDefaultDateType());
+            assertSame(ow, ow.getOuterIdentity());
+            assertTrue(ow.isClassIntrospectionCacheRestricted());
+            assertNull(ow.getMethodAppearanceFineTuner());
+            assertNull(ow.getMethodSorter());
+
+            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());
+            assertFalse(ow.getUseModelCache());
+
+            assertSame(ow, getDefaultObjectWrapperWithSetting(Configuration.VERSION_3_0_0, false));
+            
+            hardReferences.add(ow);
+        }
+        
+        {
+            DefaultObjectWrapper.Builder factory = new DefaultObjectWrapper.Builder(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());
+            assertEquals(DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY, ow1.getExposureLevel());
+            assertFalse(ow1.isStrict());
+            assertEquals(TemplateDateModel.UNKNOWN, ow1.getDefaultDateType());
+            assertSame(ow1, ow1.getOuterIdentity());
+            
+            hardReferences.add(ow1);
+        }
+        
+        {
+            DefaultObjectWrapper.Builder factory = new DefaultObjectWrapper.Builder(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.isExposeFields());
+            
+            hardReferences.add(ow1);
+        }
+        
+        {
+            DefaultObjectWrapper.Builder factory = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+            factory.setStrict(true);
+            factory.setDefaultDateType(TemplateDateModel.DATETIME);
+            factory.setOuterIdentity(new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+            DefaultObjectWrapper ow = factory.build();
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertTrue(ow.isStrict());
+            assertEquals(TemplateDateModel.DATETIME, ow.getDefaultDateType());
+            assertSame(RestrictedObjectWrapper.class, ow.getOuterIdentity().getClass());
+            
+            hardReferences.add(ow);
+        }
+        
+        // Effect of reference and cache clearings:
+        {
+            DefaultObjectWrapper bw1 = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(5, getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            clearDefaultObjectWrapperInstanceCacheReferences(false);
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(0, getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            DefaultObjectWrapper bw2 = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertNotSame(bw1, bw2);
+            assertEquals(5, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(1, getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            assertSame(bw2, new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+            assertEquals(1, getDefaultObjectWrapperNonClearedInstanceCacheSize());
+            
+            clearDefaultObjectWrapperInstanceCacheReferences(true);
+            DefaultObjectWrapper bw3 = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertNotSame(bw2, bw3);
+            assertEquals(1, getDefaultObjectWrapperInstanceCacheSize());
+            assertEquals(1, getDefaultObjectWrapperNonClearedInstanceCacheSize());
+        }
+
+        {
+            DefaultObjectWrapper.Builder factory = new DefaultObjectWrapper.Builder(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) {
+        DefaultObjectWrapper.Builder f = new DefaultObjectWrapper.Builder(ici);
+        f.setUseModelCache(useModelCache);
+        return f.build();
+    }
+
+    public void testMultipleTCCLs() {
+        List<DefaultObjectWrapper> hardReferences = new LinkedList<>();
+        
+        assertEquals(0, getDefaultObjectWrapperInstanceCacheSize());
+        
+        DefaultObjectWrapper bw1 = new DefaultObjectWrapper.Builder(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 DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertEquals(2, getDefaultObjectWrapperInstanceCacheSize());
+            hardReferences.add(bw2);
+            
+            assertNotSame(bw1, bw2);
+            assertSame(bw2, new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+        } finally {
+            Thread.currentThread().setContextClassLoader(oldTCCL);
+        }
+        
+        assertSame(bw1, new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+        assertEquals(2, getDefaultObjectWrapperInstanceCacheSize());
+
+        DefaultObjectWrapper bw3;
+        Thread.currentThread().setContextClassLoader(newTCCL);
+        try {
+            assertSame(bw2, new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+            
+            DefaultObjectWrapper.Builder bwb = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
+            bwb.setExposeFields(true);
+            bw3 = bwb.build();
+            assertEquals(3, getDefaultObjectWrapperInstanceCacheSize());
+            hardReferences.add(bw3);
+        } finally {
+            Thread.currentThread().setContextClassLoader(oldTCCL);
+        }
+        
+        {
+            DefaultObjectWrapper.Builder bwb = new DefaultObjectWrapper.Builder(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.Builder(Configuration.VERSION_3_0_0)
+                .usePrivateCaches(true).build().isClassIntrospectionCacheRestricted());
+        assertTrue(new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .build().isClassIntrospectionCacheRestricted());
+        
+        ClassIntrospector.Builder.clearInstanceCache();
+        DefaultObjectWrapper.Builder.clearInstanceCache();
+        checkClassIntrospectorCacheSize(0);
+        
+        List<DefaultObjectWrapper> hardReferences = new LinkedList<>();
+        DefaultObjectWrapper.Builder builder;
+
+        {
+            builder = new DefaultObjectWrapper.Builder(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 DefaultObjectWrapper.Builder(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 DefaultObjectWrapper.Builder(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:
+        DefaultObjectWrapper.Builder.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 DefaultObjectWrapper.Builder(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 DefaultObjectWrapper.Builder(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 DefaultObjectWrapper.Builder(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 DefaultObjectWrapper.Builder(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());
+    }
+
+    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 = ClassIntrospector.Builder.getInstanceCache();
+        synchronized (instanceCache) {
+            return instanceCache.size();
+        }
+    }
+
+    static int getClassIntrospectorNonClearedInstanceCacheSize() {
+        Map instanceCache = ClassIntrospector.Builder.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 = ClassIntrospector.Builder.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 = DefaultObjectWrapper.Builder.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 = DefaultObjectWrapper.Builder.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 = DefaultObjectWrapper.Builder.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/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
new file mode 100644
index 0000000..6e9ae25
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
@@ -0,0 +1,901 @@
+/*
+ * 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.apache.freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+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;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreAPI;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+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.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class DefaultObjectWrapperTest {
+
+    private final static DefaultObjectWrapper OW = new DefaultObjectWrapper.Builder(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<>();
+        expected.add(Configuration.VERSION_3_0_0);
+
+        List<Version> actual = new ArrayList<>();
+        int i = _CoreAPI.VERSION_INT_3_0_0;
+        while (i <= Configuration.getVersion().intValue()) {
+            int major = i / 1000000;
+            int minor = i % 1000000 / 1000;
+            int micro = i % 1000;
+            final Version version = new Version(major, minor, micro);
+            
+            final Version normalizedVersion = DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(version);
+            actual.add(normalizedVersion);
+
+            final DefaultObjectWrapper.Builder builder = new DefaultObjectWrapper.Builder(version);
+            assertEquals(normalizedVersion, builder.getIncompatibleImprovements());
+            assertEquals(normalizedVersion, builder.build().getIncompatibleImprovements());
+            
+            i++;
+        }
+
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testIncompatibleImprovementsVersionOutOfBounds() throws Exception {
+        try {
+            DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(new Version(2, 2, 0));
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        Version curVersion = Configuration.getVersion();
+        final Version futureVersion = new Version(curVersion.getMajor(), curVersion.getMicro(),
+                curVersion.getMicro() + 1);
+        try {
+            DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(futureVersion);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        try {
+            new DefaultObjectWrapper.Builder(futureVersion);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testBuilder() throws Exception {
+        DefaultObjectWrapper.Builder builder = new DefaultObjectWrapper.Builder(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());
+    }
+
+    @Test
+    public void testWrappedTypes() throws Exception {
+        DefaultObjectWrapper.Builder builder = new DefaultObjectWrapper.Builder(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
+    public void testConstructors() throws Exception {
+        {
+            DefaultObjectWrapper ow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                    .usePrivateCaches(true).build();
+            assertEquals(Configuration.VERSION_3_0_0, ow.getIncompatibleImprovements());
+        }
+        
+        try {
+            new DefaultObjectWrapper.Builder(new Version(99, 9, 9)).build();
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("version"));
+        }
+    }
+    
+    
+    @Test
+    public void testCustomization() throws TemplateModelException {
+        CustomizedDefaultObjectWrapper ow = new CustomizedDefaultObjectWrapper(Configuration.VERSION_3_0_0);
+        assertEquals(Configuration.VERSION_3_0_0, ow.getIncompatibleImprovements());
+
+        TemplateSequenceModel seq = (TemplateSequenceModel) ow.wrap(new Tupple(11, 22));
+        assertEquals(2, seq.size());
+        assertEquals(11, ow.unwrap(seq.get(0)));
+        assertEquals(22, ow.unwrap(seq.get(1)));
+        
+        assertTrue(ow.wrap("x") instanceof SimpleScalar);
+        assertTrue(ow.wrap(1.5) instanceof SimpleNumber);
+        assertTrue(ow.wrap(new Date()) instanceof SimpleDate);
+        assertEquals(TemplateBooleanModel.TRUE, ow.wrap(true));
+        
+        assertTrue(ow.wrap(Collections.emptyMap()) instanceof DefaultMapAdapter);
+        assertTrue(ow.wrap(Collections.emptyList()) instanceof DefaultListAdapter);
+        assertTrue(ow.wrap(new boolean[] { }) instanceof DefaultArrayAdapter);
+        assertTrue(ow.wrap(new HashSet()) instanceof DefaultNonListCollectionAdapter);
+        assertTrue(ow.wrap('c') instanceof TemplateScalarModel); // BeanAndStringModel right now, but should change later
+        
+        TemplateHashModel bean = (TemplateHashModel) ow.wrap(new TestBean());
+        assertEquals(1, ow.unwrap(bean.get("x")));
+        {
+            // Check method calls, and also if the return value is wrapped with the overidden "wrap".
+            final TemplateModel mr = (TemplateModel) ((TemplateMethodModelEx) bean.get("m")).exec(Collections.emptyList());
+            assertEquals(
+                    Collections.singletonList(1),
+                    ow.unwrap(mr));
+            assertTrue(DefaultListAdapter.class.isInstance(mr));
+        }
+        {
+            // Check custom TM usage and round trip:
+            final TemplateModel mr = (TemplateModel) ((TemplateMethodModelEx) bean.get("incTupple"))
+                    .exec(Collections.singletonList(ow.wrap(new Tupple<>(1, 2))));
+            assertEquals(new Tupple<>(2, 3), ow.unwrap(mr));
+            assertTrue(TuppleAdapter.class.isInstance(mr));
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testCompositeValueWrapping() throws TemplateModelException, ClassNotFoundException {
+        DefaultObjectWrapper ow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+
+        final Map hashMap = new HashMap();
+        inintTestMap(hashMap);
+        final Map treeMap = new TreeMap();
+        inintTestMap(treeMap);
+        final Map linkedHashMap = new LinkedHashMap();
+        inintTestMap(linkedHashMap);
+        final Map gMap = ImmutableMap.<String, Object> of("a", 1, "b", 2, "c", 3);
+        final LinkedList linkedList = new LinkedList();
+        linkedList.add("a");
+        linkedList.add("b");
+        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(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, pureIterable, DefaultIterableAdapter.class, PureIterable.class, pureIterable.toString());
+        assertRoundtrip(ow, hashSet, DefaultNonListCollectionAdapter.class, HashSet.class, hashSet.toString());
+    }
+
+    @SuppressWarnings("boxing")
+    private void inintTestMap(Map map) {
+        map.put("a", 1);
+        map.put("b", 2);
+        map.put("c", 3);
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testMapAdapter() throws TemplateModelException {
+        HashMap<String, Object> testMap = new LinkedHashMap<>();
+        testMap.put("a", 1);
+        testMap.put("b", null);
+        testMap.put("c", "C");
+        testMap.put("d", Collections.singletonList("x"));
+
+        {
+            TemplateHashModelEx hash = (TemplateHashModelEx) OW.wrap(testMap);
+            assertEquals(4, hash.size());
+            assertFalse(hash.isEmpty());
+            assertNull(hash.get("e"));
+            assertEquals(1, ((TemplateNumberModel) hash.get("a")).getAsNumber());
+            assertNull(hash.get("b"));
+            assertEquals("C", ((TemplateScalarModel) hash.get("c")).getAsString());
+            assertTrue(hash.get("d") instanceof DefaultListAdapter);
+
+            assertCollectionTMEquals(hash.keys(), "a", "b", "c", "d");
+            assertCollectionTMEquals(hash.values(), 1, null, "C", Collections.singletonList("x"));
+            
+            assertSizeThroughAPIModel(4, hash);
+        }
+
+        {
+            assertTrue(((TemplateHashModel) OW.wrap(Collections.emptyMap())).isEmpty());
+        }
+    }
+
+    private void assertCollectionTMEquals(TemplateCollectionModel coll, Object... expectedItems)
+            throws TemplateModelException {
+        for (int i = 0; i < 2; i++) { // Run twice to check if we always get a new iterator
+            int idx = 0;
+            TemplateModelIterator it2 = null;
+            for (TemplateModelIterator it = coll.iterator(); it.hasNext(); ) {
+                TemplateModel actualItem = it.next();
+                if (idx >= expectedItems.length) {
+                    fail("Number of items is more than the expected " + expectedItems.length);
+                }
+                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], OW.unwrap(it2.next()));
+                }
+                idx++;
+            }
+            if (expectedItems.length != idx) {
+                fail("Number of items is " + idx + ", which is less than the expected " + expectedItems.length);
+            }
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testListAdapter() throws TemplateModelException {
+        {
+            List testList = new ArrayList<>();
+            testList.add(1);
+            testList.add(null);
+            testList.add("c");
+            testList.add(new String[] { "x" });
+
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testList);
+            assertTrue(seq instanceof DefaultListAdapter);
+            assertFalse(seq instanceof TemplateCollectionModel); // Maybe changes at 2.4.0
+            assertEquals(4, seq.size());
+            assertNull(seq.get(-1));
+            assertEquals(1, ((TemplateNumberModel) seq.get(0)).getAsNumber());
+            assertNull(seq.get(1));
+            assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
+            assertTrue(seq.get(3) instanceof DefaultArrayAdapter);
+            assertNull(seq.get(4));
+            
+            assertSizeThroughAPIModel(4, seq);
+        }
+
+        {
+            List testList = new LinkedList<>();
+            testList.add(1);
+            testList.add(null);
+            testList.add("c");
+
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testList);
+            assertTrue(seq instanceof DefaultListAdapter);
+            assertTrue(seq instanceof TemplateCollectionModel); // Maybe changes at 2.4.0
+            assertEquals(3, seq.size());
+            assertNull(seq.get(-1));
+            assertEquals(1, ((TemplateNumberModel) seq.get(0)).getAsNumber());
+            assertNull(seq.get(1));
+            assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
+            assertNull(seq.get(3));
+
+            assertCollectionTMEquals((TemplateCollectionModel) seq, 1, null, "c");
+
+            TemplateModelIterator it = ((TemplateCollectionModel) seq).iterator();
+            it.next();
+            it.next();
+            it.next();
+            try {
+                it.next();
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), containsString("no more"));
+            }
+        }
+    }
+
+    @Test
+    public void testArrayAdapterTypes() throws TemplateModelException {
+        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) {
+        assertTrue(adaptedArray instanceof DefaultArrayAdapter);
+        assertThat(adaptedArray.getClass().getName(),
+                containsString("$" + adapterCompType.substring(0, 1).toUpperCase() + adapterCompType.substring(1)));
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testArrayAdapters() throws TemplateModelException {
+        {
+            final String[] testArray = new String[] { "a", null, "c" };
+
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testArray);
+            assertEquals(3, seq.size());
+            assertNull(seq.get(-1));
+            assertEquals("a", ((TemplateScalarModel) seq.get(0)).getAsString());
+            assertNull(seq.get(1));
+            assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
+            assertNull(seq.get(3));
+        }
+
+        {
+            final int[] testArray = new int[] { 11, 22 };
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testArray);
+            assertEquals(2, seq.size());
+            assertNull(seq.get(-1));
+            assertEqualsAndSameClass(Integer.valueOf(11), ((TemplateNumberModel) seq.get(0)).getAsNumber());
+            assertEqualsAndSameClass(Integer.valueOf(22), ((TemplateNumberModel) seq.get(1)).getAsNumber());
+            assertNull(seq.get(2));
+        }
+
+        {
+            final double[] testArray = new double[] { 11, 22 };
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testArray);
+            assertEquals(2, seq.size());
+            assertNull(seq.get(-1));
+            assertEqualsAndSameClass(Double.valueOf(11), ((TemplateNumberModel) seq.get(0)).getAsNumber());
+            assertEqualsAndSameClass(Double.valueOf(22), ((TemplateNumberModel) seq.get(1)).getAsNumber());
+            assertNull(seq.get(2));
+        }
+
+        {
+            final boolean[] testArray = new boolean[] { true, false };
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testArray);
+            assertEquals(2, seq.size());
+            assertNull(seq.get(-1));
+            assertEqualsAndSameClass(Boolean.valueOf(true), ((TemplateBooleanModel) seq.get(0)).getAsBoolean());
+            assertEqualsAndSameClass(Boolean.valueOf(false), ((TemplateBooleanModel) seq.get(1)).getAsBoolean());
+            assertNull(seq.get(2));
+        }
+
+        {
+            final char[] testArray = new char[] { 'a', 'b' };
+            TemplateSequenceModel seq = (TemplateSequenceModel) OW.wrap(testArray);
+            assertEquals(2, seq.size());
+            assertNull(seq.get(-1));
+            assertEquals("a", ((TemplateScalarModel) seq.get(0)).getAsString());
+            assertEquals("b", ((TemplateScalarModel) seq.get(1)).getAsString());
+            assertNull(seq.get(2));
+        }
+    }
+
+    private void assertEqualsAndSameClass(Object expected, Object actual) {
+        assertEquals(expected, actual);
+        if (expected != null) {
+            assertEquals(expected.getClass(), actual.getClass());
+        }
+    }
+
+    private void assertRoundtrip(DefaultObjectWrapper dow, Object obj, Class expectedTMClass,
+            Class expectedPojoClass,
+            String expectedPojoToString)
+            throws TemplateModelException {
+        final TemplateModel objTM = dow.wrap(obj);
+        assertThat(objTM.getClass(), typeCompatibleWith(expectedTMClass));
+
+        final TemplateHashModel testBeanTM = (TemplateHashModel) dow.wrap(new RoundtripTesterBean());
+
+        {
+            TemplateMethodModelEx getClassM = (TemplateMethodModelEx) testBeanTM.get("getClass");
+            Object r = getClassM.exec(Collections.singletonList(objTM));
+            final Class rClass = (Class) ((WrapperTemplateModel) r).getWrappedObject();
+            assertThat(rClass, typeCompatibleWith(expectedPojoClass));
+        }
+
+        if (expectedPojoToString != null) {
+            TemplateMethodModelEx getToStringM = (TemplateMethodModelEx) testBeanTM.get("toString");
+            Object r = getToStringM.exec(Collections.singletonList(objTM));
+            assertEquals(expectedPojoToString, ((TemplateScalarModel) r).getAsString());
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testCollectionAdapterBasics() throws TemplateModelException {
+        {
+            Set set = new TreeSet();
+            set.add("a");
+            set.add("b");
+            set.add("c");
+            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW.wrap(set);
+            assertTrue(coll instanceof DefaultNonListCollectionAdapter);
+            assertEquals(3, coll.size());
+            assertFalse(coll.isEmpty());
+            assertCollectionTMEquals(coll, "a", "b", "c");
+
+            assertRoundtrip(OW, set, DefaultNonListCollectionAdapter.class, TreeSet.class, "[a, b, c]");
+            
+            assertSizeThroughAPIModel(3, coll);
+        }
+
+        {
+            Set set = new HashSet();
+            final List<String> list = Collections.singletonList("b");
+            set.add(list);
+            set.add(null);
+            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW.wrap(set);
+            TemplateModelIterator it = coll.iterator();
+            final TemplateModel tm1 = it.next();
+            Object obj1 = OW.unwrap(tm1);
+            final TemplateModel tm2 = it.next();
+            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);
+
+            assertRoundtrip(OW, set, DefaultNonListCollectionAdapter.class, HashSet.class, "[" + obj1 + ", "
+                    + obj2 + "]");
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testCollectionAdapterOutOfBounds() throws TemplateModelException {
+        Set set = Collections.singleton(123);
+
+        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW.wrap(set);
+        TemplateModelIterator it = coll.iterator();
+
+        for (int i = 0; i < 3; i++) {
+            assertTrue(it.hasNext());
+        }
+
+        assertEquals(123, OW.unwrap(it.next()));
+
+        for (int i = 0; i < 3; i++) {
+            assertFalse(it.hasNext());
+            try {
+                it.next();
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), containsStringIgnoringCase("no more"));
+            }
+        }
+    }
+
+    @Test
+    public void testCollectionAdapterAndNulls() throws TemplateModelException {
+        Set set = new HashSet();
+        set.add(null);
+
+        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW.wrap(set);
+        assertEquals(1, coll.size());
+        assertFalse(coll.isEmpty());
+        assertNull(coll.iterator().next());
+    }
+
+    @Test
+    public void testIteratorWrapping() throws TemplateModelException, ClassNotFoundException {
+        final List<String> list = ImmutableList.of("a", "b", "c");
+        Iterator<String> it = list.iterator();
+        TemplateCollectionModel coll = (TemplateCollectionModel) OW.wrap(it);
+
+        assertRoundtrip(OW, coll, DefaultIteratorAdapter.class, Iterator.class, null);
+
+        TemplateModelIterator itIt = coll.iterator();
+        TemplateModelIterator itIt2 = coll.iterator(); // used later
+        assertTrue(itIt.hasNext());
+        assertEquals("a", OW.unwrap(itIt.next()));
+        assertTrue(itIt.hasNext());
+        assertEquals("b", OW.unwrap(itIt.next()));
+        assertTrue(itIt.hasNext());
+        assertEquals("c", OW.unwrap(itIt.next()));
+        assertFalse(itIt.hasNext());
+        try {
+            itIt.next();
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("no more"));
+        }
+
+        try {
+            itIt2.hasNext();
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("can be listed only once"));
+        }
+
+        TemplateModelIterator itIt3 = coll.iterator();
+        try {
+            itIt3.hasNext();
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("can be listed only once"));
+        }
+    }
+
+    @Test
+    public void testIteratorApiSupport() throws TemplateModelException {
+        TemplateModel wrappedIterator = OW.wrap(Collections.emptyIterator());
+        assertThat(wrappedIterator, instanceOf(DefaultIteratorAdapter.class));
+        DefaultIteratorAdapter iteratorAdapter = (DefaultIteratorAdapter) wrappedIterator;
+
+        TemplateHashModel api = (TemplateHashModel) iteratorAdapter.getAPI();
+        assertFalse(((TemplateBooleanModel) ((TemplateMethodModelEx)
+                api.get("hasNext")).exec(Collections.emptyList())).getAsBoolean());
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testCharKeyFallback() throws TemplateModelException {
+        Map hashMapS = new HashMap<>();
+        hashMapS.put("a", 1);
+        Map sortedMapS = new TreeMap<>();
+        sortedMapS.put("a", 1);
+        Map hashMapC = new HashMap<>();
+        hashMapC.put('a', 1);
+        Map sortedMapC = new TreeMap<>();
+        sortedMapC.put('a', 1);
+        
+        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) OW.wrap(sortedMapC)).get("a");
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("String key"));
+        }
+        
+        assertNull(((TemplateHashModel) OW.wrap(hashMapS)).get("b"));
+        assertNull(((TemplateHashModel) OW.wrap(hashMapC)).get("b"));
+        assertNull(((TemplateHashModel) OW.wrap(sortedMapS)).get("b"));
+        try {
+            ((TemplateHashModel) OW.wrap(sortedMapC)).get("b");
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("String key"));
+        }
+    }
+    
+    @Test
+    public void testIterableSupport() throws TemplateException, IOException {
+        Iterable<String> iterable = new PureIterable();
+        
+        String listingFTL = "<#list value as x>${x}<#sep>, </#list>";
+        
+        DefaultObjectWrapper ow = OW;
+        TemplateModel tm = ow.wrap(iterable);
+        assertThat(tm, instanceOf(TemplateCollectionModel.class));
+        TemplateCollectionModel iterableTM = (TemplateCollectionModel) tm;
+
+        for (int i = 0; i < 2; i++) {
+            TemplateModelIterator iteratorTM = iterableTM.iterator();
+            assertTrue(iteratorTM.hasNext());
+            assertEquals("a", ow.unwrap(iteratorTM.next()));
+            assertTrue(iteratorTM.hasNext());
+            assertEquals("b", ow.unwrap(iteratorTM.next()));
+            assertTrue(iteratorTM.hasNext());
+            assertEquals("c", ow.unwrap(iteratorTM.next()));
+            assertFalse(iteratorTM.hasNext());
+            try {
+                iteratorTM.next();
+                fail();
+            } catch (TemplateModelException e) {
+                assertThat(e.getMessage(), containsStringIgnoringCase("no more"));
+            }
+        }
+
+        assertTemplateOutput(OW, iterable, listingFTL, "a, b, c");
+    }
+
+    @Test
+    public void testEnumerationAdapter() throws TemplateModelException {
+        Vector<String> vector = new Vector<String>();
+        vector.add("a");
+        vector.add("b");
+
+        TemplateModel wrappedEnumeration = OW.wrap(vector.elements());
+        assertThat(wrappedEnumeration, instanceOf(DefaultEnumerationAdapter.class));
+        DefaultEnumerationAdapter enumAdapter = (DefaultEnumerationAdapter) wrappedEnumeration;
+        TemplateModelIterator iterator = enumAdapter.iterator();
+        assertTrue(iterator.hasNext());
+        assertEquals("a", ((TemplateScalarModel) iterator.next()).getAsString());
+        assertTrue(iterator.hasNext());
+        assertEquals("b", ((TemplateScalarModel) iterator.next()).getAsString());
+        assertFalse(iterator.hasNext());
+
+        iterator = enumAdapter.iterator();
+        try {
+            iterator.hasNext();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("only once"));
+        }
+
+        TemplateHashModel api = (TemplateHashModel) enumAdapter.getAPI();
+        assertFalse(((TemplateBooleanModel) ((TemplateMethodModelEx)
+                api.get("hasMoreElements")).exec(Collections.emptyList())).getAsBoolean());
+    }
+
+    @Test
+    public void assertCanWrapDOM() throws SAXException, IOException, ParserConfigurationException,
+            TemplateModelException {
+        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        InputSource is = new InputSource();
+        is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
+        Document doc = db.parse(is);        
+        assertTrue(OW.wrap(doc) instanceof TemplateNodeModel);
+    }
+
+    @Test
+    public void testExposureLevel() throws Exception {
+        TestBean bean = new TestBean();
+
+        {
+            TemplateHashModel tm = wrapWithExposureLevel(bean, DefaultObjectWrapper.EXPOSE_SAFE);
+            assertNotNull(tm.get("hashCode"));
+            assertNotNull(tm.get("class"));
+        }
+
+        {
+            TemplateHashModel tm = wrapWithExposureLevel(bean, DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY);
+            assertNull(tm.get("hashCode"));
+            assertNotNull(tm.get("class"));
+        }
+
+        {
+            TemplateHashModel tm = wrapWithExposureLevel(bean, DefaultObjectWrapper.EXPOSE_NOTHING);
+            assertNull(tm.get("hashCode"));
+            assertNull(tm.get("class"));
+        }
+
+        {
+            TemplateHashModel tm = wrapWithExposureLevel(bean, DefaultObjectWrapper.EXPOSE_ALL);
+            assertNotNull(tm.get("hashCode"));
+            assertNotNull(tm.get("class"));
+        }
+    }
+
+    private TemplateHashModel wrapWithExposureLevel(Object bean, int exposureLevel) throws TemplateModelException {
+        return (TemplateHashModel) new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .exposureLevel(exposureLevel).build()
+                .wrap(bean);
+    }
+
+    private void assertSizeThroughAPIModel(int expectedSize, TemplateModel normalModel) throws TemplateModelException {
+        if (!(normalModel instanceof TemplateModelWithAPISupport)) {
+            fail(); 
+        }
+        TemplateHashModel apiModel = (TemplateHashModel) ((TemplateModelWithAPISupport) normalModel).getAPI();
+        TemplateMethodModelEx sizeMethod = (TemplateMethodModelEx) apiModel.get("size");
+        TemplateNumberModel r = (TemplateNumberModel) sizeMethod.exec(Collections.emptyList());
+        assertEquals(expectedSize, r.getAsNumber().intValue());
+    }
+
+    private void assertTemplateOutput(ObjectWrapper objectWrapper, Object value, String ftl, String expectedOutput)
+            throws TemplateException, IOException {
+        assertEquals(expectedOutput, processTemplate(objectWrapper, value, ftl));
+    }
+
+    private void assertTemplateFails(ObjectWrapper objectWrapper, Object value, String ftl, String expectedMessagePart)
+            throws TemplateException, IOException {
+        try {
+            processTemplate(objectWrapper, value, ftl);
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsString(expectedMessagePart));
+        }
+    }
+    
+    private String processTemplate(ObjectWrapper objectWrapper, Object value, String ftl)
+            throws TemplateException, IOException {
+        Configuration cfg = new TestConfigurationBuilder()
+                .logTemplateExceptions(false)
+                .objectWrapper(objectWrapper)
+                .build();
+        StringWriter out = new StringWriter();
+        new Template(null, ftl, cfg).process(ImmutableMap.of("value", value), out);
+        return out.toString();
+    }
+
+    private static final class PureIterable implements Iterable<String> {
+        @Override
+        public Iterator<String> iterator() {
+            return ImmutableList.of("a", "b", "c").iterator();
+        }
+    }
+
+    public static class RoundtripTesterBean {
+
+        public Class getClass(Object o) {
+            return o.getClass();
+        }
+
+        public String toString(Object o) {
+            return o.toString();
+        }
+
+    }
+    
+    private static class Tupple<E1, E2> {
+        
+        private final E1 e1;
+        private final E2 e2;
+
+        public Tupple(E1 e1, E2 e2) {
+            if (e1 == null || e2 == null) throw new NullPointerException();
+            this.e1 = e1;
+            this.e2 = e2;
+        }
+
+        public E1 getE1() {
+            return e1;
+        }
+
+        public E2 getE2() {
+            return e2;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((e1 == null) ? 0 : e1.hashCode());
+            result = prime * result + ((e2 == null) ? 0 : e2.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Tupple other = (Tupple) obj;
+            if (e1 == null) {
+                if (other.e1 != null) return false;
+            } else if (!e1.equals(other.e1)) return false;
+            if (e2 == null) {
+                if (other.e2 != null) return false;
+            } else if (!e2.equals(other.e2)) return false;
+            return true;
+        }
+        
+    }
+    
+    @SuppressWarnings("boxing")
+    public static class TestBean {
+        
+        public int getX() {
+            return 1;
+        }
+        
+        public List<Integer> m() {
+            return Collections.singletonList(1);
+        }
+
+        public Tupple incTupple(Tupple<Integer, Integer> tupple) {
+            return new Tupple(tupple.e1 + 1, tupple.e2 + 1);
+        }
+        
+    }
+    
+    private static class CustomizedDefaultObjectWrapper extends DefaultObjectWrapper {
+
+        private CustomizedDefaultObjectWrapper(Version incompatibleImprovements) {
+            super(new DefaultObjectWrapper.Builder(incompatibleImprovements), true);
+        }
+        
+        @Override
+        protected TemplateModel handleNonBasicTypes(final Object obj) throws TemplateModelException {
+            if (obj instanceof Tupple) {
+                return new TuppleAdapter((Tupple<?, ?>) obj, this);
+            }
+            
+            return super.handleNonBasicTypes(obj);
+        }
+        
+    }
+    
+    private static class TuppleAdapter extends WrappingTemplateModel implements TemplateSequenceModel,
+            AdapterTemplateModel {
+        
+        private final Tupple<?, ?> tupple;
+        
+        public TuppleAdapter(Tupple<?, ?> tupple, ObjectWrapper ow) {
+            super(ow);
+            this.tupple = tupple;
+        }
+
+        @Override
+        public int size() throws TemplateModelException {
+            return 2;
+        }
+        
+        @Override
+        public TemplateModel get(int index) throws TemplateModelException {
+            switch (index) {
+            case 0: return wrap(tupple.getE1());
+            case 1: return wrap(tupple.getE2());
+            default: return null;
+            }
+        }
+
+        @Override
+        public Object getAdaptedObject(Class hint) {
+            return tupple;
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java
new file mode 100644
index 0000000..fc19bb7
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/EnumModelsTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.Builder(Configuration.VERSION_3_0_0).usePrivateCaches(true)
+                .build();
+        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();
+        }
+        
+    }
+
+}


Mime
View raw message