freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [13/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th
Date Sun, 14 May 2017 10:52:56 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
new file mode 100644
index 0000000..b9c9e80
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
@@ -0,0 +1,805 @@
+/*
+ * 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 java.util.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._CoreAPI;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.impl.BeanAndStringModel;
+import org.apache.freemarker.core.model.impl.BeanModel;
+
+/**
+ * Static utility methods that perform tasks specific to the FreeMarker Template Language (FTL).
+ * This is meant to be used from outside FreeMarker (i.e., it's an official, published API), not just from inside it.
+ *
+ * @since 3.0.0
+ */
+public final class FTLUtil {
+
+    private static final char[] ESCAPES = createEscapes();
+
+    private FTLUtil() {
+        // Not meant to be instantiated
+    }
+
+    private static char[] createEscapes() {
+        char[] escapes = new char['\\' + 1];
+        for (int i = 0; i < 32; ++i) {
+            escapes[i] = 1;
+        }
+        escapes['\\'] = '\\';
+        escapes['\''] = '\'';
+        escapes['"'] = '"';
+        escapes['<'] = 'l';
+        escapes['>'] = 'g';
+        escapes['&'] = 'a';
+        escapes['\b'] = 'b';
+        escapes['\t'] = 't';
+        escapes['\n'] = 'n';
+        escapes['\f'] = 'f';
+        escapes['\r'] = 'r';
+        return escapes;
+    }
+
+    /**
+     * Escapes a string according the FTL string literal escaping rules, assuming the literal is quoted with
+     * {@code quotation}; it doesn't add the quotation marks themselves.
+     *
+     * @param quotation Either {@code '"'} or {@code '\''}. It's assumed that the string literal whose part we calculate is
+     *                  enclosed within this kind of quotation mark. Thus, the other kind of quotation character will not be
+     *                  escaped in the result.
+     * @since 2.3.22
+     */
+    public static String escapeStringLiteralPart(String s, char quotation) {
+        return escapeStringLiteralPart(s, quotation, false);
+    }
+
+    /**
+     * Escapes a string according the FTL string literal escaping rules; it doesn't add the quotation marks themselves.
+     * As this method doesn't know if the string literal is quoted with regular quotation marks or apostrophe quote, it
+     * will escape both.
+     *
+     * @see #escapeStringLiteralPart(String, char)
+     */
+    public static String escapeStringLiteralPart(String s) {
+        return escapeStringLiteralPart(s, (char) 0, false);
+    }
+
+    private static String escapeStringLiteralPart(String s, char quotation, boolean addQuotation) {
+        final int ln = s.length();
+
+        final char otherQuotation;
+        if (quotation == 0) {
+            otherQuotation = 0;
+        } else if (quotation == '"') {
+            otherQuotation = '\'';
+        } else if (quotation == '\'') {
+            otherQuotation = '"';
+        } else {
+            throw new IllegalArgumentException("Unsupported quotation character: " + quotation);
+        }
+
+        final int escLn = ESCAPES.length;
+        StringBuilder buf = null;
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            char escape =
+                    c < escLn ? ESCAPES[c] :
+                            c == '{' && i > 0 && isInterpolationStart(s.charAt(i - 1)) ? '{' :
+                                    0;
+            if (escape == 0 || escape == otherQuotation) {
+                if (buf != null) {
+                    buf.append(c);
+                }
+            } else {
+                if (buf == null) {
+                    buf = new StringBuilder(s.length() + 4 + (addQuotation ? 2 : 0));
+                    if (addQuotation) {
+                        buf.append(quotation);
+                    }
+                    buf.append(s.substring(0, i));
+                }
+                if (escape == 1) {
+                    // hex encoding for characters below 0x20
+                    // that have no other escape representation
+                    buf.append("\\x00");
+                    int c2 = (c >> 4) & 0x0F;
+                    c = (char) (c & 0x0F);
+                    buf.append((char) (c2 < 10 ? c2 + '0' : c2 - 10 + 'A'));
+                    buf.append((char) (c < 10 ? c + '0' : c - 10 + 'A'));
+                } else {
+                    buf.append('\\');
+                    buf.append(escape);
+                }
+            }
+        }
+
+        if (buf == null) {
+            return addQuotation ? quotation + s + quotation : s;
+        } else {
+            if (addQuotation) {
+                buf.append(quotation);
+            }
+            return buf.toString();
+        }
+    }
+
+    private static boolean isInterpolationStart(char c) {
+        return c == '$' || c == '#';
+    }
+
+    /**
+     * Unescapes a string that was escaped to be part of an FTL string literal. The string to unescape most not include
+     * the two quotation marks or two apostrophe-quotes that delimit the literal.
+     * <p>
+     * \\, \", \', \n, \t, \r, \b and \f will be replaced according to
+     * Java rules. In additional, it knows \g, \l, \a and \{ which are
+     * replaced with &lt;, &gt;, &amp; and { respectively.
+     * \x works as hexadecimal character code escape. The character
+     * codes are interpreted according to UCS basic plane (Unicode).
+     * "f\x006Fo", "f\x06Fo" and "f\x6Fo" will be "foo".
+     * "f\x006F123" will be "foo123" as the maximum number of digits is 4.
+     * <p>
+     * All other \X (where X is any character not mentioned above or End-of-string)
+     * will cause a ParseException.
+     *
+     * @param s String literal <em>without</em> the surrounding quotation marks
+     * @return String with all escape sequences resolved
+     * @throws GenericParseException if there string contains illegal escapes
+     */
+    public static String unescapeStringLiteralPart(String s) throws GenericParseException {
+
+        int idx = s.indexOf('\\');
+        if (idx == -1) {
+            return s;
+        }
+
+        int lidx = s.length() - 1;
+        int bidx = 0;
+        StringBuilder buf = new StringBuilder(lidx);
+        do {
+            buf.append(s.substring(bidx, idx));
+            if (idx >= lidx) {
+                throw new GenericParseException("The last character of string literal is backslash");
+            }
+            char c = s.charAt(idx + 1);
+            switch (c) {
+                case '"':
+                    buf.append('"');
+                    bidx = idx + 2;
+                    break;
+                case '\'':
+                    buf.append('\'');
+                    bidx = idx + 2;
+                    break;
+                case '\\':
+                    buf.append('\\');
+                    bidx = idx + 2;
+                    break;
+                case 'n':
+                    buf.append('\n');
+                    bidx = idx + 2;
+                    break;
+                case 'r':
+                    buf.append('\r');
+                    bidx = idx + 2;
+                    break;
+                case 't':
+                    buf.append('\t');
+                    bidx = idx + 2;
+                    break;
+                case 'f':
+                    buf.append('\f');
+                    bidx = idx + 2;
+                    break;
+                case 'b':
+                    buf.append('\b');
+                    bidx = idx + 2;
+                    break;
+                case 'g':
+                    buf.append('>');
+                    bidx = idx + 2;
+                    break;
+                case 'l':
+                    buf.append('<');
+                    bidx = idx + 2;
+                    break;
+                case 'a':
+                    buf.append('&');
+                    bidx = idx + 2;
+                    break;
+                case '{':
+                    buf.append('{');
+                    bidx = idx + 2;
+                    break;
+                case 'x': {
+                    idx += 2;
+                    int x = idx;
+                    int y = 0;
+                    int z = lidx > idx + 3 ? idx + 3 : lidx;
+                    while (idx <= z) {
+                        char b = s.charAt(idx);
+                        if (b >= '0' && b <= '9') {
+                            y <<= 4;
+                            y += b - '0';
+                        } else if (b >= 'a' && b <= 'f') {
+                            y <<= 4;
+                            y += b - 'a' + 10;
+                        } else if (b >= 'A' && b <= 'F') {
+                            y <<= 4;
+                            y += b - 'A' + 10;
+                        } else {
+                            break;
+                        }
+                        idx++;
+                    }
+                    if (x < idx) {
+                        buf.append((char) y);
+                    } else {
+                        throw new GenericParseException("Invalid \\x escape in a string literal");
+                    }
+                    bidx = idx;
+                    break;
+                }
+                default:
+                    throw new GenericParseException("Invalid escape sequence (\\" + c + ") in a string literal");
+            }
+            idx = s.indexOf('\\', bidx);
+        } while (idx != -1);
+        buf.append(s.substring(bidx));
+
+        return buf.toString();
+    }
+
+    /**
+     * Creates a <em>quoted</em> FTL string literal from a string, using escaping where necessary. The result either
+     * uses regular quotation marks (UCS 0x22) or apostrophe-quotes (UCS 0x27), depending on the string content.
+     * (Currently, apostrophe-quotes will be chosen exactly when the string contains regular quotation character and
+     * doesn't contain apostrophe-quote character.)
+     *
+     * @param s The value that should be converted to an FTL string literal whose evaluated value equals to {@code s}
+     * @since 2.3.22
+     */
+    public static String toStringLiteral(String s) {
+        char quotation;
+        if (s.indexOf('"') != -1 && s.indexOf('\'') == -1) {
+            quotation = '\'';
+        } else {
+            quotation = '\"';
+        }
+        return escapeStringLiteralPart(s, quotation, true);
+    }
+
+    /**
+     * Tells if a character can occur on the beginning of an FTL identifier expression (without escaping).
+     *
+     * @since 2.3.22
+     */
+    public static boolean isNonEscapedIdentifierStart(final char c) {
+        // This code was generated on JDK 1.8.0_20 Win64 with src/main/misc/identifierChars/IdentifierCharGenerator.java
+        if (c < 0xAA) { // This branch was edited for speed.
+            if (c >= 'a' && c <= 'z' || c >= '@' && c <= 'Z') {
+                return true;
+            } else {
+                return c == '$' || c == '_';
+            }
+        } else { // c >= 0xAA
+            if (c < 0xA7F8) {
+                if (c < 0x2D6F) {
+                    if (c < 0x2128) {
+                        if (c < 0x2090) {
+                            if (c < 0xD8) {
+                                if (c < 0xBA) {
+                                    return c == 0xAA || c == 0xB5;
+                                } else { // c >= 0xBA
+                                    return c == 0xBA || c >= 0xC0 && c <= 0xD6;
+                                }
+                            } else { // c >= 0xD8
+                                if (c < 0x2071) {
+                                    return c >= 0xD8 && c <= 0xF6 || c >= 0xF8 && c <= 0x1FFF;
+                                } else { // c >= 0x2071
+                                    return c == 0x2071 || c == 0x207F;
+                                }
+                            }
+                        } else { // c >= 0x2090
+                            if (c < 0x2115) {
+                                if (c < 0x2107) {
+                                    return c >= 0x2090 && c <= 0x209C || c == 0x2102;
+                                } else { // c >= 0x2107
+                                    return c == 0x2107 || c >= 0x210A && c <= 0x2113;
+                                }
+                            } else { // c >= 0x2115
+                                if (c < 0x2124) {
+                                    return c == 0x2115 || c >= 0x2119 && c <= 0x211D;
+                                } else { // c >= 0x2124
+                                    return c == 0x2124 || c == 0x2126;
+                                }
+                            }
+                        }
+                    } else { // c >= 0x2128
+                        if (c < 0x2C30) {
+                            if (c < 0x2145) {
+                                if (c < 0x212F) {
+                                    return c == 0x2128 || c >= 0x212A && c <= 0x212D;
+                                } else { // c >= 0x212F
+                                    return c >= 0x212F && c <= 0x2139 || c >= 0x213C && c <= 0x213F;
+                                }
+                            } else { // c >= 0x2145
+                                if (c < 0x2183) {
+                                    return c >= 0x2145 && c <= 0x2149 || c == 0x214E;
+                                } else { // c >= 0x2183
+                                    return c >= 0x2183 && c <= 0x2184 || c >= 0x2C00 && c <= 0x2C2E;
+                                }
+                            }
+                        } else { // c >= 0x2C30
+                            if (c < 0x2D00) {
+                                if (c < 0x2CEB) {
+                                    return c >= 0x2C30 && c <= 0x2C5E || c >= 0x2C60 && c <= 0x2CE4;
+                                } else { // c >= 0x2CEB
+                                    return c >= 0x2CEB && c <= 0x2CEE || c >= 0x2CF2 && c <= 0x2CF3;
+                                }
+                            } else { // c >= 0x2D00
+                                if (c < 0x2D2D) {
+                                    return c >= 0x2D00 && c <= 0x2D25 || c == 0x2D27;
+                                } else { // c >= 0x2D2D
+                                    return c == 0x2D2D || c >= 0x2D30 && c <= 0x2D67;
+                                }
+                            }
+                        }
+                    }
+                } else { // c >= 0x2D6F
+                    if (c < 0x31F0) {
+                        if (c < 0x2DD0) {
+                            if (c < 0x2DB0) {
+                                if (c < 0x2DA0) {
+                                    return c == 0x2D6F || c >= 0x2D80 && c <= 0x2D96;
+                                } else { // c >= 0x2DA0
+                                    return c >= 0x2DA0 && c <= 0x2DA6 || c >= 0x2DA8 && c <= 0x2DAE;
+                                }
+                            } else { // c >= 0x2DB0
+                                if (c < 0x2DC0) {
+                                    return c >= 0x2DB0 && c <= 0x2DB6 || c >= 0x2DB8 && c <= 0x2DBE;
+                                } else { // c >= 0x2DC0
+                                    return c >= 0x2DC0 && c <= 0x2DC6 || c >= 0x2DC8 && c <= 0x2DCE;
+                                }
+                            }
+                        } else { // c >= 0x2DD0
+                            if (c < 0x3031) {
+                                if (c < 0x2E2F) {
+                                    return c >= 0x2DD0 && c <= 0x2DD6 || c >= 0x2DD8 && c <= 0x2DDE;
+                                } else { // c >= 0x2E2F
+                                    return c == 0x2E2F || c >= 0x3005 && c <= 0x3006;
+                                }
+                            } else { // c >= 0x3031
+                                if (c < 0x3040) {
+                                    return c >= 0x3031 && c <= 0x3035 || c >= 0x303B && c <= 0x303C;
+                                } else { // c >= 0x3040
+                                    return c >= 0x3040 && c <= 0x318F || c >= 0x31A0 && c <= 0x31BA;
+                                }
+                            }
+                        }
+                    } else { // c >= 0x31F0
+                        if (c < 0xA67F) {
+                            if (c < 0xA4D0) {
+                                if (c < 0x3400) {
+                                    return c >= 0x31F0 && c <= 0x31FF || c >= 0x3300 && c <= 0x337F;
+                                } else { // c >= 0x3400
+                                    return c >= 0x3400 && c <= 0x4DB5 || c >= 0x4E00 && c <= 0xA48C;
+                                }
+                            } else { // c >= 0xA4D0
+                                if (c < 0xA610) {
+                                    return c >= 0xA4D0 && c <= 0xA4FD || c >= 0xA500 && c <= 0xA60C;
+                                } else { // c >= 0xA610
+                                    return c >= 0xA610 && c <= 0xA62B || c >= 0xA640 && c <= 0xA66E;
+                                }
+                            }
+                        } else { // c >= 0xA67F
+                            if (c < 0xA78B) {
+                                if (c < 0xA717) {
+                                    return c >= 0xA67F && c <= 0xA697 || c >= 0xA6A0 && c <= 0xA6E5;
+                                } else { // c >= 0xA717
+                                    return c >= 0xA717 && c <= 0xA71F || c >= 0xA722 && c <= 0xA788;
+                                }
+                            } else { // c >= 0xA78B
+                                if (c < 0xA7A0) {
+                                    return c >= 0xA78B && c <= 0xA78E || c >= 0xA790 && c <= 0xA793;
+                                } else { // c >= 0xA7A0
+                                    return c >= 0xA7A0 && c <= 0xA7AA;
+                                }
+                            }
+                        }
+                    }
+                }
+            } else { // c >= 0xA7F8
+                if (c < 0xAB20) {
+                    if (c < 0xAA44) {
+                        if (c < 0xA8FB) {
+                            if (c < 0xA840) {
+                                if (c < 0xA807) {
+                                    return c >= 0xA7F8 && c <= 0xA801 || c >= 0xA803 && c <= 0xA805;
+                                } else { // c >= 0xA807
+                                    return c >= 0xA807 && c <= 0xA80A || c >= 0xA80C && c <= 0xA822;
+                                }
+                            } else { // c >= 0xA840
+                                if (c < 0xA8D0) {
+                                    return c >= 0xA840 && c <= 0xA873 || c >= 0xA882 && c <= 0xA8B3;
+                                } else { // c >= 0xA8D0
+                                    return c >= 0xA8D0 && c <= 0xA8D9 || c >= 0xA8F2 && c <= 0xA8F7;
+                                }
+                            }
+                        } else { // c >= 0xA8FB
+                            if (c < 0xA984) {
+                                if (c < 0xA930) {
+                                    return c == 0xA8FB || c >= 0xA900 && c <= 0xA925;
+                                } else { // c >= 0xA930
+                                    return c >= 0xA930 && c <= 0xA946 || c >= 0xA960 && c <= 0xA97C;
+                                }
+                            } else { // c >= 0xA984
+                                if (c < 0xAA00) {
+                                    return c >= 0xA984 && c <= 0xA9B2 || c >= 0xA9CF && c <= 0xA9D9;
+                                } else { // c >= 0xAA00
+                                    return c >= 0xAA00 && c <= 0xAA28 || c >= 0xAA40 && c <= 0xAA42;
+                                }
+                            }
+                        }
+                    } else { // c >= 0xAA44
+                        if (c < 0xAAC0) {
+                            if (c < 0xAA80) {
+                                if (c < 0xAA60) {
+                                    return c >= 0xAA44 && c <= 0xAA4B || c >= 0xAA50 && c <= 0xAA59;
+                                } else { // c >= 0xAA60
+                                    return c >= 0xAA60 && c <= 0xAA76 || c == 0xAA7A;
+                                }
+                            } else { // c >= 0xAA80
+                                if (c < 0xAAB5) {
+                                    return c >= 0xAA80 && c <= 0xAAAF || c == 0xAAB1;
+                                } else { // c >= 0xAAB5
+                                    return c >= 0xAAB5 && c <= 0xAAB6 || c >= 0xAAB9 && c <= 0xAABD;
+                                }
+                            }
+                        } else { // c >= 0xAAC0
+                            if (c < 0xAAF2) {
+                                if (c < 0xAADB) {
+                                    return c == 0xAAC0 || c == 0xAAC2;
+                                } else { // c >= 0xAADB
+                                    return c >= 0xAADB && c <= 0xAADD || c >= 0xAAE0 && c <= 0xAAEA;
+                                }
+                            } else { // c >= 0xAAF2
+                                if (c < 0xAB09) {
+                                    return c >= 0xAAF2 && c <= 0xAAF4 || c >= 0xAB01 && c <= 0xAB06;
+                                } else { // c >= 0xAB09
+                                    return c >= 0xAB09 && c <= 0xAB0E || c >= 0xAB11 && c <= 0xAB16;
+                                }
+                            }
+                        }
+                    }
+                } else { // c >= 0xAB20
+                    if (c < 0xFB46) {
+                        if (c < 0xFB13) {
+                            if (c < 0xAC00) {
+                                if (c < 0xABC0) {
+                                    return c >= 0xAB20 && c <= 0xAB26 || c >= 0xAB28 && c <= 0xAB2E;
+                                } else { // c >= 0xABC0
+                                    return c >= 0xABC0 && c <= 0xABE2 || c >= 0xABF0 && c <= 0xABF9;
+                                }
+                            } else { // c >= 0xAC00
+                                if (c < 0xD7CB) {
+                                    return c >= 0xAC00 && c <= 0xD7A3 || c >= 0xD7B0 && c <= 0xD7C6;
+                                } else { // c >= 0xD7CB
+                                    return c >= 0xD7CB && c <= 0xD7FB || c >= 0xF900 && c <= 0xFB06;
+                                }
+                            }
+                        } else { // c >= 0xFB13
+                            if (c < 0xFB38) {
+                                if (c < 0xFB1F) {
+                                    return c >= 0xFB13 && c <= 0xFB17 || c == 0xFB1D;
+                                } else { // c >= 0xFB1F
+                                    return c >= 0xFB1F && c <= 0xFB28 || c >= 0xFB2A && c <= 0xFB36;
+                                }
+                            } else { // c >= 0xFB38
+                                if (c < 0xFB40) {
+                                    return c >= 0xFB38 && c <= 0xFB3C || c == 0xFB3E;
+                                } else { // c >= 0xFB40
+                                    return c >= 0xFB40 && c <= 0xFB41 || c >= 0xFB43 && c <= 0xFB44;
+                                }
+                            }
+                        }
+                    } else { // c >= 0xFB46
+                        if (c < 0xFF21) {
+                            if (c < 0xFDF0) {
+                                if (c < 0xFD50) {
+                                    return c >= 0xFB46 && c <= 0xFBB1 || c >= 0xFBD3 && c <= 0xFD3D;
+                                } else { // c >= 0xFD50
+                                    return c >= 0xFD50 && c <= 0xFD8F || c >= 0xFD92 && c <= 0xFDC7;
+                                }
+                            } else { // c >= 0xFDF0
+                                if (c < 0xFE76) {
+                                    return c >= 0xFDF0 && c <= 0xFDFB || c >= 0xFE70 && c <= 0xFE74;
+                                } else { // c >= 0xFE76
+                                    return c >= 0xFE76 && c <= 0xFEFC || c >= 0xFF10 && c <= 0xFF19;
+                                }
+                            }
+                        } else { // c >= 0xFF21
+                            if (c < 0xFFCA) {
+                                if (c < 0xFF66) {
+                                    return c >= 0xFF21 && c <= 0xFF3A || c >= 0xFF41 && c <= 0xFF5A;
+                                } else { // c >= 0xFF66
+                                    return c >= 0xFF66 && c <= 0xFFBE || c >= 0xFFC2 && c <= 0xFFC7;
+                                }
+                            } else { // c >= 0xFFCA
+                                if (c < 0xFFDA) {
+                                    return c >= 0xFFCA && c <= 0xFFCF || c >= 0xFFD2 && c <= 0xFFD7;
+                                } else { // c >= 0xFFDA
+                                    return c >= 0xFFDA && c <= 0xFFDC;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Tells if a character can occur in an FTL identifier expression (without escaping) as other than the first
+     * character.
+     */
+    public static boolean isNonEscapedIdentifierPart(final char c) {
+        return isNonEscapedIdentifierStart(c) || (c >= '0' && c <= '9');
+    }
+
+    /**
+     * Tells if a given character, for which {@link #isNonEscapedIdentifierStart(char)} and
+     * {@link #isNonEscapedIdentifierPart(char)} is {@code false}, can occur in an identifier if it's preceded by a
+     * backslash. Currently it return {@code true} for these: {@code '-'}, {@code '.'} and {@code ':'}.
+     */
+    public static boolean isEscapedIdentifierCharacter(final char c) {
+        return c == '-' || c == '.' || c == ':';
+    }
+
+    /**
+     * Escapes characters in the string that can only occur in FTL identifiers (variable names) escaped.
+     * This means adding a backslash before any character for which {@link #isEscapedIdentifierCharacter(char)}
+     * is {@code true}. Other characters will be left unescaped, even if they aren't valid in FTL identifiers.
+     *
+     * @param s The identifier to escape. If {@code null}, {@code null} is returned.
+     */
+    public static String escapeIdentifier(String s) {
+        if (s == null) {
+            return null;
+        }
+
+        int ln = s.length();
+
+        // First we find out if we need to escape, and if so, what the length of the output will be:
+        int firstEscIdx = -1;
+        int lastEscIdx = 0;
+        int plusOutLn = 0;
+        for (int i = 0; i < ln; i++) {
+            char c = s.charAt(i);
+            if (isEscapedIdentifierCharacter(c)) {
+                if (firstEscIdx == -1) {
+                    firstEscIdx = i;
+                }
+                lastEscIdx = i;
+                plusOutLn++;
+            }
+        }
+
+        if (firstEscIdx == -1) {
+            return s; // Nothing to escape
+        } else {
+            char[] esced = new char[ln + plusOutLn];
+            if (firstEscIdx != 0) {
+                s.getChars(0, firstEscIdx, esced, 0);
+            }
+            int dst = firstEscIdx;
+            for (int i = firstEscIdx; i <= lastEscIdx; i++) {
+                char c = s.charAt(i);
+                if (isEscapedIdentifierCharacter(c)) {
+                    esced[dst++] = '\\';
+                }
+                esced[dst++] = c;
+            }
+            if (lastEscIdx != ln - 1) {
+                s.getChars(lastEscIdx + 1, ln, esced, dst);
+            }
+
+            return String.valueOf(esced);
+        }
+    }
+
+    /**
+     * Returns the type description of a value with FTL terms (not plain class name), as it should be used in
+     * type-related error messages and for debugging purposes. The exact format is not specified and might change over
+     * time, but currently it's something like {@code "string (wrapper: f.t.SimpleScalar)"} or
+     * {@code "sequence+hash+string (ArrayList wrapped into f.e.b.CollectionModel)"}.
+     *
+     * @param tm The value whose type we will describe. If {@code null}, then {@code "Null"} is returned (without the
+     *           quotation marks).
+     *
+     * @since 2.3.20
+     */
+    public static String getTypeDescription(TemplateModel tm) {
+        if (tm == null) {
+            return "Null";
+        } else {
+            Set typeNamesAppended = new HashSet();
+
+            StringBuilder sb = new StringBuilder();
+
+            Class primaryInterface = getPrimaryTemplateModelInterface(tm);
+            if (primaryInterface != null) {
+                appendTemplateModelTypeName(sb, typeNamesAppended, primaryInterface);
+            }
+
+            if (_CoreAPI.isMacroOrFunction(tm)) {
+                appendTypeName(sb, typeNamesAppended, _CoreAPI.isFunction(tm) ? "function" : "macro");
+            }
+
+            appendTemplateModelTypeName(sb, typeNamesAppended, tm.getClass());
+
+            String javaClassName;
+            Class unwrappedClass = getUnwrappedClass(tm);
+            if (unwrappedClass != null) {
+                javaClassName = _ClassUtil.getShortClassName(unwrappedClass, true);
+            } else {
+                javaClassName = null;
+            }
+
+            sb.append(" (");
+            String modelClassName = _ClassUtil.getShortClassName(tm.getClass(), true);
+            if (javaClassName == null) {
+                sb.append("wrapper: ");
+                sb.append(modelClassName);
+            } else {
+                sb.append(javaClassName);
+                sb.append(" wrapped into ");
+                sb.append(modelClassName);
+            }
+            sb.append(")");
+
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Returns the {@link TemplateModel} interface that is the most characteristic of the object, or {@code null}.
+     */
+    private static Class getPrimaryTemplateModelInterface(TemplateModel tm) {
+        if (tm instanceof BeanModel) {
+            if (tm instanceof BeanAndStringModel) {
+                Object wrapped = ((BeanModel) tm).getWrappedObject();
+                return wrapped instanceof String
+                        ? TemplateScalarModel.class
+                        : (tm instanceof TemplateHashModelEx ? TemplateHashModelEx.class : null);
+            } else {
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    private static void appendTemplateModelTypeName(StringBuilder sb, Set typeNamesAppended, Class cl) {
+        int initalLength = sb.length();
+
+        if (TemplateNodeModelEx.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "extended node");
+        } else if (TemplateNodeModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "node");
+        }
+
+        if (TemplateDirectiveModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "directive");
+        } else if (TemplateTransformModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "transform");
+        }
+
+        if (TemplateSequenceModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "sequence");
+        } else if (TemplateCollectionModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended,
+                    TemplateCollectionModelEx.class.isAssignableFrom(cl) ? "extended_collection" : "collection");
+        } else if (TemplateModelIterator.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "iterator");
+        }
+
+        if (TemplateMethodModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "method");
+        }
+
+        if (Environment.Namespace.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "namespace");
+        } else if (TemplateHashModelEx.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "extended_hash");
+        } else if (TemplateHashModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "hash");
+        }
+
+        if (TemplateNumberModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "number");
+        }
+
+        if (TemplateDateModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "date_or_time_or_datetime");
+        }
+
+        if (TemplateBooleanModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "boolean");
+        }
+
+        if (TemplateScalarModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "string");
+        }
+
+        if (TemplateMarkupOutputModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "markup_output");
+        }
+
+        if (sb.length() == initalLength) {
+            appendTypeName(sb, typeNamesAppended, "misc_template_model");
+        }
+    }
+
+    private static Class getUnwrappedClass(TemplateModel tm) {
+        Object unwrapped;
+        try {
+            if (tm instanceof WrapperTemplateModel) {
+                unwrapped = ((WrapperTemplateModel) tm).getWrappedObject();
+            } else if (tm instanceof AdapterTemplateModel) {
+                unwrapped = ((AdapterTemplateModel) tm).getAdaptedObject(Object.class);
+            } else {
+                unwrapped = null;
+            }
+        } catch (Throwable e) {
+            unwrapped = null;
+        }
+        return unwrapped != null ? unwrapped.getClass() : null;
+    }
+
+    private static void appendTypeName(StringBuilder sb, Set typeNamesAppended, String name) {
+        if (!typeNamesAppended.contains(name)) {
+            if (sb.length() != 0) sb.append("+");
+            sb.append(name);
+            typeNamesAppended.add(name);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java
new file mode 100644
index 0000000..6e53a3c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/GenericParseException.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core.util;
+
+import org.apache.freemarker.core.ParseException;
+
+/**
+ * Exception thrown when a we want to parse some text but its format doesn't match the expectations. This is a quite
+ * generic exception, which we use in cases that don't deserve a dedicated exception.
+ * 
+ * @see ParseException
+ */
+@SuppressWarnings("serial")
+public class GenericParseException extends Exception {
+
+    public GenericParseException(String message) {
+        super(message);
+    }
+
+    public GenericParseException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java
new file mode 100644
index 0000000..3aa8d1d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/HtmlEscape.java
@@ -0,0 +1,109 @@
+/*
+ * 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 java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * Performs an HTML escape of a given template fragment. Specifically,
+ * &lt; &gt; &quot; and &amp; are all turned into entities.
+ *
+ * <p>Usage:<br>
+ * From java:</p>
+ * <pre>
+ * SimpleHash root = new SimpleHash();
+ *
+ * root.put( "htmlEscape", new org.apache.freemarker.core.util.HtmlEscape() );
+ *
+ * ...
+ * </pre>
+ *
+ * <p>From your FreeMarker template:</p>
+ * <pre>
+ *
+ * The following is HTML-escaped:
+ * &lt;transform htmlEscape&gt;
+ *   &lt;p&gt;This paragraph has all HTML special characters escaped.&lt;/p&gt;
+ * &lt;/transform&gt;
+ *
+ * ...
+ * </pre>
+ *
+ * @see org.apache.freemarker.core.util.XmlEscape
+ */
+// [FM3] Remove (or move to o.a.f.test)
+public class HtmlEscape implements TemplateTransformModel {
+
+    private static final char[] LT = "&lt;".toCharArray();
+    private static final char[] GT = "&gt;".toCharArray();
+    private static final char[] AMP = "&amp;".toCharArray();
+    private static final char[] QUOT = "&quot;".toCharArray();
+
+    @Override
+    public Writer getWriter(final Writer out, Map args) {
+        return new Writer()
+        {
+            @Override
+            public void write(int c)
+            throws IOException {
+                switch(c)
+                {
+                    case '<': out.write(LT, 0, 4); break;
+                    case '>': out.write(GT, 0, 4); break;
+                    case '&': out.write(AMP, 0, 5); break;
+                    case '"': out.write(QUOT, 0, 6); break;
+                    default: out.write(c);
+                }
+            }
+
+            @Override
+            public void write(char cbuf[], int off, int len)
+            throws IOException {
+                int lastoff = off;
+                int lastpos = off + len;
+                for (int i = off; i < lastpos; i++) {
+                    switch (cbuf[i])
+                    {
+                        case '<': out.write(cbuf, lastoff, i - lastoff); out.write(LT, 0, 4); lastoff = i + 1; break;
+                        case '>': out.write(cbuf, lastoff, i - lastoff); out.write(GT, 0, 4); lastoff = i + 1; break;
+                        case '&': out.write(cbuf, lastoff, i - lastoff); out.write(AMP, 0, 5); lastoff = i + 1; break;
+                        case '"': out.write(cbuf, lastoff, i - lastoff); out.write(QUOT, 0, 6); lastoff = i + 1; break;
+                    }
+                }
+                int remaining = lastpos - lastoff;
+                if (remaining > 0) {
+                    out.write(cbuf, lastoff, remaining);
+                }
+            }
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java
new file mode 100644
index 0000000..f4bc5a6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/NormalizeNewlines.java
@@ -0,0 +1,115 @@
+/*
+ * 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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * <p>Transformer that supports FreeMarker legacy behavior: all newlines appearing
+ * within the transformed area will be transformed into the platform's default
+ * newline. Unlike the old behavior, however, newlines generated by the data
+ * model are also converted. Legacy behavior was to leave newlines in the
+ * data model unaltered.</p>
+ *
+ * <p>Usage:<br>
+ * From java:</p>
+ * <pre>
+ * SimpleHash root = new SimpleHash();
+ *
+ * root.put( "normalizeNewlines", new org.apache.freemarker.core.util.NormalizeNewlines() );
+ *
+ * ...
+ * </pre>
+ *
+ * <p>From your FreeMarker template:</p>
+ * <pre>
+ * &lt;transform normalizeNewlines&gt;
+ *   &lt;html&gt;
+ *   &lt;head&gt;
+ *   ...
+ *   &lt;p&gt;This template has all newlines normalized to the current platform's
+ *   default.&lt;/p&gt;
+ *   ...
+ *   &lt;/body&gt;
+ *   &lt;/html&gt;
+ * &lt;/transform&gt;
+ * </pre>
+ */
+// [FM3] Remove (or move to o.a.f.test)
+public class NormalizeNewlines implements TemplateTransformModel {
+
+    @Override
+    public Writer getWriter(final Writer out,
+                            final Map args) {
+        final StringBuilder buf = new StringBuilder();
+        return new Writer() {
+            @Override
+            public void write(char cbuf[], int off, int len) {
+                buf.append(cbuf, off, len);
+            }
+
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() throws IOException {
+                StringReader sr = new StringReader(buf.toString());
+                StringWriter sw = new StringWriter();
+                transform(sr, sw);
+                out.write(sw.toString());
+            }
+        };
+    }
+
+    /**
+     * Performs newline normalization on FreeMarker output.
+     *
+     * @param in the input to be transformed
+     * @param out the destination of the transformation
+     */
+    public void transform(Reader in, Writer out) throws IOException {
+        BufferedReader br = (in instanceof BufferedReader)
+                            ? (BufferedReader) in
+                            : new BufferedReader(in);
+        PrintWriter pw = (out instanceof PrintWriter)
+                         ? (PrintWriter) out
+                         : new PrintWriter(out);
+        String line = br.readLine();
+        if (line != null) {
+            if ( line.length() > 0 ) {
+                pw.println(line);
+            }
+        }
+        while ((line = br.readLine()) != null) {
+            pw.println(line);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java
new file mode 100644
index 0000000..370d08d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ObjectFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.util;
+
+/**
+ * Used for the trivial cases of the factory pattern. Has a generic type argument since 2.3.24.
+ * 
+ * @since 2.3.22
+ */
+public interface ObjectFactory<T> {
+    
+    T createObject() throws Exception;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java
new file mode 100644
index 0000000..e1edfcb
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/OptInTemplateClassResolver.java
@@ -0,0 +1,160 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateClassResolver;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core._MiscTemplateException;
+
+/**
+ * A {@link TemplateClassResolver} that resolves only the classes whose name
+ * was specified in the constructor.
+ */
+public class OptInTemplateClassResolver implements TemplateClassResolver {
+    
+    private final Set/*<String>*/ allowedClasses;
+    private final List/*<String>*/ trustedTemplatePrefixes;
+    private final Set/*<String>*/ trustedTemplateNames;
+    
+    /**
+     * Creates a new instance. 
+     *
+     * @param allowedClasses the {@link Set} of {@link String}-s that contains
+     *     the full-qualified names of the allowed classes.
+     *     Can be <code>null</code> (means not class is allowed).
+     * @param trustedTemplates the {@link List} of {@link String}-s that contains
+     *     template names (i.e., template root directory relative paths)
+     *     and prefix patterns (like <code>"include/*"</code>) of templates
+     *     for which {@link TemplateClassResolver#UNRESTRICTED_RESOLVER} will be 
+     *     used (which is not as safe as {@link OptInTemplateClassResolver}).
+     *     The list items need not start with <code>"/"</code> (if they are, it
+     *     will be removed). List items ending with <code>"*"</code> are treated
+     *     as prefixes (i.e. <code>"foo*"</code> matches <code>"foobar"</code>,
+     *     <code>"foo/bar/baaz"</code>, <code>"foowhatever/bar/baaz"</code>,
+     *     etc.). The <code>"*"</code> has no special meaning anywhere else.
+     *     The matched template name is the name (template root directory
+     *     relative path) of the template that directly (lexically) contains the
+     *     operation (like <code>?new</code>) that wants to get the class. Thus,
+     *     if a trusted template includes a non-trusted template, the
+     *     <code>allowedClasses</code> restriction will apply in the included
+     *     template.
+     *     This parameter can be <code>null</code> (means no trusted templates).
+     */
+    public OptInTemplateClassResolver(
+            Set allowedClasses, List<String> trustedTemplates) {
+        this.allowedClasses = allowedClasses != null ? allowedClasses : Collections.EMPTY_SET;
+        if (trustedTemplates != null) {
+            trustedTemplateNames = new HashSet();
+            trustedTemplatePrefixes = new ArrayList();
+            
+            Iterator<String> it = trustedTemplates.iterator();
+            while (it.hasNext()) {
+                String li = it.next();
+                if (li.startsWith("/")) li = li.substring(1);
+                if (li.endsWith("*")) {
+                    trustedTemplatePrefixes.add(li.substring(0, li.length() - 1));
+                } else {
+                    trustedTemplateNames.add(li);
+                }
+            }
+        } else {
+            trustedTemplateNames = Collections.EMPTY_SET;
+            trustedTemplatePrefixes = Collections.EMPTY_LIST;
+        }
+    }
+
+    @Override
+    public Class resolve(String className, Environment env, Template template)
+    throws TemplateException {
+        String templateName = safeGetTemplateName(template);
+        
+        if (templateName != null
+                && (trustedTemplateNames.contains(templateName)
+                        || hasMatchingPrefix(templateName))) {
+            return TemplateClassResolver.UNRESTRICTED_RESOLVER.resolve(className, env, template);
+        } else {
+            if (!allowedClasses.contains(className)) {
+                throw new _MiscTemplateException(env,
+                        "Instantiating ", className, " is not allowed in the template for security reasons. (If you "
+                        + "run into this problem when using ?new in a template, you may want to check the \"",
+                        MutableProcessingConfiguration.NEW_BUILTIN_CLASS_RESOLVER_KEY,
+                        "\" setting in the FreeMarker configuration.)");
+            } else {
+                try {
+                    return _ClassUtil.forName(className);
+                } catch (ClassNotFoundException e) {
+                    throw new _MiscTemplateException(e, env);
+                }
+            }
+        }
+    }
+
+    /**
+     * Extract the template name from the template object which will be matched
+     * against the trusted template names and pattern. 
+     */
+    protected String safeGetTemplateName(Template template) {
+        if (template == null) return null;
+        
+        String name = template.getLookupName();
+        if (name == null) return null;
+
+        // Detect exploits, return null if one is suspected:
+        String decodedName = name;
+        if (decodedName.indexOf('%') != -1) {
+            decodedName = _StringUtil.replace(decodedName, "%2e", ".", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%2E", ".", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%2f", "/", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%2F", "/", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%5c", "\\", false, false);
+            decodedName = _StringUtil.replace(decodedName, "%5C", "\\", false, false);
+        }
+        int dotDotIdx = decodedName.indexOf("..");
+        if (dotDotIdx != -1) {
+            int before = dotDotIdx - 1 >= 0 ? decodedName.charAt(dotDotIdx - 1) : -1;
+            int after = dotDotIdx + 2 < decodedName.length() ? decodedName.charAt(dotDotIdx + 2) : -1;
+            if ((before == -1 || before == '/' || before == '\\')
+                    && (after == -1 || after == '/' || after == '\\')) {
+                return null;
+            }
+        }
+        
+        return name.startsWith("/") ? name.substring(1) : name;
+    }
+
+    private boolean hasMatchingPrefix(String name) {
+        for (int i = 0; i < trustedTemplatePrefixes.size(); i++) {
+            String prefix = (String) trustedTemplatePrefixes.get(i);
+            if (name.startsWith(prefix)) return true;
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
new file mode 100644
index 0000000..4b76dc5
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/ProductWrappingBuilder.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * A builder that encloses an already built product. {@link #build()} will always return the same product object.
+ */
+public class ProductWrappingBuilder<ProductT> implements CommonBuilder<ProductT> {
+
+    private final ProductT product;
+
+    public ProductWrappingBuilder(ProductT product) {
+        _NullArgumentException.check("product", product);
+        this.product = product;
+    }
+
+    @Override
+    public ProductT build() {
+        return product;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java
new file mode 100644
index 0000000..0943622
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/StandardCompress.java
@@ -0,0 +1,239 @@
+/*
+ * 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 java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * <p>A filter that compresses each sequence of consecutive whitespace
+ * to a single line break (if the sequence contains a line break) or a
+ * single space. In addition, leading and trailing whitespace is
+ * completely removed.</p>
+ * 
+ * <p>Specify the transform parameter <code>single_line = true</code>
+ * to always compress to a single space instead of a line break.</p>
+ * 
+ * <p>The default buffer size can be overridden by specifying a
+ * <code>buffer_size</code> transform parameter (in bytes).</p>
+ *
+ * <p><b>Note:</b> The compress tag is implemented using this filter</p>
+ * 
+ * <p>Usage:<br>
+ * From java:</p>
+ * <pre>
+ * SimpleHash root = new SimpleHash();
+ *
+ * root.put( "standardCompress", new org.apache.freemarker.core.util.StandardCompress() );
+ *
+ * ...
+ * </pre>
+ *
+ * <p>From your FreeMarker template:</p>
+ * <pre>
+ * &lt;transform standardCompress&gt;
+ *   &lt;p&gt;This    paragraph will have
+ *       extraneous
+ *
+ * whitespace removed.&lt;/p&gt;
+ * &lt;/transform&gt;
+ * </pre>
+ *
+ * <p>Output:</p>
+ * <pre>
+ * &lt;p&gt;This paragraph will have
+ * extraneous
+ * whitespace removed.&lt;/p&gt;
+ * </pre>
+ */
+// [FM3] Remove (or move to o.a.f.test), instead extend #compress
+public class StandardCompress implements TemplateTransformModel {
+    private static final String BUFFER_SIZE_KEY = "buffer_size";
+    private static final String SINGLE_LINE_KEY = "single_line";
+    private int defaultBufferSize;
+
+    public static final StandardCompress INSTANCE = new StandardCompress();
+    
+    public StandardCompress() {
+        this(2048);
+    }
+
+    /**
+     * @param defaultBufferSize the default amount of characters to buffer
+     */
+    public StandardCompress(int defaultBufferSize) {
+        this.defaultBufferSize = defaultBufferSize;
+    }
+
+    @Override
+    public Writer getWriter(final Writer out, Map args)
+    throws TemplateModelException {
+        int bufferSize = defaultBufferSize;
+        boolean singleLine = false;
+        if (args != null) {
+            try {
+                TemplateNumberModel num = (TemplateNumberModel) args.get(BUFFER_SIZE_KEY);
+                if (num != null)
+                    bufferSize = num.getAsNumber().intValue();
+            } catch (ClassCastException e) {
+                throw new TemplateModelException("Expecting numerical argument to " + BUFFER_SIZE_KEY);
+            }
+            try {
+                TemplateBooleanModel flag = (TemplateBooleanModel) args.get(SINGLE_LINE_KEY);
+                if (flag != null)
+                    singleLine = flag.getAsBoolean();
+            } catch (ClassCastException e) {
+                throw new TemplateModelException("Expecting boolean argument to " + SINGLE_LINE_KEY);
+            }
+        }
+        return new StandardCompressWriter(out, bufferSize, singleLine);
+    }
+
+    private static class StandardCompressWriter extends Writer {
+        private static final int MAX_EOL_LENGTH = 2; // CRLF is two bytes
+        
+        private static final int AT_BEGINNING = 0;
+        private static final int SINGLE_LINE = 1;
+        private static final int INIT = 2;
+        private static final int SAW_CR = 3;
+        private static final int LINEBREAK_CR = 4;
+        private static final int LINEBREAK_CRLF = 5;
+        private static final int LINEBREAK_LF = 6;
+
+        private final Writer out;
+        private final char[] buf;
+        private final boolean singleLine;
+    
+        private int pos = 0;
+        private boolean inWhitespace = true;
+        private int lineBreakState = AT_BEGINNING;
+
+        public StandardCompressWriter(Writer out, int bufSize, boolean singleLine) {
+            this.out = out;
+            this.singleLine = singleLine;
+            buf = new char[bufSize];
+        }
+
+        @Override
+        public void write(char[] cbuf, int off, int len) throws IOException {
+            for (; ; ) {
+                // Need to reserve space for the EOL potentially left in the state machine
+                int room = buf.length - pos - MAX_EOL_LENGTH; 
+                if (room >= len) {
+                    writeHelper(cbuf, off, len);
+                    break;
+                } else if (room <= 0) {
+                    flushInternal();
+                } else {
+                    writeHelper(cbuf, off, room);
+                    flushInternal();
+                    off += room;
+                    len -= room;
+                }
+            }
+        }
+
+        private void writeHelper(char[] cbuf, int off, int len) {
+            for (int i = off, end = off + len; i < end; i++) {
+                char c = cbuf[i];
+                if (Character.isWhitespace(c)) {
+                    inWhitespace = true;
+                    updateLineBreakState(c);
+                } else if (inWhitespace) {
+                    inWhitespace = false;
+                    writeLineBreakOrSpace();
+                    buf[pos++] = c;
+                } else {
+                    buf[pos++] = c;
+                }
+            }
+        }
+
+        /*
+          \r\n    => CRLF
+          \r[^\n] => CR
+          \r$     => CR
+          [^\r]\n => LF
+          ^\n     => LF
+        */
+        private void updateLineBreakState(char c) {
+            switch (lineBreakState) {
+            case INIT:
+                if (c == '\r') {
+                    lineBreakState = SAW_CR;
+                } else if (c == '\n') {
+                    lineBreakState = LINEBREAK_LF;
+                }
+                break;
+            case SAW_CR:
+                if (c == '\n') {
+                    lineBreakState = LINEBREAK_CRLF;
+                } else {
+                    lineBreakState = LINEBREAK_CR;
+                }
+            }
+        }
+
+        private void writeLineBreakOrSpace() {
+            switch (lineBreakState) {
+            case SAW_CR:
+                // whitespace ended with CR, fall through
+            case LINEBREAK_CR:
+                buf[pos++] = '\r';
+                break;
+            case LINEBREAK_CRLF:
+                buf[pos++] = '\r';
+                // fall through
+            case LINEBREAK_LF:
+                buf[pos++] = '\n';
+                break;
+            case AT_BEGINNING:
+                // ignore leading whitespace
+                break;
+            case INIT:
+            case SINGLE_LINE:
+                buf[pos++] = ' ';
+            }
+            lineBreakState = (singleLine) ? SINGLE_LINE : INIT;
+        }
+
+        private void flushInternal() throws IOException {
+            out.write(buf, 0, pos);
+            pos = 0;
+        }
+
+        @Override
+        public void flush() throws IOException {
+            flushInternal();
+            out.flush();
+        }
+
+        @Override
+        public void close() throws IOException {
+            flushInternal();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java
new file mode 100644
index 0000000..5b5cf97
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UndeclaredThrowableException.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+
+/**
+ * The equivalent of JDK 1.3 UndeclaredThrowableException.
+ */
+public class UndeclaredThrowableException extends RuntimeException {
+    
+    public UndeclaredThrowableException(Throwable t) {
+        super(t);
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    public UndeclaredThrowableException(String message, Throwable t) {
+        super(message, t);
+    }
+    
+    public Throwable getUndeclaredThrowable() {
+        return getCause();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java
new file mode 100644
index 0000000..4a820a0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnrecognizedTimeZoneException.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * Indicates that the time zone name is not recognized.
+ */
+public class UnrecognizedTimeZoneException extends Exception {
+    
+    private final String timeZoneName;
+
+    public UnrecognizedTimeZoneException(String timeZoneName) {
+        super("Unrecognized time zone: " + _StringUtil.jQuote(timeZoneName));
+        this.timeZoneName = timeZoneName;
+    }
+    
+    public String getTimeZoneName() {
+        return timeZoneName;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java
new file mode 100644
index 0000000..bcd9375
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/UnsupportedNumberClassException.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * Thrown when FreeMarker runs into a {@link Number} subclass that it doesn't yet support.  
+ */
+public class UnsupportedNumberClassException extends RuntimeException {
+
+    private final Class fClass;
+    
+    public UnsupportedNumberClassException(Class pClass) {
+        super("Unsupported number class: " + pClass.getName());
+        fClass = pClass;
+    }
+    
+    public Class getUnsupportedClass() {
+        return fClass;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java
new file mode 100644
index 0000000..43a2344
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/XmlEscape.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateTransformModel;
+
+/**
+ * Performs an XML escaping of a given template fragment. Specifically,
+ * <tt>&lt;</tt> <tt>&gt;</tt> <tt>&quot;</tt> <tt>'</tt> and <tt>&amp;</tt> are all turned into entity references.
+ *
+ * <p>An instance of this transform is initially visible as shared
+ * variable called <tt>xml_escape</tt>.</p>
+ */
+// [FM3] Remove (or move to o.a.f.test)
+public class XmlEscape implements TemplateTransformModel {
+
+    private static final char[] LT = "&lt;".toCharArray();
+    private static final char[] GT = "&gt;".toCharArray();
+    private static final char[] AMP = "&amp;".toCharArray();
+    private static final char[] QUOT = "&quot;".toCharArray();
+    private static final char[] APOS = "&apos;".toCharArray();
+
+    @Override
+    public Writer getWriter(final Writer out, Map args) {
+        return new Writer()
+        {
+            @Override
+            public void write(int c)
+            throws IOException {
+                switch(c)
+                {
+                    case '<': out.write(LT, 0, 4); break;
+                    case '>': out.write(GT, 0, 4); break;
+                    case '&': out.write(AMP, 0, 5); break;
+                    case '"': out.write(QUOT, 0, 6); break;
+                    case '\'': out.write(APOS, 0, 6); break;
+                    default: out.write(c);
+                }
+            }
+
+            @Override
+            public void write(char cbuf[], int off, int len)
+            throws IOException {
+                int lastoff = off;
+                int lastpos = off + len;
+                for (int i = off; i < lastpos; i++) {
+                    switch (cbuf[i])
+                    {
+                        case '<': out.write(cbuf, lastoff, i - lastoff); out.write(LT, 0, 4); lastoff = i + 1; break;
+                        case '>': out.write(cbuf, lastoff, i - lastoff); out.write(GT, 0, 4); lastoff = i + 1; break;
+                        case '&': out.write(cbuf, lastoff, i - lastoff); out.write(AMP, 0, 5); lastoff = i + 1; break;
+                        case '"': out.write(cbuf, lastoff, i - lastoff); out.write(QUOT, 0, 6); lastoff = i + 1; break;
+                        case '\'': out.write(cbuf, lastoff, i - lastoff); out.write(APOS, 0, 6); lastoff = i + 1; break;
+                    }
+                }
+                int remaining = lastpos - lastoff;
+                if (remaining > 0) {
+                    out.write(cbuf, lastoff, remaining);
+                }
+            }
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            @Override
+            public void close() {
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java
new file mode 100644
index 0000000..1c82658
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayEnumeration.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.util.Enumeration;
+import java.util.NoSuchElementException;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _ArrayEnumeration implements Enumeration {
+
+    private final Object[] array;
+    private final int size;
+    private int nextIndex;
+
+    public _ArrayEnumeration(Object[] array, int size) {
+        this.array = array;
+        this.size = size;
+        nextIndex = 0;
+    }
+
+    @Override
+    public boolean hasMoreElements() {
+        return nextIndex < size;
+    }
+
+    @Override
+    public Object nextElement() {
+        if (nextIndex >= size) {
+            throw new NoSuchElementException();
+        }
+        return array[nextIndex++];
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
new file mode 100644
index 0000000..7e02449
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
@@ -0,0 +1,54 @@
+/*
+ * 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 java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _ArrayIterator implements Iterator {
+
+    private final Object[] array;
+    private int nextIndex;
+
+    public _ArrayIterator(Object[] array) {
+        this.array = array;
+        nextIndex = 0;
+    }
+
+    @Override
+    public boolean hasNext() {
+        return nextIndex < array.length;
+    }
+
+    @Override
+    public Object next() {
+        if (nextIndex >= array.length) {
+            throw new NoSuchElementException();
+        }
+        return array[nextIndex++];
+    }
+
+    @Override
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+}


Mime
View raw message