| /* |
| * 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.ArrayList; |
| |
| import freemarker.template.Template; |
| import freemarker.template.TemplateException; |
| import freemarker.template.TemplateHashModelEx; |
| import freemarker.template.TemplateHashModelEx2; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateModelException; |
| import freemarker.template.utility.StringUtil; |
| |
| /** |
| * Used internally only, might changes without notice! |
| * Utilities for creating error messages (and other messages). |
| */ |
| public class _MessageUtil { |
| |
| static final String UNKNOWN_DATE_TO_STRING_ERROR_MESSAGE |
| = "Can't convert the date-like value to string because it isn't " |
| + "known if it's a date (no time part), time or date-time value."; |
| |
| static final String UNKNOWN_DATE_PARSING_ERROR_MESSAGE |
| = "Can't parse the string to date-like value because it isn't " |
| + "known if it's desired result should be a date (no time part), a time, or a date-time value."; |
| |
| static final String UNKNOWN_DATE_TYPE_ERROR_TIP = |
| "Use ?date, ?time, or ?datetime to tell FreeMarker the exact type."; |
| |
| static final Object[] UNKNOWN_DATE_TO_STRING_TIPS = { |
| UNKNOWN_DATE_TYPE_ERROR_TIP, |
| "If you need a particular format only once, use ?string(pattern), like ?string('dd.MM.yyyy HH:mm:ss'), " |
| + "to specify which fields to display. " |
| }; |
| |
| static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n"; |
| |
| static final String EMBEDDED_MESSAGE_END = "\n---end-message---"; |
| |
| // Can't be instantiated |
| private _MessageUtil() { } |
| |
| static String formatLocationForSimpleParsingError(Template template, int line, int column) { |
| return formatLocation("in", template, line, column); |
| } |
| |
| static String formatLocationForSimpleParsingError(String templateSourceName, int line, int column) { |
| return formatLocation("in", templateSourceName, line, column); |
| } |
| |
| static String formatLocationForDependentParsingError(Template template, int line, int column) { |
| return formatLocation("on", template, line, column); |
| } |
| |
| static String formatLocationForDependentParsingError(String templateSourceName, int line, int column) { |
| return formatLocation("on", templateSourceName, line, column); |
| } |
| |
| static String formatLocationForEvaluationError(Template template, int line, int column) { |
| return formatLocation("at", template, line, column); |
| } |
| |
| static String formatLocationForEvaluationError(Macro macro, int line, int column) { |
| Template t = macro.getTemplate(); |
| return formatLocation("at", t != null ? t.getSourceName() : null, macro.getName(), macro.isFunction(), line, column); |
| } |
| |
| static String formatLocationForEvaluationError(String templateSourceName, int line, int column) { |
| return formatLocation("at", templateSourceName, line, column); |
| } |
| |
| private static String formatLocation(String preposition, Template template, int line, int column) { |
| return formatLocation(preposition, template != null ? template.getSourceName() : null, line, column); |
| } |
| |
| private static String formatLocation(String preposition, String templateSourceName, int line, int column) { |
| return formatLocation( |
| preposition, templateSourceName, |
| null, false, |
| line, column); |
| } |
| |
| private static String formatLocation( |
| String preposition, String templateSourceName, |
| String macroOrFuncName, boolean isFunction, |
| int line, int column) { |
| String templateDesc; |
| if (line < 0) { |
| templateDesc = "?eval-ed string"; |
| macroOrFuncName = null; |
| } else { |
| templateDesc = templateSourceName != null |
| ? "template " + StringUtil.jQuoteNoXSS(templateSourceName) |
| : "nameless template"; |
| } |
| return "in " + templateDesc |
| + (macroOrFuncName != null |
| ? " in " + (isFunction ? "function " : "macro ") + StringUtil.jQuote(macroOrFuncName) |
| : "") |
| + " " |
| + preposition + " " + formatPosition(line, column); |
| } |
| |
| static String formatPosition(int line, int column) { |
| return "line " + (line >= 0 ? line : line - (TemplateObject.RUNTIME_EVAL_LINE_DISPLACEMENT - 1)) |
| + ", column " + column; |
| } |
| |
| /** |
| * Returns a single line string that is no longer than {@code maxLength}. |
| * If will truncate the string at line-breaks too. |
| * The truncation is always signaled with a a {@code "..."} at the end of the result string. |
| */ |
| public static String shorten(String s, int maxLength) { |
| if (maxLength < 5) maxLength = 5; |
| |
| boolean isTruncated = false; |
| |
| int brIdx = s.indexOf('\n'); |
| if (brIdx != -1) { |
| s = s.substring(0, brIdx); |
| isTruncated = true; |
| }; |
| brIdx = s.indexOf('\r'); |
| if (brIdx != -1) { |
| s = s.substring(0, brIdx); |
| isTruncated = true; |
| } |
| |
| if (s.length() > maxLength) { |
| s = s.substring(0, maxLength - 3); |
| isTruncated = true; |
| } |
| |
| if (!isTruncated) { |
| return s; |
| } else { |
| if (s.endsWith(".")) { |
| if (s.endsWith("..")) { |
| if (s.endsWith("...")) { |
| return s; |
| } else { |
| return s + "."; |
| } |
| } else { |
| return s + ".."; |
| } |
| } else { |
| return s + "..."; |
| } |
| } |
| } |
| |
| public static StringBuilder appendExpressionAsUntearable(StringBuilder sb, Expression argExp) { |
| boolean needParen = |
| !(argExp instanceof NumberLiteral) |
| && !(argExp instanceof StringLiteral) |
| && !(argExp instanceof BooleanLiteral) |
| && !(argExp instanceof ListLiteral) |
| && !(argExp instanceof HashLiteral) |
| && !(argExp instanceof Identifier) |
| && !(argExp instanceof Dot) |
| && !(argExp instanceof DynamicKeyName) |
| && !(argExp instanceof MethodCall) |
| && !(argExp instanceof BuiltIn) |
| && !(argExp instanceof ExistsExpression) |
| && !(argExp instanceof ParentheticalExpression); |
| if (needParen) sb.append('('); |
| sb.append(argExp.getCanonicalForm()); |
| if (needParen) sb.append(')'); |
| return sb; |
| } |
| |
| public static TemplateModelException newArgCntError(String methodName, int argCnt, int expectedCnt) { |
| return newArgCntError(methodName, argCnt, expectedCnt, expectedCnt); |
| } |
| |
| public static TemplateModelException newArgCntError(String methodName, int argCnt, int minCnt, int maxCnt) { |
| ArrayList/*<Object>*/ desc = new ArrayList(20); |
| |
| desc.add(methodName); |
| |
| desc.add("("); |
| if (maxCnt != 0) desc.add("..."); |
| desc.add(") expects "); |
| |
| if (minCnt == maxCnt) { |
| if (maxCnt == 0) { |
| desc.add("no"); |
| } else { |
| desc.add(Integer.valueOf(maxCnt)); |
| } |
| } else if (maxCnt - minCnt == 1) { |
| desc.add(Integer.valueOf(minCnt)); |
| desc.add(" or "); |
| desc.add(Integer.valueOf(maxCnt)); |
| } else { |
| desc.add(Integer.valueOf(minCnt)); |
| if (maxCnt != Integer.MAX_VALUE) { |
| desc.add(" to "); |
| desc.add(Integer.valueOf(maxCnt)); |
| } else { |
| desc.add(" or more (unlimited)"); |
| } |
| } |
| desc.add(" argument"); |
| if (maxCnt > 1) desc.add("s"); |
| |
| desc.add(" but has received "); |
| if (argCnt == 0) { |
| desc.add("none"); |
| } else { |
| desc.add(Integer.valueOf(argCnt)); |
| } |
| desc.add("."); |
| |
| return new _TemplateModelException(desc.toArray()); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeStringException(String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "string", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeNumberException(String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "number", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeBooleanException(String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "boolean", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeExtendedHashException( |
| String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "extended hash", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeExtendedHashOrSequnceException( |
| String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "extended hash or sequence", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeSequenceException( |
| String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "sequence", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeSequenceOrCollectionException( |
| String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "sequence or collection", arg); |
| } |
| |
| public static TemplateModelException newMethodArgMustBeStringOrMarkupOutputException( |
| String methodName, int argIdx, TemplateModel arg) { |
| return newMethodArgUnexpectedTypeException(methodName, argIdx, "string or markup output", arg); |
| } |
| |
| public static TemplateModelException newMethodArgUnexpectedTypeException( |
| String methodName, int argIdx, String expectedType, TemplateModel arg) { |
| return new _TemplateModelException( |
| methodName, "(...) expects ", new _DelayedAOrAn(expectedType), " as argument #", Integer.valueOf(argIdx + 1), |
| ", but received ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(arg)), "."); |
| } |
| |
| /** |
| * The type of the argument was good, but it's value wasn't. |
| */ |
| public static TemplateModelException newMethodArgInvalidValueException( |
| String methodName, int argIdx, Object... details) { |
| return new _TemplateModelException( |
| methodName, "(...) argument #", Integer.valueOf(argIdx + 1), |
| " had invalid value: ", details); |
| } |
| |
| /** |
| * The type of the argument was good, but the values of two or more arguments are inconsistent with each other. |
| */ |
| public static TemplateModelException newMethodArgsInvalidValueException( |
| String methodName, Object... details) { |
| return new _TemplateModelException(methodName, "(...) arguments have invalid value: ", details); |
| } |
| |
| public static TemplateException newInstantiatingClassNotAllowedException(String className, Environment env) { |
| return new _MiscTemplateException(env, |
| "Instantiating ", className, " is not allowed in the template for security reasons."); |
| } |
| |
| public static _TemplateModelException newCantFormatUnknownTypeDateException( |
| Expression dateSourceExpr, UnknownDateTypeFormattingUnsupportedException cause) { |
| return new _TemplateModelException(cause, null, new _ErrorDescriptionBuilder( |
| _MessageUtil.UNKNOWN_DATE_TO_STRING_ERROR_MESSAGE) |
| .blame(dateSourceExpr) |
| .tips(_MessageUtil.UNKNOWN_DATE_TO_STRING_TIPS)); |
| } |
| |
| public static TemplateException newCantFormatDateException(TemplateDateFormat format, Expression dataSrcExp, |
| TemplateValueFormatException e, boolean useTempModelExc) { |
| _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( |
| "Failed to format date/time/datetime with format ", new _DelayedJQuote(format.getDescription()), ": ", |
| e.getMessage()) |
| .blame(dataSrcExp); |
| return useTempModelExc |
| ? new _TemplateModelException(e, null, desc) |
| : new _MiscTemplateException(e, null, desc); |
| } |
| |
| public static TemplateException newCantFormatNumberException(TemplateNumberFormat format, Expression dataSrcExp, |
| TemplateValueFormatException e, boolean useTempModelExc) { |
| _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( |
| "Failed to format number with format ", new _DelayedJQuote(format.getDescription()), ": ", |
| e.getMessage()) |
| .blame(dataSrcExp); |
| return useTempModelExc |
| ? new _TemplateModelException(e, null, desc) |
| : new _MiscTemplateException(e, null, desc); |
| } |
| |
| public static TemplateModelException newKeyValuePairListingNonStringKeyExceptionMessage( |
| TemplateModel key, TemplateHashModelEx listedHashEx) { |
| return new _TemplateModelException(new _ErrorDescriptionBuilder( |
| "When listing key-value pairs of traditional hash " |
| + "implementations, all keys must be strings, but one of them " |
| + "was ", |
| new _DelayedAOrAn(new _DelayedFTLTypeDescription(key)), "." |
| ).tip("The listed value's TemplateModel class was ", |
| new _DelayedShortClassName(listedHashEx.getClass()), |
| ", which doesn't implement ", |
| new _DelayedShortClassName(TemplateHashModelEx2.class), |
| ", which leads to this restriction.")); |
| } |
| |
| /** |
| * Because of the limitations of FTL lambdas (called "local lambdas"), sometimes we must condense the lazy result |
| * down into a sequence. However, doing that automatically is only allowed if the input was a sequence as well. If |
| * it wasn't a sequence, we don't dare to collect the result into a sequence automatically (because it's possibly |
| * too long), and that's when this error message comes. |
| */ |
| public static TemplateException newLazilyGeneratedCollectionMustBeSequenceException(Expression blamed) { |
| return new _MiscTemplateException(blamed, |
| "The result is a listable value with lazy transformation(s) applied on it, but it's not " + |
| "an FTL sequence (it's not a List-like value, but an Iterator-like value). The place doesn't " + |
| "support such values due to technical limitations. So either pass it to a construct that supports " + |
| "such values (like ", "<#list transformedListable as x>", "), or, if you know that you don't have " + |
| "too many elements, use transformedListable?sequence to allow it to be treated as an FTL sequence."); |
| } |
| |
| /** |
| * @return "a" or "an" or "a(n)" (or "" for empty string) for an FTL type name |
| */ |
| static public String getAOrAn(String s) { |
| if (s == null) return null; |
| if (s.length() == 0) return ""; |
| |
| char fc = Character.toLowerCase(s.charAt(0)); |
| if (fc == 'a' || fc == 'e' || fc == 'i') { |
| return "an"; |
| } else if (fc == 'h') { |
| String ls = s.toLowerCase(); |
| if (ls.startsWith("has") || ls.startsWith("hi")) { |
| return "a"; |
| } else if (ls.startsWith("ht")) { |
| return "an"; |
| } else { |
| return "a(n)"; |
| } |
| } else if (fc == 'u' || fc == 'o') { |
| return "a(n)"; |
| } else { |
| char sc = (s.length() > 1) ? s.charAt(1) : '\0'; |
| if (fc == 'x' && !(sc == 'a' || sc == 'e' || sc == 'i' || sc == 'a' || sc == 'o' || sc == 'u')) { |
| return "an"; |
| } else { |
| return "a"; |
| } |
| } |
| } |
| } |