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.") ); + } + +}
