http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/TypeFlagsTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/TypeFlagsTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/TypeFlagsTest.java
new file mode 100644
index 0000000..fe11d1c
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/TypeFlagsTest.java
@@ -0,0 +1,671 @@
+/*
+ * 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.File;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.freemarker.core.Configuration;
+
+import junit.framework.TestCase;
+
+public class TypeFlagsTest extends TestCase {
+
+    public TypeFlagsTest(String name) {
+        super(name);
+    }
+    
+    private final DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+
+    public void testSingleNumType() {
+        checkTypeFlags(SingleNumTypeC.class, "mInt",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.INTEGER | 
TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | TypeFlags.ACCEPTS_NUMBER
+                | TypeFlags.ACCEPTS_STRING);
+        checkTypeFlags(SingleNumTypeC.class, "mLong",
+                TypeFlags.LONG | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(SingleNumTypeC.class, "mShort",
+                TypeFlags.SHORT | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(SingleNumTypeC.class, "mByte",
+                TypeFlags.BYTE | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.ACCEPTS_ANY_OBJECT);
+        checkTypeFlags(SingleNumTypeC.class, "mDouble",
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(SingleNumTypeC.class, "mFloat",
+                TypeFlags.FLOAT | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(SingleNumTypeC.class, "mUnknown",
+                TypeFlags.UNKNOWN_NUMERICAL_TYPE | TypeFlags.ACCEPTS_NUMBER);
+        
+        checkTypeFlags(SingleNumTypeC.class, "mVarParamCnt",
+                TypeFlags.BIG_DECIMAL | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(SingleNumTypeC.class, "mVarParamCnt",
+                TypeFlags.BIG_INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(SingleNumTypeC.class, "mVarParamCnt",
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.FLOAT | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER);
+    }
+    
+    static public class SingleNumTypeC {
+        public void mInt(int a1, String a2) { }
+        public void mInt(int a1, int a2) { }
+        public void mLong(long a1) { }
+        public void mLong(Long a1) { }
+        public void mShort(short a1) { }
+        public void mByte(byte a1, boolean a2) { }
+        public void mByte(byte a1, String a2) { }
+        public void mByte(byte a1, Object a2) { }
+        public void mDouble(double a1) { }
+        public void mFloat(float a1) { }
+        public void mUnknown(RationalNumber a1) { };
+        
+        public void mVarParamCnt(BigDecimal a1) { }
+        public void mVarParamCnt(BigInteger a1, Double a2) { }
+        public void mVarParamCnt(Double a1,     Float a2, Integer a3) { }
+        public void mVarParamCnt(Object a1,     char a2,  boolean a3, File a4, 
Map a5,    Boolean a6) { }
+        public void mVarParamCnt(Long a1,       int a2,   short a3,   byte a4, 
double a5, float a6) { }
+    }
+
+    public void testMultipleNumTypes() {
+        checkTypeFlags(MultiNumTypeC.class, "m1",
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT 
+                | TypeFlags.BYTE | TypeFlags.DOUBLE | TypeFlags.INTEGER
+                | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(MultiNumTypeC.class, "m2",
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT 
+                | TypeFlags.SHORT | TypeFlags.LONG | TypeFlags.FLOAT
+                | TypeFlags.ACCEPTS_NUMBER | TypeFlags.CHARACTER
+                );
+
+        checkTypeFlags(MultiNumTypeC.class, "m3",
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT 
+                | TypeFlags.BIG_DECIMAL | TypeFlags.BIG_INTEGER
+                | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.BIG_INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT
+                | TypeFlags.BIG_DECIMAL | TypeFlags.UNKNOWN_NUMERICAL_TYPE
+                | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(MultiNumTypeC.class, "m4",
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | TypeFlags.FLOAT 
| TypeFlags.ACCEPTS_NUMBER
+                | TypeFlags.CHARACTER
+                );
+        
+        checkTypeFlags(MultiNumTypeC.class, "m5",
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT
+                | TypeFlags.FLOAT | TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(MultiNumTypeC.class, "m6",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER
+                );
+        assertSame(getTypeFlags(MultiNumTypeC.class, "m6", false, 2), 
OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        assertSame(getTypeFlags(MultiNumTypeC.class, "m6", true, 2), 
OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        assertSame(getTypeFlags(MultiNumTypeC.class, "m6", false, 3), 
OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        assertSame(getTypeFlags(MultiNumTypeC.class, "m6", true, 3), 
OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        checkTypeFlags(MultiNumTypeC.class, "m6",
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | TypeFlags.DOUBLE 
| TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | TypeFlags.DOUBLE 
| TypeFlags.ACCEPTS_NUMBER,
+                0
+                );
+    }
+    
+    static public class MultiNumTypeC {
+        public void m1(byte a1) { };
+        public void m1(int a1) { };
+        public void m1(double a2) { };
+        
+        public void m2(short a1) { };
+        public void m2(long a1) { };
+        public void m2(float a1) { };
+        public void m2(char a1) { };
+        
+        public void m3(BigInteger a1, BigInteger a2, BigDecimal a3) { };
+        public void m3(BigDecimal a1, BigInteger a2, RationalNumber a3) { };
+        
+        public void m4(float a1) { };
+        public void m4(char a1) { };
+        
+        public void m5(Float a1) { };
+        public void m5(Double a1) { };
+        public void m5(Enum a1) { };
+        
+        public void m6(int a1) { };
+        public void m6(File a1, Throwable a2) { };
+        public void m6(File a1, Throwable a2, StringBuilder a3) { };
+        public void m6(File a1, Throwable a2, Throwable a3) { };
+        public void m6(double a1, int a2, File a3, File a4) { };
+        public void m6(File a1, int a2, double a3, File a4) { };
+    }
+
+    public void testVarargsNums() {
+        checkTypeFlags(VarArgsC.class, "m1",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER
+                );
+        checkTypeFlags(VarArgsC.class, "m2",
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m3",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER
+                );
+        checkTypeFlags(VarArgsC.class, "m3",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.INTEGER
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        checkTypeFlags(VarArgsC.class, "m3",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.INTEGER
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.INTEGER
+                | TypeFlags.BIG_DECIMAL | 
TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m4",
+                TypeFlags.INTEGER | TypeFlags.LONG
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m5",
+                TypeFlags.LONG | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m6",
+                TypeFlags.LONG | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                | TypeFlags.ACCEPTS_STRING
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m7",
+                TypeFlags.INTEGER | TypeFlags.BYTE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.FLOAT
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+
+        checkTypeFlags(VarArgsC.class, "m8",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.FLOAT
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m9",
+                TypeFlags.INTEGER | TypeFlags.BYTE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m10",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER
+                );
+        checkTypeFlags(VarArgsC.class, "m10",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.LONG | TypeFlags.DOUBLE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m11",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.SHORT | TypeFlags.ACCEPTS_NUMBER
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        checkTypeFlags(VarArgsC.class, "m11",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.SHORT
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.LONG | TypeFlags.DOUBLE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m12",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER
+                );
+        checkTypeFlags(VarArgsC.class, "m12",
+                TypeFlags.INTEGER | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.SHORT
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.BYTE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.LONG | TypeFlags.DOUBLE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+        
+        checkTypeFlags(VarArgsC.class, "m13",
+                TypeFlags.CHARACTER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER);
+        checkTypeFlags(VarArgsC.class, "m13",
+                TypeFlags.CHARACTER,
+                TypeFlags.DOUBLE | TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.UNKNOWN_NUMERICAL_TYPE
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER,
+                TypeFlags.DOUBLE | TypeFlags.LONG
+                | TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT | 
TypeFlags.ACCEPTS_NUMBER
+                );
+    }
+    
+    static public class VarArgsC {
+        public void m1(int... va) { }
+        
+        public void m2(double a1, int... va) { }
+        
+        public void m3(int... va) { }
+        public void m3(int a1, double... va) { }
+        public void m3(int a1, double a2, BigDecimal... va) { }
+        
+        public void m4(int... va) { }
+        public void m4(long... va) { }
+
+        public void m5(Long... va) { }
+        public void m5(long... va) { }
+        
+        public void m6(long... va) { }
+        public void m6(String... va) { }
+        
+        public void m7(int a1, double... va) { }
+        public void m7(byte a1, float... va) { }
+
+        public void m8(int a1, double... va) { }
+        public void m8(int a1, float... va) { }
+        
+        public void m9(int a1, double... va) { }
+        public void m9(byte a1, double... va) { }
+        
+        public void m10(int a1, double a2, long... va) { }
+        public void m10(int a1, double... va) { }
+        
+        public void m11(int a1, short a2, long... va) { }
+        public void m11(int a1, double... va) { }
+        
+        public void m12(int a1, short a2, byte a3, long... va) { }
+        public void m12(int a1, double... va) { }
+        
+        public void m13(char a1, double a2, RationalNumber a3, Long... va) { }
+        public void m13(char a1, Double... va) { }
+    }
+    
+    public void testAllZeros() {
+        for (boolean reverse : new boolean[] { true, false }) {
+            assertSame(OverloadedMethodsSubset.ALL_ZEROS_ARRAY, 
getTypeFlags(AllZeroC.class, "m1", reverse, 0));
+            assertSame(OverloadedMethodsSubset.ALL_ZEROS_ARRAY, 
getTypeFlags(AllZeroC.class, "m2", reverse, 2));
+            assertSame(OverloadedMethodsSubset.ALL_ZEROS_ARRAY, 
getTypeFlags(AllZeroC.class, "m3", reverse, 1));
+        }
+    }
+    
+    static public class AllZeroC {
+        public void m1() {}
+        
+        public void m2(File a1, File a2) {}
+        
+        public void m3(File a1) {}
+        public void m3(StringBuilder a1) {}
+    }
+
+    public void testAcceptanceNonOverloaded() {
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber1",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.UNKNOWN_NUMERICAL_TYPE);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber2",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.BYTE);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber3",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.BYTE);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber4",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.SHORT);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber5",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.SHORT);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber6",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.INTEGER);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber7",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.INTEGER);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber8",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.LONG);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber9",  
TypeFlags.ACCEPTS_NUMBER | TypeFlags.LONG);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber10", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.FLOAT);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber11", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.FLOAT);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber12", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.DOUBLE);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber13", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.DOUBLE);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber14", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.BIG_INTEGER);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber15", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.BIG_DECIMAL);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mNumber16", 
TypeFlags.ACCEPTS_NUMBER | TypeFlags.UNKNOWN_NUMERICAL_TYPE);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mDate", 
TypeFlags.ACCEPTS_DATE);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mSQLDate1", 0);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mSQLDate2", 0);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mString", 
TypeFlags.ACCEPTS_STRING);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mCharSequence", 
TypeFlags.ACCEPTS_STRING);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mStringBuilder", 0);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mBool", 
TypeFlags.ACCEPTS_BOOLEAN);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mBoolean", 
TypeFlags.ACCEPTS_BOOLEAN);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mMap1", 
TypeFlags.ACCEPTS_MAP);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mMap2", 0);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mList1", 
TypeFlags.ACCEPTS_LIST);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mList2", 0);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mSet1", 
TypeFlags.ACCEPTS_SET);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mSet2", 0);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mCollection", 
TypeFlags.ACCEPTS_SET | TypeFlags.ACCEPTS_LIST);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mChar1", 
TypeFlags.CHARACTER);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mChar2", 
TypeFlags.CHARACTER);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mArray1", 
TypeFlags.ACCEPTS_ARRAY);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mArray2", 
TypeFlags.ACCEPTS_ARRAY);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mArray3", 
TypeFlags.ACCEPTS_ARRAY);
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mArray4", 
TypeFlags.ACCEPTS_ARRAY);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mObject", 
TypeFlags.ACCEPTS_ANY_OBJECT);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_NUMBER) 
!= 0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_STRING) 
!= 0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_BOOLEAN) 
!= 0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_MAP) != 
0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_LIST) != 
0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_SET) != 
0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.ACCEPTS_ARRAY) != 
0);
+        assertTrue((TypeFlags.ACCEPTS_ANY_OBJECT & TypeFlags.CHARACTER) == 0); 
 // deliberatly 0 
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mMapDate", 0);
+        
+        checkTypeFlags(AcceptanceNonoverloadedC.class, "mDateBooleanList",
+                TypeFlags.ACCEPTS_DATE, TypeFlags.ACCEPTS_BOOLEAN, 
TypeFlags.ACCEPTS_LIST);
+    }
+    
+    static public class AcceptanceNonoverloadedC {
+        public void mNumber1(Number a1) {}
+        public void mNumber2(Byte a1) {}
+        public void mNumber3(byte a1) {}
+        public void mNumber4(Short a1) {}
+        public void mNumber5(short a1) {}
+        public void mNumber6(Integer a1) {}
+        public void mNumber7(int a1) {}
+        public void mNumber8(Long a1) {}
+        public void mNumber9(long a1) {}
+        public void mNumber10(Float a1) {}
+        public void mNumber11(float a1) {}
+        public void mNumber12(Double a1) {}
+        public void mNumber13(double a1) {}
+        public void mNumber14(BigInteger a1) {}
+        public void mNumber15(BigDecimal a1) {}
+        public void mNumber16(RationalNumber a1) {}
+        
+        public void mDate(Date a1) {}
+        public void mSQLDate1(java.sql.Date a1) {}
+        public void mSQLDate2(java.sql.Timestamp a1) {}
+        
+        public void mString(String a1) {}
+        public void mCharSequence(CharSequence a1) {}
+        public void mStringBuilder(StringBuilder a1) {}
+        
+        public void mBool(boolean a1) {}
+        public void mBoolean(Boolean a1) {}
+
+        public void mMap1(Map a1) {}
+        public void mMap2(LinkedHashMap a1) {}
+        
+        public void mList1(List a1) {}
+        public void mList2(ArrayList a1) {}
+        
+        public void mSet1(Set a1) {}
+        public void mSet2(HashSet a1) {}
+        
+        public void mCollection(Collection a1) {}
+        
+        public void mMapDate(MapDate a1) {}
+
+        public void mChar1(Character a1) {}
+        public void mChar2(char a1) {}
+
+        public void mArray1(Object[] a1) {}
+        public void mArray2(int[] a1) {}
+        public void mArray3(Integer[] a1) {}
+        public void mArray4(Void[] a1) {}
+        
+        public void mObject(Object a1) {}
+        
+        public void mDateBooleanList(Date a1, boolean a2, List a3) {}
+    }
+
+    public void testAcceptanceOverloaded() {
+        checkTypeFlags(AcceptanceOverloadedC.class, "mLongDateList",
+                TypeFlags.ACCEPTS_NUMBER | TypeFlags.LONG | 
TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT
+                | TypeFlags.ACCEPTS_DATE | TypeFlags.ACCEPTS_LIST);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mBoolean", 
TypeFlags.ACCEPTS_BOOLEAN);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mStringChar",
+                TypeFlags.ACCEPTS_STRING | TypeFlags.CHARACTER);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mStringFile", 
TypeFlags.ACCEPTS_STRING);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mMapObject", 
TypeFlags.ACCEPTS_ANY_OBJECT);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mSetMap", 
TypeFlags.ACCEPTS_MAP | TypeFlags.ACCEPTS_SET);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mCollectionMap",
+                TypeFlags.ACCEPTS_MAP | TypeFlags.ACCEPTS_SET | 
TypeFlags.ACCEPTS_LIST);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mArray", 
TypeFlags.ACCEPTS_ARRAY);
+        checkTypeFlags(AcceptanceOverloadedC.class, "mArrayList", 
TypeFlags.ACCEPTS_ARRAY | TypeFlags.ACCEPTS_LIST);
+        
+        checkTypeFlags(AcceptanceOverloadedC.class, 
"mStringCollectionThenBooleanThenMapList",
+                TypeFlags.ACCEPTS_STRING | TypeFlags.ACCEPTS_LIST | 
TypeFlags.ACCEPTS_SET,
+                TypeFlags.ACCEPTS_BOOLEAN,
+                TypeFlags.ACCEPTS_MAP | TypeFlags.ACCEPTS_LIST);
+    }
+    
+    static public class AcceptanceOverloadedC {
+        public void mLongDateList(long a1) {}
+        public void mLongDateList(Date a1) {}
+        public void mLongDateList(List a1) {}
+
+        public void mBoolean(boolean a1) {}
+        public void mBoolean(Boolean a1) {}
+        
+        public void mDate(Date a1) {}
+        public void mDate(java.sql.Date a1) {}
+        public void mDate(java.sql.Timestamp a1) {}
+        
+        public void mStringChar(String a1) {}
+        public void mStringChar(char a1) {}
+        public void mStringChar(Character a1) {}
+
+        public void mStringFile(String a1) {}
+        public void mStringFile(File a1) {}
+        
+        public void mMapObject(Map a1) {}
+        public void mMapObject(Object a1) {}
+        
+        public void mSetMap(Set a1) {}
+        public void mSetMap(Map a1) {}
+        
+        public void mCollectionMap(Collection a1) {}
+        public void mCollectionMap(Map a1) {}
+        
+        public void mArray(Object[] a1) {}
+        public void mArray(int[] a1) {}
+        public void mArray(Integer[] a1) {}
+        public void mArray(Void[] a1) {}
+        
+        public void mArrayList(String[] a1) {}
+        public void mArrayList(List a1) {}
+        
+        public void mStringCollectionThenBooleanThenMapList(String a1, boolean 
a2, Map a3) {}
+        public void mStringCollectionThenBooleanThenMapList(Collection a1, 
boolean a2, Map a3) {}
+        public void mStringCollectionThenBooleanThenMapList(String a1, boolean 
a2, List a3) {}
+    }
+
+    public void testAcceptanceVarargsC() {
+        checkTypeFlags(AcceptanceVarArgsC.class, "m1",
+                TypeFlags.ACCEPTS_LIST | TypeFlags.ACCEPTS_STRING);
+        
+        checkTypeFlags(AcceptanceVarArgsC.class, "m2",
+                TypeFlags.ACCEPTS_MAP,
+                TypeFlags.ACCEPTS_MAP | TypeFlags.ACCEPTS_STRING | 
TypeFlags.ACCEPTS_BOOLEAN,
+                TypeFlags.ACCEPTS_MAP | TypeFlags.ACCEPTS_STRING);
+        checkTypeFlags(AcceptanceVarArgsC.class, "m2",
+                TypeFlags.ACCEPTS_MAP,
+                TypeFlags.ACCEPTS_MAP | TypeFlags.ACCEPTS_STRING | 
TypeFlags.ACCEPTS_BOOLEAN);
+        checkTypeFlags(AcceptanceVarArgsC.class, "m2",
+                TypeFlags.ACCEPTS_MAP);
+        
+        checkTypeFlags(AcceptanceVarArgsC.class, "m3",
+                TypeFlags.ACCEPTS_BOOLEAN);
+        checkTypeFlags(AcceptanceVarArgsC.class, "m3",
+                TypeFlags.ACCEPTS_BOOLEAN | TypeFlags.ACCEPTS_STRING,
+                TypeFlags.ACCEPTS_BOOLEAN | TypeFlags.ACCEPTS_MAP,
+                TypeFlags.ACCEPTS_BOOLEAN | TypeFlags.CHARACTER,
+                TypeFlags.ACCEPTS_BOOLEAN);
+    }
+    
+    static public class AcceptanceVarArgsC {
+        public void m1(List... a1) {}
+        public void m1(String... a1) {}
+
+        public void m2(Map a1, String... a2) {}
+        public void m2(Map a1, boolean a2, String... a3) {}
+        public void m2(Map... a1) {}
+        
+        public void m3(String a1, Map a2, char a3, Boolean... a4) {}
+        public void m3(boolean... a1) {}
+    }
+
+    static public class MapDate extends Date implements Map {
+    
+        @Override
+        public int size() {
+            return 0;
+        }
+    
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+    
+        @Override
+        public boolean containsKey(Object key) {
+            return false;
+        }
+    
+        @Override
+        public boolean containsValue(Object value) {
+            return false;
+        }
+    
+        @Override
+        public Object get(Object key) {
+            return null;
+        }
+    
+        @Override
+        public Object put(Object key, Object value) {
+            return null;
+        }
+    
+        @Override
+        public Object remove(Object key) {
+            return null;
+        }
+    
+        @Override
+        public void putAll(Map m) {
+        }
+    
+        @Override
+        public void clear() {
+        }
+    
+        @Override
+        public Set keySet() {
+            return null;
+        }
+    
+        @Override
+        public Collection values() {
+            return null;
+        }
+    
+        @Override
+        public Set entrySet() {
+            return null;
+        }
+        
+    }
+
+    private OverloadedMethodsSubset newOverloadedMethodsSubset(Class cl, 
String methodName, final boolean desc) {
+        final Method[] ms = cl.getMethods();
+        
+        final List<Method> filteredMethods = new ArrayList();
+        for (Method m : ms) {
+            if (m.getName().equals(methodName)) {
+                filteredMethods.add(m);
+            }
+        }
+        // As the order in which getMethods() returns the methods is 
undefined, we sort them for test predictability: 
+        Collections.sort(filteredMethods, new Comparator<Method>() {
+            @Override
+            public int compare(Method o1, Method o2) {
+                int res = o1.toString().compareTo(o2.toString());
+                return desc ? -res : res;
+            }
+        });
+        
+        final OverloadedMethodsSubset oms = cl.getName().indexOf("VarArgs") == 
-1
+                ? new OverloadedFixArgsMethods() : new 
OverloadedVarArgsMethods();
+        for (Method m : filteredMethods) {
+            oms.addCallableMemberDescriptor(new 
ReflectionCallableMemberDescriptor(m, m.getParameterTypes()));
+        }
+        return oms;
+    }
+    
+    private void checkTypeFlags(Class cl, String methodName, int... 
expectedTypeFlags) {
+        checkTypeFlags(cl, methodName, false, expectedTypeFlags);
+        checkTypeFlags(cl, methodName, true, expectedTypeFlags);
+    }
+    
+    private void checkTypeFlags(Class cl, String methodName, boolean 
revMetOrder, int... expectedTypeFlags) {
+        int[] actualParamTypes = getTypeFlags(cl, methodName, revMetOrder, 
expectedTypeFlags.length);
+        assertNotNull("Method " + methodName + "(#" + expectedTypeFlags.length 
+ ") doesn't exist", actualParamTypes);
+        if (actualParamTypes != OverloadedMethodsSubset.ALL_ZEROS_ARRAY) {
+            assertEquals(expectedTypeFlags.length, actualParamTypes.length);
+            for (int i = 0; i < expectedTypeFlags.length; i++) {
+                assertEquals(expectedTypeFlags[i], actualParamTypes[i]);
+            }
+        } else {
+            for (int expectedTypeFlag : expectedTypeFlags) {
+                assertEquals(expectedTypeFlag, 0);
+            }
+        }
+    }
+
+    private int[] getTypeFlags(Class cl, String methodName, boolean 
revMetOrder, int paramCnt) {
+        OverloadedMethodsSubset oms = newOverloadedMethodsSubset(cl, 
methodName, revMetOrder);
+        int[] actualParamTypes = oms.getTypeFlags(paramCnt);
+        return actualParamTypes;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/_OutputFormatTestAPI.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/_OutputFormatTestAPI.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/_OutputFormatTestAPI.java
new file mode 100644
index 0000000..0a479aa
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/_OutputFormatTestAPI.java
@@ -0,0 +1,35 @@
+/*
+ * 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.outputformat;
+
+public final class _OutputFormatTestAPI {
+
+    private _OutputFormatTestAPI() {
+        //
+    }
+    
+    public static String getMarkupContent(CommonTemplateMarkupOutputModel<?> 
tm) {
+        return tm.getMarkupContent();
+    }
+    
+    public static String 
getPlainTextContent(CommonTemplateMarkupOutputModel<?> tm) {
+        return tm.getPlainTextContent();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormatTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormatTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormatTest.java
new file mode 100644
index 0000000..19e45de
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/CombinedMarkupOutputFormatTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.outputformat.impl;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat._OutputFormatTestAPI;
+import org.junit.Test; 
+
+public class CombinedMarkupOutputFormatTest {
+    
+    private static final CombinedMarkupOutputFormat HTML_RTF = new 
CombinedMarkupOutputFormat(
+            HTMLOutputFormat.INSTANCE, RTFOutputFormat.INSTANCE);
+    private static final CombinedMarkupOutputFormat XML_XML = new 
CombinedMarkupOutputFormat(
+            XMLOutputFormat.INSTANCE, XMLOutputFormat.INSTANCE);
+
+    @Test
+    public void testName() {
+        assertEquals("HTML{RTF}", HTML_RTF.getName());
+        assertEquals("XML{XML}", XML_XML.getName());
+    }
+    
+    @Test
+    public void testOutputMO() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       
+       HTML_RTF.output(HTML_RTF.fromMarkup("<pre>\\par Test "), out);
+       HTML_RTF.output(HTML_RTF.fromPlainTextByEscaping("foo { bar } \\ "), 
out);
+       HTML_RTF.output(HTML_RTF.fromPlainTextByEscaping("& baaz "), out);
+       HTML_RTF.output(HTML_RTF.fromPlainTextByEscaping("\\par & qwe"), out);
+       HTML_RTF.output(HTML_RTF.fromMarkup("\\par{0} End</pre>"), out);
+       
+       assertEquals(
+               "<pre>\\par Test "
+               + "foo \\{ bar \\} \\\\ "
+               + "&amp; baaz "
+               + "\\\\par &amp; qwe"
+               + "\\par{0} End</pre>",
+               out.toString());
+    }
+
+    @Test
+    public void testOutputMO2() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       
+       XML_XML.output(XML_XML.fromMarkup("<pre>&lt;p&gt; Test "), out);
+       XML_XML.output(XML_XML.fromPlainTextByEscaping("a & b < c"), out);
+       XML_XML.output(XML_XML.fromMarkup(" End</pre>"), out);
+       
+       assertEquals(
+               "<pre>&lt;p&gt; Test "
+               + "a &amp;amp; b &amp;lt; c"
+               + " End</pre>",
+               out.toString());
+    }
+
+    @Test
+    public void testOutputMO3() throws TemplateModelException, IOException {
+        MarkupOutputFormat outputFormat = new CombinedMarkupOutputFormat(
+                RTFOutputFormat.INSTANCE,
+                new CombinedMarkupOutputFormat(RTFOutputFormat.INSTANCE, 
RTFOutputFormat.INSTANCE));
+        StringWriter out = new StringWriter();
+        
+        outputFormat.output(outputFormat.fromPlainTextByEscaping("b{}"), out);
+        outputFormat.output(outputFormat.fromMarkup("a{}"), out);
+        
+        assertEquals(
+                "b\\\\\\\\\\\\\\{\\\\\\\\\\\\\\}"
+                + "a{}",
+                out.toString());
+    }
+    
+    @Test
+    public void testOutputString() throws TemplateModelException, IOException {
+        StringWriter out = new StringWriter();
+        
+        HTML_RTF.output("a", out);
+        HTML_RTF.output("{", out);
+        HTML_RTF.output("<b>}c", out);
+        
+        assertEquals("a\\{&lt;b&gt;\\}c", out.toString());
+    }
+    
+    @Test
+    public void testOutputString2() throws TemplateModelException, IOException 
{
+        StringWriter out = new StringWriter();
+        
+        XML_XML.output("a", out);
+        XML_XML.output("&", out);
+        XML_XML.output("<b>", out);
+        
+        assertEquals("a&amp;amp;&amp;lt;b&amp;gt;", out.toString());
+    }
+    
+    @Test
+    public void testFromPlainTextByEscaping() throws TemplateModelException {
+        String plainText = "a\\b&c";
+        TemplateCombinedMarkupOutputModel mo = 
HTML_RTF.fromPlainTextByEscaping(plainText);
+        assertSame(plainText, _OutputFormatTestAPI.getPlainTextContent(mo));
+        assertNull(_OutputFormatTestAPI.getMarkupContent(mo)); // Not the MO's 
duty to calculate it!
+    }
+
+    @Test
+    public void testFromMarkup() throws TemplateModelException {
+        String markup = "a \\par <b>";
+        TemplateCombinedMarkupOutputModel mo = HTML_RTF.fromMarkup(markup);
+        assertSame(markup, _OutputFormatTestAPI.getMarkupContent(mo));
+        assertNull(_OutputFormatTestAPI.getPlainTextContent(mo)); // Not the 
MO's duty to calculate it!
+    }
+    
+    @Test
+    public void testGetMarkup() throws TemplateModelException {
+        {
+            String markup = "a \\par <b>";
+            TemplateCombinedMarkupOutputModel mo = HTML_RTF.fromMarkup(markup);
+            assertSame(markup, HTML_RTF.getMarkupString(mo));
+        }
+        
+        {
+            String safe = "abc";
+            TemplateCombinedMarkupOutputModel mo = 
HTML_RTF.fromPlainTextByEscaping(safe);
+            assertSame(safe, HTML_RTF.getMarkupString(mo));
+        }
+    }
+    
+    @Test
+    public void testConcat() throws Exception {
+        assertMO(
+                "ab", null,
+                HTML_RTF.concat(
+                        new TemplateCombinedMarkupOutputModel("a", null, 
HTML_RTF),
+                        new TemplateCombinedMarkupOutputModel("b", null, 
HTML_RTF)));
+        assertMO(
+                null, "ab",
+                HTML_RTF.concat(
+                        new TemplateCombinedMarkupOutputModel(null, "a", 
HTML_RTF),
+                        new TemplateCombinedMarkupOutputModel(null, "b", 
HTML_RTF)));
+        assertMO(
+                null, "{<a>}\\{&lt;b&gt;\\}",
+                HTML_RTF.concat(
+                        new TemplateCombinedMarkupOutputModel(null, "{<a>}", 
HTML_RTF),
+                        new TemplateCombinedMarkupOutputModel("{<b>}", null, 
HTML_RTF)));
+        assertMO(
+                null, "\\{&lt;a&gt;\\}{<b>}",
+                HTML_RTF.concat(
+                        new TemplateCombinedMarkupOutputModel("{<a>}", null, 
HTML_RTF),
+                        new TemplateCombinedMarkupOutputModel(null, "{<b>}", 
HTML_RTF)));
+    }
+    
+    @Test
+    public void testEscaplePlainText() throws TemplateModelException {
+        assertEquals("", HTML_RTF.escapePlainText(""));
+        assertEquals("a", HTML_RTF.escapePlainText("a"));
+        assertEquals("\\{a\\\\b&amp;\\}", HTML_RTF.escapePlainText("{a\\b&}"));
+        assertEquals("a\\\\b&amp;", HTML_RTF.escapePlainText("a\\b&"));
+        assertEquals("\\{\\}&amp;", HTML_RTF.escapePlainText("{}&"));
+        
+        assertEquals("a", XML_XML.escapePlainText("a"));
+        assertEquals("a&amp;apos;b", XML_XML.escapePlainText("a'b"));
+    }
+    
+    private void assertMO(String pc, String mc, 
TemplateCombinedMarkupOutputModel mo) {
+        assertEquals(pc, _OutputFormatTestAPI.getPlainTextContent(mo));
+        assertEquals(mc, _OutputFormatTestAPI.getMarkupContent(mo));
+    }
+    
+    @Test
+    public void testGetMimeType() {
+        assertEquals("text/html", HTML_RTF.getMimeType());
+        assertEquals("application/xml", XML_XML.getMimeType());
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormatTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormatTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormatTest.java
new file mode 100644
index 0000000..2199179
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/HTMLOutputFormatTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.outputformat.impl;
+
+import static org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.CommonMarkupOutputFormat;
+import org.apache.freemarker.core.outputformat._OutputFormatTestAPI;
+import org.junit.Test; 
+
+/**
+ * This actually more a {@link CommonMarkupOutputFormat} test.
+ */
+public class HTMLOutputFormatTest {
+    
+    @Test
+    public void testOutputMO() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       
+       INSTANCE.output(INSTANCE.fromMarkup("<p>Test "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("foo & bar "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("baaz "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("<b>A</b> <b>B</b> 
<b>C</b>"), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping(""), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("\"' x's \"y\" \""), 
out);
+       INSTANCE.output(INSTANCE.fromMarkup("</p>"), out);
+       
+       assertEquals(
+               "<p>Test "
+               + "foo &amp; bar "
+               + "baaz "
+               + "&lt;b&gt;A&lt;/b&gt; &lt;b&gt;B&lt;/b&gt; 
&lt;b&gt;C&lt;/b&gt;"
+               + "&quot;&#39; x&#39;s &quot;y&quot; &quot;"
+               + "</p>",
+               out.toString());
+    }
+    
+    @Test
+    public void testOutputString() throws TemplateModelException, IOException {
+        StringWriter out = new StringWriter();
+        
+        INSTANCE.output("a", out);
+        INSTANCE.output("<", out);
+        INSTANCE.output("b'c", out);
+        
+        assertEquals("a&lt;b&#39;c", out.toString());
+    }
+    
+    @Test
+    public void testFromPlainTextByEscaping() throws TemplateModelException {
+        String plainText = "a&b";
+        TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping(plainText);
+        assertSame(plainText, _OutputFormatTestAPI.getPlainTextContent(mo));
+        assertNull(_OutputFormatTestAPI.getMarkupContent(mo)); // Not the MO's 
duty to calculate it!
+    }
+
+    @Test
+    public void testFromMarkup() throws TemplateModelException {
+        String markup = "a&amp;b";
+        TemplateHTMLOutputModel mo = INSTANCE.fromMarkup(markup);
+        assertSame(markup, _OutputFormatTestAPI.getMarkupContent(mo));
+        assertNull(_OutputFormatTestAPI.getPlainTextContent(mo)); // Not the 
MO's duty to calculate it!
+    }
+    
+    @Test
+    public void testGetMarkup() throws TemplateModelException {
+        {
+            String markup = "a&amp;b";
+            TemplateHTMLOutputModel mo = INSTANCE.fromMarkup(markup);
+            assertSame(markup, INSTANCE.getMarkupString(mo));
+        }
+        
+        {
+            String safe = "abc";
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping(safe);
+            assertSame(safe, INSTANCE.getMarkupString(mo));
+        }
+        {
+            String safe = "";
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping(safe);
+            assertSame(safe, INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("<abc");
+            assertEquals("&lt;abc", INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("abc>");
+            assertEquals("abc&gt;", INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("<abc>");
+            assertEquals("&lt;abc&gt;", INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("a&bc");
+            assertEquals("a&amp;bc", INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("a&b&c");
+            assertEquals("a&amp;b&amp;c", INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("a<&>b&c");
+            assertEquals("a&lt;&amp;&gt;b&amp;c", 
INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = 
INSTANCE.fromPlainTextByEscaping("\"<a<&>b&c>\"");
+            assertEquals("&quot;&lt;a&lt;&amp;&gt;b&amp;c&gt;&quot;", 
INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("<");
+            assertEquals("&lt;", INSTANCE.getMarkupString(mo));
+        }
+        {
+            TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("'");
+            String mc = INSTANCE.getMarkupString(mo);
+            assertEquals("&#39;", mc);
+            assertSame(mc, INSTANCE.getMarkupString(mo)); // cached
+        }
+    }
+    
+    @Test
+    public void testConcat() throws Exception {
+        assertMO(
+                "ab", null,
+                INSTANCE.concat(new TemplateHTMLOutputModel("a", null), new 
TemplateHTMLOutputModel("b", null)));
+        assertMO(
+                null, "ab",
+                INSTANCE.concat(new TemplateHTMLOutputModel(null, "a"), new 
TemplateHTMLOutputModel(null, "b")));
+        assertMO(
+                null, "<a>&lt;b&gt;",
+                INSTANCE.concat(new TemplateHTMLOutputModel(null, "<a>"), new 
TemplateHTMLOutputModel("<b>", null)));
+        assertMO(
+                null, "&lt;a&gt;<b>",
+                INSTANCE.concat(new TemplateHTMLOutputModel("<a>", null), new 
TemplateHTMLOutputModel(null, "<b>")));
+    }
+    
+    @Test
+    public void testEscaplePlainText() {
+        assertEquals("", INSTANCE.escapePlainText(""));
+        assertEquals("a", INSTANCE.escapePlainText("a"));
+        assertEquals("&lt;a&amp;b&#39;c&quot;d&gt;", 
INSTANCE.escapePlainText("<a&b'c\"d>"));
+        assertEquals("a&amp;b", INSTANCE.escapePlainText("a&b"));
+        assertEquals("&lt;&gt;", INSTANCE.escapePlainText("<>"));
+    }
+    
+    @Test
+    public void testIsEmpty() throws Exception {
+        assertTrue(INSTANCE.isEmpty(INSTANCE.fromMarkup("")));
+        assertTrue(INSTANCE.isEmpty(INSTANCE.fromPlainTextByEscaping("")));
+        assertFalse(INSTANCE.isEmpty(INSTANCE.fromMarkup(" ")));
+        assertFalse(INSTANCE.isEmpty(INSTANCE.fromPlainTextByEscaping(" ")));
+    }
+    
+    private void assertMO(String pc, String mc, TemplateHTMLOutputModel mo) {
+        assertEquals(pc, _OutputFormatTestAPI.getPlainTextContent(mo));
+        assertEquals(mc, _OutputFormatTestAPI.getMarkupContent(mo));
+    }
+    
+    @Test
+    public void testGetMimeType() {
+        assertEquals("text/html", INSTANCE.getMimeType());
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormatTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormatTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormatTest.java
new file mode 100644
index 0000000..13563f5
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/outputformat/impl/RTFOutputFormatTest.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.outputformat.impl;
+
+import static org.apache.freemarker.core.outputformat.impl.RTFOutputFormat.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat._OutputFormatTestAPI;
+import org.junit.Test; 
+
+public class RTFOutputFormatTest {
+    
+    @Test
+    public void testOutputMO() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       
+       INSTANCE.output(INSTANCE.fromMarkup("\\par Test "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("foo { bar } \\ "), 
out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("baaz "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("\\par qweqwe"), out);
+       INSTANCE.output(INSTANCE.fromMarkup("\\par{0} End"), out);
+       
+       assertEquals(
+               "\\par Test "
+               + "foo \\{ bar \\} \\\\ "
+               + "baaz "
+               + "\\\\par qweqwe"
+               + "\\par{0} End",
+               out.toString());
+    }
+    
+    @Test
+    public void testOutputString() throws TemplateModelException, IOException {
+        StringWriter out = new StringWriter();
+        
+        INSTANCE.output("a", out);
+        INSTANCE.output("{", out);
+        INSTANCE.output("b}c", out);
+        
+        assertEquals("a\\{b\\}c", out.toString());
+    }
+    
+    @Test
+    public void testFromPlainTextByEscaping() throws TemplateModelException {
+        String plainText = "a\\b";
+        TemplateRTFOutputModel mo = 
INSTANCE.fromPlainTextByEscaping(plainText);
+        assertSame(plainText, _OutputFormatTestAPI.getPlainTextContent(mo));
+        assertNull(_OutputFormatTestAPI.getMarkupContent(mo)); // Not the MO's 
duty to calculate it!
+    }
+
+    @Test
+    public void testFromMarkup() throws TemplateModelException {
+        String markup = "a \\par b";
+        TemplateRTFOutputModel mo = INSTANCE.fromMarkup(markup);
+        assertSame(markup, _OutputFormatTestAPI.getMarkupContent(mo));
+        assertNull(_OutputFormatTestAPI.getPlainTextContent(mo)); // Not the 
MO's duty to calculate it!
+    }
+    
+    @Test
+    public void testGetMarkup() throws TemplateModelException {
+        {
+            String markup = "a \\par b";
+            TemplateRTFOutputModel mo = INSTANCE.fromMarkup(markup);
+            assertSame(markup, INSTANCE.getMarkupString(mo));
+        }
+        
+        {
+            String safe = "abc";
+            TemplateRTFOutputModel mo = INSTANCE.fromPlainTextByEscaping(safe);
+            assertSame(safe, INSTANCE.getMarkupString(mo));
+        }
+    }
+    
+    @Test
+    public void testConcat() throws Exception {
+        assertMO(
+                "ab", null,
+                INSTANCE.concat(new TemplateRTFOutputModel("a", null), new 
TemplateRTFOutputModel("b", null)));
+        assertMO(
+                null, "ab",
+                INSTANCE.concat(new TemplateRTFOutputModel(null, "a"), new 
TemplateRTFOutputModel(null, "b")));
+        assertMO(
+                null, "{a}\\{b\\}",
+                INSTANCE.concat(new TemplateRTFOutputModel(null, "{a}"), new 
TemplateRTFOutputModel("{b}", null)));
+        assertMO(
+                null, "\\{a\\}{b}",
+                INSTANCE.concat(new TemplateRTFOutputModel("{a}", null), new 
TemplateRTFOutputModel(null, "{b}")));
+    }
+    
+    @Test
+    public void testEscaplePlainText() {
+        assertEquals("", INSTANCE.escapePlainText(""));
+        assertEquals("a", INSTANCE.escapePlainText("a"));
+        assertEquals("\\{a\\\\b\\}", INSTANCE.escapePlainText("{a\\b}"));
+        assertEquals("a\\\\b", INSTANCE.escapePlainText("a\\b"));
+        assertEquals("\\{\\}", INSTANCE.escapePlainText("{}"));
+    }
+    
+    private void assertMO(String pc, String mc, TemplateRTFOutputModel mo) {
+        assertEquals(pc, _OutputFormatTestAPI.getPlainTextContent(mo));
+        assertEquals(mc, _OutputFormatTestAPI.getMarkupContent(mo));
+    }
+    
+    @Test
+    public void testGetMimeType() {
+        assertEquals("application/rtf", INSTANCE.getMimeType());
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolverTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolverTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolverTest.java
new file mode 100644
index 0000000..a7063fd
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolverTest.java
@@ -0,0 +1,365 @@
+/*
+ * 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.templateresolver;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.StrongCacheStorage;
+import org.apache.freemarker.test.MonitoredTemplateLoader;
+import org.apache.freemarker.test.MonitoredTemplateLoader.CloseSessionEvent;
+import org.apache.freemarker.test.MonitoredTemplateLoader.CreateSessionEvent;
+import org.apache.freemarker.test.MonitoredTemplateLoader.LoadEvent;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultTemplateResolverTest {
+
+    @Test
+    public void testCachedException() throws Exception {
+        MockTemplateLoader loader = new MockTemplateLoader();
+        DefaultTemplateResolver tr = new DefaultTemplateResolver(
+                loader,
+                new StrongCacheStorage(), 1000L,
+                DefaultTemplateLookupStrategy.INSTANCE, true,
+                DefaultTemplateNameFormat.INSTANCE,
+                null,
+                new TestConfigurationBuilder().build());
+        loader.setThrowException(true);
+        try {
+            tr.getTemplate("t", Locale.getDefault(), null).getTemplate();
+            fail();
+        } catch (IOException e) {
+            assertEquals("mock IO exception", e.getMessage());
+            assertEquals(1, loader.getLoadAttemptCount());
+            try {
+                tr.getTemplate("t", Locale.getDefault(), null).getTemplate();
+                fail();
+            } catch (IOException e2) {
+                // Still 1 - returned cached exception
+                assertThat(e2.getMessage(),
+                        Matchers.allOf(Matchers.containsString("There was an 
error loading the template on an " +
+                        "earlier attempt")));
+                assertSame(e, e2.getCause());
+                assertEquals(1, loader.getLoadAttemptCount());
+                try {
+                    Thread.sleep(1100L);
+                    tr.getTemplate("t", Locale.getDefault(), 
null).getTemplate();
+                    fail();
+                } catch (IOException e3) {
+                    // Cache had to retest
+                    assertEquals("mock IO exception", e.getMessage());
+                    assertEquals(2, loader.getLoadAttemptCount());
+                }
+            }
+        }
+    }
+    
+    @Test
+    public void testCachedNotFound() throws Exception {
+        MockTemplateLoader loader = new MockTemplateLoader();
+        DefaultTemplateResolver cache = new DefaultTemplateResolver(
+                loader,
+                new StrongCacheStorage(), 1000L,
+                DefaultTemplateLookupStrategy.INSTANCE, false,
+                DefaultTemplateNameFormat.INSTANCE,
+                null, new TestConfigurationBuilder().build());
+        assertNull(cache.getTemplate("t", Locale.getDefault(), 
null).getTemplate());
+        assertEquals(1, loader.getLoadAttemptCount());
+        assertNull(cache.getTemplate("t", Locale.getDefault(), 
null).getTemplate());
+        // Still 1 - returned cached exception
+        assertEquals(1, loader.getLoadAttemptCount());
+        Thread.sleep(1100L);
+        assertNull(cache.getTemplate("t", Locale.getDefault(), 
null).getTemplate());
+        // Cache had to retest
+        assertEquals(2, loader.getLoadAttemptCount());
+    }
+
+    private static class MockTemplateLoader implements TemplateLoader {
+        private boolean throwException;
+        private int loadAttemptCount; 
+        
+        public void setThrowException(boolean throwException) {
+           this.throwException = throwException;
+        }
+        
+        public int getLoadAttemptCount() {
+            return loadAttemptCount;
+        }
+        
+        @Override
+        public TemplateLoaderSession createSession() {
+            return null;
+        }
+
+        @Override
+        public TemplateLoadingResult load(String name, TemplateLoadingSource 
ifSourceDiffersFrom,
+                Serializable ifVersionDiffersFrom, TemplateLoaderSession 
session) throws IOException {
+            ++loadAttemptCount;
+            if (throwException) {
+                throw new IOException("mock IO exception");
+            }
+            return TemplateLoadingResult.NOT_FOUND;
+        }
+
+        @Override
+        public void resetState() {
+            //
+        }
+        
+    }
+    
+    @Test
+    public void testManualRemovalPlain() throws Exception {
+        StringTemplateLoader loader = new StringTemplateLoader();
+        Configuration cfg = new TestConfigurationBuilder()
+                .cacheStorage(new StrongCacheStorage())
+                .templateLoader(loader)
+                .templateUpdateDelayMilliseconds(Long.MAX_VALUE)
+                .build();
+
+        loader.putTemplate("1.ftl", "1 v1");
+        loader.putTemplate("2.ftl", "2 v1");
+        assertEquals("1 v1", cfg.getTemplate("1.ftl").toString()); 
+        assertEquals("2 v1", cfg.getTemplate("2.ftl").toString());
+        
+        loader.putTemplate("1.ftl", "1 v2");
+        loader.putTemplate("2.ftl", "2 v2");
+        assertEquals("1 v1", cfg.getTemplate("1.ftl").toString()); // no 
change 
+        assertEquals("2 v1", cfg.getTemplate("2.ftl").toString()); // no change
+        
+        cfg.removeTemplateFromCache("1.ftl", cfg.getLocale(), null);
+        assertEquals("1 v2", cfg.getTemplate("1.ftl").toString()); // changed 
+        assertEquals("2 v1", cfg.getTemplate("2.ftl").toString());
+        
+        cfg.removeTemplateFromCache("2.ftl", cfg.getLocale(), null);
+        assertEquals("1 v2", cfg.getTemplate("1.ftl").toString()); 
+        assertEquals("2 v2", cfg.getTemplate("2.ftl").toString()); // changed
+    }
+
+    @Test
+    public void testManualRemovalI18ed() throws Exception {
+        StringTemplateLoader loader = new StringTemplateLoader();
+        Configuration cfg = new TestConfigurationBuilder()
+                .cacheStorage(new StrongCacheStorage())
+                .templateLoader(loader)
+                .templateUpdateDelayMilliseconds(Long.MAX_VALUE)
+                .build();
+
+        loader.putTemplate("1_en_US.ftl", "1_en_US v1");
+        loader.putTemplate("1_en.ftl", "1_en v1");
+        loader.putTemplate("1.ftl", "1 v1");
+        
+        assertEquals("1_en_US v1", cfg.getTemplate("1.ftl").toString());       
 
+        assertEquals("1_en v1", cfg.getTemplate("1.ftl", 
Locale.UK).toString());        
+        assertEquals("1 v1", cfg.getTemplate("1.ftl", 
Locale.GERMANY).toString());
+        
+        loader.putTemplate("1_en_US.ftl", "1_en_US v2");
+        loader.putTemplate("1_en.ftl", "1_en v2");
+        loader.putTemplate("1.ftl", "1 v2");
+        assertEquals("1_en_US v1", cfg.getTemplate("1.ftl").toString());       
 
+        assertEquals("1_en v1", cfg.getTemplate("1.ftl", 
Locale.UK).toString());        
+        assertEquals("1 v1", cfg.getTemplate("1.ftl", 
Locale.GERMANY).toString());
+        
+        cfg.removeTemplateFromCache("1.ftl", cfg.getLocale(), null);
+        assertEquals("1_en_US v2", cfg.getTemplate("1.ftl").toString());       
 
+        assertEquals("1_en v1", cfg.getTemplate("1.ftl", 
Locale.UK).toString());        
+        assertEquals("1 v1", cfg.getTemplate("1.ftl", 
Locale.GERMANY).toString());
+        assertEquals("1 v2", cfg.getTemplate("1.ftl", 
Locale.ITALY).toString());
+        
+        cfg.removeTemplateFromCache("1.ftl", Locale.GERMANY, null);
+        assertEquals("1_en v1", cfg.getTemplate("1.ftl", 
Locale.UK).toString());        
+        assertEquals("1 v2", cfg.getTemplate("1.ftl", 
Locale.GERMANY).toString());
+
+        cfg.removeTemplateFromCache("1.ftl", Locale.CANADA, null);
+        assertEquals("1_en v1", cfg.getTemplate("1.ftl", 
Locale.UK).toString());
+        
+        cfg.removeTemplateFromCache("1.ftl", Locale.UK, null);
+        assertEquals("1_en v2", cfg.getTemplate("1.ftl", 
Locale.UK).toString());        
+    }
+
+    @Test
+    public void testZeroUpdateDelay() throws Exception {
+        MonitoredTemplateLoader loader = new MonitoredTemplateLoader();
+        TestConfigurationBuilder cfgB = new TestConfigurationBuilder()
+                .cacheStorage(new StrongCacheStorage())
+                .templateLoader(loader)
+                .templateUpdateDelayMilliseconds(0);
+
+        Configuration cfg = cfgB.build();
+
+        for (int i = 1; i <= 3; i++) {
+            loader.putTextTemplate("t.ftl", "v" + i);
+            assertEquals("v" + i, cfg.getTemplate("t.ftl").toString());
+        }
+
+        loader.clearEvents();
+        loader.putTextTemplate("t.ftl", "v8");
+        assertEquals("v8", cfg.getTemplate("t.ftl").toString());
+        assertEquals("v8", cfg.getTemplate("t.ftl").toString());
+        loader.putTextTemplate("t.ftl", "v9");
+        assertEquals("v9", cfg.getTemplate("t.ftl").toString());
+        assertEquals("v9", cfg.getTemplate("t.ftl").toString());
+        assertEquals(
+                ImmutableList.of(
+                        new LoadEvent("t_en_US.ftl", 
TemplateLoadingResultStatus.NOT_FOUND), // v8
+                        new LoadEvent("t_en.ftl", 
TemplateLoadingResultStatus.NOT_FOUND),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.OPENED),
+
+                        new LoadEvent("t_en_US.ftl", 
TemplateLoadingResultStatus.NOT_FOUND), // v8
+                        new LoadEvent("t_en.ftl", 
TemplateLoadingResultStatus.NOT_FOUND),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.NOT_MODIFIED),
+                        
+                        new LoadEvent("t_en_US.ftl", 
TemplateLoadingResultStatus.NOT_FOUND), // v9
+                        new LoadEvent("t_en.ftl", 
TemplateLoadingResultStatus.NOT_FOUND),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.OPENED),
+
+                        new LoadEvent("t_en_US.ftl", 
TemplateLoadingResultStatus.NOT_FOUND), // v9
+                        new LoadEvent("t_en.ftl", 
TemplateLoadingResultStatus.NOT_FOUND),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.NOT_MODIFIED)
+                ),
+                loader.getEvents(LoadEvent.class));
+        
+        cfg = cfgB.localizedLookup(false).build();
+        loader.clearEvents();
+        loader.putTextTemplate("t.ftl", "v10");
+        assertEquals("v10", cfg.getTemplate("t.ftl").toString());
+        loader.putTextTemplate("t.ftl", "v11"); // same time stamp, different 
content
+        assertEquals("v11", cfg.getTemplate("t.ftl").toString());
+        assertEquals("v11", cfg.getTemplate("t.ftl").toString());
+        assertEquals("v11", cfg.getTemplate("t.ftl").toString());
+        Thread.sleep(17L);
+        assertEquals("v11", cfg.getTemplate("t.ftl").toString());
+        loader.putTextTemplate("t.ftl", "v12");
+        assertEquals("v12", cfg.getTemplate("t.ftl").toString());
+        assertEquals("v12", cfg.getTemplate("t.ftl").toString());
+        assertEquals(
+                ImmutableList.of(
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.OPENED), // v10
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.OPENED), // v11
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.NOT_MODIFIED),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.NOT_MODIFIED),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.NOT_MODIFIED),
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.OPENED), // v12
+                        new LoadEvent("t.ftl", 
TemplateLoadingResultStatus.NOT_MODIFIED)
+                ),
+                loader.getEvents(LoadEvent.class));
+    }
+    
+    @Test
+    public void testWrongEncodingReload() throws Exception {
+        MonitoredTemplateLoader loader = new MonitoredTemplateLoader();
+        loader.putBinaryTemplate("utf-8_en.ftl", "<#ftl 
encoding='utf-8'>Béka");
+        loader.putBinaryTemplate("utf-8.ftl", "Bar");
+        loader.putBinaryTemplate("iso-8859-1_en_US.ftl", "<#ftl 
encoding='ISO-8859-1'>Béka",
+                StandardCharsets.ISO_8859_1, "v1");
+        Configuration cfg = new 
TestConfigurationBuilder().templateLoader(loader).build();
+
+        {
+            Template t = cfg.getTemplate("utf-8.ftl");
+            assertEquals("utf-8.ftl", t.getLookupName());
+            assertEquals("utf-8_en.ftl", t.getSourceName());
+            assertEquals(StandardCharsets.UTF_8, t.getActualSourceEncoding());
+            assertEquals("Béka", t.toString());
+            
+            assertEquals(
+                    ImmutableList.of(
+                            CreateSessionEvent.INSTANCE,
+                            new LoadEvent("utf-8_en_US.ftl", 
TemplateLoadingResultStatus.NOT_FOUND),
+                            new LoadEvent("utf-8_en.ftl", 
TemplateLoadingResultStatus.OPENED),
+                            CloseSessionEvent.INSTANCE),
+                    loader.getEvents());
+        }
+
+        {
+            loader.clearEvents();
+            
+            Template t = cfg.getTemplate("iso-8859-1.ftl");
+            assertEquals("iso-8859-1.ftl", t.getLookupName());
+            assertEquals("iso-8859-1_en_US.ftl", t.getSourceName());
+            assertEquals(StandardCharsets.ISO_8859_1, 
t.getActualSourceEncoding());
+            assertEquals("Béka", t.toString());
+            
+            assertEquals(
+                    ImmutableList.of(
+                            CreateSessionEvent.INSTANCE,
+                            new LoadEvent("iso-8859-1_en_US.ftl", 
TemplateLoadingResultStatus.OPENED),
+                            CloseSessionEvent.INSTANCE),
+                    loader.getEvents());
+        }
+    }
+
+    @Test
+    public void testNoWrongEncodingForTemplateLoader2WithReader() throws 
Exception {
+        MonitoredTemplateLoader loader = new MonitoredTemplateLoader();
+        loader.putTextTemplate("foo_en.ftl", "<#ftl encoding='utf-8'>ő");
+        loader.putTextTemplate("foo.ftl", "B");
+        Configuration cfg = new 
TestConfigurationBuilder().templateLoader(loader).build();
+        
+        {
+            Template t = cfg.getTemplate("foo.ftl");
+            assertEquals("foo.ftl", t.getLookupName());
+            assertEquals("foo_en.ftl", t.getSourceName());
+            assertNull(t.getActualSourceEncoding());
+            assertEquals("ő", t.toString());
+            
+            assertEquals(
+                    ImmutableList.of(
+                            CreateSessionEvent.INSTANCE,
+                            new LoadEvent("foo_en_US.ftl", 
TemplateLoadingResultStatus.NOT_FOUND),
+                            new LoadEvent("foo_en.ftl", 
TemplateLoadingResultStatus.OPENED),
+                            CloseSessionEvent.INSTANCE),                
+                    loader.getEvents());
+        }
+    }
+
+    @Test
+    public void testTemplateNameFormatException() throws Exception {
+        Configuration cfg = new TestConfigurationBuilder()
+                .templateNameFormat(DefaultTemplateNameFormat.INSTANCE)
+                .build();
+        try {
+            cfg.getTemplate("../x");
+            fail();
+        } catch (MalformedTemplateNameException e) {
+            // expected
+        }
+        try {
+            cfg.getTemplate("\\x");
+            fail();
+        } catch (MalformedTemplateNameException e) {
+            // expected
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/FileTemplateLoaderTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/FileTemplateLoaderTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/FileTemplateLoaderTest.java
new file mode 100644
index 0000000..393dbae
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/FileTemplateLoaderTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.templateresolver;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.io.Files;
+
+public class FileTemplateLoaderTest {
+    
+    private File templateRootDir;
+    
+    private Configuration cfg;
+    
+    @Before
+    public void setup() throws IOException {
+        templateRootDir = Files.createTempDir();
+        File sub1Dir = new File(templateRootDir, "sub1");
+        File sub2Dir = new File(sub1Dir, "sub2");
+        if (!sub2Dir.mkdirs()) {
+            throw new IOException("Failed to invoke subdirectories");
+        }
+        File tFile = new File(sub2Dir, "t.ftl");
+        FileUtils.write(tFile, "foo");
+
+        cfg = new TestConfigurationBuilder().templateLoader(new 
FileTemplateLoader(templateRootDir)).build();
+    }
+    
+    @Test
+    public void testSuccessful() throws Exception {
+        for (int i = 0; i < 2; i++) {
+            assertEquals("foo", cfg.getTemplate("sub1/sub2/t.ftl").toString());
+        }
+    }
+
+    @Test
+    public void testSuccessful2() throws Exception {
+        ((FileTemplateLoader) 
cfg.getTemplateLoader()).setEmulateCaseSensitiveFileSystem(true);
+        for (int i = 0; i < 2; i++) {
+            cfg.clearTemplateCache();
+            assertEquals("foo", cfg.getTemplate("sub1/sub2/t.ftl").toString());
+        }
+    }
+    
+    
+    @Test
+    public void testNotFound() throws Exception {
+        for (int i = 0; i < 2; i++) {
+            try {
+                cfg.getTemplate("sub1X/sub2/t.ftl");
+                fail();
+            } catch (TemplateNotFoundException e) {
+                assertThat(e.getMessage(), containsString("sub1X"));
+                assertNull(e.getCause());
+            }
+        }
+    }
+
+    @Test
+    public void testCaseSensitivity() throws Exception {
+        for (boolean emuCaseSensFS : new boolean[] { false, true }) {
+            for (String nameWithBadCase : new String[] { "SUB1/sub2/t.ftl", 
"sub1/SUB2/t.ftl", "sub1/sub2/T.FTL" }) {
+                ((FileTemplateLoader) 
cfg.getTemplateLoader()).setEmulateCaseSensitiveFileSystem(emuCaseSensFS);
+                cfg.clearTemplateCache();
+                
+                if ((SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_MAC_OSX) 
&& !emuCaseSensFS) {
+                    assertEquals("foo", 
cfg.getTemplate(nameWithBadCase).toString());
+                } else {
+                    assertEquals("foo", 
cfg.getTemplate(nameWithBadCase.toLowerCase()).toString());
+                    try {
+                        cfg.getTemplate(nameWithBadCase);
+                        fail();
+                    } catch (TemplateNotFoundException e) {
+                        assertThat(e.getMessage(), 
containsString(nameWithBadCase));
+                        assertNull(e.getCause());
+                    }
+                }
+            }
+        }
+    }
+    
+    @Test
+    public void testDefault() throws IOException {
+        assertFalse(new 
FileTemplateLoader(templateRootDir).getEmulateCaseSensitiveFileSystem());
+    }
+    
+    @After
+    public void tearDown() throws IOException {
+        FileUtils.deleteDirectory(templateRootDir);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/MultiTemplateLoaderTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/MultiTemplateLoaderTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/MultiTemplateLoaderTest.java
new file mode 100644
index 0000000..c8531c5
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/templateresolver/MultiTemplateLoaderTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.templateresolver;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.io.IOUtils;
+import 
org.apache.freemarker.core.templateresolver.impl.ByteArrayTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.MultiTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.junit.Test;
+
+public class MultiTemplateLoaderTest {
+
+    @Test
+    public void testBasics() throws IOException {
+        StringTemplateLoader stl1 = new StringTemplateLoader();
+        stl1.putTemplate("1.ftl", "1");
+        stl1.putTemplate("both.ftl", "both 1");
+
+        StringTemplateLoader stl2 = new StringTemplateLoader();
+        stl2.putTemplate("2.ftl", "2");
+        stl2.putTemplate("both.ftl", "both 2");
+        
+        MultiTemplateLoader mtl = new MultiTemplateLoader(stl1, stl2);
+        assertEquals("1", getTemplateContent(mtl, "1.ftl"));
+        assertEquals("2", getTemplateContent(mtl, "2.ftl"));
+        assertEquals("both 1", getTemplateContent(mtl, "both.ftl"));
+        assertNull(getTemplateContent(mtl, "neither.ftl"));
+    }
+
+    @Test
+    public void testSticky() throws IOException {
+        testStickiness(true);
+    }
+
+    @Test
+    public void testNonSticky() throws IOException {
+        testStickiness(false);
+    }
+    
+    private void testStickiness(boolean sticky) throws IOException {
+        StringTemplateLoader stl1 = new StringTemplateLoader();
+        stl1.putTemplate("both.ftl", "both 1");
+        
+        ByteArrayTemplateLoader stl2 = new ByteArrayTemplateLoader();
+        stl2.putTemplate("both.ftl", "both 
2".getBytes(StandardCharsets.UTF_8));
+
+        MultiTemplateLoader mtl = new MultiTemplateLoader(stl1, stl2);
+        mtl.setSticky(sticky);
+        
+        assertEquals("both 1", getTemplateContent(mtl, "both.ftl"));
+        assertTrue(stl1.removeTemplate("both.ftl"));
+        assertEquals("both 2", getTemplateContent(mtl, "both.ftl"));
+        stl1.putTemplate("both.ftl", "both 1");
+        assertEquals(sticky ? "both 2" : "both 1", getTemplateContent(mtl, 
"both.ftl"));
+        assertTrue(stl2.removeTemplate("both.ftl"));
+        assertEquals("both 1", getTemplateContent(mtl, "both.ftl"));
+    }
+    
+    private String getTemplateContent(TemplateLoader tl, String name) throws 
IOException {
+        TemplateLoaderSession ses = tl.createSession();
+        try {
+            TemplateLoadingResult res = tl.load(name, null, null, ses);
+            if (res.getStatus() == TemplateLoadingResultStatus.NOT_FOUND) {
+                return null;
+            }
+            return IOUtils.toString(
+                    res.getReader() != null
+                            ? res.getReader()
+                            : new InputStreamReader(res.getInputStream(), 
StandardCharsets.UTF_8));
+        } finally {
+            if (ses != null) {
+                ses.close();
+            }
+        }
+    }
+    
+}

Reply via email to