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