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

[35/51] [abbrv] [partial] incubator-freemarker git commit: Restructured project so that freemarker-test-utils depends on freemarker-core (and hence can provide common classes for testing templates, and can use utility classes defined in the core). As a c

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
new file mode 100644
index 0000000..2a0ae9d
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
@@ -0,0 +1,403 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.regex.Pattern;
+
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+public class StringUtilTest {
+
+    @Test
+    public void testV2319() {
+        assertEquals("\\n\\r\\f\\b\\t\\x00\\x19", _StringUtil.javaScriptStringEnc("\n\r\f\b\t\u0000\u0019"));
+    }
+
+    @Test
+    public void testControlChars() {
+        assertEsc(
+                "\n\r\f\b\t \u0000\u0019\u001F \u007F\u0080\u009F \u2028\u2029",
+                "\\n\\r\\f\\b\\t \\x00\\x19\\x1F \\x7F\\x80\\x9F \\u2028\\u2029",
+                "\\n\\r\\f\\b\\t \\u0000\\u0019\\u001F \\u007F\\u0080\\u009F \\u2028\\u2029");
+    }
+
+    @Test
+    public void testHtmlChars() {
+        assertEsc(
+                "<safe>/>->]> </foo> <!-- --> <![CDATA[ ]]> <?php?>",
+                "<safe>/>->]> <\\/foo> \\x3C!-- --\\> \\x3C![CDATA[ ]]\\> \\x3C?php?>",
+                "<safe>/>->]> <\\/foo> \\u003C!-- --\\u003E \\u003C![CDATA[ ]]\\u003E \\u003C?php?>");
+        assertEsc("<!c", "\\x3C!c", "\\u003C!c");
+        assertEsc("c<!", "c\\x3C!", "c\\u003C!");
+        assertEsc("c<", "c\\x3C", "c\\u003C");
+        assertEsc("c<c", "c<c", "c<c");
+        assertEsc("<c", "<c", "<c");
+        assertEsc(">", "\\>", "\\u003E");
+        assertEsc("->", "-\\>", "-\\u003E");
+        assertEsc("-->", "--\\>", "--\\u003E");
+        assertEsc("c-->", "c--\\>", "c--\\u003E");
+        assertEsc("-->c", "--\\>c", "--\\u003Ec");
+        assertEsc("]>", "]\\>", "]\\u003E");
+        assertEsc("]]>", "]]\\>", "]]\\u003E");
+        assertEsc("c]]>", "c]]\\>", "c]]\\u003E");
+        assertEsc("]]>c", "]]\\>c", "]]\\u003Ec");
+        assertEsc("c->", "c->", "c->");
+        assertEsc("c>", "c>", "c>");
+        assertEsc("-->", "--\\>", "--\\u003E");
+        assertEsc("/", "\\/", "\\/");
+        assertEsc("/c", "\\/c", "\\/c");
+        assertEsc("</", "<\\/", "<\\/");
+        assertEsc("</c", "<\\/c", "<\\/c");
+        assertEsc("c/", "c/", "c/");
+    }
+
+    @Test
+    public void testJSChars() {
+        assertEsc("\"", "\\\"", "\\\"");
+        assertEsc("'", "\\'", "'");
+        assertEsc("\\", "\\\\", "\\\\");
+    }
+
+    @Test
+    public void testSameStringsReturned() {
+        String s = "==> I/m <safe>!";
+        assertTrue(s == _StringUtil.jsStringEnc(s, false));  // "==" because is must return the same object
+        assertTrue(s == _StringUtil.jsStringEnc(s, true));
+
+        s = "";
+        assertTrue(s == _StringUtil.jsStringEnc(s, false));
+        assertTrue(s == _StringUtil.jsStringEnc(s, true));
+
+        s = "\u00E1rv\u00EDzt\u0171r\u0151 \u3020";
+        assertEquals(s, _StringUtil.jsStringEnc(s, false));
+        assertTrue(s == _StringUtil.jsStringEnc(s, false));
+        assertTrue(s == _StringUtil.jsStringEnc(s, true));
+    }
+
+    @Test
+    public void testOneOffs() {
+        assertEsc("c\"c\"cc\"\"c", "c\\\"c\\\"cc\\\"\\\"c", "c\\\"c\\\"cc\\\"\\\"c");
+        assertEsc("\"c\"cc\"", "\\\"c\\\"cc\\\"", "\\\"c\\\"cc\\\"");
+        assertEsc("c/c/cc//c", "c/c/cc//c", "c/c/cc//c");
+        assertEsc("c<c<cc<<c", "c<c<cc<<c", "c<c<cc<<c");
+        assertEsc("/<", "\\/\\x3C", "\\/\\u003C");
+        assertEsc(">", "\\>", "\\u003E");
+        assertEsc("]>", "]\\>", "]\\u003E");
+        assertEsc("->", "-\\>", "-\\u003E");
+    }
+
+    private void assertEsc(String s, String javaScript, String json) {
+        assertEquals(javaScript, _StringUtil.jsStringEnc(s, false));
+        assertEquals(json, _StringUtil.jsStringEnc(s, true));
+    }
+
+    @Test
+    public void testTrim() {
+        assertSame(_CollectionUtil.EMPTY_CHAR_ARRAY, _StringUtil.trim(_CollectionUtil.EMPTY_CHAR_ARRAY));
+        assertSame(_CollectionUtil.EMPTY_CHAR_ARRAY, _StringUtil.trim(" \t\u0001 ".toCharArray()));
+        {
+            char[] cs = "foo".toCharArray();
+            assertSame(cs, cs);
+        }
+        assertArrayEquals("foo".toCharArray(), _StringUtil.trim("foo ".toCharArray()));
+        assertArrayEquals("foo".toCharArray(), _StringUtil.trim(" foo".toCharArray()));
+        assertArrayEquals("foo".toCharArray(), _StringUtil.trim(" foo ".toCharArray()));
+        assertArrayEquals("foo".toCharArray(), _StringUtil.trim("\t\tfoo \r\n".toCharArray()));
+        assertArrayEquals("x".toCharArray(), _StringUtil.trim(" x ".toCharArray()));
+        assertArrayEquals("x y z".toCharArray(), _StringUtil.trim(" x y z ".toCharArray()));
+    }
+
+    @Test
+    public void testIsTrimmedToEmpty() {
+        assertTrue(_StringUtil.isTrimmableToEmpty("".toCharArray()));
+        assertTrue(_StringUtil.isTrimmableToEmpty("\r\r\n\u0001".toCharArray()));
+        assertFalse(_StringUtil.isTrimmableToEmpty("x".toCharArray()));
+        assertFalse(_StringUtil.isTrimmableToEmpty("  x  ".toCharArray()));
+    }
+    
+    @Test
+    public void testJQuote() {
+        assertEquals("null", _StringUtil.jQuote(null));
+        assertEquals("\"foo\"", _StringUtil.jQuote("foo"));
+        assertEquals("\"123\"", _StringUtil.jQuote(Integer.valueOf(123)));
+        assertEquals("\"foo's \\\"bar\\\"\"",
+                _StringUtil.jQuote("foo's \"bar\""));
+        assertEquals("\"\\n\\r\\t\\u0001\"",
+                _StringUtil.jQuote("\n\r\t\u0001"));
+        assertEquals("\"<\\nb\\rc\\td\\u0001>\"",
+                _StringUtil.jQuote("<\nb\rc\td\u0001>"));
+    }
+
+    @Test
+    public void testJQuoteNoXSS() {
+        assertEquals("null", _StringUtil.jQuoteNoXSS(null));
+        assertEquals("\"foo\"", _StringUtil.jQuoteNoXSS("foo"));
+        assertEquals("\"123\"", _StringUtil.jQuoteNoXSS(Integer.valueOf(123)));
+        assertEquals("\"foo's \\\"bar\\\"\"",
+                _StringUtil.jQuoteNoXSS("foo's \"bar\""));
+        assertEquals("\"\\n\\r\\t\\u0001\"",
+                _StringUtil.jQuoteNoXSS("\n\r\t\u0001"));
+        assertEquals("\"\\u003C\\nb\\rc\\td\\u0001>\"",
+                _StringUtil.jQuoteNoXSS("<\nb\rc\td\u0001>"));
+        assertEquals("\"\\u003C\\nb\\rc\\td\\u0001>\"",
+                _StringUtil.jQuoteNoXSS((Object) "<\nb\rc\td\u0001>"));
+    }
+
+    @Test
+    public void testGlobToRegularExpression() {
+        assertGlobMatches("a/b/c.ftl", "a/b/c.ftl");
+        assertGlobDoesNotMatch("/a/b/cxftl", "/a/b/c.ftl", "a/b/C.ftl");
+        
+        assertGlobMatches("a/b/*.ftl", "a/b/.ftl", "a/b/x.ftl", "a/b/xx.ftl");
+        assertGlobDoesNotMatch("a/b/*.ftl", "a/c/x.ftl", "a/b/c/x.ftl", "/a/b/x.ftl", "a/b/xxftl");
+        
+        assertGlobMatches("a/b/?.ftl", "a/b/x.ftl");
+        assertGlobDoesNotMatch("a/b/?.ftl", "a/c/x.ftl", "a/b/.ftl", "a/b/xx.ftl", "a/b/xxftl");
+        
+        assertGlobMatches("a/**/c.ftl", "a/b/c.ftl", "a/c.ftl", "a/b/b2/b3/c.ftl", "a//c.ftl");
+        assertGlobDoesNotMatch("a/**/c.ftl", "x/b/c.ftl", "a/b/x.ftl");
+        
+        assertGlobMatches("**/c.ftl", "a/b/c.ftl", "c.ftl", "/c.ftl", "///c.ftl");
+        assertGlobDoesNotMatch("**/c.ftl", "a/b/x.ftl");
+
+        assertGlobMatches("a/b/**", "a/b/c.ftl", "a/b/c2/c.ftl", "a/b/", "a/b/c/");
+        assertGlobDoesNotMatch("a/b.ftl");
+
+        assertGlobMatches("**", "a/b/c.ftl", "");
+
+        assertGlobMatches("\\[\\{\\*\\?\\}\\]\\\\", "[{*?}]\\");
+        assertGlobDoesNotMatch("\\[\\{\\*\\?\\}\\]\\\\", "[{xx}]\\");
+
+        assertGlobMatches("a/b/\\?.ftl", "a/b/?.ftl");
+        assertGlobDoesNotMatch("a/b/\\?.ftl", "a/b/x.ftl");
+
+        assertGlobMatches("\\?\\?.ftl", "??.ftl");
+        assertGlobMatches("\\\\\\\\", "\\\\");
+        assertGlobMatches("\\\\\\\\?", "\\\\x");
+        assertGlobMatches("x\\", "x");
+
+        assertGlobMatches("???*", "123", "1234", "12345");
+        assertGlobDoesNotMatch("???*", "12", "1", "");
+
+        assertGlobMatches("**/a??/b*.ftl", "a11/b1.ftl", "x/a11/b123.ftl", "x/y/a11/b.ftl");
+        assertGlobDoesNotMatch("**/a??/b*.ftl", "a1/b1.ftl", "x/a11/c123.ftl");
+        
+        assertFalse(_StringUtil.globToRegularExpression("ab*").matcher("aBc").matches());
+        assertTrue(_StringUtil.globToRegularExpression("ab*", true).matcher("aBc").matches());
+        assertTrue(_StringUtil.globToRegularExpression("ab", true).matcher("aB").matches());
+        assertTrue(_StringUtil.globToRegularExpression("\u00E1b*", true).matcher("\u00C1bc").matches());
+        
+        try {
+            _StringUtil.globToRegularExpression("x**/y");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), Matchers.containsString("**"));
+        }
+        
+        try {
+            _StringUtil.globToRegularExpression("**y");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), Matchers.containsString("**"));
+        }
+        
+        try {
+            _StringUtil.globToRegularExpression("[ab]c");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), Matchers.containsString("unsupported"));
+        }
+        
+        try {
+            _StringUtil.globToRegularExpression("{aa,bb}c");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), Matchers.containsString("unsupported"));
+        }
+    }
+    
+    private void assertGlobMatches(String glob, String... ss) {
+        Pattern pattern = _StringUtil.globToRegularExpression(glob);
+        for (String s : ss) {
+            if (!pattern.matcher(s).matches()) {
+                fail("Glob " + glob + " (regexp: " + pattern + ") doesn't match " + s);
+            }
+        }
+    }
+
+    private void assertGlobDoesNotMatch(String glob, String... ss) {
+        Pattern pattern = _StringUtil.globToRegularExpression(glob);
+        for (String s : ss) {
+            if (pattern.matcher(s).matches()) {
+                fail("Glob " + glob + " (regexp: " + pattern + ") matches " + s);
+            }
+        }
+    }
+    
+    @Test
+    public void testHTMLEnc() {
+        String s = "";
+        assertSame(s, _StringUtil.XMLEncNA(s));
+        
+        s = "asd";
+        assertSame(s, _StringUtil.XMLEncNA(s));
+        
+        assertEquals("a&amp;b&lt;c&gt;d&quot;e'f", _StringUtil.XMLEncNA("a&b<c>d\"e'f"));
+        assertEquals("&lt;", _StringUtil.XMLEncNA("<"));
+        assertEquals("&lt;a", _StringUtil.XMLEncNA("<a"));
+        assertEquals("&lt;a&gt;", _StringUtil.XMLEncNA("<a>"));
+        assertEquals("a&gt;", _StringUtil.XMLEncNA("a>"));
+        assertEquals("&lt;&gt;", _StringUtil.XMLEncNA("<>"));
+        assertEquals("a&lt;&gt;b", _StringUtil.XMLEncNA("a<>b"));
+    }
+
+    @Test
+    public void testXHTMLEnc() throws IOException {
+        String s = "";
+        assertSame(s, _StringUtil.XHTMLEnc(s));
+        
+        s = "asd";
+        assertSame(s, _StringUtil.XHTMLEnc(s));
+        
+        testXHTMLEnc("a&amp;b&lt;c&gt;d&quot;e&#39;f", "a&b<c>d\"e'f");
+        testXHTMLEnc("&lt;", "<");
+        testXHTMLEnc("&lt;a", "<a");
+        testXHTMLEnc("&lt;a&gt;", "<a>");
+        testXHTMLEnc("a&gt;", "a>");
+        testXHTMLEnc("&lt;&gt;", "<>");
+        testXHTMLEnc("a&lt;&gt;b", "a<>b");
+    }
+    
+    private void testXHTMLEnc(String expected, String in) throws IOException {
+        assertEquals(expected, _StringUtil.XHTMLEnc(in));
+        
+        StringWriter sw = new StringWriter();
+        _StringUtil.XHTMLEnc(in, sw);
+        assertEquals(expected, sw.toString());
+    }
+
+    @Test
+    public void testXMLEnc() throws IOException {
+        String s = "";
+        assertSame(s, _StringUtil.XMLEnc(s));
+        
+        s = "asd";
+        assertSame(s, _StringUtil.XMLEnc(s));
+        
+        testXMLEnc("a&amp;b&lt;c&gt;d&quot;e&apos;f", "a&b<c>d\"e'f");
+        testXMLEnc("&lt;", "<");
+        testXMLEnc("&lt;a", "<a");
+        testXMLEnc("&lt;a&gt;", "<a>");
+        testXMLEnc("a&gt;", "a>");
+        testXMLEnc("&lt;&gt;", "<>");
+        testXMLEnc("a&lt;&gt;b", "a<>b");
+    }
+    
+    private void testXMLEnc(String expected, String in) throws IOException {
+        assertEquals(expected, _StringUtil.XMLEnc(in));
+        
+        StringWriter sw = new StringWriter();
+        _StringUtil.XMLEnc(in, sw);
+        assertEquals(expected, sw.toString());
+    }
+
+    @Test
+    public void testXMLEncQAttr() throws IOException {
+        String s = "";
+        assertSame(s, _StringUtil.XMLEncQAttr(s));
+        
+        s = "asd";
+        assertSame(s, _StringUtil.XMLEncQAttr(s));
+        
+        assertEquals("a&amp;b&lt;c>d&quot;e'f", _StringUtil.XMLEncQAttr("a&b<c>d\"e'f"));
+        assertEquals("&lt;", _StringUtil.XMLEncQAttr("<"));
+        assertEquals("&lt;a", _StringUtil.XMLEncQAttr("<a"));
+        assertEquals("&lt;a>", _StringUtil.XMLEncQAttr("<a>"));
+        assertEquals("a>", _StringUtil.XMLEncQAttr("a>"));
+        assertEquals("&lt;>", _StringUtil.XMLEncQAttr("<>"));
+        assertEquals("a&lt;>b", _StringUtil.XMLEncQAttr("a<>b"));
+    }
+    
+    @Test
+    public void testXMLEncNQG() throws IOException {
+        String s = "";
+        assertSame(s, _StringUtil.XMLEncNQG(s));
+        
+        s = "asd";
+        assertSame(s, _StringUtil.XMLEncNQG(s));
+        
+        assertEquals("a&amp;b&lt;c>d\"e'f", _StringUtil.XMLEncNQG("a&b<c>d\"e'f"));
+        assertEquals("&lt;", _StringUtil.XMLEncNQG("<"));
+        assertEquals("&lt;a", _StringUtil.XMLEncNQG("<a"));
+        assertEquals("&lt;a>", _StringUtil.XMLEncNQG("<a>"));
+        assertEquals("a>", _StringUtil.XMLEncNQG("a>"));
+        assertEquals("&lt;>", _StringUtil.XMLEncNQG("<>"));
+        assertEquals("a&lt;>b", _StringUtil.XMLEncNQG("a<>b"));
+        
+        assertEquals("&gt;", _StringUtil.XMLEncNQG(">"));
+        assertEquals("]&gt;", _StringUtil.XMLEncNQG("]>"));
+        assertEquals("]]&gt;", _StringUtil.XMLEncNQG("]]>"));
+        assertEquals("x]]&gt;", _StringUtil.XMLEncNQG("x]]>"));
+        assertEquals("x]>", _StringUtil.XMLEncNQG("x]>"));
+        assertEquals("]x>", _StringUtil.XMLEncNQG("]x>"));
+    }
+
+    @Test
+    public void testRTFEnc() throws IOException {
+        String s = "";
+        assertSame(s, _StringUtil.RTFEnc(s));
+        
+        s = "asd";
+        assertSame(s, _StringUtil.RTFEnc(s));
+        
+        testRTFEnc("a\\{b\\}c\\\\d", "a{b}c\\d");
+        testRTFEnc("\\{", "{");
+        testRTFEnc("\\{a", "{a");
+        testRTFEnc("\\{a\\}", "{a}");
+        testRTFEnc("a\\}", "a}");
+        testRTFEnc("\\{\\}", "{}");
+        testRTFEnc("a\\{\\}b", "a{}b");
+    }
+
+    private void testRTFEnc(String expected, String in) throws IOException {
+        assertEquals(expected, _StringUtil.RTFEnc(in));
+        
+        StringWriter sw = new StringWriter();
+        _StringUtil.RTFEnc(in, sw);
+        assertEquals(expected, sw.toString());
+    }
+
+    @Test
+    public void testNormalizeEOLs() {
+        assertNull(_StringUtil.normalizeEOLs(null));
+        assertEquals("", _StringUtil.normalizeEOLs(""));
+        assertEquals("x", _StringUtil.normalizeEOLs("x"));
+        assertEquals("x\ny", _StringUtil.normalizeEOLs("x\ny"));
+        assertEquals("x\ny", _StringUtil.normalizeEOLs("x\r\ny"));
+        assertEquals("x\ny", _StringUtil.normalizeEOLs("x\ry"));
+        assertEquals("\n\n\n\n\n\n", _StringUtil.normalizeEOLs("\n\r\r\r\n\r\n\r"));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.java
new file mode 100644
index 0000000..8900d2b
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/NumberFormatTest.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.valueformat;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
+import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory;
+import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory;
+import org.apache.freemarker.core.userpkg.LocaleSensitiveTemplateNumberFormatFactory;
+import org.apache.freemarker.core.userpkg.PrintfGTemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.AliasTemplateNumberFormatFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+@SuppressWarnings("boxing")
+public class NumberFormatTest extends TemplateTest {
+    
+    @Test
+    public void testUnknownCustomFormat() throws Exception {
+        {
+            setConfigurationWithNumberFormat("@noSuchFormat");
+            Throwable exc = assertErrorContains("${1}", "\"@noSuchFormat\"", "\"noSuchFormat\"");
+            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
+        }
+
+        {
+            setConfigurationWithNumberFormat("number");
+            Throwable exc = assertErrorContains("${1?string('@noSuchFormat2')}",
+                    "\"@noSuchFormat2\"", "\"noSuchFormat2\"");
+            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
+        }
+    }
+    
+    @Test
+    public void testStringBI() throws Exception {
+        setConfigurationWithNumberFormat(null);
+        assertOutput("${11} ${11?string.@hex} ${12} ${12?string.@hex}", "11 b 12 c");
+    }
+
+    @Test
+    public void testSetting() throws Exception {
+        setConfigurationWithNumberFormat("@hex");
+        assertOutput("${11?string.number} ${11} ${12?string.number} ${12}", "11 b 12 c");
+    }
+
+    @Test
+    public void testSetting2() throws Exception {
+        setConfigurationWithNumberFormat(null);
+        assertOutput(
+                "<#setting numberFormat='@hex'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}"
+                + "<#setting numberFormat='@loc'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}",
+                "11 b 12 c d"
+                + "11 11_en_US 12 12_en_US 13_en_US");
+    }
+    
+    @Test
+    public void testUnformattableNumber() throws Exception {
+        setConfigurationWithNumberFormat("@hex");
+        assertErrorContains("${1.1}", "hexadecimal int", "doesn't fit into an int");
+    }
+
+    @Test
+    public void testLocaleSensitive() throws Exception {
+        setConfigurationWithNumberFormat("@loc");
+        assertOutput("${1.1}", "1.1_en_US");
+        setConfigurationWithNumberFormat("@loc", null, null, Locale.GERMANY);
+        assertOutput("${1.1}", "1.1_de_DE");
+    }
+
+    @Test
+    public void testLocaleSensitive2() throws Exception {
+        setConfigurationWithNumberFormat("@loc");
+        assertOutput("${1.1} <#setting locale='de_DE'>${1.1}", "1.1_en_US 1.1_de_DE");
+    }
+
+    @Test
+    public void testCustomParameterized() throws Exception {
+        setConfigurationWithNumberFormat("@base 2");
+        assertOutput("${11}", "1011");
+        assertOutput("${11?string}", "1011");
+        assertOutput("${11?string.@base_3}", "102");
+        
+        assertErrorContains("${11?string.@base_xyz}", "\"@base_xyz\"", "\"xyz\"");
+        setConfigurationWithNumberFormat("@base");
+        assertErrorContains("${11}", "\"@base\"", "format parameter is required");
+    }
+
+    @Test
+    public void testCustomWithFallback() throws Exception {
+        Configuration cfg = getConfiguration();
+        setConfigurationWithNumberFormat("@base 2|0.0#");
+        assertOutput("${11}", "1011");
+        assertOutput("${11.34}", "11.34");
+        assertOutput("${11?string('@base 3|0.00')}", "102");
+        assertOutput("${11.2?string('@base 3|0.00')}", "11.20");
+    }
+
+    @Test
+    public void testEnvironmentGetters() throws Exception {
+        setConfigurationWithNumberFormat(null);
+
+        Template t = new Template(null, "", getConfiguration());
+        Environment env = t.createProcessingEnvironment(null, null);
+        
+        TemplateNumberFormat defF = env.getTemplateNumberFormat();
+        //
+        TemplateNumberFormat explF = env.getTemplateNumberFormat("0.00");
+        assertEquals("1.25", explF.formatToPlainText(new SimpleNumber(1.25)));
+        //
+        TemplateNumberFormat expl2F = env.getTemplateNumberFormat("@loc");
+        assertEquals("1.25_en_US", expl2F.formatToPlainText(new SimpleNumber(1.25)));
+        
+        TemplateNumberFormat explFFr = env.getTemplateNumberFormat("0.00", Locale.FRANCE);
+        assertNotSame(explF, explFFr);
+        assertEquals("1,25", explFFr.formatToPlainText(new SimpleNumber(1.25)));
+        //
+        TemplateNumberFormat expl2FFr = env.getTemplateNumberFormat("@loc", Locale.FRANCE);
+        assertEquals("1.25_fr_FR", expl2FFr.formatToPlainText(new SimpleNumber(1.25)));
+        
+        assertSame(env.getTemplateNumberFormat(), defF);
+        //
+        assertSame(env.getTemplateNumberFormat("0.00"), explF);
+        //
+        assertSame(env.getTemplateNumberFormat("@loc"), expl2F);
+    }
+    
+    /**
+     * ?string formats lazily (at least in 2.3.x), so it must make a snapshot of the format inputs when it's called.
+     */
+    @Test
+    @Ignore // [FM3] We want to rework BI-s so that lazy evaluation won't be needed. Then this will go away too.
+    public void testStringBIDoesSnapshot() throws Exception {
+        // TemplateNumberModel-s shouldn't change, but we have to keep BC when that still happens.
+        final MutableTemplateNumberModel nm = new MutableTemplateNumberModel();
+        nm.setNumber(123);
+        addToDataModel("n", nm);
+        addToDataModel("incN", new TemplateDirectiveModel() {
+            
+            @Override
+            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                    throws TemplateException, IOException {
+                nm.setNumber(nm.getAsNumber().intValue() + 1);
+            }
+        });
+        assertOutput(
+                "<#assign s1 = n?string>"
+                + "<#setting numberFormat='@loc'>"
+                + "<#assign s2 = n?string>"
+                + "<#setting numberFormat='@hex'>"
+                + "<#assign s3 = n?string>"
+                + "${s1} ${s2} ${s3}",
+                "123 123_en_US 7b");
+        assertOutput(
+                "<#assign s1 = n?string>"
+                + "<@incN />"
+                + "<#assign s2 = n?string>"
+                + "${s1} ${s2}",
+                "123 124");
+    }
+
+    @Test
+    public void testNullInModel() throws Exception {
+        addToDataModel("n", new MutableTemplateNumberModel());
+        assertErrorContains("${n}", "nothing inside it");
+        assertErrorContains("${n?string}", "nothing inside it");
+    }
+    
+    @Test
+    public void testAtPrefix() throws Exception {
+        Configuration cfg = getConfiguration();
+
+        setConfigurationWithNumberFormat("@hex");
+        assertOutput("${10}", "a");
+        setConfigurationWithNumberFormat("'@'0");
+        assertOutput("${10}", "@10");
+        setConfigurationWithNumberFormat("@@0");
+        assertOutput("${10}", "@@10");
+
+        setConfigurationWithNumberFormat(
+                "@hex", Collections.<String, TemplateNumberFormatFactory>emptyMap());
+        assertErrorContains("${10}", "custom", "\"hex\"");
+
+        setConfigurationWithNumberFormat(
+                "'@'0", Collections.<String, TemplateNumberFormatFactory>emptyMap());
+        assertOutput("${10}", "@10");
+
+        setConfigurationWithNumberFormat(
+                "@@0", Collections.<String, TemplateNumberFormatFactory>emptyMap());
+        assertOutput("${10}", "@@10");
+    }
+
+    @Test
+    public void testAlieses() throws Exception {
+        setConfigurationWithNumberFormat(
+                "'@'0",
+                ImmutableMap.of(
+                        "f", new AliasTemplateNumberFormatFactory("0.#'f'"),
+                        "d", new AliasTemplateNumberFormatFactory("0.0#"),
+                        "hex", HexTemplateNumberFormatFactory.INSTANCE),
+                new ConditionalTemplateConfigurationFactory(
+                        new FileNameGlobMatcher("*2*"),
+                        new TemplateConfiguration.Builder()
+                                .customNumberFormats(ImmutableMap.<String, TemplateNumberFormatFactory>of(
+                                        "d", new AliasTemplateNumberFormatFactory("0.#'d'"),
+                                        "i", new AliasTemplateNumberFormatFactory("@hex")))
+                                .build()));
+
+        String commonFtl = "${1?string.@f} ${1?string.@d} "
+                + "<#setting locale='fr_FR'>${1.5?string.@d} "
+                + "<#attempt>${10?string.@i}<#recover>E</#attempt>";
+        addTemplate("t1.ftl", commonFtl);
+        addTemplate("t2.ftl", commonFtl);
+        
+        assertOutputForNamed("t1.ftl", "1f 1.0 1,5 E");
+        assertOutputForNamed("t2.ftl", "1f 1d 1,5d a");
+    }
+
+    @Test
+    public void testAlieses2() throws Exception {
+        setConfigurationWithNumberFormat(
+                "@n",
+                ImmutableMap.<String, TemplateNumberFormatFactory>of(
+                        "n", new AliasTemplateNumberFormatFactory("0.0",
+                                ImmutableMap.of(
+                                        new Locale("en"), "0.0'_en'",
+                                        Locale.UK, "0.0'_en_GB'",
+                                        Locale.FRANCE, "0.0'_fr_FR'"))));
+        assertOutput(
+                "<#setting locale='en_US'>${1} "
+                + "<#setting locale='en_GB'>${1} "
+                + "<#setting locale='en_GB_Win'>${1} "
+                + "<#setting locale='fr_FR'>${1} "
+                + "<#setting locale='hu_HU'>${1}",
+                "1.0_en 1.0_en_GB 1.0_en_GB 1,0_fr_FR 1,0");
+    }
+    
+    @Test
+    public void testMarkupFormat() throws IOException, TemplateException {
+        setConfigurationWithNumberFormat("@printfG_3");
+
+        String commonFTL = "${1234567} ${'cat:' + 1234567} ${0.0000123}";
+        String commonOutput = "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>";
+        assertOutput(commonFTL, commonOutput);
+        assertOutput("<#ftl outputFormat='HTML'>" + commonFTL, commonOutput);
+        assertOutput("<#escape x as x?html>" + commonFTL + "</#escape>", commonOutput);
+        assertOutput("<#escape x as x?xhtml>" + commonFTL + "</#escape>", commonOutput);
+        assertOutput("<#escape x as x?xml>" + commonFTL + "</#escape>", commonOutput);
+        assertOutput("${\"" + commonFTL + "\"}", "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>");
+        assertErrorContains("<#ftl outputFormat='plainText'>" + commonFTL, "HTML", "plainText", "conversion");
+    }
+
+    @Test
+    public void testPrintG() throws IOException, TemplateException {
+        setConfigurationWithNumberFormat(null);
+        for (Number n : new Number[] {
+                1234567, 1234567L, 1234567d, 1234567f, BigInteger.valueOf(1234567), BigDecimal.valueOf(1234567) }) {
+            addToDataModel("n", n);
+            
+            assertOutput("${n?string.@printfG}", "1.23457E+06");
+            assertOutput("${n?string.@printfG_3}", "1.23E+06");
+            assertOutput("${n?string.@printfG_7}", "1234567");
+            assertOutput("${0.0000123?string.@printfG}", "1.23000E-05");
+        }
+    }
+
+    private void setConfigurationWithNumberFormat(
+            String numberFormat,
+            Map<String, TemplateNumberFormatFactory> customNumberFormats,
+            TemplateConfigurationFactory templateConfigurationFactory,
+            Locale locale) {
+        TestConfigurationBuilder cfgB = new TestConfigurationBuilder(Configuration.VERSION_3_0_0);
+
+        if (numberFormat != null) {
+            cfgB.setNumberFormat(numberFormat);
+        }
+        cfgB.setCustomNumberFormats(
+                customNumberFormats != null ? customNumberFormats
+                        : ImmutableMap.of(
+                                "hex", HexTemplateNumberFormatFactory.INSTANCE,
+                                "loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE,
+                                "base", BaseNTemplateNumberFormatFactory.INSTANCE,
+                                "printfG", PrintfGTemplateNumberFormatFactory.INSTANCE));
+        if (locale != null) {
+            cfgB.setLocale(locale);
+        }
+        if (templateConfigurationFactory != null) {
+            cfgB.setTemplateConfigurations(templateConfigurationFactory);
+        }
+
+        setConfiguration(cfgB.build());
+    }
+
+    private void setConfigurationWithNumberFormat(String numberFormat) {
+        setConfigurationWithNumberFormat(numberFormat, null, null, null);
+    }
+
+    private void setConfigurationWithNumberFormat(
+            String numberFormat, Map<String, TemplateNumberFormatFactory> customNumberFormats) {
+        setConfigurationWithNumberFormat(numberFormat, customNumberFormats, null, null);
+    }
+
+    private void setConfigurationWithNumberFormat(
+            String numberFormat, Map<String, TemplateNumberFormatFactory> customNumberFormats,
+            TemplateConfigurationFactory templateConfigurationFactory) {
+        setConfigurationWithNumberFormat(numberFormat, customNumberFormats, templateConfigurationFactory, null);
+    }
+
+    private static class MutableTemplateNumberModel implements TemplateNumberModel {
+        
+        private Number number;
+
+        public void setNumber(Number number) {
+            this.number = number;
+        }
+
+        @Override
+        public Number getAsNumber() throws TemplateModelException {
+            return number;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatTest.java
new file mode 100644
index 0000000..76c0bfc
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatTest.java
@@ -0,0 +1,343 @@
+/*
+ * 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.valueformat.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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class ExtendedDecimalFormatTest extends TemplateTest {
+    
+    private static final Locale LOC = Locale.US;
+    private static final DecimalFormatSymbols SYMS = DecimalFormatSymbols.getInstance(LOC);
+
+    @Test
+    public void testNonExtended() throws ParseException {
+        for (String fStr : new String[] { "0.00", "0.###", "#,#0.###", "#0.####", "0.0;m", "0.0;",
+                "0'x'", "0'x';'m'", "0';'", "0';';m", "0';';'#'m';'", "0';;'", "" }) {
+            assertFormatsEquivalent(new DecimalFormat(fStr, SYMS), ExtendedDecimalFormatParser.parse(fStr, LOC));
+        }
+        
+        try {
+            new DecimalFormat(";");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";", LOC);
+        } catch (ParseException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testNonExtended2() throws ParseException {
+        assertFormatsEquivalent(new DecimalFormat("0.0", SYMS), ExtendedDecimalFormatParser.parse("0.0;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0.0", SYMS), ExtendedDecimalFormatParser.parse("0.0;;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0.0;m", SYMS), ExtendedDecimalFormatParser.parse("0.0;m;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("", SYMS), ExtendedDecimalFormatParser.parse(";;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0'x'", SYMS), ExtendedDecimalFormatParser.parse("0'x';;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0'x';'m'", SYMS),
+                ExtendedDecimalFormatParser.parse("0'x';'m';", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0';'", SYMS), ExtendedDecimalFormatParser.parse("0';';;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0';';m", SYMS), ExtendedDecimalFormatParser.parse("0';';m;", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0';';'#'m';'", SYMS),
+                ExtendedDecimalFormatParser.parse("0';';'#'m';';", LOC));
+        assertFormatsEquivalent(new DecimalFormat("0';;'", SYMS),
+                ExtendedDecimalFormatParser.parse("0';;';;", LOC));
+
+        try {
+            new DecimalFormat(";m");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            new DecimalFormat("; ;");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            ExtendedDecimalFormatParser.parse("; ;", LOC);
+            fail();
+        } catch (ParseException e) {
+            // Expected
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";m", LOC);
+            fail();
+        } catch (ParseException e) {
+            // Expected
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";m;", LOC);
+            fail();
+        } catch (ParseException e) {
+            // Expected
+        }
+    }
+    
+    @SuppressWarnings("boxing")
+    @Test
+    public void testExtendedParamsParsing() throws ParseException {
+        for (String fs : new String[] {
+                "00.##;; decimalSeparator='D'",
+                "00.##;;decimalSeparator=D",
+                "00.##;;  decimalSeparator  =  D ", "00.##;; decimalSeparator = 'D' " }) {
+            assertFormatted(fs, 1.125, "01D12");
+        }
+        for (String fs : new String[] {
+                ",#0.0;; decimalSeparator=D, groupingSeparator=_",
+                ",#0.0;;decimalSeparator=D,groupingSeparator=_",
+                ",#0.0;; decimalSeparator = D , groupingSeparator = _ ",
+                ",#0.0;; decimalSeparator='D', groupingSeparator='_'"
+                }) {
+            assertFormatted(fs, 12345, "1_23_45D0");
+        }
+        
+        assertFormatted("0.0;;infinity=infinity", Double.POSITIVE_INFINITY, "infinity");
+        assertFormatted("0.0;;infinity='infinity'", Double.POSITIVE_INFINITY, "infinity");
+        assertFormatted("0.0;;infinity=\"infinity\"", Double.POSITIVE_INFINITY, "infinity");
+        assertFormatted("0.0;;infinity=''", Double.POSITIVE_INFINITY, "");
+        assertFormatted("0.0;;infinity=\"\"", Double.POSITIVE_INFINITY, "");
+        assertFormatted("0.0;;infinity='x''y'", Double.POSITIVE_INFINITY, "x'y");
+        assertFormatted("0.0;;infinity=\"x'y\"", Double.POSITIVE_INFINITY, "x'y");
+        assertFormatted("0.0;;infinity='x\"\"y'", Double.POSITIVE_INFINITY, "x\"\"y");
+        assertFormatted("0.0;;infinity=\"x''y\"", Double.POSITIVE_INFINITY, "x''y");
+        assertFormatted("0.0;;decimalSeparator=''''", 1, "1'0");
+        assertFormatted("0.0;;decimalSeparator=\"'\"", 1, "1'0");
+        assertFormatted("0.0;;decimalSeparator='\"'", 1, "1\"0");
+        assertFormatted("0.0;;decimalSeparator=\"\"\"\"", 1, "1\"0");
+        
+        try {
+            ExtendedDecimalFormatParser.parse(";;decimalSeparator=D,", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsStringIgnoringCase("expected a(n) name"), containsString(" end of ")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";;foo=D,", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("\"foo\""), containsString("name")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";;decimalSeparator='D", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("quotation"), containsString("closed")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";;decimalSeparator=\"D", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("quotation"), containsString("closed")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";;decimalSeparator='D'groupingSeparator=G", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(), allOf(
+                    containsString("separator"), containsString("whitespace"), containsString("comma")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse(";;decimalSeparator=., groupingSeparator=G", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(), allOf(
+                    containsStringIgnoringCase("expected a(n) value"), containsString("., gr[...]")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse("0.0;;decimalSeparator=''", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(), allOf(
+                    containsStringIgnoringCase("\"decimalSeparator\""), containsString("exactly 1 char")));
+        }
+        try {
+            ExtendedDecimalFormatParser.parse("0.0;;multipier=ten", LOC);
+            fail();
+        } catch (java.text.ParseException e) {
+            assertThat(e.getMessage(), allOf(
+                    containsString("\"multipier\""), containsString("\"ten\""), containsString("integer")));
+        }
+    }
+    
+    @SuppressWarnings("boxing")
+    @Test
+    public void testExtendedParamsEffect() throws ParseException {
+        assertFormatted("0",
+                1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2");
+        assertFormatted("0;; roundingMode=halfEven",
+                1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2");
+        assertFormatted("0;; roundingMode=halfUp",
+                1.5, "2", 2.5, "3", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-3", -1.6, "-2");
+        assertFormatted("0;; roundingMode=halfDown",
+                1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-2");
+        assertFormatted("0;; roundingMode=floor",
+                1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2");
+        assertFormatted("0;; roundingMode=ceiling",
+                1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1");
+        assertFormatted("0;; roundingMode=up",
+                1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2");
+        assertFormatted("0;; roundingMode=down",
+                1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1");
+        assertFormatted("0;; roundingMode=unnecessary", 2, "2");
+        try {
+            assertFormatted("0;; roundingMode=unnecessary", 2.5, "2");
+            fail();
+        } catch (ArithmeticException e) {
+            // Expected
+        }
+
+        assertFormatted("0.##;; multipier=100", 12.345, "1234.5");
+        assertFormatted("0.##;; multipier=1000", 12.345, "12345");
+        
+        assertFormatted(",##0.##;; groupingSeparator=_ decimalSeparator=D", 12345.1, "12_345D1", 1, "1");
+        
+        assertFormatted("0.##E0;; exponentSeparator='*10^'", 12345.1, "1.23*10^4");
+        
+        assertFormatted("0.##;; minusSign=m", -1, "m1", 1, "1");
+        
+        assertFormatted("0.##;; infinity=foo", Double.POSITIVE_INFINITY, "foo", Double.NEGATIVE_INFINITY, "-foo");
+        
+        assertFormatted("0.##;; nan=foo", Double.NaN, "foo");
+        
+        assertFormatted("0%;; percent='c'", 0.75, "75c");
+        
+        assertFormatted("0\u2030;; perMill='m'", 0.75, "750m");
+        
+        assertFormatted("0.00;; zeroDigit='@'", 10.5, "A@.E@");
+        
+        assertFormatted("0;; currencyCode=USD", 10, "10");
+        assertFormatted("0 \u00A4;; currencyCode=USD", 10, "10 $");
+        assertFormatted("0 \u00A4\u00A4;; currencyCode=USD", 10, "10 USD");
+        assertFormatted(Locale.GERMANY, "0 \u00A4;; currencyCode=EUR", 10, "10 \u20AC");
+        assertFormatted(Locale.GERMANY, "0 \u00A4\u00A4;; currencyCode=EUR", 10, "10 EUR");
+        try {
+            assertFormatted("0;; currencyCode=USDX", 10, "10");
+        } catch (ParseException e) {
+            assertThat(e.getMessage(), containsString("ISO 4217"));
+        }
+        assertFormatted("0 \u00A4;; currencyCode=USD currencySymbol=bucks", 10, "10 bucks");
+     // Order doesn't mater:
+        assertFormatted("0 \u00A4;; currencySymbol=bucks currencyCode=USD", 10, "10 bucks");
+        // International symbol isn't affected:
+        assertFormatted("0 \u00A4\u00A4;; currencyCode=USD currencySymbol=bucks", 10, "10 USD");
+        
+        assertFormatted("0.0 \u00A4;; monetaryDecimalSeparator=m", 10.5, "10m5 $");
+        assertFormatted("0.0 kg;; monetaryDecimalSeparator=m", 10.5, "10.5 kg");
+        assertFormatted("0.0 \u00A4;; decimalSeparator=d", 10.5, "10.5 $");
+        assertFormatted("0.0 kg;; decimalSeparator=d", 10.5, "10d5 kg");
+        assertFormatted("0.0 \u00A4;; monetaryDecimalSeparator=m decimalSeparator=d", 10.5, "10m5 $");
+        assertFormatted("0.0 kg;; monetaryDecimalSeparator=m decimalSeparator=d", 10.5, "10d5 kg");
+    }
+    
+    @Test
+    public void testLocale() throws ParseException {
+        assertEquals("1000.0", ExtendedDecimalFormatParser.parse("0.0", Locale.US).format(1000));
+        assertEquals("1000,0", ExtendedDecimalFormatParser.parse("0.0", Locale.FRANCE).format(1000));
+        assertEquals("1_000.0", ExtendedDecimalFormatParser.parse(",000.0;;groupingSeparator=_", Locale.US).format(1000));
+        assertEquals("1_000,0", ExtendedDecimalFormatParser.parse(",000.0;;groupingSeparator=_", Locale.FRANCE).format(1000));
+    }
+    
+    @Test
+    public void testTemplates() throws IOException, TemplateException {
+        TestConfigurationBuilder cfgB = new TestConfigurationBuilder();
+
+        setConfiguration(cfgB.numberFormat(",000.#").build());
+        assertOutput("${1000.15} ${1000.25}", "1,000.2 1,000.2");
+        setConfiguration(cfgB.numberFormat(",000.#;; roundingMode=halfUp groupingSeparator=_").build());;
+        assertOutput("${1000.15} ${1000.25}", "1_000.2 1_000.3");
+        setConfiguration(cfgB.locale(Locale.GERMANY).build());;
+        assertOutput("${1000.15} ${1000.25}", "1_000,2 1_000,3");
+        setConfiguration(cfgB.locale(Locale.US).build());;
+        assertOutput(
+                "${1000.15}; "
+                + "${1000.15?string(',##.#;;groupingSeparator=\" \"')}; "
+                + "<#setting locale='de_DE'>${1000.15}; "
+                + "<#setting numberFormat='0.0;;roundingMode=down'>${1000.15}",
+                "1_000.2; 10 00.2; 1_000,2; 1000,1");
+        assertErrorContains("${1?string('#E')}",
+                TemplateException.class, "\"#E\"", "format string", "exponential");
+        assertErrorContains("<#setting numberFormat='#E'>${1}",
+                TemplateException.class, "\"#E\"", "format string", "exponential");
+        assertErrorContains("<#setting numberFormat=';;foo=bar'>${1}",
+                TemplateException.class, "\"foo\"", "supported");
+        assertErrorContains("<#setting numberFormat='0;;roundingMode=unnecessary'>${1.5}",
+                TemplateException.class, "can't format", "1.5", "UNNECESSARY");
+    }
+
+    private void assertFormatted(String formatString, Object... numberAndExpectedOutput) throws ParseException {
+        assertFormatted(LOC, formatString, numberAndExpectedOutput);
+    }
+    
+    private void assertFormatted(Locale loc, String formatString, Object... numberAndExpectedOutput) throws ParseException {
+        if (numberAndExpectedOutput.length % 2 != 0) {
+            throw new IllegalArgumentException();
+        }
+        
+        DecimalFormat df = ExtendedDecimalFormatParser.parse(formatString, loc);
+        Number num = null;
+        for (int i = 0; i < numberAndExpectedOutput.length; i++) {
+            if (i % 2 == 0) {
+                num = (Number) numberAndExpectedOutput[i];
+            } else {
+                assertEquals(numberAndExpectedOutput[i], df.format(num));
+            }
+        }
+    }
+    
+    private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual) {
+        for (int signum : new int[] { 1, -1 }) {
+            assertFormatsEquivalent(dfExpected, dfActual, 0);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 0.5);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 0.25);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 0.125);
+            assertFormatsEquivalent(dfExpected, dfActual, signum);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 10);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 100);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 1000);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 10000);
+            assertFormatsEquivalent(dfExpected, dfActual, signum * 100000);
+        }
+    }
+
+    private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual, double n) {
+        assertEquals(dfExpected.format(n), dfActual.format(n));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java
new file mode 100644
index 0000000..56a15eb
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.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.dom;
+
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.XMLLoader;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class DOMSiblingTest extends TemplateTest {
+
+    @Before
+    public void setUp() throws SAXException, IOException, ParserConfigurationException {
+        InputSource is = new InputSource(getClass().getResourceAsStream("DOMSiblingTest.xml"));
+        addToDataModel("doc", XMLLoader.toModel(is));
+    }
+
+    @Test
+    public void testBlankPreviousSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.name?previousSibling}", "\n    ");
+        assertOutput("${doc.person.name?previous_sibling}", "\n    ");
+    }
+
+    @Test
+    public void testNonBlankPreviousSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.address?previousSibling}", "12th August");
+    }
+
+    @Test
+    public void testBlankNextSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.name?nextSibling}", "\n    ");
+        assertOutput("${doc.person.name?next_sibling}", "\n    ");
+    }
+
+    @Test
+    public void testNonBlankNextSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.dob?nextSibling}", "Chennai, India");
+    }
+
+    @Test
+    public void testNullPreviousSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person?previousSibling?? ?c}", "false");
+    }
+
+    @Test
+    public void testSignificantPreviousSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.name.@@previous_sibling_element}", "male");
+    }
+
+    @Test
+    public void testSignificantNextSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.name.@@next_sibling_element}", "12th August");
+    }
+
+    @Test
+    public void testNullSignificantPreviousSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.phone.@@next_sibling_element?size}", "0");
+    }
+
+    @Test
+    public void testSkippingCommentNode() throws IOException, TemplateException {
+        assertOutput("${doc.person.profession.@@previous_sibling_element}", "Chennai, India");
+    }
+
+    @Test
+    public void testSkippingEmptyCDataNode() throws IOException, TemplateException {
+        assertOutput("${doc.person.hobby.@@previous_sibling_element}", "Software Engineer");
+    }
+
+    @Test
+    public void testValidCDataNode() throws IOException, TemplateException {
+        assertOutput("${doc.person.phone.@@previous_sibling_element?size}", "0");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java
new file mode 100644
index 0000000..f135873
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.dom;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.test.XMLLoader;
+import org.junit.Test;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.Comment;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.ProcessingInstruction;
+import org.w3c.dom.Text;
+import org.xml.sax.SAXException;
+
+public class DOMSimplifiersTest {
+
+    private static final String COMMON_TEST_XML
+            = "<!DOCTYPE a []><?p?><a>x<![CDATA[y]]><!--c--><?p?>z<?p?><b><!--c--></b><c></c>"
+              + "<d>a<e>c</e>b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
+              + "<f><![CDATA[1]]>2</f></a><!--c-->";
+
+    private static final String TEXT_MERGE_CONTENT =
+            "<a>"
+            + "a<!--c--><s/>"
+            + "<!--c-->a<s/>"
+            + "a<!--c-->b<s/>"
+            + "<!--c-->a<!--c-->b<!--c--><s/>"
+            + "a<b>b</b>c<s/>"
+            + "a<b>b</b><!--c-->c<s/>"
+            + "a<!--c-->1<b>b<!--c--></b>c<!--c-->1<s/>"
+            + "a<!--c-->1<b>b<!--c-->c</b>d<!--c-->1<s/>"
+            + "a<!--c-->1<b>b<!--c-->c</b>d<!--c-->1<s/>"
+            + "a<!--c-->1<b>b<!--c-->1<e>c<!--c-->1</e>d<!--c-->1</b>e<!--c-->1<s/>"
+            + "</a>";
+    private static final String TEXT_MERGE_EXPECTED =
+            "<a>"
+            + "%a<s/>"
+            + "%a<s/>"
+            + "%ab<s/>"
+            + "%ab<s/>"
+            + "%a<b>%b</b>%c<s/>"
+            + "%a<b>%b</b>%c<s/>"
+            + "%a1<b>%b</b>%c1<s/>"
+            + "%a1<b>%bc</b>%d1<s/>"
+            + "%a1<b>%bc</b>%d1<s/>"
+            + "%a1<b>%b1<e>%c1</e>%d1</b>%e1<s/>"
+            + "</a>";
+    
+    @Test
+    public void testTest() throws Exception {
+        String expected = "<!DOCTYPE ...><?p?><a>%x<![CDATA[y]]><!--c--><?p?>%z<?p?><b><!--c--></b><c/>"
+                   + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
+                   + "<f><![CDATA[1]]>%2</f></a><!--c-->";
+        assertEquals(expected, toString(XMLLoader.toDOM(COMMON_TEST_XML)));
+    }
+
+    @Test
+    public void testMergeAdjacentText() throws Exception {
+        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
+        NodeModel.mergeAdjacentText(dom);
+        assertEquals(
+                "<!DOCTYPE ...><?p?><a>%xy<!--c--><?p?>%z<?p?><b><!--c--></b><c/>"
+                + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
+                + "<f><![CDATA[12]]></f></a><!--c-->",
+                toString(dom));
+    }
+
+    @Test
+    public void testRemoveComments() throws Exception {
+        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
+        NodeModel.removeComments(dom);
+        assertEquals(
+                "<!DOCTYPE ...><?p?><a>%x<![CDATA[y]]><?p?>%z<?p?><b/><c/>"
+                + "<d>%a<e>%c</e>%b<?p?><?p?><?p?></d>"
+                + "<f><![CDATA[1]]>%2</f></a>",
+                toString(dom));
+    }
+
+    @Test
+    public void testRemovePIs() throws Exception {
+        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
+        NodeModel.removePIs(dom);
+        assertEquals(
+                "<!DOCTYPE ...><a>%x<![CDATA[y]]><!--c-->%z<b><!--c--></b><c/>"
+                + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--></d>"
+                + "<f><![CDATA[1]]>%2</f></a><!--c-->",
+                toString(dom));
+    }
+    
+    @Test
+    public void testSimplify() throws Exception {
+        testSimplify(
+                "<!DOCTYPE ...><a>%xyz<b/><c/>"
+                + "<d>%a<e>%c</e>%b</d><f><![CDATA[12]]></f></a>",
+                COMMON_TEST_XML);
+    }
+
+    @Test
+    public void testSimplify2() throws Exception {
+        testSimplify(TEXT_MERGE_EXPECTED, TEXT_MERGE_CONTENT);
+    }
+
+    @Test
+    public void testSimplify3() throws Exception {
+        testSimplify("<a/>", "<a/>");
+    }
+    
+    private void testSimplify(String expected, String content)
+            throws SAXException, IOException, ParserConfigurationException {
+        {
+            Document dom = XMLLoader.toDOM(content);
+            NodeModel.simplify(dom);
+            assertEquals(expected, toString(dom));
+        }
+        
+        // Must be equivalent:
+        {
+            Document dom = XMLLoader.toDOM(content);
+            NodeModel.removeComments(dom);
+            NodeModel.removePIs(dom);
+            NodeModel.mergeAdjacentText(dom);
+            assertEquals(expected, toString(dom));
+        }
+        
+        // Must be equivalent:
+        {
+            Document dom = XMLLoader.toDOM(content);
+            NodeModel.removeComments(dom);
+            NodeModel.removePIs(dom);
+            NodeModel.simplify(dom);
+            assertEquals(expected, toString(dom));
+        }
+    }
+
+    private String toString(Document doc) {
+        StringBuilder sb = new StringBuilder();
+        toString(doc, sb);
+        return sb.toString();
+    }
+
+    private void toString(Node node, StringBuilder sb) {
+        if (node instanceof Document) {
+            childrenToString(node, sb);
+        } else if (node instanceof Element) {
+            if (node.hasChildNodes()) {
+                sb.append("<").append(node.getNodeName()).append(">");
+                childrenToString(node, sb);
+                sb.append("</").append(node.getNodeName()).append(">");
+            } else {
+                sb.append("<").append(node.getNodeName()).append("/>");
+            }
+        } else if (node instanceof Text) {
+            if (node instanceof CDATASection) {
+                sb.append("<![CDATA[").append(node.getNodeValue()).append("]]>");
+            } else {
+                sb.append("%").append(node.getNodeValue());
+            }
+        } else if (node instanceof Comment) {
+            sb.append("<!--").append(node.getNodeValue()).append("-->");
+        } else if (node instanceof ProcessingInstruction) {
+            sb.append("<?").append(node.getNodeName()).append("?>");
+        } else if (node instanceof DocumentType) {
+            sb.append("<!DOCTYPE ...>");
+        } else {
+            throw new IllegalStateException("Unhandled node type: " + node.getClass().getName());
+        }
+    }
+
+    private void childrenToString(Node node, StringBuilder sb) {
+        Node child = node.getFirstChild();
+        while (child != null) {
+            toString(child, sb);
+            child = child.getNextSibling();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMTest.java
new file mode 100644
index 0000000..847f503
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/dom/DOMTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.dom;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.XMLLoader;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class DOMTest extends TemplateTest {
+
+    @Test
+    public void xpathDetectionBugfix() throws Exception {
+        addDocToDataModel("<root><a>A</a><b>B</b><c>C</c></root>");
+        assertOutput("${doc.root.b['following-sibling::c']}", "C");
+        assertOutput("${doc.root.b['following-sibling::*']}", "C");
+    }
+
+    @Test
+    public void xmlnsPrefixes() throws Exception {
+        addDocToDataModel("<root xmlns='http://example.com/ns1' xmlns:ns2='http://example.com/ns2'>"
+                + "<a>A</a><ns2:b>B</ns2:b><c a1='1' ns2:a2='2'/></root>");
+
+        String ftlHeader = "<#ftl ns_prefixes={'D':'http://example.com/ns1', 'n2':'http://example.com/ns2'}>";
+        
+        // @@markup:
+        assertOutput("${doc.@@markup}",
+                "<a:root xmlns:a=\"http://example.com/ns1\" xmlns:b=\"http://example.com/ns2\">"
+                + "<a:a>A</a:a><b:b>B</b:b><a:c a1=\"1\" b:a2=\"2\" />"
+                + "</a:root>");
+        assertOutput(ftlHeader
+                + "${doc.@@markup}",
+                "<root xmlns=\"http://example.com/ns1\" xmlns:n2=\"http://example.com/ns2\">"
+                + "<a>A</a><n2:b>B</n2:b><c a1=\"1\" n2:a2=\"2\" /></root>");
+        assertOutput("<#ftl ns_prefixes={'D':'http://example.com/ns1'}>"
+                + "${doc.@@markup}",
+                "<root xmlns=\"http://example.com/ns1\" xmlns:a=\"http://example.com/ns2\">"
+                + "<a>A</a><a:b>B</a:b><c a1=\"1\" a:a2=\"2\" /></root>");
+        
+        // When there's no matching prefix declared via the #ftl header, return null for qname:
+        assertOutput("${doc?children[0].@@qname!'null'}", "null");
+        assertOutput("${doc?children[0]?children[1].@@qname!'null'}", "null");
+        assertOutput("${doc?children[0]?children[2]['@*'][1].@@qname!'null'}", "null");
+        
+        // When we have prefix declared in the #ftl header:
+        assertOutput(ftlHeader + "${doc?children[0].@@qname}", "root");
+        assertOutput(ftlHeader + "${doc?children[0]?children[1].@@qname}", "n2:b");
+        assertOutput(ftlHeader + "${doc?children[0]?children[2].@@qname}", "c");
+        assertOutput(ftlHeader + "${doc?children[0]?children[2]['@*'][0].@@qname}", "a1");
+        assertOutput(ftlHeader + "${doc?children[0]?children[2]['@*'][1].@@qname}", "n2:a2");
+        // Unfortunately these include the xmlns attributes, but that would be non-BC to fix now:
+        assertThat(getOutput(ftlHeader + "${doc?children[0].@@start_tag}"), startsWith("<root"));
+        assertThat(getOutput(ftlHeader + "${doc?children[0]?children[1].@@start_tag}"), startsWith("<n2:b"));
+    }
+    
+    @Test
+    public void namespaceUnaware() throws Exception {
+        addNSUnawareDocToDataModel("<root><x:a>A</x:a><:>B</:><xyz::c>C</xyz::c></root>");
+        assertOutput("${doc.root['x:a']}", "A");
+        assertOutput("${doc.root[':']}", "B");
+        try {
+            assertOutput("${doc.root['xyz::c']}", "C");
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsString("xyz"));
+        }
+    }
+    
+    private void addDocToDataModel(String xml) throws SAXException, IOException, ParserConfigurationException {
+        addToDataModel("doc", XMLLoader.toModel(new InputSource(new StringReader(xml))));
+    }
+
+    private void addDocToDataModelNoSimplification(String xml) throws SAXException, IOException, ParserConfigurationException {
+        addToDataModel("doc", XMLLoader.toModel(new InputSource(new StringReader(xml)), false));
+    }
+    
+    private void addNSUnawareDocToDataModel(String xml) throws ParserConfigurationException, SAXException, IOException {
+        DocumentBuilderFactory newFactory = DocumentBuilderFactory.newInstance();
+        newFactory.setNamespaceAware(false);
+        DocumentBuilder builder = newFactory.newDocumentBuilder();
+        Document doc = builder.parse(new InputSource(new StringReader(xml)));
+        addToDataModel("doc", doc);
+    }
+
+    @Test
+    public void testInvalidAtAtKeyErrors() throws Exception {
+        addDocToDataModel("<r><multipleMatches /><multipleMatches /></r>");
+        assertErrorContains("${doc.r.@@invalid_key}", "Unsupported @@ key", "@invalid_key");
+        assertErrorContains("${doc.@@start_tag}", "@@start_tag", "not supported", "document");
+        assertErrorContains("${doc.@@}", "\"@@\"", "not supported", "document");
+        assertErrorContains("${doc.r.noMatch.@@invalid_key}", "Unsupported @@ key", "@invalid_key");
+        assertErrorContains("${doc.r.multipleMatches.@@invalid_key}", "Unsupported @@ key", "@invalid_key");
+        assertErrorContains("${doc.r.noMatch.@@attributes_markup}", "single XML node", "@@attributes_markup");
+        assertErrorContains("${doc.r.multipleMatches.@@attributes_markup}", "single XML node", "@@attributes_markup");
+    }
+    
+    @Test
+    public void testAtAtSiblingElement() throws Exception {
+        addDocToDataModel("<r><a/><b/></r>");
+        assertOutput("${doc.r.@@previous_sibling_element?size}", "0");
+        assertOutput("${doc.r.@@next_sibling_element?size}", "0");
+        assertOutput("${doc.r.a.@@previous_sibling_element?size}", "0");
+        assertOutput("${doc.r.a.@@next_sibling_element.@@qname}", "b");
+        assertOutput("${doc.r.b.@@previous_sibling_element.@@qname}", "a");
+        assertOutput("${doc.r.b.@@next_sibling_element?size}", "0");
+        
+        addDocToDataModel("<r>\r\n\t <a/>\r\n\t <b/>\r\n\t </r>");
+        assertOutput("${doc.r.@@previous_sibling_element?size}", "0");
+        assertOutput("${doc.r.@@next_sibling_element?size}", "0");
+        assertOutput("${doc.r.a.@@previous_sibling_element?size}", "0");
+        assertOutput("${doc.r.a.@@next_sibling_element.@@qname}", "b");
+        assertOutput("${doc.r.b.@@previous_sibling_element.@@qname}", "a");
+        assertOutput("${doc.r.b.@@next_sibling_element?size}", "0");
+        
+        addDocToDataModel("<r>t<a/>t<b/>t</r>");
+        assertOutput("${doc.r.a.@@previous_sibling_element?size}", "0");
+        assertOutput("${doc.r.a.@@next_sibling_element?size}", "0");
+        assertOutput("${doc.r.b.@@previous_sibling_element?size}", "0");
+        assertOutput("${doc.r.b.@@next_sibling_element?size}", "0");
+        
+        addDocToDataModelNoSimplification("<r><a/> <!-- --><?pi?>&#x20;<b/></r>");
+        assertOutput("${doc.r.a.@@next_sibling_element.@@qname}", "b");
+        assertOutput("${doc.r.b.@@previous_sibling_element.@@qname}", "a");
+        
+        addDocToDataModelNoSimplification("<r><a/> <!-- -->t<!-- --> <b/></r>");
+        assertOutput("${doc.r.a.@@next_sibling_element?size}", "0");
+        assertOutput("${doc.r.b.@@previous_sibling_element?size}", "0");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/AutoEscapingExample.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/AutoEscapingExample.java b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/AutoEscapingExample.java
new file mode 100644
index 0000000..036bace
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/AutoEscapingExample.java
@@ -0,0 +1,72 @@
+/*
+ * 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.manualtest;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class AutoEscapingExample extends TemplateTest {
+
+    @Test
+    public void testInfoBox() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-infoBox.ftlh");
+    }
+
+    @Test
+    public void testCapture() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-capture.ftlh");
+    }
+
+    @Test
+    public void testMarkup() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-markup.ftlh");
+    }
+
+    @Test
+    public void testConvert() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-convert.ftlh");
+    }
+
+    @Test
+    public void testConvert2() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-convert2.ftl");
+    }
+
+    @Test
+    public void testStringLiteral() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-stringLiteral.ftlh");
+    }
+
+    @Test
+    public void testStringLiteral2() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-stringLiteral2.ftlh");
+    }
+
+    @Test
+    public void testStringConcat() throws Exception {
+        assertOutputForNamed("AutoEscapingExample-stringConcat.ftlh");
+    }
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws Exception {
+        return new TestConfigurationBuilder(AutoEscapingExample.class).build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/ConfigureOutputFormatExamples.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/ConfigureOutputFormatExamples.java b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/ConfigureOutputFormatExamples.java
new file mode 100644
index 0000000..40c1297
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/ConfigureOutputFormatExamples.java
@@ -0,0 +1,105 @@
+/*
+ * 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.manualtest;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileExtensionMatcher;
+import org.apache.freemarker.core.templateresolver.FirstMatchTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.OrMatcher;
+import org.apache.freemarker.core.templateresolver.PathGlobMatcher;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class ConfigureOutputFormatExamples extends TemplateTest {
+    
+    @Test
+    public void test() throws Exception {
+        addTemplate("mail/t.ftl", "");
+        addTemplate("t.html", "");
+        addTemplate("t.htm", "");
+        addTemplate("t.xml", "");
+        addTemplate("t.rtf", "");
+
+        example2(true);
+        example2(false);
+        example3(true);
+        example3(false);
+    }
+
+    private void example2(boolean javaCfg) throws IOException {
+        setConfiguration(
+                javaCfg
+                        ? new TestConfigurationBuilder()
+                                .templateConfigurations(
+                                        new ConditionalTemplateConfigurationFactory(
+                                                new PathGlobMatcher("mail/**"),
+                                                new TemplateConfiguration.Builder()
+                                                        .outputFormat(HTMLOutputFormat.INSTANCE)
+                                                        .build()))
+                                .build()
+                        : new TestConfigurationBuilder()
+                                .settings(loadPropertiesFile("ConfigureOutputFormatExamples1.properties"))
+                                .build());
+        assertEquals(HTMLOutputFormat.INSTANCE, getConfiguration().getTemplate("mail/t.ftl").getOutputFormat());
+    }
+
+    private void example3(boolean javaCfg) throws IOException {
+        setConfiguration(
+                javaCfg
+                        ? new TestConfigurationBuilder()
+                                .templateConfigurations(
+                                        new FirstMatchTemplateConfigurationFactory(
+                                                new ConditionalTemplateConfigurationFactory(
+                                                        new FileExtensionMatcher("xml"),
+                                                        new TemplateConfiguration.Builder()
+                                                                .outputFormat(XMLOutputFormat.INSTANCE)
+                                                                .build()),
+                                                new ConditionalTemplateConfigurationFactory(
+                                                        new OrMatcher(
+                                                                new FileExtensionMatcher("html"),
+                                                                new FileExtensionMatcher("htm")),
+                                                        new TemplateConfiguration.Builder()
+                                                                .outputFormat(HTMLOutputFormat.INSTANCE)
+                                                                .build()),
+                                                new ConditionalTemplateConfigurationFactory(
+                                                        new FileExtensionMatcher("rtf"),
+                                                        new TemplateConfiguration.Builder()
+                                                                .outputFormat(RTFOutputFormat.INSTANCE)
+                                                                .build()))
+                                        .allowNoMatch(true))
+                                .build()
+                        : new TestConfigurationBuilder()
+                                .settings(loadPropertiesFile("ConfigureOutputFormatExamples2.properties"))
+                                .build());
+        assertEquals(HTMLOutputFormat.INSTANCE, getConfiguration().getTemplate("t.html").getOutputFormat());
+        assertEquals(HTMLOutputFormat.INSTANCE, getConfiguration().getTemplate("t.htm").getOutputFormat());
+        assertEquals(XMLOutputFormat.INSTANCE, getConfiguration().getTemplate("t.xml").getOutputFormat());
+        assertEquals(RTFOutputFormat.INSTANCE, getConfiguration().getTemplate("t.rtf").getOutputFormat());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/CustomFormatsExample.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/CustomFormatsExample.java b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/CustomFormatsExample.java
new file mode 100644
index 0000000..f38bb14
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/CustomFormatsExample.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.manualtest;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Date;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.AliasTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.AliasTemplateNumberFormatFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+@SuppressWarnings("boxing")
+public class CustomFormatsExample extends TemplateTest {
+
+    @Test
+    public void aliases1() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder(this.getClass())
+            .customNumberFormats(ImmutableMap.<String, TemplateNumberFormatFactory>of(
+                    "price", new AliasTemplateNumberFormatFactory(",000.00"),
+                    "weight", new AliasTemplateNumberFormatFactory("0.##;; roundingMode=halfUp")))
+            .customDateFormats(ImmutableMap.<String, TemplateDateFormatFactory>of(
+                    "fileDate", new AliasTemplateDateFormatFactory("dd/MMM/yy hh:mm a"),
+                    "logEventTime", new AliasTemplateDateFormatFactory("iso ms u")
+                    ))
+            .build());
+
+        addToDataModel("p", 10000);
+        addToDataModel("w", new BigDecimal("10.305"));
+        addToDataModel("fd", new Date(1450904944213L));
+        addToDataModel("let", new Date(1450904944213L));
+        
+        assertOutputForNamed("CustomFormatsExample-alias1.ftlh");
+    }
+
+    @Test
+    public void aliases2() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder(this.getClass())
+                .customNumberFormats(ImmutableMap.of(
+                        "base", BaseNTemplateNumberFormatFactory.INSTANCE,
+                        "oct", new AliasTemplateNumberFormatFactory("@base 8")))
+                .build());
+
+        assertOutputForNamed("CustomFormatsExample-alias2.ftlh");
+    }
+
+    @Test
+    public void modelAware() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder(this.getClass())
+                .customNumberFormats(ImmutableMap.<String, TemplateNumberFormatFactory>of(
+                        "ua", UnitAwareTemplateNumberFormatFactory.INSTANCE))
+                .numberFormat("@ua 0.####;; roundingMode=halfUp")
+                .build());
+
+        addToDataModel("weight", new UnitAwareTemplateNumberModel(1.5, "kg"));
+        
+        assertOutputForNamed("CustomFormatsExample-modelAware.ftlh");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/GettingStartedExample.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/GettingStartedExample.java b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/GettingStartedExample.java
new file mode 100644
index 0000000..a676bc4
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/manualtest/GettingStartedExample.java
@@ -0,0 +1,69 @@
+/*
+ * 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.manualtest;
+
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateExceptionHandler;
+import org.apache.freemarker.core.templateresolver.impl.ClassTemplateLoader;
+import org.junit.Test;
+
+public class GettingStartedExample {
+
+    @Test
+    public void main() throws Exception {
+        /* ------------------------------------------------------------------------ */    
+        /* You should do this ONLY ONCE in the whole application life-cycle:        */    
+    
+        /* Create the configuration singleton (using builder pattern) */
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .templateLoader(new ClassTemplateLoader(GettingStartedExample.class, ""))
+                .sourceEncoding(StandardCharsets.UTF_8)
+                .templateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER)
+                .logTemplateExceptions(false)
+                .build();
+
+        /* ------------------------------------------------------------------------ */    
+        /* You usually do these for MULTIPLE TIMES in the application life-cycle:   */    
+
+        /* Create a data-model */
+        Map<String, Object> root = new HashMap();
+        root.put("user", "Big Joe");
+        Product latest = new Product();
+        latest.setUrl("products/greenmouse.html");
+        latest.setName("green mouse");
+        root.put("latestProduct", latest);
+
+        /* Get the template (uses cache internally) */
+        Template temp = cfg.getTemplate("test.ftlh");
+
+        /* Merge data-model with template */
+        Writer out = new OutputStreamWriter(System.out);
+        temp.process(root, out);
+        // Note: Depending on what `out` is, you may need to call `out.close()`.
+        // This is usually the case for file output, but not for servlet output.
+    }
+    
+}