/*
 * 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.
/** * 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; }
Params:
  • descriptionParts – These will be concatenated to a single String in toString(). String array items that look like FTL tag (must start with "<" and end with ">") will be converted to the actual template syntax if blamed or template was set.
/** * @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.
/** * 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 parts that look like an FTL tag to be converted, if there's no blamed.
/** * 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; } }