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(); + } + + } + +}
