freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [46/54] [partial] incubator-freemarker git commit: Unifying the o.a.f.core and o.a.f.core.ast
Date Thu, 23 Feb 2017 21:36:13 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/AliasTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/AliasTemplateNumberFormatFactory.java b/src/main/java/org/apache/freemarker/core/AliasTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..9fcf6d0
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/AliasTemplateNumberFormatFactory.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.util._LocaleUtil;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to
+     */
+    public AliasTemplateNumberFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @param localizedTargetFormatStrings
+     *            Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+     *            specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+     *            {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+     *            this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+     *            {@code new Locale("en")}. If there's still no matching key, the value of the
+     *            {@code targetFormatString} will be used.
+     */
+    public AliasTemplateNumberFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateNumberFormat(targetFormatString, locale);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java b/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java
new file mode 100644
index 0000000..b3ffe02
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java
@@ -0,0 +1,549 @@
+/*
+ * 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;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._NumberUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Class to perform arithmetic operations.
+ */
+
+public abstract class ArithmeticEngine {
+
+    /**
+     * Arithmetic engine that converts all numbers to {@link BigDecimal} and
+     * then operates on them. This is FreeMarker's default arithmetic engine.
+     */
+    public static final BigDecimalEngine BIGDECIMAL_ENGINE = new BigDecimalEngine();
+    /**
+     * Arithmetic engine that uses (more-or-less) the widening conversions of
+     * Java language to determine the type of result of operation, instead of
+     * converting everything to BigDecimal up front.
+     */
+    public static final ConservativeEngine CONSERVATIVE_ENGINE = new ConservativeEngine();
+
+    public abstract int compareNumbers(Number first, Number second) throws TemplateException;
+    public abstract Number add(Number first, Number second) throws TemplateException;
+    public abstract Number subtract(Number first, Number second) throws TemplateException;
+    public abstract Number multiply(Number first, Number second) throws TemplateException;
+    public abstract Number divide(Number first, Number second) throws TemplateException;
+    public abstract Number modulus(Number first, Number second) throws TemplateException;
+    
+    /**
+     * Should be able to parse all FTL numerical literals, Java Double toString results, and XML Schema numbers.
+     * This means these should be parsed successfully, except if the arithmetical engine
+     * couldn't support the resulting value anyway (such as NaN, infinite, even non-integers):
+     * {@code -123.45}, {@code 1.5e3}, {@code 1.5E3}, {@code 0005}, {@code +0}, {@code -0}, {@code NaN},
+     * {@code INF}, {@code -INF}, {@code Infinity}, {@code -Infinity}. 
+     */    
+    public abstract Number toNumber(String s);
+
+    protected int minScale = 12;
+    protected int maxScale = 12;
+    protected int roundingPolicy = BigDecimal.ROUND_HALF_UP;
+
+    /**
+     * Sets the minimal scale to use when dividing BigDecimal numbers. Default
+     * value is 12.
+     */
+    public void setMinScale(int minScale) {
+        if (minScale < 0) {
+            throw new IllegalArgumentException("minScale < 0");
+        }
+        this.minScale = minScale;
+    }
+    
+    /**
+     * Sets the maximal scale to use when multiplying BigDecimal numbers. 
+     * Default value is 100.
+     */
+    public void setMaxScale(int maxScale) {
+        if (maxScale < minScale) {
+            throw new IllegalArgumentException("maxScale < minScale");
+        }
+        this.maxScale = maxScale;
+    }
+
+    public void setRoundingPolicy(int roundingPolicy) {
+        if (roundingPolicy != BigDecimal.ROUND_CEILING
+            && roundingPolicy != BigDecimal.ROUND_DOWN
+            && roundingPolicy != BigDecimal.ROUND_FLOOR
+            && roundingPolicy != BigDecimal.ROUND_HALF_DOWN
+            && roundingPolicy != BigDecimal.ROUND_HALF_EVEN
+            && roundingPolicy != BigDecimal.ROUND_HALF_UP
+            && roundingPolicy != BigDecimal.ROUND_UNNECESSARY
+            && roundingPolicy != BigDecimal.ROUND_UP) {
+            throw new IllegalArgumentException("invalid rounding policy");        
+        }
+        
+        this.roundingPolicy = roundingPolicy;
+    }
+
+    /**
+     * This is the default arithmetic engine in FreeMarker. It converts every
+     * number it receives into {@link BigDecimal}, then operates on these
+     * converted {@link BigDecimal}s.
+     */
+    public static class BigDecimalEngine
+    extends
+        ArithmeticEngine {
+        @Override
+        public int compareNumbers(Number first, Number second) {
+            // We try to find the result based on the sign (+/-/0) first, because:
+            // - It's much faster than converting to BigDecial, and comparing to 0 is the most common comparison.
+            // - It doesn't require any type conversions, and thus things like "Infinity > 0" won't fail.
+            int firstSignum = _NumberUtil.getSignum(first); 
+            int secondSignum = _NumberUtil.getSignum(second);
+            if (firstSignum != secondSignum) {
+                return firstSignum < secondSignum ? -1 : (firstSignum > secondSignum ? 1 : 0); 
+            } else if (firstSignum == 0 && secondSignum == 0) {
+                return 0;
+            } else {
+                BigDecimal left = toBigDecimal(first);
+                BigDecimal right = toBigDecimal(second);
+                return left.compareTo(right);
+            }
+        }
+    
+        @Override
+        public Number add(Number first, Number second) {
+            BigDecimal left = toBigDecimal(first);
+            BigDecimal right = toBigDecimal(second);
+            return left.add(right);
+        }
+    
+        @Override
+        public Number subtract(Number first, Number second) {
+            BigDecimal left = toBigDecimal(first);
+            BigDecimal right = toBigDecimal(second);
+            return left.subtract(right);
+        }
+    
+        @Override
+        public Number multiply(Number first, Number second) {
+            BigDecimal left = toBigDecimal(first);
+            BigDecimal right = toBigDecimal(second);
+            BigDecimal result = left.multiply(right);
+            if (result.scale() > maxScale) {
+                result = result.setScale(maxScale, roundingPolicy);
+            }
+            return result;
+        }
+    
+        @Override
+        public Number divide(Number first, Number second) {
+            BigDecimal left = toBigDecimal(first);
+            BigDecimal right = toBigDecimal(second);
+            return divide(left, right);
+        }
+    
+        @Override
+        public Number modulus(Number first, Number second) {
+            long left = first.longValue();
+            long right = second.longValue();
+            return Long.valueOf(left % right);
+        }
+    
+        @Override
+        public Number toNumber(String s) {
+            return toBigDecimalOrDouble(s);
+        }
+        
+        private BigDecimal divide(BigDecimal left, BigDecimal right) {
+            int scale1 = left.scale();
+            int scale2 = right.scale();
+            int scale = Math.max(scale1, scale2);
+            scale = Math.max(minScale, scale);
+            return left.divide(right, scale, roundingPolicy);
+        }
+    }
+
+    /**
+     * An arithmetic engine that conservatively widens the operation arguments
+     * to extent that they can hold the result of the operation. Widening 
+     * conversions occur in following situations:
+     * <ul>
+     * <li>byte and short are always widened to int (alike to Java language).</li>
+     * <li>To preserve magnitude: when operands are of different types, the 
+     * result type is the type of the wider operand.</li>
+     * <li>to avoid overflows: if add, subtract, or multiply would overflow on
+     * integer types, the result is widened from int to long, or from long to 
+     * BigInteger.</li>
+     * <li>to preserve fractional part: if a division of integer types would 
+     * have a fractional part, int and long are converted to double, and 
+     * BigInteger is converted to BigDecimal. An operation on a float and a 
+     * long results in a double. An operation on a float or double and a
+     * BigInteger results in a BigDecimal.</li>
+     * </ul>
+     */
+    public static class ConservativeEngine extends ArithmeticEngine {
+        private static final int INTEGER = 0;
+        private static final int LONG = 1;
+        private static final int FLOAT = 2;
+        private static final int DOUBLE = 3;
+        private static final int BIGINTEGER = 4;
+        private static final int BIGDECIMAL = 5;
+        
+        private static final Map classCodes = createClassCodesMap();
+        
+        @Override
+        public int compareNumbers(Number first, Number second) throws TemplateException {
+            switch(getCommonClassCode(first, second)) {
+                case INTEGER: {
+                    int n1 = first.intValue();
+                    int n2 = second.intValue();
+                    return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+                }
+                case LONG: {
+                    long n1 = first.longValue();
+                    long n2 = second.longValue();
+                    return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+                }
+                case FLOAT: {
+                    float n1 = first.floatValue();
+                    float n2 = second.floatValue();
+                    return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+                }
+                case DOUBLE: {
+                    double n1 = first.doubleValue();
+                    double n2 = second.doubleValue();
+                    return  n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
+                }
+                case BIGINTEGER: {
+                    BigInteger n1 = toBigInteger(first);
+                    BigInteger n2 = toBigInteger(second);
+                    return n1.compareTo(n2);
+                }
+                case BIGDECIMAL: {
+                    BigDecimal n1 = toBigDecimal(first);
+                    BigDecimal n2 = toBigDecimal(second);
+                    return n1.compareTo(n2);
+                }
+            }
+            // Make the compiler happy. getCommonClassCode() is guaranteed to 
+            // return only above codes, or throw an exception.
+            throw new Error();
+        }
+    
+        @Override
+        public Number add(Number first, Number second) throws TemplateException {
+            switch(getCommonClassCode(first, second)) {
+                case INTEGER: {
+                    int n1 = first.intValue();
+                    int n2 = second.intValue();
+                    int n = n1 + n2;
+                    return
+                        ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check
+                        ? Long.valueOf(((long) n1) + n2)
+                        : Integer.valueOf(n);
+                }
+                case LONG: {
+                    long n1 = first.longValue();
+                    long n2 = second.longValue();
+                    long n = n1 + n2;
+                    return
+                        ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check
+                        ? toBigInteger(first).add(toBigInteger(second))
+                        : Long.valueOf(n);
+                }
+                case FLOAT: {
+                    return Float.valueOf(first.floatValue() + second.floatValue());
+                }
+                case DOUBLE: {
+                    return Double.valueOf(first.doubleValue() + second.doubleValue());
+                }
+                case BIGINTEGER: {
+                    BigInteger n1 = toBigInteger(first);
+                    BigInteger n2 = toBigInteger(second);
+                    return n1.add(n2);
+                }
+                case BIGDECIMAL: {
+                    BigDecimal n1 = toBigDecimal(first);
+                    BigDecimal n2 = toBigDecimal(second);
+                    return n1.add(n2);
+                }
+            }
+            // Make the compiler happy. getCommonClassCode() is guaranteed to 
+            // return only above codes, or throw an exception.
+            throw new Error();
+        }
+    
+        @Override
+        public Number subtract(Number first, Number second) throws TemplateException {
+            switch(getCommonClassCode(first, second)) {
+                case INTEGER: {
+                    int n1 = first.intValue();
+                    int n2 = second.intValue();
+                    int n = n1 - n2;
+                    return
+                        ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check
+                        ? Long.valueOf(((long) n1) - n2)
+                        : Integer.valueOf(n);
+                }
+                case LONG: {
+                    long n1 = first.longValue();
+                    long n2 = second.longValue();
+                    long n = n1 - n2;
+                    return
+                        ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check
+                        ? toBigInteger(first).subtract(toBigInteger(second))
+                        : Long.valueOf(n);
+                }
+                case FLOAT: {
+                    return Float.valueOf(first.floatValue() - second.floatValue());
+                }
+                case DOUBLE: {
+                    return Double.valueOf(first.doubleValue() - second.doubleValue());
+                }
+                case BIGINTEGER: {
+                    BigInteger n1 = toBigInteger(first);
+                    BigInteger n2 = toBigInteger(second);
+                    return n1.subtract(n2);
+                }
+                case BIGDECIMAL: {
+                    BigDecimal n1 = toBigDecimal(first);
+                    BigDecimal n2 = toBigDecimal(second);
+                    return n1.subtract(n2);
+                }
+            }
+            // Make the compiler happy. getCommonClassCode() is guaranteed to 
+            // return only above codes, or throw an exception.
+            throw new Error();
+        }
+    
+        @Override
+        public Number multiply(Number first, Number second) throws TemplateException {
+            switch(getCommonClassCode(first, second)) {
+                case INTEGER: {
+                    int n1 = first.intValue();
+                    int n2 = second.intValue();
+                    int n = n1 * n2;
+                    return
+                        n1 == 0 || n / n1 == n2 // overflow check
+                        ? Integer.valueOf(n)
+                        : Long.valueOf(((long) n1) * n2);
+                }
+                case LONG: {
+                    long n1 = first.longValue();
+                    long n2 = second.longValue();
+                    long n = n1 * n2;
+                    return
+                        n1 == 0L || n / n1 == n2 // overflow check
+                        ? Long.valueOf(n)
+                        : toBigInteger(first).multiply(toBigInteger(second));
+                }
+                case FLOAT: {
+                    return Float.valueOf(first.floatValue() * second.floatValue());
+                }
+                case DOUBLE: {
+                    return Double.valueOf(first.doubleValue() * second.doubleValue());
+                }
+                case BIGINTEGER: {
+                    BigInteger n1 = toBigInteger(first);
+                    BigInteger n2 = toBigInteger(second);
+                    return n1.multiply(n2);
+                }
+                case BIGDECIMAL: {
+                    BigDecimal n1 = toBigDecimal(first);
+                    BigDecimal n2 = toBigDecimal(second);
+                    BigDecimal r = n1.multiply(n2);
+                    return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r;
+                }
+            }
+            // Make the compiler happy. getCommonClassCode() is guaranteed to 
+            // return only above codes, or throw an exception.
+            throw new Error();
+        }
+    
+        @Override
+        public Number divide(Number first, Number second) throws TemplateException {
+            switch(getCommonClassCode(first, second)) {
+                case INTEGER: {
+                    int n1 = first.intValue();
+                    int n2 = second.intValue();
+                    if (n1 % n2 == 0) {
+                        return Integer.valueOf(n1 / n2);
+                    }
+                    return Double.valueOf(((double) n1) / n2);
+                }
+                case LONG: {
+                    long n1 = first.longValue();
+                    long n2 = second.longValue();
+                    if (n1 % n2 == 0) {
+                        return Long.valueOf(n1 / n2);
+                    }
+                    return Double.valueOf(((double) n1) / n2);
+                }
+                case FLOAT: {
+                    return Float.valueOf(first.floatValue() / second.floatValue());
+                }
+                case DOUBLE: {
+                    return Double.valueOf(first.doubleValue() / second.doubleValue());
+                }
+                case BIGINTEGER: {
+                    BigInteger n1 = toBigInteger(first);
+                    BigInteger n2 = toBigInteger(second);
+                    BigInteger[] divmod = n1.divideAndRemainder(n2);
+                    if (divmod[1].equals(BigInteger.ZERO)) {
+                        return divmod[0];
+                    } else {
+                        BigDecimal bd1 = new BigDecimal(n1);
+                        BigDecimal bd2 = new BigDecimal(n2);
+                        return bd1.divide(bd2, minScale, roundingPolicy);
+                    }
+                }
+                case BIGDECIMAL: {
+                    BigDecimal n1 = toBigDecimal(first);
+                    BigDecimal n2 = toBigDecimal(second);
+                    int scale1 = n1.scale();
+                    int scale2 = n2.scale();
+                    int scale = Math.max(scale1, scale2);
+                    scale = Math.max(minScale, scale);
+                    return n1.divide(n2, scale, roundingPolicy);
+                }
+            }
+            // Make the compiler happy. getCommonClassCode() is guaranteed to 
+            // return only above codes, or throw an exception.
+            throw new Error();
+        }
+    
+        @Override
+        public Number modulus(Number first, Number second) throws TemplateException {
+            switch(getCommonClassCode(first, second)) {
+                case INTEGER: {
+                    return Integer.valueOf(first.intValue() % second.intValue());
+                }
+                case LONG: {
+                    return Long.valueOf(first.longValue() % second.longValue());
+                }
+                case FLOAT: {
+                    return Float.valueOf(first.floatValue() % second.floatValue());
+                }
+                case DOUBLE: {
+                    return Double.valueOf(first.doubleValue() % second.doubleValue());
+                }
+                case BIGINTEGER: {
+                    BigInteger n1 = toBigInteger(first);
+                    BigInteger n2 = toBigInteger(second);
+                    return n1.mod(n2);
+                }
+                case BIGDECIMAL: {
+                    throw new _MiscTemplateException("Can't calculate remainder on BigDecimals");
+                }
+            }
+            // Make the compiler happy. getCommonClassCode() is guaranteed to 
+            // return only above codes, or throw an exception.
+            throw new BugException();
+        }
+    
+        @Override
+        public Number toNumber(String s) {
+            Number n = toBigDecimalOrDouble(s);
+            return n instanceof BigDecimal ? _NumberUtil.optimizeNumberRepresentation(n) : n;
+        }
+        
+        private static Map createClassCodesMap() {
+            Map map = new HashMap(17);
+            Integer intcode = Integer.valueOf(INTEGER);
+            map.put(Byte.class, intcode);
+            map.put(Short.class, intcode);
+            map.put(Integer.class, intcode);
+            map.put(Long.class, Integer.valueOf(LONG));
+            map.put(Float.class, Integer.valueOf(FLOAT));
+            map.put(Double.class, Integer.valueOf(DOUBLE));
+            map.put(BigInteger.class, Integer.valueOf(BIGINTEGER));
+            map.put(BigDecimal.class, Integer.valueOf(BIGDECIMAL));
+            return map;
+        }
+        
+        private static int getClassCode(Number num) throws TemplateException {
+            try {
+                return ((Integer) classCodes.get(num.getClass())).intValue();
+            } catch (NullPointerException e) {
+                if (num == null) {
+                    throw new _MiscTemplateException("The Number object was null.");
+                } else {
+                    throw new _MiscTemplateException("Unknown number type ", num.getClass().getName());
+                }
+            }
+        }
+        
+        private static int getCommonClassCode(Number num1, Number num2) throws TemplateException {
+            int c1 = getClassCode(num1);
+            int c2 = getClassCode(num2);
+            int c = c1 > c2 ? c1 : c2;
+            // If BigInteger is combined with a Float or Double, the result is a
+            // BigDecimal instead of BigInteger in order not to lose the 
+            // fractional parts. If Float is combined with Long, the result is a
+            // Double instead of Float to preserve the bigger bit width.
+            switch(c) {
+                case FLOAT: {
+                    if ((c1 < c2 ? c1 : c2) == LONG) {
+                        return DOUBLE;
+                    }
+                    break;
+                }
+                case BIGINTEGER: {
+                    int min = c1 < c2 ? c1 : c2;
+                    if (min == DOUBLE || min == FLOAT) {
+                        return BIGDECIMAL;
+                    }
+                    break;
+                }
+            }
+            return c;
+        }
+        
+        private static BigInteger toBigInteger(Number num) {
+            return num instanceof BigInteger ? (BigInteger) num : new BigInteger(num.toString());
+        }
+    }
+
+    private static BigDecimal toBigDecimal(Number num) {
+        try {
+            return num instanceof BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString());
+        } catch (NumberFormatException e) {
+            // The exception message is useless, so we add a new one:
+            throw new NumberFormatException("Can't parse this as BigDecimal number: " + _StringUtil.jQuote(num));
+        }
+    }
+    
+    private static Number toBigDecimalOrDouble(String s) {
+        if (s.length() > 2) {
+            char c = s.charAt(0);
+            if (c == 'I' && (s.equals("INF") || s.equals("Infinity"))) {
+                return Double.valueOf(Double.POSITIVE_INFINITY);
+            } else if (c == 'N' && s.equals("NaN")) {
+                return Double.valueOf(Double.NaN);
+            } else if (c == '-' && s.charAt(1) == 'I' && (s.equals("-INF") || s.equals("-Infinity"))) {
+                return Double.valueOf(Double.NEGATIVE_INFINITY);
+            }
+        }
+        return new BigDecimal(s);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java b/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java
new file mode 100644
index 0000000..f477b95
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * An operator for arithmetic operations. Note that the + operator is in {@link ASTExpAddOrConcat}, because its
+ * overloaded (does string concatenation and more).
+ */
+final class ArithmeticExpression extends ASTExpression {
+
+    static final int TYPE_SUBSTRACTION = 0;
+    static final int TYPE_MULTIPLICATION = 1;
+    static final int TYPE_DIVISION = 2;
+    static final int TYPE_MODULO = 3;
+
+    private static final char[] OPERATOR_IMAGES = new char[] { '-', '*', '/', '%' };
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+    private final int operator;
+
+    ArithmeticExpression(ASTExpression lho, ASTExpression rho, int operator) {
+        this.lho = lho;
+        this.rho = rho;
+        this.operator = operator;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return _eval(env, this, lho.evalToNumber(env), operator, rho.evalToNumber(env));
+    }
+
+    static TemplateModel _eval(Environment env, ASTNode parent, Number lhoNumber, int operator, Number rhoNumber)
+            throws TemplateException, _MiscTemplateException {
+        ArithmeticEngine ae = EvalUtil.getArithmeticEngine(env, parent); 
+        switch (operator) {
+            case TYPE_SUBSTRACTION : 
+                return new SimpleNumber(ae.subtract(lhoNumber, rhoNumber));
+            case TYPE_MULTIPLICATION :
+                return new SimpleNumber(ae.multiply(lhoNumber, rhoNumber));
+            case TYPE_DIVISION :
+                return new SimpleNumber(ae.divide(lhoNumber, rhoNumber));
+            case TYPE_MODULO :
+                return new SimpleNumber(ae.modulus(lhoNumber, rhoNumber));
+            default:
+                if (parent instanceof ASTExpression) {
+                    throw new _MiscTemplateException((ASTExpression) parent,
+                            "Unknown operation: ", Integer.valueOf(operator));
+                } else {
+                    throw new _MiscTemplateException("Unknown operation: ", Integer.valueOf(operator));
+                }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + ' ' + getOperatorSymbol(operator) + ' ' + rho.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return String.valueOf(getOperatorSymbol(operator));
+    }
+
+    static char getOperatorSymbol(int operator) {
+        return OPERATOR_IMAGES[operator];
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ArithmeticExpression(
+    	        lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        operator);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 3;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        case 2: return Integer.valueOf(operator);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.LEFT_HAND_OPERAND;
+        case 1: return ParameterRole.RIGHT_HAND_OPERAND;
+        case 2: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BackwardCompatibleTemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BackwardCompatibleTemplateNumberFormat.java b/src/main/java/org/apache/freemarker/core/BackwardCompatibleTemplateNumberFormat.java
new file mode 100644
index 0000000..de5983b
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BackwardCompatibleTemplateNumberFormat.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;
+
+/**
+ * Only exists for emulating pre-2.3.24-IcI {@code ?string} behavior. 
+ * 
+ * @since 2.3.24
+ */
+// [FM3] Still needed?
+abstract class BackwardCompatibleTemplateNumberFormat extends TemplateNumberFormat {
+
+    abstract String format(Number number) throws UnformattableValueException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java b/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java
new file mode 100644
index 0000000..2d34a42
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BoundedRangeModel.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+
+/**
+ * A range between two integers (maybe 0 long).
+ */
+final class BoundedRangeModel extends RangeModel {
+
+    private final int step, size;
+    private final boolean rightAdaptive;
+    private final boolean affectedByStringSlicingBug;
+    
+    /**
+     * @param inclusiveEnd Tells if the {@code end} index is part of the range. 
+     * @param rightAdaptive Tells if the right end of the range adapts to the size of the sliced value, if otherwise
+     *     it would be bigger than that. 
+     */
+    BoundedRangeModel(int begin, int end, boolean inclusiveEnd, boolean rightAdaptive) {
+        super(begin);
+        step = begin <= end ? 1 : -1;
+        size = Math.abs(end - begin) + (inclusiveEnd ? 1 : 0);
+        this.rightAdaptive = rightAdaptive;
+        affectedByStringSlicingBug = inclusiveEnd;
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+    
+    @Override
+    int getStep() {
+        return step;
+    }
+
+    @Override
+    boolean isRightUnbounded() {
+        return false;
+    }
+
+    @Override
+    boolean isRightAdaptive() {
+        return rightAdaptive;
+    }
+
+    @Override
+    boolean isAffactedByStringSlicingBug() {
+        return affectedByStringSlicingBug;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java b/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java
new file mode 100644
index 0000000..e9c3dd6
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInBannedWhenAutoEscaping.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ * A string built-in whose usage is banned when auto-escaping with a markup-output format is active.
+ * This is just a marker; the actual checking is in {@code FTL.jj}.
+ */
+abstract class BuiltInBannedWhenAutoEscaping extends SpecialBuiltIn {
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForDate.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForDate.java b/src/main/java/org/apache/freemarker/core/BuiltInForDate.java
new file mode 100644
index 0000000..47bf910
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForDate.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+abstract class BuiltInForDate extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateDateModel) {
+            TemplateDateModel tdm = (TemplateDateModel) model;
+            return calculateResult(EvalUtil.modelToDate(tdm, target), tdm.getDateType(), env);
+        } else {
+            throw newNonDateException(env, model, target);
+        }
+    }
+
+    /** Override this to implement the built-in. */
+    protected abstract TemplateModel calculateResult(
+            Date date, int dateType, Environment env)
+    throws TemplateException;
+    
+    static TemplateException newNonDateException(Environment env, TemplateModel model, ASTExpression target)
+            throws InvalidReferenceException {
+        TemplateException e;
+        if (model == null) {
+            e = InvalidReferenceException.getInstance(target, env);
+        } else {
+            e = new NonDateException(target, model, "date", env);
+        }
+        return e;
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java b/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java
new file mode 100644
index 0000000..9497365
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForHashEx.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+abstract class BuiltInForHashEx extends ASTExpBuiltIn {
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateHashModelEx) {
+            return calculateResult((TemplateHashModelEx) model, env);
+        }
+        throw new NonExtendedHashException(target, model, env);
+    }
+    
+    abstract TemplateModel calculateResult(TemplateHashModelEx hashExModel, Environment env)
+            throws TemplateModelException, InvalidReferenceException;
+    
+    protected InvalidReferenceException newNullPropertyException(
+            String propertyName, TemplateModel tm, Environment env) {
+        if (env.getFastInvalidReferenceExceptions()) {
+            return InvalidReferenceException.FAST_INSTANCE;
+        } else {
+            return new InvalidReferenceException(
+                    new _ErrorDescriptionBuilder(
+                        "The exteneded hash (of class ", tm.getClass().getName(), ") has returned null for its \"",
+                        propertyName,
+                        "\" property. This is maybe a bug. The extended hash was returned by this expression:")
+                    .blame(target),
+                    env, this);
+        }
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java b/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java
new file mode 100644
index 0000000..26d64ad
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForLegacyEscaping.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * A string built-in whose usage is banned when auto-escaping with a markup-output format is active.
+ * This is just a marker; the actual checking is in {@code FTL.jj}.
+ */
+abstract class BuiltInForLegacyEscaping extends BuiltInBannedWhenAutoEscaping {
+    
+    @Override
+    TemplateModel _eval(Environment env)
+    throws TemplateException {
+        TemplateModel tm = target.eval(env);
+        Object moOrStr = EvalUtil.coerceModelToStringOrMarkup(tm, target, null, env);
+        if (moOrStr instanceof String) {
+            return calculateResult((String) moOrStr, env);
+        } else {
+            TemplateMarkupOutputModel<?> mo = (TemplateMarkupOutputModel<?>) moOrStr;
+            if (mo.getOutputFormat().isLegacyBuiltInBypassed(key)) {
+                return mo;
+            }
+            throw new NonStringException(target, tm, env);
+        }
+    }
+    
+    abstract TemplateModel calculateResult(String s, Environment env) throws TemplateException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java b/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java
new file mode 100644
index 0000000..c4f3ed3
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForLoopVariable.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.ASTDirList.IterationContext;
+import org.apache.freemarker.core.model.TemplateModel;
+
+abstract class BuiltInForLoopVariable extends SpecialBuiltIn {
+    
+    private String loopVarName;
+    
+    void bindToLoopVariable(String loopVarName) {
+        this.loopVarName = loopVarName;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        IterationContext iterCtx = ASTDirList.findEnclosingIterationContext(env, loopVarName);
+        if (iterCtx == null) {
+            // The parser should prevent this situation
+            throw new _MiscTemplateException(
+                    this, env,
+                    "There's no iteration in context that uses loop variable ", new _DelayedJQuote(loopVarName), ".");
+        }
+        
+        return calculateResult(iterCtx, env);
+    }
+
+    abstract TemplateModel calculateResult(IterationContext iterCtx, Environment env) throws TemplateException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java b/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java
new file mode 100644
index 0000000..a153d5a
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForMarkupOutput.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+abstract class BuiltInForMarkupOutput extends ASTExpBuiltIn {
+    
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (!(model instanceof TemplateMarkupOutputModel)) {
+            throw new NonMarkupOutputException(target, model, env);
+        }
+        return calculateResult((TemplateMarkupOutputModel) model);
+    }
+    
+    protected abstract TemplateModel calculateResult(TemplateMarkupOutputModel model) throws TemplateModelException;
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForNode.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForNode.java b/src/main/java/org/apache/freemarker/core/BuiltInForNode.java
new file mode 100644
index 0000000..569952d
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForNode.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+
+abstract class BuiltInForNode extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateNodeModel) {
+            return calculateResult((TemplateNodeModel) model, env);
+        } else {
+            throw new NonNodeException(target, model, env);
+        }
+    }
+    abstract TemplateModel calculateResult(TemplateNodeModel nodeModel, Environment env)
+            throws TemplateModelException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java b/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java
new file mode 100644
index 0000000..424f6d0
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForNodeEx.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+
+public abstract class BuiltInForNodeEx extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (model instanceof TemplateNodeModelEx) {
+            return calculateResult((TemplateNodeModelEx) model, env);
+        } else {
+            throw new NonExtendedNodeException(target, model, env);
+        }
+    }
+    abstract TemplateModel calculateResult(TemplateNodeModelEx nodeModel, Environment env)
+            throws TemplateModelException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java b/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java
new file mode 100644
index 0000000..9cd4cf0
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForNumber.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+abstract class BuiltInForNumber extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        return calculateResult(target.modelToNumber(model, env), model);
+    }
+    
+    abstract TemplateModel calculateResult(Number num, TemplateModel model)
+    throws TemplateModelException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java b/src/main/java/org/apache/freemarker/core/BuiltInForSequence.java
new file mode 100644
index 0000000..0e5d659
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForSequence.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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+abstract class BuiltInForSequence extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+            throws TemplateException {
+        TemplateModel model = target.eval(env);
+        if (!(model instanceof TemplateSequenceModel)) {
+            throw new NonSequenceException(target, model, env);
+        }
+        return calculateResult((TemplateSequenceModel) model);
+    }
+    abstract TemplateModel calculateResult(TemplateSequenceModel tsm)
+    throws TemplateModelException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInForString.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInForString.java b/src/main/java/org/apache/freemarker/core/BuiltInForString.java
new file mode 100644
index 0000000..0d1bcaf
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInForString.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+abstract class BuiltInForString extends ASTExpBuiltIn {
+    @Override
+    TemplateModel _eval(Environment env)
+    throws TemplateException {
+        return calculateResult(getTargetString(target, env), env);
+    }
+    abstract TemplateModel calculateResult(String s, Environment env) throws TemplateException;
+    
+    static String getTargetString(ASTExpression target, Environment env) throws TemplateException {
+        return target.evalAndCoerceToStringOrUnsupportedMarkup(env);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java b/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java
new file mode 100644
index 0000000..60910b3
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInWithParseTimeParameters.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import org.apache.freemarker.core.Token;
+
+
+abstract class BuiltInWithParseTimeParameters extends SpecialBuiltIn {
+
+    abstract void bindToParameters(List/*<ASTExpression>*/ parameters, Token openParen, Token closeParen)
+            throws ParseException;
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder();
+        
+        buf.append(super.getCanonicalForm());
+        
+        buf.append("(");
+        List/*<ASTExpression>*/args = getArgumentsAsList();
+        int size = args.size();
+        for (int i = 0; i < size; i++) {
+            if (i != 0) {
+                buf.append(", ");
+            }
+            ASTExpression arg = (ASTExpression) args.get(i);
+            buf.append(arg.getCanonicalForm());
+        }
+        buf.append(")");
+        
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return super.getNodeTypeSymbol() + "(...)";
+    }        
+    
+    @Override
+    int getParameterCount() {
+        return super.getParameterCount() + getArgumentsCount();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        final int superParamCnt = super.getParameterCount();
+        if (idx < superParamCnt) {
+            return super.getParameterValue(idx); 
+        }
+        
+        final int argIdx = idx - superParamCnt;
+        return getArgumentParameterValue(argIdx);
+    }
+    
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        final int superParamCnt = super.getParameterCount();
+        if (idx < superParamCnt) {
+            return super.getParameterRole(idx); 
+        }
+        
+        if (idx - superParamCnt < getArgumentsCount()) {
+            return ParameterRole.ARGUMENT_VALUE;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    protected ParseException newArgumentCountException(String ordinalityDesc, Token openParen, Token closeParen) {
+        return new ParseException(
+                "?" + key + "(...) " + ordinalityDesc + " parameters", getTemplate(),
+                openParen.beginLine, openParen.beginColumn,
+                closeParen.endLine, closeParen.endColumn);
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        final ASTExpression clone = super.deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, replacementState);
+        cloneArguments(clone, replacedIdentifier, replacement, replacementState);
+        return clone;
+    }
+
+    protected abstract List getArgumentsAsList();
+    
+    protected abstract int getArgumentsCount();
+
+    protected abstract ASTExpression getArgumentParameterValue(int argIdx);
+    
+    protected abstract void cloneArguments(ASTExpression clone, String replacedIdentifier,
+            ASTExpression replacement, ReplacemenetState replacementState);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java b/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java
new file mode 100644
index 0000000..8c2e392
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInsForDates.java
@@ -0,0 +1,212 @@
+/*
+ * 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;
+
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util.UnrecognizedTimeZoneException;
+import org.apache.freemarker.core.util._DateUtil;
+
+/**
+ * A holder for built-ins that operate exclusively on date left-hand values.
+ */
+class BuiltInsForDates {
+    
+    static class dateType_if_unknownBI extends ASTExpBuiltIn {
+        
+        private final int dateType;
+
+        dateType_if_unknownBI(int dateType) {
+            this.dateType = dateType;
+        }
+
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = target.eval(env);
+            if (model instanceof TemplateDateModel) {
+                TemplateDateModel tdm = (TemplateDateModel) model;
+                int tdmDateType = tdm.getDateType();
+                if (tdmDateType != TemplateDateModel.UNKNOWN) {
+                    return tdm;
+                }
+                return new SimpleDate(EvalUtil.modelToDate(tdm, target), dateType);
+            } else {
+                throw BuiltInForDate.newNonDateException(env, model, target);
+            }
+        }
+
+        protected TemplateModel calculateResult(Date date, int dateType, Environment env) throws TemplateException {
+            // TODO Auto-generated method stub
+            return null;
+        }
+        
+    }
+    
+    /**
+     * Implements {@code ?iso(timeZone)}.
+     */
+    static class iso_BI extends AbstractISOBI {
+        
+        class Result implements TemplateMethodModelEx {
+            private final Date date;
+            private final int dateType;
+            private final Environment env;
+            
+            Result(Date date, int dateType, Environment env) {
+                this.date = date;
+                this.dateType = dateType;
+                this.env = env;
+            }
+
+            @Override
+            public Object exec(List args) throws TemplateModelException {
+                checkMethodArgCount(args, 1);
+                
+                TemplateModel tzArgTM = (TemplateModel) args.get(0);
+                TimeZone tzArg; 
+                Object adaptedObj;
+                if (tzArgTM instanceof AdapterTemplateModel
+                        && (adaptedObj =
+                                ((AdapterTemplateModel) tzArgTM)
+                                .getAdaptedObject(TimeZone.class))
+                            instanceof TimeZone) {
+                    tzArg = (TimeZone) adaptedObj;                    
+                } else if (tzArgTM instanceof TemplateScalarModel) {
+                    String tzName = EvalUtil.modelToString((TemplateScalarModel) tzArgTM, null, null);
+                    try {
+                        tzArg = _DateUtil.getTimeZone(tzName);
+                    } catch (UnrecognizedTimeZoneException e) {
+                        throw new _TemplateModelException(
+                                "The time zone string specified for ?", key,
+                                "(...) is not recognized as a valid time zone name: ",
+                                new _DelayedJQuote(tzName));
+                    }
+                } else {
+                    throw MessageUtil.newMethodArgUnexpectedTypeException(
+                            "?" + key, 0, "string or java.util.TimeZone", tzArgTM);
+                }
+                
+                return new SimpleScalar(_DateUtil.dateToISO8601String(
+                        date,
+                        dateType != TemplateDateModel.TIME,
+                        dateType != TemplateDateModel.DATE,
+                        shouldShowOffset(date, dateType, env),
+                        accuracy,
+                        tzArg, 
+                        env.getISOBuiltInCalendarFactory()));
+            }
+            
+        }
+
+        iso_BI(Boolean showOffset, int accuracy) {
+            super(showOffset, accuracy);
+        }
+        
+        @Override
+        protected TemplateModel calculateResult(
+                Date date, int dateType, Environment env)
+        throws TemplateException {
+            checkDateTypeNotUnknown(dateType);
+            return new Result(date, dateType, env);
+        }
+        
+    }
+
+    /**
+     * Implements {@code ?iso_utc} and {@code ?iso_local} variants, but not
+     * {@code ?iso(timeZone)}.
+     */
+    static class iso_utc_or_local_BI extends AbstractISOBI {
+        
+        private final boolean useUTC;
+        
+        iso_utc_or_local_BI(Boolean showOffset, int accuracy, boolean useUTC) {
+            super(showOffset, accuracy);
+            this.useUTC = useUTC;
+        }
+
+        @Override
+        protected TemplateModel calculateResult(
+                Date date, int dateType, Environment env)
+        throws TemplateException {
+            checkDateTypeNotUnknown(dateType);
+            return new SimpleScalar(_DateUtil.dateToISO8601String(
+                    date,
+                    dateType != TemplateDateModel.TIME,
+                    dateType != TemplateDateModel.DATE,
+                    shouldShowOffset(date, dateType, env),
+                    accuracy,
+                    useUTC
+                            ? _DateUtil.UTC
+                            : env.shouldUseSQLDTTZ(date.getClass())
+                                    ? env.getSQLDateAndTimeTimeZone()
+                                    : env.getTimeZone(),
+                    env.getISOBuiltInCalendarFactory()));
+        }
+
+    }
+    
+    // Can't be instantiated
+    private BuiltInsForDates() { }
+
+    static abstract class AbstractISOBI extends BuiltInForDate {
+        protected final Boolean showOffset;
+        protected final int accuracy;
+    
+        protected AbstractISOBI(Boolean showOffset, int accuracy) {
+            this.showOffset = showOffset;
+            this.accuracy = accuracy;
+        }
+        
+        protected void checkDateTypeNotUnknown(int dateType)
+        throws TemplateException {
+            if (dateType == TemplateDateModel.UNKNOWN) {
+                throw new _MiscTemplateException(new _ErrorDescriptionBuilder(
+                            "The value of the following has unknown date type, but ?", key,
+                            " needs a value where it's known if it's a date (no time part), time, or date-time value:"                        
+                        ).blame(target).tip(MessageUtil.UNKNOWN_DATE_TYPE_ERROR_TIP));
+            }
+        }
+    
+        protected boolean shouldShowOffset(Date date, int dateType, Environment env) {
+            if (dateType == TemplateDateModel.DATE) {
+                return false;  // ISO 8061 doesn't allow zone for date-only values
+            } else if (showOffset != null) {
+                return showOffset.booleanValue();
+            } else {
+                // java.sql.Time values meant to carry calendar field values only, so we don't show offset for them.
+                return !(date instanceof java.sql.Time);
+            }
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java b/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
new file mode 100644
index 0000000..cbbffc9
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInsForExistenceHandling.java
@@ -0,0 +1,133 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+/**
+ * A holder for builtins that deal with null left-hand values.
+ */
+class BuiltInsForExistenceHandling {
+
+    // Can't be instantiated
+    private BuiltInsForExistenceHandling() { }
+
+    private static abstract class ExistenceBuiltIn extends ASTExpBuiltIn {
+    
+        protected TemplateModel evalMaybeNonexistentTarget(Environment env) throws TemplateException {
+            TemplateModel tm;
+            if (target instanceof ASTExpParenthesis) {
+                boolean lastFIRE = env.setFastInvalidReferenceExceptions(true);
+                try {
+                    tm = target.eval(env);
+                } catch (InvalidReferenceException ire) {
+                    tm = null;
+                } finally {
+                    env.setFastInvalidReferenceExceptions(lastFIRE);
+                }
+            } else {
+                tm = target.eval(env);
+            }
+            return tm;
+        }
+        
+    }
+    
+    static class defaultBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        
+        @Override
+        TemplateModel _eval(final Environment env) throws TemplateException {
+            TemplateModel model = evalMaybeNonexistentTarget(env);
+            return model == null ? FIRST_NON_NULL_METHOD : new ConstantMethod(model);
+        }
+
+        private static class ConstantMethod implements TemplateMethodModelEx {
+            private final TemplateModel constant;
+
+            ConstantMethod(TemplateModel constant) {
+                this.constant = constant;
+            }
+
+            @Override
+            public Object exec(List args) {
+                return constant;
+            }
+        }
+
+        /**
+         * A method that goes through the arguments one by one and returns
+         * the first one that is non-null. If all args are null, returns null.
+         */
+        private static final TemplateMethodModelEx FIRST_NON_NULL_METHOD =
+            new TemplateMethodModelEx() {
+                @Override
+                public Object exec(List args) throws TemplateModelException {
+                    int argCnt = args.size();
+                    if (argCnt == 0) throw MessageUtil.newArgCntError("?default", argCnt, 1, Integer.MAX_VALUE);
+                    for (int i = 0; i < argCnt; i++ ) {
+                        TemplateModel result = (TemplateModel) args.get(i);
+                        if (result != null) return result;
+                    }
+                    return null;
+                }
+            };
+    }
+    
+    static class existsBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            return evalMaybeNonexistentTarget(env) == null ? TemplateBooleanModel.FALSE : TemplateBooleanModel.TRUE;
+        }
+    
+        @Override
+        boolean evalToBoolean(Environment env) throws TemplateException {
+            return _eval(env) == TemplateBooleanModel.TRUE;
+        }
+    }
+
+    static class has_contentBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env) throws TemplateException {
+            return ASTExpression.isEmpty(evalMaybeNonexistentTarget(env))
+                    ? TemplateBooleanModel.FALSE
+                    : TemplateBooleanModel.TRUE;
+        }
+    
+        @Override
+        boolean evalToBoolean(Environment env) throws TemplateException {
+            return _eval(env) == TemplateBooleanModel.TRUE;
+        }
+    }
+
+    static class if_existsBI extends BuiltInsForExistenceHandling.ExistenceBuiltIn {
+        @Override
+        TemplateModel _eval(Environment env)
+                throws TemplateException {
+            TemplateModel model = evalMaybeNonexistentTarget(env);
+            return model == null ? TemplateModel.NOTHING : model;
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java b/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java
new file mode 100644
index 0000000..562a6d2
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/BuiltInsForHashes.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.CollectionAndSequence;
+
+/**
+ * A holder for builtins that operate exclusively on hash left-hand value.
+ */
+class BuiltInsForHashes {
+
+    static class keysBI extends BuiltInForHashEx {
+
+        @Override
+        TemplateModel calculateResult(TemplateHashModelEx hashExModel, Environment env)
+                throws TemplateModelException, InvalidReferenceException {
+            TemplateCollectionModel keys = hashExModel.keys();
+            if (keys == null) throw newNullPropertyException("keys", hashExModel, env);
+            return keys instanceof TemplateSequenceModel ? keys : new CollectionAndSequence(keys);
+        }
+        
+    }
+    
+    static class valuesBI extends BuiltInForHashEx {
+        @Override
+        TemplateModel calculateResult(TemplateHashModelEx hashExModel, Environment env)
+                throws TemplateModelException, InvalidReferenceException {
+            TemplateCollectionModel values = hashExModel.values();
+            if (values == null) throw newNullPropertyException("values", hashExModel, env);
+            return values instanceof TemplateSequenceModel ? values : new CollectionAndSequence(values);
+        }
+    }
+
+    // Can't be instantiated
+    private BuiltInsForHashes() { }
+    
+}


Mime
View raw message