blob: 886308cbc38834c32d667ba8e072fecbf06809df [file] [log] [blame]
/*
* 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 freemarker.core;
import java.util.Date;
import freemarker.ext.beans.BeanModel;
import freemarker.ext.beans._BeansAPI;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
/**
* Internally used static utilities for evaluation expressions.
*/
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, but may results in lower performance in classic-compatible mode
*/
static String modelToString(TemplateScalarModel model, Expression expr, Environment env)
throws TemplateModelException {
String value = model.getAsString();
if (value == null) {
if (env == null) env = Environment.getCurrentEnvironment();
if (env != null && env.isClassicCompatible()) {
return "";
} else {
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, Expression 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, Expression 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. */
static TemplateModelException newModelHasStoredNullException(
Class expected, TemplateModel model, Expression 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(
Expression leftExp,
int operator, String operatorString,
Expression rightExp,
Expression 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 Expression}-s are
* accessible, use {@link #compare(Expression, int, String, Expression, Expression, 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, Expression leftExp,
int operator, String operatorString,
TemplateModel rightValue, Expression rightExp,
Expression defaultBlamed, boolean quoteOperandsInErrors,
boolean typeMismatchMeansNotEqual,
boolean leftNullReturnsFalse, boolean rightNullReturnsFalse,
Environment env) throws TemplateException {
if (leftValue == null) {
if (env != null && env.isClassicCompatible()) {
leftValue = TemplateScalarModel.EMPTY_STRING;
} else {
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 (env != null && env.isClassicCompatible()) {
rightValue = TemplateScalarModel.EMPTY_STRING;
} else {
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()
: ArithmeticEngine.BIGDECIMAL_ENGINE);
try {
cmpResult = ae.compareNumbers(leftNum, rightNum);
} catch (RuntimeException e) {
throw new _MiscTemplateException(defaultBlamed, e, env, new Object[]
{ "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;
Expression 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 (env.isClassicCompatible()) {
String leftSting = leftExp.evalAndCoerceToPlainText(env);
String rightString = rightExp.evalAndCoerceToPlainText(env);
cmpResult = env.getCollator().compare(leftSting, rightString);
} 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 " }
: (Object) ""),
"is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(leftValue)), ".\n",
"Right hand operand ",
(quoteOperandsInErrors && rightExp != null
? new Object[] { "(", new _DelayedGetCanonicalForm(rightExp), ") value " }
: (Object) ""),
"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, Expression exp, 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, env);
}
}
/**
* Like {@link #coerceModelToStringOrMarkup(TemplateModel, Expression, 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, Expression 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, 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, Expression 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, env);
}
}
/**
* @param tm
* If {@code null} that's an exception, unless we are in classic compatible mode.
*
* @param supportsTOM
* Whether the caller {@code coerceModelTo...} method could handle a {@link TemplateMarkupOutputModel}.
*
* @return Never {@code null}
*/
private static String coerceModelToTextualCommon(
TemplateModel tm, Expression exp, String seqHint, boolean supportsTOM, Environment env)
throws TemplateModelException, InvalidReferenceException, TemplateException,
NonStringOrTemplateOutputException, NonStringException {
if (tm instanceof TemplateScalarModel) {
return modelToString((TemplateScalarModel) tm, exp, env);
} else if (tm == null) {
if (env.isClassicCompatible()) {
return "";
} else {
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) {
// This should be before TemplateScalarModel, but automatic boolean-to-string is only non-error since 2.3.20
// (and before that when classic_compatible was true), so to keep backward compatibility we couldn't insert
// this before TemplateScalarModel.
boolean booleanValue = ((TemplateBooleanModel) tm).getAsBoolean();
int compatMode = env.getClassicCompatibleAsInt();
if (compatMode == 0) {
return env.formatBoolean(booleanValue, false);
} else {
if (compatMode == 1) {
return booleanValue ? MiscUtil.C_TRUE : "";
} else if (compatMode == 2) {
if (tm instanceof BeanModel) {
// In 2.1, bean-wrapped booleans where strings, so that has overridden the boolean behavior:
return _BeansAPI.getAsClassicCompatibleString((BeanModel) tm);
} else {
return booleanValue ? MiscUtil.C_TRUE : "";
}
} else {
throw new BugException("Unsupported classic_compatible variation: " + compatMode);
}
}
} else {
if (env.isClassicCompatible() && tm instanceof BeanModel) {
return _BeansAPI.getAsClassicCompatibleString((BeanModel) tm);
}
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, Expression 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(TemplateObject 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 Expression) {
throw new _MiscTemplateException((Expression) 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, TemplateObject tObj) {
return env != null
? env.getArithmeticEngine()
: tObj.getTemplate().getParserConfiguration().getArithmeticEngine();
}
}