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

[29/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

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java
new file mode 100644
index 0000000..2d09062
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ErrorDescriptionBuilder.java
@@ -0,0 +1,356 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+
+import org.apache.freemarker.core.model.impl._MethodUtil;
+import org.apache.freemarker.core.util._ClassUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * Used internally only, might changes without notice!
+ * Packs a structured from of the error description from which the error message can be rendered on-demand.
+ * Note that this class isn't serializable, thus the containing exception should render the message before it's
+ * serialized.
+ */
+public class _ErrorDescriptionBuilder {
+
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+
+    private final String description;
+    private final Object[] descriptionParts;
+    private ASTExpression blamed;
+    private boolean showBlamer;
+    private Object/*String|Object[]*/ tip;
+    private Object[]/*String[]|Object[][]*/ tips;
+    private Template template;
+
+    public _ErrorDescriptionBuilder(String description) {
+        this.description = description;
+        descriptionParts = null;
+    }
+
+    /**
+     * @param descriptionParts These will be concatenated to a single {@link String} in {@link #toString()}.
+     *      {@link String} array items that look like FTL tag (must start with {@code "<"} and end with {@code ">"})
+     *      will be converted to the actual template syntax if {@link #blamed} or {@link #template} was set.
+     */
+    public _ErrorDescriptionBuilder(Object... descriptionParts) {
+        this.descriptionParts = descriptionParts;
+        description = null;
+    }
+
+    @Override
+    public String toString() {
+        return toString(null, true);
+    }
+    
+    public String toString(ASTElement parentElement, boolean showTips) {
+        if (blamed == null && tips == null && tip == null && descriptionParts == null) return description;
+
+        StringBuilder sb = new StringBuilder(200);
+        
+        if (parentElement != null && blamed != null && showBlamer) {
+            try {
+                Blaming blaming = findBlaming(parentElement, blamed, 0);
+                if (blaming != null) {
+                    sb.append("For ");
+                    String nss = blaming.blamer.getNodeTypeSymbol();
+                    char q = nss.indexOf('"') == -1 ? '\"' : '`';
+                    sb.append(q).append(nss).append(q);
+                    sb.append(" ").append(blaming.roleOfblamed).append(": ");
+                }
+            } catch (Throwable e) {
+                // Should not happen. But we rather give a not-so-good error message than replace it with another...
+                // So we ignore this.
+                LOG.error("Error when searching blamer for better error message.", e);
+            }
+        }
+        
+        if (description != null) {
+            sb.append(description);
+        } else {
+            appendParts(sb, descriptionParts);
+        }
+
+        String extraTip = null;
+        if (blamed != null) {
+            // Right-trim:
+            for (int idx = sb.length() - 1; idx >= 0 && Character.isWhitespace(sb.charAt(idx)); idx --) {
+                sb.deleteCharAt(idx);
+            }
+            
+            char lastChar = sb.length() > 0 ? (sb.charAt(sb.length() - 1)) : 0;
+            if (lastChar != 0) {
+                sb.append('\n');
+            }
+            if (lastChar != ':') {
+                sb.append("The blamed expression:\n");
+            }
+            
+            String[] lines = splitToLines(blamed.toString());
+            for (int i = 0; i < lines.length; i++) {
+                sb.append(i == 0 ? "==> " : "\n    ");
+                sb.append(lines[i]);
+            }
+            
+            sb.append("  [");
+            sb.append(blamed.getStartLocation());
+            sb.append(']');
+            
+            
+            if (containsSingleInterpolatoinLiteral(blamed, 0)) {
+                extraTip = "It has been noticed that you are using ${...} as the sole content of a quoted string. That "
+                        + "does nothing but forcably converts the value inside ${...} to string (as it inserts it into "
+                        + "the enclosing string). "
+                        + "If that's not what you meant, just remove the quotation marks, ${ and }; you don't need "
+                        + "them. If you indeed wanted to convert to string, use myExpression?string instead.";
+            }
+        }
+        
+        if (showTips) {
+            int allTipsLen = (tips != null ? tips.length : 0) + (tip != null ? 1 : 0) + (extraTip != null ? 1 : 0);
+            Object[] allTips;
+            if (tips != null && allTipsLen == tips.length) {
+                allTips = tips;
+            } else {
+                allTips = new Object[allTipsLen];
+                int dst = 0;
+                if (tip != null) allTips[dst++] = tip; 
+                if (tips != null) {
+                    for (Object t : tips) {
+                        allTips[dst++] = t;
+                    }
+                }
+                if (extraTip != null) allTips[dst++] = extraTip; 
+            }
+            if (allTips != null && allTips.length > 0) {
+                sb.append("\n\n");
+                for (int i = 0; i < allTips.length; i++) {
+                    if (i != 0) sb.append('\n');
+                    sb.append(MessageUtil.ERROR_MESSAGE_HR).append('\n');
+                    sb.append("Tip: ");
+                    Object tip = allTips[i];
+                    if (!(tip instanceof Object[])) {
+                        sb.append(allTips[i]);
+                    } else {
+                        appendParts(sb, (Object[]) tip);
+                    }
+                }
+                sb.append('\n').append(MessageUtil.ERROR_MESSAGE_HR);
+            }
+        }
+        
+        return sb.toString();
+    }
+
+    private boolean containsSingleInterpolatoinLiteral(ASTExpression exp, int recursionDepth) {
+        if (exp == null) return false;
+        
+        // Just in case a loop ever gets into the AST somehow, try not fill the stack and such: 
+        if (recursionDepth > 20) return false;
+        
+        if (exp instanceof ASTExpStringLiteral && ((ASTExpStringLiteral) exp).isSingleInterpolationLiteral()) return true;
+        
+        int paramCnt = exp.getParameterCount();
+        for (int i = 0; i < paramCnt; i++) {
+            Object paramValue = exp.getParameterValue(i);
+            if (paramValue instanceof ASTExpression) {
+                boolean result = containsSingleInterpolatoinLiteral((ASTExpression) paramValue, recursionDepth + 1);
+                if (result) return true;
+            }
+        }
+        
+        return false;
+    }
+
+    private Blaming findBlaming(ASTNode parent, ASTExpression blamed, int recursionDepth) {
+        // Just in case a loop ever gets into the AST somehow, try not fill the stack and such: 
+        if (recursionDepth > 50) return null;
+        
+        int paramCnt = parent.getParameterCount();
+        for (int i = 0; i < paramCnt; i++) {
+            Object paramValue = parent.getParameterValue(i);
+            if (paramValue == blamed) {
+                Blaming blaming = new Blaming();
+                blaming.blamer = parent;
+                blaming.roleOfblamed = parent.getParameterRole(i);
+                return blaming;
+            } else if (paramValue instanceof ASTNode) {
+                Blaming blaming = findBlaming((ASTNode) paramValue, blamed, recursionDepth + 1);
+                if (blaming != null) return blaming;
+            }
+        }
+        return null;
+    }
+
+    private void appendParts(StringBuilder sb, Object[] parts) {
+        Template template = this.template != null ? this.template : (blamed != null ? blamed.getTemplate() : null);
+        for (Object partObj : parts) {
+            if (partObj instanceof Object[]) {
+                appendParts(sb, (Object[]) partObj);
+            } else {
+                String partStr;
+                partStr = tryToString(partObj);
+                if (partStr == null) {
+                    partStr = "null";
+                }
+
+                if (template != null) {
+                    if (partStr.length() > 4
+                            && partStr.charAt(0) == '<'
+                            && (
+                            (partStr.charAt(1) == '#' || partStr.charAt(1) == '@')
+                                    || (partStr.charAt(1) == '/') && (partStr.charAt(2) == '#' || partStr.charAt(2) == '@')
+                    )
+                            && partStr.charAt(partStr.length() - 1) == '>') {
+                        if (template.getActualTagSyntax() == ParsingConfiguration.SQUARE_BRACKET_TAG_SYNTAX) {
+                            sb.append('[');
+                            sb.append(partStr.substring(1, partStr.length() - 1));
+                            sb.append(']');
+                        } else {
+                            sb.append(partStr);
+                        }
+                    } else {
+                        sb.append(partStr);
+                    }
+                } else {
+                    sb.append(partStr);
+                }
+            }
+        }
+    }
+
+    /**
+     * A twist on Java's toString that generates more appropriate results for generating error messages.
+     */
+    public static String toString(Object partObj) {
+        return toString(partObj, false);
+    }
+
+    public static String tryToString(Object partObj) {
+        return toString(partObj, true);
+    }
+    
+    private static String toString(Object partObj, boolean suppressToStringException) {
+        final String partStr;
+        if (partObj == null) {
+            return null;
+        } else if (partObj instanceof Class) {
+            partStr = _ClassUtil.getShortClassName((Class) partObj);
+        } else if (partObj instanceof Method || partObj instanceof Constructor) {
+            partStr = _MethodUtil.toString((Member) partObj);
+        } else {
+            partStr = suppressToStringException ? _StringUtil.tryToString(partObj) : partObj.toString();
+        }
+        return partStr;
+    }
+
+    private String[] splitToLines(String s) {
+        s = _StringUtil.replace(s, "\r\n", "\n");
+        s = _StringUtil.replace(s, "\r", "\n");
+        return _StringUtil.split(s, '\n');
+    }
+    
+    /**
+     * Needed for description <em>parts</em> that look like an FTL tag to be converted, if there's no {@link #blamed}.
+     */
+    public _ErrorDescriptionBuilder template(Template template) {
+        this.template = template;
+        return this;
+    }
+
+    public _ErrorDescriptionBuilder blame(ASTExpression blamed) {
+        this.blamed = blamed;
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder showBlamer(boolean showBlamer) {
+        this.showBlamer = showBlamer;
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder tip(String tip) {
+        tip((Object) tip);
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder tip(Object... tip) {
+        tip((Object) tip);
+        return this;
+    }
+    
+    private _ErrorDescriptionBuilder tip(Object tip) {
+        if (tip == null) {
+            return this;
+        }
+        
+        if (this.tip == null) {
+            this.tip = tip;
+        } else {
+            if (tips == null) {
+                tips = new Object[] { tip };
+            } else {
+                final int origTipsLen = tips.length;
+                
+                Object[] newTips = new Object[origTipsLen + 1];
+                for (int i = 0; i < origTipsLen; i++) {
+                    newTips[i] = tips[i];
+                }
+                newTips[origTipsLen] = tip;
+                tips = newTips;
+            }
+        }
+        return this;
+    }
+    
+    public _ErrorDescriptionBuilder tips(Object... tips) {
+        if (tips == null || tips.length == 0) {
+            return this;
+        }
+        
+        if (this.tips == null) {
+            this.tips = tips;
+        } else {
+            final int origTipsLen = this.tips.length;
+            final int additionalTipsLen = tips.length;
+            
+            Object[] newTips = new Object[origTipsLen + additionalTipsLen];
+            for (int i = 0; i < origTipsLen; i++) {
+                newTips[i] = this.tips[i];
+            }
+            for (int i = 0; i < additionalTipsLen; i++) {
+                newTips[origTipsLen + i] = tips[i];
+            }
+            this.tips = newTips;
+        }
+        return this;
+    }
+    
+    private static class Blaming {
+        ASTNode blamer;
+        ParameterRole roleOfblamed;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java
new file mode 100644
index 0000000..727085f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_EvalUtil.java
@@ -0,0 +1,545 @@
+/*
+ * 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.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormat;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Internally used static utilities for evaluation expressions.
+ */
+public class _EvalUtil {
+    static final int CMP_OP_EQUALS = 1;
+    static final int CMP_OP_NOT_EQUALS = 2;
+    static final int CMP_OP_LESS_THAN = 3;
+    static final int CMP_OP_GREATER_THAN = 4;
+    static final int CMP_OP_LESS_THAN_EQUALS = 5;
+    static final int CMP_OP_GREATER_THAN_EQUALS = 6;
+    // If you add a new operator here, update the "compare" and "cmpOpToString" methods!
+    
+    // Prevents instantination.
+    private _EvalUtil() { }
+    
+    /**
+     * @param expr {@code null} is allowed, but may results in less helpful error messages
+     * @param env {@code null} is allowed
+     */
+    static String modelToString(TemplateScalarModel model, ASTExpression expr, Environment env)
+    throws TemplateModelException {
+        String value = model.getAsString();
+        if (value == null) {
+            throw newModelHasStoredNullException(String.class, model, expr);
+        }
+        return value;
+    }
+    
+    /**
+     * @param expr {@code null} is allowed, but may results in less helpful error messages
+     */
+    static Number modelToNumber(TemplateNumberModel model, ASTExpression expr)
+        throws TemplateModelException {
+        Number value = model.getAsNumber();
+        if (value == null) throw newModelHasStoredNullException(Number.class, model, expr);
+        return value;
+    }
+
+    /**
+     * @param expr {@code null} is allowed, but may results in less helpful error messages
+     */
+    static Date modelToDate(TemplateDateModel model, ASTExpression expr)
+        throws TemplateModelException {
+        Date value = model.getAsDate();
+        if (value == null) throw newModelHasStoredNullException(Date.class, model, expr);
+        return value;
+    }
+    
+    /** Signals the buggy case where we have a non-null model, but it wraps a null. */
+    public static TemplateModelException newModelHasStoredNullException(
+            Class expected, TemplateModel model, ASTExpression expr) {
+        return new _TemplateModelException(expr,
+                _TemplateModelException.modelHasStoredNullDescription(expected, model));
+    }
+
+    /**
+     * Compares two expressions according the rules of the FTL comparator operators.
+     * 
+     * @param leftExp not {@code null}
+     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
+     * @param operatorString can be null {@code null}; the actual operator used, used for more accurate error message.
+     * @param rightExp not {@code null}
+     * @param env {@code null} is tolerated, but should be avoided
+     */
+    static boolean compare(
+            ASTExpression leftExp,
+            int operator, String  operatorString,
+            ASTExpression rightExp,
+            ASTExpression defaultBlamed,
+            Environment env) throws TemplateException {
+        TemplateModel ltm = leftExp.eval(env);
+        TemplateModel rtm = rightExp.eval(env);
+        return compare(
+                ltm, leftExp,
+                operator, operatorString,
+                rtm, rightExp,
+                defaultBlamed, false,
+                false, false, false,
+                env);
+    }
+    
+    /**
+     * Compares values according the rules of the FTL comparator operators; if the {@link ASTExpression}-s are
+     * accessible, use {@link #compare(ASTExpression, int, String, ASTExpression, ASTExpression, Environment)} instead,
+     * as that gives better error messages.
+     * 
+     * @param leftValue maybe {@code null}, which will usually cause the appropriate {@link TemplateException}. 
+     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
+     * @param rightValue maybe {@code null}, which will usually cause the appropriate {@link TemplateException}.
+     * @param env {@code null} is tolerated, but should be avoided
+     */
+    static boolean compare(
+            TemplateModel leftValue, int operator, TemplateModel rightValue,
+            Environment env) throws TemplateException {
+        return compare(
+                leftValue, null,
+                operator, null,
+                rightValue, null,
+                null, false,
+                false, false, false,
+                env);
+    }
+
+    /**
+     * Same as {@link #compare(TemplateModel, int, TemplateModel, Environment)}, but if the two types are incompatible,
+     *     they are treated as non-equal instead of throwing an exception. Comparing dates of different types will
+     *     still throw an exception, however.
+     */
+    static boolean compareLenient(
+            TemplateModel leftValue, int operator, TemplateModel rightValue,
+            Environment env) throws TemplateException {
+        return compare(
+                leftValue, null,
+                operator, null,
+                rightValue, null,
+                null, false,
+                true, false, false,
+                env);
+    }
+    
+    private static final String VALUE_OF_THE_COMPARISON_IS_UNKNOWN_DATE_LIKE
+            = "value of the comparison is a date-like value where "
+              + "it's not known if it's a date (no time part), time, or date-time, "
+              + "and thus can't be used in a comparison.";
+    
+    /**
+     * @param leftExp {@code null} is allowed, but may results in less helpful error messages
+     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
+     * @param operatorString can be null {@code null}; the actual operator used, used for more accurate error message.
+     * @param rightExp {@code null} is allowed, but may results in less helpful error messages
+     * @param defaultBlamed {@code null} allowed; the expression to which the error will point to if something goes
+     *        wrong that is not specific to the left or right side expression, or if that expression is {@code null}.
+     * @param typeMismatchMeansNotEqual If the two types are incompatible, they are treated as non-equal instead
+     *     of throwing an exception. Comparing dates of different types will still throw an exception, however. 
+     * @param leftNullReturnsFalse if {@code true}, a {@code null} left value will not cause exception, but make the
+     *     expression {@code false}.  
+     * @param rightNullReturnsFalse if {@code true}, a {@code null} right value will not cause exception, but make the
+     *     expression {@code false}.  
+     */
+    static boolean compare(
+            TemplateModel leftValue, ASTExpression leftExp,
+            int operator, String operatorString,
+            TemplateModel rightValue, ASTExpression rightExp,
+            ASTExpression defaultBlamed, boolean quoteOperandsInErrors,
+            boolean typeMismatchMeansNotEqual,
+            boolean leftNullReturnsFalse, boolean rightNullReturnsFalse,
+            Environment env) throws TemplateException {
+        if (leftValue == null) {
+            if (leftNullReturnsFalse) { 
+                return false;
+            } else {
+                if (leftExp != null) {
+                    throw InvalidReferenceException.getInstance(leftExp, env);
+                } else {
+                    throw new _MiscTemplateException(defaultBlamed, env, 
+                                "The left operand of the comparison was undefined or null.");
+                }
+            }
+        }
+
+        if (rightValue == null) {
+            if (rightNullReturnsFalse) { 
+                return false;
+            } else {
+                if (rightExp != null) {
+                    throw InvalidReferenceException.getInstance(rightExp, env);
+                } else {
+                    throw new _MiscTemplateException(defaultBlamed, env,
+                                "The right operand of the comparison was undefined or null.");
+                }
+            }
+        }
+
+        final int cmpResult;
+        if (leftValue instanceof TemplateNumberModel && rightValue instanceof TemplateNumberModel) {
+            Number leftNum = _EvalUtil.modelToNumber((TemplateNumberModel) leftValue, leftExp);
+            Number rightNum = _EvalUtil.modelToNumber((TemplateNumberModel) rightValue, rightExp);
+            ArithmeticEngine ae =
+                    env != null
+                        ? env.getArithmeticEngine()
+                        : (leftExp != null
+                            ? leftExp.getTemplate().getArithmeticEngine()
+                            : BigDecimalArithmeticEngine.INSTANCE);
+            try {
+                cmpResult = ae.compareNumbers(leftNum, rightNum);
+            } catch (RuntimeException e) {
+                throw new _MiscTemplateException(defaultBlamed, e, env,
+                        "Unexpected error while comparing two numbers: ", e);
+            }
+        } else if (leftValue instanceof TemplateDateModel && rightValue instanceof TemplateDateModel) {
+            TemplateDateModel leftDateModel = (TemplateDateModel) leftValue;
+            TemplateDateModel rightDateModel = (TemplateDateModel) rightValue;
+            
+            int leftDateType = leftDateModel.getDateType();
+            int rightDateType = rightDateModel.getDateType();
+            
+            if (leftDateType == TemplateDateModel.UNKNOWN || rightDateType == TemplateDateModel.UNKNOWN) {
+                String sideName;
+                ASTExpression sideExp;
+                if (leftDateType == TemplateDateModel.UNKNOWN) {
+                    sideName = "left";
+                    sideExp = leftExp;
+                } else {
+                    sideName = "right";
+                    sideExp = rightExp;
+                }
+                
+                throw new _MiscTemplateException(sideExp != null ? sideExp : defaultBlamed, env,
+                        "The ", sideName, " ", VALUE_OF_THE_COMPARISON_IS_UNKNOWN_DATE_LIKE);
+            }
+            
+            if (leftDateType != rightDateType) {
+                throw new _MiscTemplateException(defaultBlamed, env,
+                        "Can't compare dates of different types. Left date type is ",
+                        TemplateDateModel.TYPE_NAMES.get(leftDateType), ", right date type is ",
+                        TemplateDateModel.TYPE_NAMES.get(rightDateType), ".");
+            }
+
+            Date leftDate = _EvalUtil.modelToDate(leftDateModel, leftExp);
+            Date rightDate = _EvalUtil.modelToDate(rightDateModel, rightExp);
+            cmpResult = leftDate.compareTo(rightDate);
+        } else if (leftValue instanceof TemplateScalarModel && rightValue instanceof TemplateScalarModel) {
+            if (operator != CMP_OP_EQUALS && operator != CMP_OP_NOT_EQUALS) {
+                throw new _MiscTemplateException(defaultBlamed, env,
+                        "Can't use operator \"", cmpOpToString(operator, operatorString), "\" on string values.");
+            }
+            String leftString = _EvalUtil.modelToString((TemplateScalarModel) leftValue, leftExp, env);
+            String rightString = _EvalUtil.modelToString((TemplateScalarModel) rightValue, rightExp, env);
+            // FIXME NBC: Don't use the Collator here. That's locale-specific, but ==/!= should not be.
+            cmpResult = env.getCollator().compare(leftString, rightString);
+        } else if (leftValue instanceof TemplateBooleanModel && rightValue instanceof TemplateBooleanModel) {
+            if (operator != CMP_OP_EQUALS && operator != CMP_OP_NOT_EQUALS) {
+                throw new _MiscTemplateException(defaultBlamed, env,
+                        "Can't use operator \"", cmpOpToString(operator, operatorString), "\" on boolean values.");
+            }
+            boolean leftBool = ((TemplateBooleanModel) leftValue).getAsBoolean();
+            boolean rightBool = ((TemplateBooleanModel) rightValue).getAsBoolean();
+            cmpResult = (leftBool ? 1 : 0) - (rightBool ? 1 : 0);
+        } else {
+            if (typeMismatchMeansNotEqual) {
+                if (operator == CMP_OP_EQUALS) {
+                    return false;
+                } else if (operator == CMP_OP_NOT_EQUALS) {
+                    return true;
+                }
+                // Falls through
+            }
+            throw new _MiscTemplateException(defaultBlamed, env,
+                    "Can't compare values of these types. ",
+                    "Allowed comparisons are between two numbers, two strings, two dates, or two booleans.\n",
+                    "Left hand operand ",
+                    (quoteOperandsInErrors && leftExp != null
+                            ? new Object[] { "(", new _DelayedGetCanonicalForm(leftExp), ") value " }
+                            : ""),
+                    "is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(leftValue)), ".\n",
+                    "Right hand operand ",
+                    (quoteOperandsInErrors && rightExp != null
+                            ? new Object[] { "(", new _DelayedGetCanonicalForm(rightExp), ") value " }
+                            : ""),
+                    "is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(rightValue)),
+                    ".");
+        }
+
+        switch (operator) {
+            case CMP_OP_EQUALS: return cmpResult == 0;
+            case CMP_OP_NOT_EQUALS: return cmpResult != 0;
+            case CMP_OP_LESS_THAN: return cmpResult < 0;
+            case CMP_OP_GREATER_THAN: return cmpResult > 0;
+            case CMP_OP_LESS_THAN_EQUALS: return cmpResult <= 0;
+            case CMP_OP_GREATER_THAN_EQUALS: return cmpResult >= 0;
+            default: throw new BugException("Unsupported comparator operator code: " + operator);
+        }
+    }
+
+    private static String cmpOpToString(int operator, String operatorString) {
+        if (operatorString != null) {
+            return operatorString;
+        } else {
+            switch (operator) {
+                case CMP_OP_EQUALS: return "equals";
+                case CMP_OP_NOT_EQUALS: return "not-equals";
+                case CMP_OP_LESS_THAN: return "less-than";
+                case CMP_OP_GREATER_THAN: return "greater-than";
+                case CMP_OP_LESS_THAN_EQUALS: return "less-than-equals";
+                case CMP_OP_GREATER_THAN_EQUALS: return "greater-than-equals";
+                default: return "???";
+            }
+        }
+    }
+
+    /**
+     * Converts a value to plain text {@link String}, or a {@link TemplateMarkupOutputModel} if that's what the
+     * {@link TemplateValueFormat} involved produces.
+     * 
+     * @param seqTip
+     *            Tip to display if the value type is not coercable, but it's sequence or collection.
+     * 
+     * @return Never {@code null}
+     */
+    static Object coerceModelToStringOrMarkup(TemplateModel tm, ASTExpression exp, String seqTip, Environment env)
+            throws TemplateException {
+        return coerceModelToStringOrMarkup(tm, exp, false, seqTip, env);
+    }
+    
+    /**
+     * @return {@code null} if the {@code returnNullOnNonCoercableType} parameter is {@code true}, and the coercion is
+     *         not possible, because of the type is not right for it.
+     * 
+     * @see #coerceModelToStringOrMarkup(TemplateModel, ASTExpression, String, Environment)
+     */
+    static Object coerceModelToStringOrMarkup(
+            TemplateModel tm, ASTExpression exp, boolean returnNullOnNonCoercableType, String seqTip, Environment env)
+            throws TemplateException {
+        if (tm instanceof TemplateNumberModel) {
+            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
+            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
+            try {
+                return assertFormatResultNotNull(format.format(tnm));
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateDateModel) {
+            TemplateDateModel tdm = (TemplateDateModel) tm;
+            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
+            try {
+                return assertFormatResultNotNull(format.format(tdm));
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateMarkupOutputModel) {
+            return tm;
+        } else { 
+            return coerceModelToTextualCommon(tm, exp, seqTip, true, returnNullOnNonCoercableType, env);
+        }
+    }
+
+    /**
+     * Like {@link #coerceModelToStringOrMarkup(TemplateModel, ASTExpression, String, Environment)}, but gives error
+     * if the result is markup. This is what you normally use where markup results can't be used.
+     *
+     * @param seqTip
+     *            Tip to display if the value type is not coercable, but it's sequence or collection.
+     * 
+     * @return Never {@code null}
+     */
+    static String coerceModelToStringOrUnsupportedMarkup(
+            TemplateModel tm, ASTExpression exp, String seqTip, Environment env)
+            throws TemplateException {
+        if (tm instanceof TemplateNumberModel) {
+            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
+            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
+            try {
+                return ensureFormatResultString(format.format(tnm), exp, env);
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateDateModel) {
+            TemplateDateModel tdm = (TemplateDateModel) tm;
+            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
+            try {
+                return ensureFormatResultString(format.format(tdm), exp, env);
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
+            }
+        } else { 
+            return coerceModelToTextualCommon(tm, exp, seqTip, false, false, env);
+        }
+    }
+
+    /**
+     * Converts a value to plain text {@link String}, even if the {@link TemplateValueFormat} involved normally produces
+     * markup. This should be used rarely, where the user clearly intend to use the plain text variant of the format.
+     * 
+     * @param seqTip
+     *            Tip to display if the value type is not coercable, but it's sequence or collection.
+     * 
+     * @return Never {@code null}
+     */
+    static String coerceModelToPlainText(TemplateModel tm, ASTExpression exp, String seqTip,
+            Environment env) throws TemplateException {
+        if (tm instanceof TemplateNumberModel) {
+            return assertFormatResultNotNull(env.formatNumberToPlainText((TemplateNumberModel) tm, exp, false));
+        } else if (tm instanceof TemplateDateModel) {
+            return assertFormatResultNotNull(env.formatDateToPlainText((TemplateDateModel) tm, exp, false));
+        } else {
+            return coerceModelToTextualCommon(tm, exp, seqTip, false, false, env);
+        }
+    }
+
+    /**
+     * @param tm
+     *            If {@code null} that's an exception
+     * 
+     * @param supportsTOM
+     *            Whether the caller {@code coerceModelTo...} method could handle a {@link TemplateMarkupOutputModel}.
+     *            
+     * @return Never {@code null}
+     */
+    private static String coerceModelToTextualCommon(
+            TemplateModel tm, ASTExpression exp, String seqHint, boolean supportsTOM, boolean returnNullOnNonCoercableType,
+            Environment env)
+            throws TemplateException {
+        if (tm instanceof TemplateScalarModel) {
+            return modelToString((TemplateScalarModel) tm, exp, env);
+        } else if (tm == null) {
+            if (exp != null) {
+                throw InvalidReferenceException.getInstance(exp, env);
+            } else {
+                throw new InvalidReferenceException(
+                        "Null/missing value (no more informatoin avilable)",
+                        env);
+            }
+        } else if (tm instanceof TemplateBooleanModel) {
+            // [FM3] This should be before TemplateScalarModel, but automatic boolean-to-string is only non-error since
+            // 2.3.20, so to keep backward compatibility we couldn't insert this before TemplateScalarModel.
+            boolean booleanValue = ((TemplateBooleanModel) tm).getAsBoolean();
+            return env.formatBoolean(booleanValue, false);
+        } else {
+            if (returnNullOnNonCoercableType) {
+                return null;
+            }
+            if (seqHint != null && (tm instanceof TemplateSequenceModel || tm instanceof TemplateCollectionModel)) {
+                if (supportsTOM) {
+                    throw new NonStringOrTemplateOutputException(exp, tm, seqHint, env);
+                } else {
+                    throw new NonStringException(exp, tm, seqHint, env);
+                }
+            } else {
+                if (supportsTOM) {
+                    throw new NonStringOrTemplateOutputException(exp, tm, env);
+                } else {
+                    throw new NonStringException(exp, tm, env);
+                }
+            }
+        }
+    }
+
+    private static String ensureFormatResultString(Object formatResult, ASTExpression exp, Environment env)
+            throws NonStringException {
+        if (formatResult instanceof String) { 
+            return (String) formatResult;
+        }
+        
+        assertFormatResultNotNull(formatResult);
+        
+        TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) formatResult;
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "Value was formatted to convert it to string, but the result was markup of ouput format ",
+                new _DelayedJQuote(mo.getOutputFormat()), ".")
+                .tip("Use value?string to force formatting to plain text.")
+                .blame(exp);
+        throw new NonStringException(null, desc);
+    }
+
+    static String assertFormatResultNotNull(String r) {
+        if (r != null) {
+            return r;
+        }
+        throw new NullPointerException("TemplateValueFormatter result can't be null");
+    }
+
+    static Object assertFormatResultNotNull(Object r) {
+        if (r != null) {
+            return r;
+        }
+        throw new NullPointerException("TemplateValueFormatter result can't be null");
+    }
+
+    static TemplateMarkupOutputModel concatMarkupOutputs(ASTNode parent, TemplateMarkupOutputModel leftMO,
+            TemplateMarkupOutputModel rightMO) throws TemplateException {
+        MarkupOutputFormat leftOF = leftMO.getOutputFormat();
+        MarkupOutputFormat rightOF = rightMO.getOutputFormat();
+        if (rightOF != leftOF) {
+            String rightPT;
+            String leftPT;
+            if ((rightPT = rightOF.getSourcePlainText(rightMO)) != null) {
+                return leftOF.concat(leftMO, leftOF.fromPlainTextByEscaping(rightPT));
+            } else if ((leftPT = leftOF.getSourcePlainText(leftMO)) != null) {
+                return rightOF.concat(rightOF.fromPlainTextByEscaping(leftPT), rightMO);
+            } else {
+                Object[] message = { "Concatenation left hand operand is in ", new _DelayedToString(leftOF),
+                        " format, while the right hand operand is in ", new _DelayedToString(rightOF),
+                        ". Conversion to common format wasn't possible." };
+                if (parent instanceof ASTExpression) {
+                    throw new _MiscTemplateException((ASTExpression) parent, message);
+                } else {
+                    throw new _MiscTemplateException(message);
+                }
+            }
+        } else {
+            return leftOF.concat(leftMO, rightMO);
+        }
+    }
+
+    /**
+     * Returns an {@link ArithmeticEngine} even if {@code env} is {@code null}, because we are in parsing phase.
+     */
+    static ArithmeticEngine getArithmeticEngine(Environment env, ASTNode tObj) {
+        return env != null
+                ? env.getArithmeticEngine()
+                : tObj.getTemplate().getParsingConfiguration().getArithmeticEngine();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java
new file mode 100644
index 0000000..037ef9a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8.java
@@ -0,0 +1,34 @@
+/*
+ * 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.lang.reflect.Method;
+
+/**
+ * Used internally only, might changes without notice!
+ * Used for accessing functionality that's only present in Java 6 or later.
+ */
+public interface _Java8 {
+
+    /**
+     * Returns if it's a Java 8 "default method".
+     */
+    boolean isDefaultMethod(Method method);
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.java
new file mode 100644
index 0000000..527a180
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_Java8Impl.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;
+
+import java.lang.reflect.Method;
+
+/**
+ * Used internally only, might changes without notice!
+ * Used for accessing functionality that's only present in Java 8 or later.
+ */
+public final class _Java8Impl implements _Java8 {
+    
+    public static final _Java8 INSTANCE = new _Java8Impl();
+
+    private final Method isDefaultMethodMethod;
+
+    private _Java8Impl() {
+        // Not meant to be instantiated
+        try {
+            isDefaultMethodMethod = Method.class.getMethod("isDefault");
+        } catch (NoSuchMethodException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public boolean isDefaultMethod(Method method) {
+        try {
+            // In FM2 this was compiled against Java 8 and this was a direct call. Doing that in a way that fits
+            // IDE-s would be an overkill (would need introducing two new modules), so we fell back to reflection.
+            return ((Boolean) isDefaultMethodMethod.invoke(method)).booleanValue();
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to call Method.isDefaultMethod()", e);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java
new file mode 100644
index 0000000..1c8abfe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_MiscTemplateException.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+/**
+ * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
+ * {@link TemplateException}-s that don't fit into any category that warrant its own class. In fact, this was added
+ * because the API of {@link TemplateException} is too simple for the purposes of the core, but it can't be
+ * extended without breaking backward compatibility and exposing internals.  
+ */
+public class _MiscTemplateException extends TemplateException {
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(String description) {
+        super(description, null);
+    }
+
+    public _MiscTemplateException(Environment env, String description) {
+        super(description, env);
+    }
+    
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+
+    public _MiscTemplateException(Throwable cause, String description) {
+        this(cause, null, description);
+    }
+
+    public _MiscTemplateException(Throwable cause, Environment env) {
+        this(cause, env, (String) null);
+    }
+
+    public _MiscTemplateException(Throwable cause) {
+        this(cause, null, (String) null);
+    }
+    
+    public _MiscTemplateException(Throwable cause, Environment env, String description) {
+        super(description, cause, env);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(_ErrorDescriptionBuilder description) {
+        this(null, description);
+    }
+
+    public _MiscTemplateException(Environment env, _ErrorDescriptionBuilder description) {
+        this(null, env, description);
+    }
+
+    public _MiscTemplateException(Throwable cause, Environment env, _ErrorDescriptionBuilder description) {
+        super(cause, env, null, description);
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(Object... descriptionParts) {
+        this((Environment) null, descriptionParts);
+    }
+
+    public _MiscTemplateException(Environment env, Object... descriptionParts) {
+        this((Throwable) null, env, descriptionParts);
+    }
+
+    public _MiscTemplateException(Throwable cause, Object... descriptionParts) {
+        this(cause, null, descriptionParts);
+    }
+
+    public _MiscTemplateException(Throwable cause, Environment env, Object... descriptionParts) {
+        super(cause, env, null, new _ErrorDescriptionBuilder(descriptionParts));
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(ASTExpression blamed, Object... descriptionParts) {
+        this(blamed, null, descriptionParts);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Environment env, Object... descriptionParts) {
+        this(blamed, null, env, descriptionParts);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Throwable cause, Environment env, Object... descriptionParts) {
+        super(cause, env, blamed, new _ErrorDescriptionBuilder(descriptionParts).blame(blamed));
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Permutation group:
+    
+    public _MiscTemplateException(ASTExpression blamed, String description) {
+        this(blamed, null, description);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Environment env, String description) {
+        this(blamed, null, env, description);
+    }
+
+    public _MiscTemplateException(ASTExpression blamed, Throwable cause, Environment env, String description) {
+        super(cause, env, blamed, new _ErrorDescriptionBuilder(description).blame(blamed));
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java
new file mode 100644
index 0000000..620399a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluationException.java
@@ -0,0 +1,46 @@
+/*
+ * 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.util._StringUtil;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Thrown by {@link _ObjectBuilderSettingEvaluator}.
+ */
+public class _ObjectBuilderSettingEvaluationException extends Exception {
+    
+    public _ObjectBuilderSettingEvaluationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public _ObjectBuilderSettingEvaluationException(String message) {
+        super(message);
+    }
+
+    public _ObjectBuilderSettingEvaluationException(String expected, String src, int location) {
+        super("Expression syntax error: Expected a(n) " + expected + ", but "
+                + (location < src.length()
+                        ? "found character " + _StringUtil.jQuote("" + src.charAt(location)) + " at position "
+                            + (location + 1) + "."
+                        : "the end of the parsed string was reached.") );
+    }
+    
+}