http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/EvalUtil.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/EvalUtil.java b/src/main/java/org/apache/freemarker/core/EvalUtil.java new file mode 100644 index 0000000..6889232 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/EvalUtil.java @@ -0,0 +1,540 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.util.Date; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateDateModel; +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.util.BugException; + +/** + * 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 + */ + 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. */ + 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() + : 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; + 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} + * @throws TemplateException + */ + 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 TemplateModelException, InvalidReferenceException, TemplateException, + NonStringOrTemplateOutputException, NonStringException { + 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().getParserConfiguration().getArithmeticEngine(); + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ExtendedDecimalFormatParser.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ExtendedDecimalFormatParser.java b/src/main/java/org/apache/freemarker/core/ExtendedDecimalFormatParser.java new file mode 100644 index 0000000..62651b6 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ExtendedDecimalFormatParser.java @@ -0,0 +1,525 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.freemarker.core; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Currency; +import java.util.HashMap; +import java.util.Locale; +import java.util.Set; + +import org.apache.freemarker.core.util._StringUtil; + +class ExtendedDecimalFormatParser { + + private static final String PARAM_ROUNDING_MODE = "roundingMode"; + private static final String PARAM_MULTIPIER = "multipier"; + private static final String PARAM_DECIMAL_SEPARATOR = "decimalSeparator"; + private static final String PARAM_MONETARY_DECIMAL_SEPARATOR = "monetaryDecimalSeparator"; + private static final String PARAM_GROUP_SEPARATOR = "groupingSeparator"; + private static final String PARAM_EXPONENT_SEPARATOR = "exponentSeparator"; + private static final String PARAM_MINUS_SIGN = "minusSign"; + private static final String PARAM_INFINITY = "infinity"; + private static final String PARAM_NAN = "nan"; + private static final String PARAM_PERCENT = "percent"; + private static final String PARAM_PER_MILL = "perMill"; + private static final String PARAM_ZERO_DIGIT = "zeroDigit"; + private static final String PARAM_CURRENCY_CODE = "currencyCode"; + private static final String PARAM_CURRENCY_SYMBOL = "currencySymbol"; + + private static final String PARAM_VALUE_RND_UP = "up"; + private static final String PARAM_VALUE_RND_DOWN = "down"; + private static final String PARAM_VALUE_RND_CEILING = "ceiling"; + private static final String PARAM_VALUE_RND_FLOOR = "floor"; + private static final String PARAM_VALUE_RND_HALF_DOWN = "halfDown"; + private static final String PARAM_VALUE_RND_HALF_EVEN = "halfEven"; + private static final String PARAM_VALUE_RND_HALF_UP = "halfUp"; + private static final String PARAM_VALUE_RND_UNNECESSARY = "unnecessary"; + + private static final HashMap<String, ? extends ParameterHandler> PARAM_HANDLERS; + static { + HashMap<String, ParameterHandler> m = new HashMap<>(); + m.put(PARAM_ROUNDING_MODE, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + RoundingMode parsedValue; + if (value.equals(PARAM_VALUE_RND_UP)) { + parsedValue = RoundingMode.UP; + } else if (value.equals(PARAM_VALUE_RND_DOWN)) { + parsedValue = RoundingMode.DOWN; + } else if (value.equals(PARAM_VALUE_RND_CEILING)) { + parsedValue = RoundingMode.CEILING; + } else if (value.equals(PARAM_VALUE_RND_FLOOR)) { + parsedValue = RoundingMode.FLOOR; + } else if (value.equals(PARAM_VALUE_RND_HALF_DOWN)) { + parsedValue = RoundingMode.HALF_DOWN; + } else if (value.equals(PARAM_VALUE_RND_HALF_EVEN)) { + parsedValue = RoundingMode.HALF_EVEN; + } else if (value.equals(PARAM_VALUE_RND_HALF_UP)) { + parsedValue = RoundingMode.HALF_UP; + } else if (value.equals(PARAM_VALUE_RND_UNNECESSARY)) { + parsedValue = RoundingMode.UNNECESSARY; + } else { + throw new InvalidParameterValueException("Should be one of: u, d, c, f, hd, he, hu, un"); + } + + parser.roundingMode = parsedValue; + } + }); + m.put(PARAM_MULTIPIER, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + try { + parser.multipier = Integer.valueOf(value); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Malformed integer."); + } + } + }); + m.put(PARAM_DECIMAL_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setDecimalSeparator(value.charAt(0)); + } + }); + m.put(PARAM_MONETARY_DECIMAL_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setMonetaryDecimalSeparator(value.charAt(0)); + } + }); + m.put(PARAM_GROUP_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setGroupingSeparator(value.charAt(0)); + } + }); + m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + parser.symbols.setExponentSeparator(value); + } + }); + m.put(PARAM_MINUS_SIGN, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setMinusSign(value.charAt(0)); + } + }); + m.put(PARAM_INFINITY, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + parser.symbols.setInfinity(value); + } + }); + m.put(PARAM_NAN, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + parser.symbols.setNaN(value); + } + }); + m.put(PARAM_PERCENT, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setPercent(value.charAt(0)); + } + }); + m.put(PARAM_PER_MILL, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setPerMill(value.charAt(0)); + } + }); + m.put(PARAM_ZERO_DIGIT, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + if (value.length() != 1) { + throw new InvalidParameterValueException("Must contain exactly 1 character."); + } + parser.symbols.setZeroDigit(value.charAt(0)); + } + }); + m.put(PARAM_CURRENCY_CODE, new ParameterHandler() { + @Override + public void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException { + Currency currency; + try { + currency = Currency.getInstance(value); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Not a known ISO 4217 code."); + } + parser.symbols.setCurrency(currency); + } + }); + PARAM_HANDLERS = m; + } + + private static final String SNIP_MARK = "[...]"; + private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than SNIP_MARK.length! + + private final String src; + private int pos = 0; + + private final DecimalFormatSymbols symbols; + private RoundingMode roundingMode; + private Integer multipier; + + static DecimalFormat parse(String formatString, Locale locale) throws ParseException { + return new ExtendedDecimalFormatParser(formatString, locale).parse(); + } + + private DecimalFormat parse() throws ParseException { + String stdPattern = fetchStandardPattern(); + skipWS(); + parseFormatStringExtension(); + + DecimalFormat decimalFormat; + try { + decimalFormat = new DecimalFormat(stdPattern, symbols); + } catch (IllegalArgumentException e) { + ParseException pe = new ParseException(e.getMessage(), 0); + if (e.getCause() != null) { + try { + e.initCause(e.getCause()); + } catch (Exception e2) { + // Supress + } + } + throw pe; + } + + if (roundingMode != null) { + decimalFormat.setRoundingMode(roundingMode); + } + + if (multipier != null) { + decimalFormat.setMultiplier(multipier.intValue()); + } + + return decimalFormat; + } + + private void parseFormatStringExtension() throws ParseException { + int ln = src.length(); + + if (pos == ln) { + return; + } + + String currencySymbol = null; // Exceptional, as must be applied after "currency code" + fetchParamters: do { + int namePos = pos; + String name = fetchName(); + if (name == null) { + throw newExpectedSgParseException("name"); + } + + skipWS(); + + if (!fetchChar('=')) { + throw newExpectedSgParseException("\"=\""); + } + + skipWS(); + + int valuePos = pos; + String value = fetchValue(); + if (value == null) { + throw newExpectedSgParseException("value"); + } + int paramEndPos = pos; + + ParameterHandler handler = PARAM_HANDLERS.get(name); + if (handler == null) { + if (name.equals(PARAM_CURRENCY_SYMBOL)) { + currencySymbol = value; + } else { + throw newUnknownParameterException(name, namePos); + } + } else { + try { + handler.handle(this, value); + } catch (InvalidParameterValueException e) { + throw newInvalidParameterValueException(name, value, valuePos, e); + } + } + + skipWS(); + + // Optional comma + if (fetchChar(',')) { + skipWS(); + } else { + if (pos == ln) { + break fetchParamters; + } + if (pos == paramEndPos) { + throw newExpectedSgParseException("parameter separator whitespace or comma"); + } + } + } while (true); + + // This is brought out to here to ensure that it's applied after "currency code": + if (currencySymbol != null) { + symbols.setCurrencySymbol(currencySymbol); + } + } + + private ParseException newInvalidParameterValueException(String name, String value, int valuePos, + InvalidParameterValueException e) { + return new java.text.ParseException( + _StringUtil.jQuote(value) + " is an invalid value for the \"" + name + "\" parameter: " + + e.message, + valuePos); + } + + private ParseException newUnknownParameterException(String name, int namePos) throws ParseException { + StringBuilder sb = new StringBuilder(128); + sb.append("Unsupported parameter name, ").append(_StringUtil.jQuote(name)); + sb.append(". The supported names are: "); + Set<String> legalNames = PARAM_HANDLERS.keySet(); + String[] legalNameArr = legalNames.toArray(new String[legalNames.size()]); + Arrays.sort(legalNameArr); + for (int i = 0; i < legalNameArr.length; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(legalNameArr[i]); + } + return new java.text.ParseException(sb.toString(), namePos); + } + + private void skipWS() { + int ln = src.length(); + while (pos < ln && isWS(src.charAt(pos))) { + pos++; + } + } + + private boolean fetchChar(char fetchedChar) { + if (pos < src.length() && src.charAt(pos) == fetchedChar) { + pos++; + return true; + } else { + return false; + } + } + + private boolean isWS(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\u00A0'; + } + + private String fetchName() throws ParseException { + int ln = src.length(); + int startPos = pos; + boolean firstChar = true; + scanUntilEnd: while (pos < ln) { + char c = src.charAt(pos); + if (firstChar) { + if (!Character.isJavaIdentifierStart(c)) { + break scanUntilEnd; + } + firstChar = false; + } else if (!Character.isJavaIdentifierPart(c)) { + break scanUntilEnd; + } + pos++; + } + return !firstChar ? src.substring(startPos, pos) : null; + } + + private String fetchValue() throws ParseException { + int ln = src.length(); + int startPos = pos; + char openedQuot = 0; + boolean needsUnescaping = false; + scanUntilEnd: while (pos < ln) { + char c = src.charAt(pos); + if (c == '\'' || c == '"') { + if (openedQuot == 0) { + if (startPos != pos) { + throw new java.text.ParseException( + "The " + c + " character can only be used for quoting values, " + + "but it was in the middle of an non-quoted value.", + pos); + } + openedQuot = c; + } else if (c == openedQuot) { + if (pos + 1 < ln && src.charAt(pos + 1) == openedQuot) { + pos++; // skip doubled quote (escaping) + needsUnescaping = true; + } else { + String str = src.substring(startPos + 1, pos); + pos++; + return needsUnescaping ? unescape(str, openedQuot) : str; + } + } + } else { + if (openedQuot == 0 && !Character.isJavaIdentifierPart(c)) { + break scanUntilEnd; + } + } + pos++; + } // while + if (openedQuot != 0) { + throw new java.text.ParseException( + "The " + openedQuot + + " quotation wasn't closed when the end of the source was reached.", + pos); + } + return startPos == pos ? null : src.substring(startPos, pos); + } + + private String unescape(String s, char openedQuot) { + return openedQuot == '\'' ? _StringUtil.replace(s, "\'\'", "\'") : _StringUtil.replace(s, "\"\"", "\""); + } + + private String fetchStandardPattern() { + int pos = this.pos; + int ln = src.length(); + int semicolonCnt = 0; + boolean quotedMode = false; + findStdPartEnd: while (pos < ln) { + char c = src.charAt(pos); + if (c == ';' && !quotedMode) { + semicolonCnt++; + if (semicolonCnt == 2) { + break findStdPartEnd; + } + } else if (c == '\'') { + if (quotedMode) { + if (pos + 1 < ln && src.charAt(pos + 1) == '\'') { + // Skips "''" used for escaping "'" + pos++; + } else { + quotedMode = false; + } + } else { + quotedMode = true; + } + } + pos++; + } + + String stdFormatStr; + if (semicolonCnt < 2) { // We have a standard DecimalFormat string + // Note that "0.0;" and "0.0" gives the same result with DecimalFormat, so we leave a ';' there + stdFormatStr = src; + } else { // `pos` points to the 2nd ';' + int stdEndPos = pos; + if (src.charAt(pos - 1) == ';') { // we have a ";;" + // Note that ";;" is illegal in DecimalFormat, so this is backward compatible. + stdEndPos--; + } + stdFormatStr = src.substring(0, stdEndPos); + } + + if (pos < ln) { + pos++; // Skips closing ';' + } + this.pos = pos; + + return stdFormatStr; + } + + private ExtendedDecimalFormatParser(String formatString, Locale locale) { + src = formatString; + symbols = new DecimalFormatSymbols(locale); + } + + private ParseException newExpectedSgParseException(String expectedThing) { + String quotation; + + // Ignore trailing WS when calculating the length: + int i = src.length() - 1; + while (i >= 0 && Character.isWhitespace(src.charAt(i))) { + i--; + } + int ln = i + 1; + + if (pos < ln) { + int qEndPos = pos + MAX_QUOTATION_LENGTH; + if (qEndPos >= ln) { + quotation = src.substring(pos, ln); + } else { + quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + SNIP_MARK; + } + } else { + quotation = null; + } + + return new ParseException( + "Expected a(n) " + expectedThing + " at position " + pos + " (0-based), but " + + (quotation == null ? "reached the end of the input." : "found: " + quotation), + pos); + } + + private interface ParameterHandler { + + void handle(ExtendedDecimalFormatParser parser, String value) + throws InvalidParameterValueException; + + } + + private static class InvalidParameterValueException extends Exception { + + private final String message; + + public InvalidParameterValueException(String message) { + this.message = message; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/HTMLOutputFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/HTMLOutputFormat.java b/src/main/java/org/apache/freemarker/core/HTMLOutputFormat.java new file mode 100644 index 0000000..5d89d6d --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/HTMLOutputFormat.java @@ -0,0 +1,75 @@ +/* + * 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.io.IOException; +import java.io.Writer; + +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util._StringUtil; + +/** + * Represents the HTML output format (MIME type "text/html", name "HTML"). This format escapes by default (via + * {@link _StringUtil#XHTMLEnc(String)}). The {@code ?html}, {@code ?xhtml} and {@code ?xml} built-ins silently bypass + * template output values of the type produced by this output format ({@link TemplateHTMLOutputModel}). + * + * @since 2.3.24 + */ +public final class HTMLOutputFormat extends CommonMarkupOutputFormat<TemplateHTMLOutputModel> { + + /** + * The only instance (singleton) of this {@link OutputFormat}. + */ + public static final HTMLOutputFormat INSTANCE = new HTMLOutputFormat(); + + private HTMLOutputFormat() { + // Only to decrease visibility + } + + @Override + public String getName() { + return "HTML"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public void output(String textToEsc, Writer out) throws IOException, TemplateModelException { + _StringUtil.XHTMLEnc(textToEsc, out); + } + + @Override + public String escapePlainText(String plainTextContent) { + return _StringUtil.XHTMLEnc(plainTextContent); + } + + @Override + public boolean isLegacyBuiltInBypassed(String builtInName) { + return builtInName.equals("html") || builtInName.equals("xml") || builtInName.equals("xhtml"); + } + + @Override + protected TemplateHTMLOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) { + return new TemplateHTMLOutputModel(plainTextContent, markupContent); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormat.java new file mode 100644 index 0000000..ca6ec02 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormat.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +abstract class ISOLikeTemplateDateFormat extends TemplateDateFormat { + + private static final String XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE + = "Less than seconds accuracy isn't allowed by the XML Schema format"; + private final ISOLikeTemplateDateFormatFactory factory; + private final Environment env; + protected final int dateType; + protected final boolean zonelessInput; + protected final TimeZone timeZone; + protected final Boolean forceUTC; + protected final Boolean showZoneOffset; + protected final int accuracy; + + /** + * @param formatString The value of the ..._format setting, like "iso nz". + * @param parsingStart The index of the char in the {@code settingValue} that directly after the prefix that has + * indicated the exact formatter class (like "iso" or "xs") + */ + public ISOLikeTemplateDateFormat( + final String formatString, int parsingStart, + int dateType, boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, Environment env) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { + this.factory = factory; + this.env = env; + if (dateType == TemplateDateModel.UNKNOWN) { + throw new UnknownDateTypeFormattingUnsupportedException(); + } + + this.dateType = dateType; + this.zonelessInput = zonelessInput; + + final int ln = formatString.length(); + boolean afterSeparator = false; + int i = parsingStart; + int accuracy = _DateUtil.ACCURACY_MILLISECONDS; + Boolean showZoneOffset = null; + Boolean forceUTC = Boolean.FALSE; + while (i < ln) { + final char c = formatString.charAt(i++); + if (c == '_' || c == ' ') { + afterSeparator = true; + } else { + if (!afterSeparator) { + throw new InvalidFormatParametersException( + "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ")."); + } + + switch (c) { + case 'h': + case 'm': + case 's': + if (accuracy != _DateUtil.ACCURACY_MILLISECONDS) { + throw new InvalidFormatParametersException( + "Character \"" + c + "\" is unexpected as accuracy was already specified earlier " + + "(at char pos. " + i + ")."); + } + switch (c) { + case 'h': + if (isXSMode()) { + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); + } + accuracy = _DateUtil.ACCURACY_HOURS; + break; + case 'm': + if (i < ln && formatString.charAt(i) == 's') { + i++; + accuracy = _DateUtil.ACCURACY_MILLISECONDS_FORCED; + } else { + if (isXSMode()) { + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); + } + accuracy = _DateUtil.ACCURACY_MINUTES; + } + break; + case 's': + accuracy = _DateUtil.ACCURACY_SECONDS; + break; + } + break; + case 'f': + if (i < ln && formatString.charAt(i) == 'u') { + checkForceUTCNotSet(forceUTC); + i++; + forceUTC = Boolean.TRUE; + break; + } + // Falls through + case 'n': + if (showZoneOffset != null) { + throw new InvalidFormatParametersException( + "Character \"" + c + "\" is unexpected as zone offset visibility was already " + + "specified earlier. (at char pos. " + i + ")."); + } + switch (c) { + case 'n': + if (i < ln && formatString.charAt(i) == 'z') { + i++; + showZoneOffset = Boolean.FALSE; + } else { + throw new InvalidFormatParametersException( + "\"n\" must be followed by \"z\" (at char pos. " + i + ")."); + } + break; + case 'f': + if (i < ln && formatString.charAt(i) == 'z') { + i++; + showZoneOffset = Boolean.TRUE; + } else { + throw new InvalidFormatParametersException( + "\"f\" must be followed by \"z\" (at char pos. " + i + ")."); + } + break; + } + break; + case 'u': + checkForceUTCNotSet(forceUTC); + forceUTC = null; // means UTC will be used except for zonelessInput + break; + default: + throw new InvalidFormatParametersException( + "Unexpected character, " + _StringUtil.jQuote(String.valueOf(c)) + + ". Expected the beginning of one of: h, m, s, ms, nz, fz, u" + + " (at char pos. " + i + ")."); + } // switch + afterSeparator = false; + } // else + } // while + + this.accuracy = accuracy; + this.showZoneOffset = showZoneOffset; + this.forceUTC = forceUTC; + this.timeZone = timeZone; + } + + private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException { + if (fourceUTC != Boolean.FALSE) { + throw new InvalidFormatParametersException( + "The UTC usage option was already set earlier."); + } + } + + @Override + public final String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException { + final Date date = TemplateFormatUtil.getNonNullDate(dateModel); + return format( + date, + dateType != TemplateDateModel.TIME, + dateType != TemplateDateModel.DATE, + showZoneOffset == null + ? !zonelessInput + : showZoneOffset.booleanValue(), + accuracy, + (forceUTC == null ? !zonelessInput : forceUTC.booleanValue()) ? _DateUtil.UTC : timeZone, + factory.getISOBuiltInCalendar(env)); + } + + protected abstract String format(Date date, + boolean datePart, boolean timePart, boolean offsetPart, + int accuracy, + TimeZone timeZone, + DateToISO8601CalendarFactory calendarFactory); + + @Override + @SuppressFBWarnings(value = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN", + justification = "Known to use the singleton Boolean-s only") + public final Date parse(String s, int dateType) throws UnparsableValueException { + CalendarFieldsToDateConverter calToDateConverter = factory.getCalendarFieldsToDateCalculator(env); + TimeZone tz = forceUTC != Boolean.FALSE ? _DateUtil.UTC : timeZone; + try { + if (dateType == TemplateDateModel.DATE) { + return parseDate(s, tz, calToDateConverter); + } else if (dateType == TemplateDateModel.TIME) { + return parseTime(s, tz, calToDateConverter); + } else if (dateType == TemplateDateModel.DATETIME) { + return parseDateTime(s, tz, calToDateConverter); + } else { + throw new BugException("Unexpected date type: " + dateType); + } + } catch (DateParseException e) { + throw new UnparsableValueException(e.getMessage(), e); + } + } + + protected abstract Date parseDate( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + protected abstract Date parseTime( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + protected abstract Date parseDateTime( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + @Override + public final String getDescription() { + switch (dateType) { + case TemplateDateModel.DATE: return getDateDescription(); + case TemplateDateModel.TIME: return getTimeDescription(); + case TemplateDateModel.DATETIME: return getDateTimeDescription(); + default: return "<error: wrong format dateType>"; + } + } + + protected abstract String getDateDescription(); + protected abstract String getTimeDescription(); + protected abstract String getDateTimeDescription(); + + @Override + public final boolean isLocaleBound() { + return false; + } + + @Override + public boolean isTimeZoneBound() { + return true; + } + + protected abstract boolean isXSMode(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormatFactory.java new file mode 100644 index 0000000..9a03a16 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ISOLikeTemplateDateFormatFactory.java @@ -0,0 +1,52 @@ +/* + * 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._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.util._DateUtil.TrivialCalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.TrivialDateToISO8601CalendarFactory; + +abstract class ISOLikeTemplateDateFormatFactory extends TemplateDateFormatFactory { + + private static final Object DATE_TO_CAL_CONVERTER_KEY = new Object(); + private static final Object CAL_TO_DATE_CONVERTER_KEY = new Object(); + + protected ISOLikeTemplateDateFormatFactory() { } + + public DateToISO8601CalendarFactory getISOBuiltInCalendar(Environment env) { + DateToISO8601CalendarFactory r = (DateToISO8601CalendarFactory) env.getCustomState(DATE_TO_CAL_CONVERTER_KEY); + if (r == null) { + r = new TrivialDateToISO8601CalendarFactory(); + env.setCustomState(DATE_TO_CAL_CONVERTER_KEY, r); + } + return r; + } + + public CalendarFieldsToDateConverter getCalendarFieldsToDateCalculator(Environment env) { + CalendarFieldsToDateConverter r = (CalendarFieldsToDateConverter) env.getCustomState(CAL_TO_DATE_CONVERTER_KEY); + if (r == null) { + r = new TrivialCalendarFieldsToDateConverter(); + env.setCustomState(CAL_TO_DATE_CONVERTER_KEY, r); + } + return r; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormat.java new file mode 100644 index 0000000..f973628 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormat.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; + +final class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat { + + ISOTemplateDateFormat( + String settingValue, int parsingStart, + int dateType, boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, + Environment env) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { + super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env); + } + + @Override + protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, + TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) { + return _DateUtil.dateToISO8601String( + date, datePart, timePart, timePart && offsetPart, accuracy, timeZone, calendarFactory); + } + + @Override + protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseISO8601Date(s, tz, calToDateConverter); + } + + @Override + protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseISO8601Time(s, tz, calToDateConverter); + } + + @Override + protected Date parseDateTime(String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { + return _DateUtil.parseISO8601DateTime(s, tz, calToDateConverter); + } + + @Override + protected String getDateDescription() { + return "ISO 8601 (subset) date"; + } + + @Override + protected String getTimeDescription() { + return "ISO 8601 (subset) time"; + } + + @Override + protected String getDateTimeDescription() { + return "ISO 8601 (subset) date-time"; + } + + @Override + protected boolean isXSMode() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormatFactory.java new file mode 100644 index 0000000..068e555 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ISOTemplateDateFormatFactory.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.util.Locale; +import java.util.TimeZone; + +class ISOTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory { + + static final ISOTemplateDateFormatFactory INSTANCE = new ISOTemplateDateFormatFactory(); + + private ISOTemplateDateFormatFactory() { + // Not meant to be instantiated + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching) + return new ISOTemplateDateFormat( + params, 3, + dateType, zonelessInput, + timeZone, this, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/InvalidFormatParametersException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/InvalidFormatParametersException.java b/src/main/java/org/apache/freemarker/core/InvalidFormatParametersException.java new file mode 100644 index 0000000..59e6458 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/InvalidFormatParametersException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.freemarker.core; + +/** + * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the parameters + * part of the format string (like some kind of pattern) is malformed. + * + * @since 2.3.24 + */ +public final class InvalidFormatParametersException extends InvalidFormatStringException { + + public InvalidFormatParametersException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidFormatParametersException(String message) { + this(message, null); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/InvalidFormatStringException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/InvalidFormatStringException.java b/src/main/java/org/apache/freemarker/core/InvalidFormatStringException.java new file mode 100644 index 0000000..e7cb59b --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/InvalidFormatStringException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.freemarker.core; + +/** + * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the format + * string (like the value of the {@code dateFormat} setting) is malformed. + * + * @since 2.3.24 + */ +public abstract class InvalidFormatStringException extends TemplateValueFormatException { + + public InvalidFormatStringException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidFormatStringException(String message) { + this(message, null); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java b/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java new file mode 100644 index 0000000..924686b --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +/** + * A subclass of {@link TemplateException} that says that an FTL expression has evaluated to {@code null} or it refers + * to something that doesn't exist. At least in FreeMarker 2.3.x these two cases aren't distinguished. + */ +public class InvalidReferenceException extends TemplateException { + + static final InvalidReferenceException FAST_INSTANCE; + static { + Environment prevEnv = Environment.getCurrentEnvironment(); + try { + Environment.setCurrentEnvironment(null); + FAST_INSTANCE = new InvalidReferenceException( + "Invalid reference. Details are unavilable, as this should have been handled by an FTL construct. " + + "If it wasn't, that's problably a bug in FreeMarker.", + null); + } finally { + Environment.setCurrentEnvironment(prevEnv); + } + } + + private static final Object[] TIP = { + "If the failing expression is known to be legally refer to something that's sometimes null or missing, " + + "either specify a default value like myOptionalVar!myDefault, or use ", + "<#if myOptionalVar??>", "when-present", "<#else>", "when-missing", "</#if>", + ". (These only cover the last step of the expression; to cover the whole expression, " + + "use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??" + }; + + private static final Object[] TIP_MISSING_ASSIGNMENT_TARGET = { + "If the target variable is known to be legally null or missing sometimes, instead of something like ", + "<#assign x += 1>", ", you could write ", "<#if x??>", "<#assign x += 1>", "</#if>", + " or ", "<#assign x = (x!0) + 1>" + }; + + private static final String TIP_NO_DOLLAR = + "Variable references must not start with \"$\", unless the \"$\" is really part of the variable name."; + + private static final String TIP_LAST_STEP_DOT = + "It's the step after the last dot that caused this error, not those before it."; + + private static final String TIP_LAST_STEP_SQUARE_BRACKET = + "It's the final [] step that caused this error, not those before it."; + + private static final String TIP_JSP_TAGLIBS = + "The \"JspTaglibs\" variable isn't a core FreeMarker feature; " + + "it's only available when templates are invoked through org.apache.freemarker.servlet.FreemarkerServlet" + + " (or other custom FreeMarker-JSP integration solution)."; + + /** + * Creates and invalid reference exception that contains no information about what was missing or null. + * As such, try to avoid this constructor. + */ + public InvalidReferenceException(Environment env) { + super("Invalid reference: The expression has evaluated to null or refers to something that doesn't exist.", + env); + } + + /** + * Creates and invalid reference exception that contains no programmatically extractable information about the + * blamed expression. As such, try to avoid this constructor, unless need to raise this expression from outside + * the FreeMarker core. + */ + public InvalidReferenceException(String description, Environment env) { + super(description, env); + } + + /** + * This is the recommended constructor, but it's only used internally, and has no backward compatibility guarantees. + * + * @param expression The expression that evaluates to missing or null. The last step of the expression should be + * the failing one, like in {@code goodStep.failingStep.furtherStep} it should only contain + * {@code goodStep.failingStep}. + */ + InvalidReferenceException(_ErrorDescriptionBuilder description, Environment env, ASTExpression expression) { + super(null, env, expression, description); + } + + /** + * Use this whenever possible, as it returns {@link #FAST_INSTANCE} instead of creating a new instance, when + * appropriate. + */ + static InvalidReferenceException getInstance(ASTExpression blamed, Environment env) { + if (env != null && env.getFastInvalidReferenceExceptions()) { + return FAST_INSTANCE; + } else { + if (blamed != null) { + final _ErrorDescriptionBuilder errDescBuilder + = new _ErrorDescriptionBuilder("The following has evaluated to null or missing:").blame(blamed); + if (endsWithDollarVariable(blamed)) { + errDescBuilder.tips(TIP_NO_DOLLAR, TIP); + } else if (blamed instanceof ASTExpDot) { + final String rho = ((ASTExpDot) blamed).getRHO(); + String nameFixTip = null; + if ("size".equals(rho)) { + nameFixTip = "To query the size of a collection or map use ?size, like myList?size"; + } else if ("length".equals(rho)) { + nameFixTip = "To query the length of a string use ?length, like myString?size"; + } + errDescBuilder.tips( + nameFixTip == null + ? new Object[] { TIP_LAST_STEP_DOT, TIP } + : new Object[] { TIP_LAST_STEP_DOT, nameFixTip, TIP }); + } else if (blamed instanceof ASTExpDynamicKeyName) { + errDescBuilder.tips(TIP_LAST_STEP_SQUARE_BRACKET, TIP); + } else if (blamed instanceof ASTExpVariable + && ((ASTExpVariable) blamed).getName().equals("JspTaglibs")) { + errDescBuilder.tips(TIP_JSP_TAGLIBS, TIP); + } else { + errDescBuilder.tip(TIP); + } + return new InvalidReferenceException(errDescBuilder, env, blamed); + } else { + return new InvalidReferenceException(env); + } + } + } + + /** + * Used for assignments that use operators like {@code +=}, when the target variable was null/missing. + */ + static InvalidReferenceException getInstance(String missingAssignedVarName, String assignmentOperator, + Environment env) { + if (env != null && env.getFastInvalidReferenceExceptions()) { + return FAST_INSTANCE; + } else { + final _ErrorDescriptionBuilder errDescBuilder = new _ErrorDescriptionBuilder( + "The target variable of the assignment, ", + new _DelayedJQuote(missingAssignedVarName), + ", was null or missing, but the \"", + assignmentOperator, "\" operator needs to get its value before assigning to it." + ); + if (missingAssignedVarName.startsWith("$")) { + errDescBuilder.tips(TIP_NO_DOLLAR, TIP_MISSING_ASSIGNMENT_TARGET); + } else { + errDescBuilder.tip(TIP_MISSING_ASSIGNMENT_TARGET); + } + return new InvalidReferenceException(errDescBuilder, env, null); + } + } + + private static boolean endsWithDollarVariable(ASTExpression blame) { + return blame instanceof ASTExpVariable && ((ASTExpVariable) blame).getName().startsWith("$") + || blame instanceof ASTExpDot && ((ASTExpDot) blame).getRHO().startsWith("$"); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/JSONOutputFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/JSONOutputFormat.java b/src/main/java/org/apache/freemarker/core/JSONOutputFormat.java new file mode 100644 index 0000000..8e76701 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/JSONOutputFormat.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * Represents the JSON output format (MIME type "application/json", name "JSON"). This format doesn't support escaping. + * + * @since 2.3.24 + */ +public class JSONOutputFormat extends OutputFormat { + + /** + * The only instance (singleton) of this {@link OutputFormat}. + */ + public static final JSONOutputFormat INSTANCE = new JSONOutputFormat(); + + private JSONOutputFormat() { + // Only to decrease visibility + } + + @Override + public String getName() { + return "JSON"; + } + + @Override + public String getMimeType() { + return "application/json"; + } + + @Override + public boolean isOutputFormatMixingAllowed() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/JavaScriptOutputFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/JavaScriptOutputFormat.java b/src/main/java/org/apache/freemarker/core/JavaScriptOutputFormat.java new file mode 100644 index 0000000..1b35f99 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/JavaScriptOutputFormat.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Represents the JavaScript output format (MIME type "application/javascript", name "JavaScript"). This format doesn't + * support escaping. + * + * @since 2.3.24 + */ +public class JavaScriptOutputFormat extends OutputFormat { + + /** + * The only instance (singleton) of this {@link OutputFormat}. + */ + public static final JavaScriptOutputFormat INSTANCE = new JavaScriptOutputFormat(); + + private JavaScriptOutputFormat() { + // Only to decrease visibility + } + + @Override + public String getName() { + return "JavaScript"; + } + + @Override + public String getMimeType() { + return "application/javascript"; + } + + @Override + public boolean isOutputFormatMixingAllowed() { + return false; + } + +}
