| /* |
| * 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.template; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.io.PrintStream; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.lang.reflect.Method; |
| |
| import freemarker.core.Environment; |
| import freemarker.core.Expression; |
| import freemarker.core.InvalidReferenceException; |
| import freemarker.core.ParseException; |
| import freemarker.core.TemplateElement; |
| import freemarker.core.TemplateObject; |
| import freemarker.core.UnboundTemplate; |
| import freemarker.core._CoreAPI; |
| import freemarker.core._ErrorDescriptionBuilder; |
| import freemarker.template.utility.CollectionUtils; |
| |
| /** |
| * Runtime exception in a template (as opposed to a parsing-time exception: {@link ParseException}). |
| * It prints a special stack trace that contains the template-language stack trace along the usual Java stack trace. |
| */ |
| public class TemplateException extends Exception { |
| |
| private static final String FTL_INSTRUCTION_STACK_TRACE_TITLE |
| = "FTL stack trace (\"~\" means nesting-related):"; |
| |
| // Set in constructor: |
| private transient _ErrorDescriptionBuilder descriptionBuilder; |
| private final transient Environment env; |
| private final transient Expression blamedExpression; |
| private transient TemplateElement[] ftlInstructionStackSnapshot; |
| |
| // Calculated on demand: |
| private String renderedFtlInstructionStackSnapshot; // clalc. from ftlInstructionStackSnapshot |
| private String renderedFtlInstructionStackSnapshotTop; // clalc. from ftlInstructionStackSnapshot |
| private String description; // calc. from descriptionBuilder, or set by the construcor |
| private transient String messageWithoutStackTop; |
| private transient String message; |
| private boolean blamedExpressionStringCalculated; |
| private String blamedExpressionString; |
| private boolean positionsCalculated; |
| private String templateName; |
| private String templateSourceName; |
| private Integer lineNumber; |
| private Integer columnNumber; |
| private Integer endLineNumber; |
| private Integer endColumnNumber; |
| |
| // Concurrency: |
| private transient Object lock = new Object(); |
| private transient ThreadLocal messageWasAlreadyPrintedForThisTrace; |
| |
| /** |
| * Constructs a TemplateException with no specified detail message |
| * or underlying cause. |
| */ |
| public TemplateException(Environment env) { |
| this((String) null, null, env); |
| } |
| |
| /** |
| * Constructs a TemplateException with the given detail message, |
| * but no underlying cause exception. |
| * |
| * @param description the description of the error that occurred |
| */ |
| public TemplateException(String description, Environment env) { |
| this(description, null, env); |
| } |
| |
| /** |
| * The same as {@link #TemplateException(Throwable, Environment)}; it's exists only for binary |
| * backward-compatibility. |
| */ |
| public TemplateException(Exception cause, Environment env) { |
| this((String) null, cause, env); |
| } |
| |
| /** |
| * Constructs a TemplateException with the given underlying Exception, |
| * but no detail message. |
| * |
| * @param cause the underlying {@link Exception} that caused this |
| * exception to be raised |
| * |
| * @since 2.3.20 |
| */ |
| public TemplateException(Throwable cause, Environment env) { |
| this((String) null, cause, env); |
| } |
| |
| /** |
| * The same as {@link #TemplateException(String, Throwable, Environment)}; it's exists only for binary |
| * backward-compatibility. |
| */ |
| public TemplateException(String description, Exception cause, Environment env) { |
| this(description, cause, env, null, null); |
| } |
| |
| /** |
| * Constructs a TemplateException with both a description of the error |
| * that occurred and the underlying Exception that caused this exception |
| * to be raised. |
| * |
| * @param description the description of the error that occurred |
| * @param cause the underlying {@link Exception} that caused this exception to be raised |
| * |
| * @since 2.3.20 |
| */ |
| public TemplateException(String description, Throwable cause, Environment env) { |
| this(description, cause, env, null, null); |
| } |
| |
| /** |
| * Don't use this; this is to be used internally by FreeMarker. No backward compatibility guarantees. |
| * |
| * @param blamedExpr Maybe {@code null}. The FTL stack in the {@link Environment} only specifies the error location |
| * with "template element" granularity, and this can be used to point to the expression inside the |
| * template element. |
| */ |
| protected TemplateException(Throwable cause, Environment env, Expression blamedExpr, |
| _ErrorDescriptionBuilder descriptionBuilder) { |
| this(null, cause, env, blamedExpr, descriptionBuilder); |
| } |
| |
| private TemplateException( |
| String renderedDescription, |
| Throwable cause, |
| Environment env, Expression blamedExpression, |
| _ErrorDescriptionBuilder descriptionBuilder) { |
| // Note: Keep this constructor lightweight. |
| |
| super(cause); // Message managed locally. |
| |
| if (env == null) env = Environment.getCurrentEnvironment(); |
| this.env = env; |
| |
| this.blamedExpression = blamedExpression; |
| |
| this.descriptionBuilder = descriptionBuilder; |
| description = renderedDescription; |
| |
| if (env != null) ftlInstructionStackSnapshot = _CoreAPI.getInstructionStackSnapshot(env); |
| } |
| |
| private void renderMessages() { |
| String description = getDescription(); |
| |
| if (description != null && description.length() != 0) { |
| messageWithoutStackTop = description; |
| } else if (getCause() != null) { |
| messageWithoutStackTop = "No error description was specified for this error; low-level message: " |
| + getCause().getClass().getName() + ": " + getCause().getMessage(); |
| } else { |
| messageWithoutStackTop = "[No error description was available.]"; |
| } |
| |
| String stackTopFew = getFTLInstructionStackTopFew(); |
| if (stackTopFew != null) { |
| message = messageWithoutStackTop + "\n\n" |
| + _CoreAPI.ERROR_MESSAGE_HR + "\n" |
| + FTL_INSTRUCTION_STACK_TRACE_TITLE + "\n" |
| + stackTopFew |
| + _CoreAPI.ERROR_MESSAGE_HR; |
| messageWithoutStackTop = message.substring(0, messageWithoutStackTop.length()); // to reuse backing char[] |
| } else { |
| message = messageWithoutStackTop; |
| } |
| } |
| |
| private void calculatePosition() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| // The expressions is the argument of the template element, so we prefer it as it's more specific. |
| TemplateObject templateObject = blamedExpression != null |
| ? (TemplateObject) blamedExpression |
| : ( |
| ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length != 0 |
| ? ftlInstructionStackSnapshot[0] : null); |
| // Line number below 0 means no info, negative means position in ?eval-ed value that we won't use here. |
| if (templateObject != null && templateObject.getBeginLine() > 0) { |
| final UnboundTemplate unboundTemplate = templateObject.getUnboundTemplate(); |
| templateName = getTemplateNameOrNull(unboundTemplate); |
| templateSourceName = unboundTemplate != null ? unboundTemplate.getSourceName() : null; |
| lineNumber = Integer.valueOf(templateObject.getBeginLine()); |
| columnNumber = Integer.valueOf(templateObject.getBeginColumn()); |
| endLineNumber = Integer.valueOf(templateObject.getEndLine()); |
| endColumnNumber = Integer.valueOf(templateObject.getEndColumn()); |
| } |
| positionsCalculated = true; |
| deleteFTLInstructionStackSnapshotIfNotNeeded(); |
| } |
| } |
| } |
| |
| private String getTemplateNameOrNull(final UnboundTemplate unboundTemplate) { |
| if (unboundTemplate == null) { |
| return null; |
| } |
| |
| Template template = env != null ? env.getCurrentTemplate() : null; |
| if (template == null) { |
| return null; |
| } |
| |
| return template.getUnboundTemplate() == unboundTemplate ? template.getName() : null; |
| } |
| |
| /** |
| * @deprecated Java 1.4 has introduced {@link #getCause()} - use that instead, especially as this can't return |
| * runtime exceptions and errors as is. |
| */ |
| @Deprecated |
| public Exception getCauseException() { |
| return getCause() instanceof Exception |
| ? (Exception) getCause() |
| : new Exception("Wrapped to Exception: " + getCause(), getCause()); |
| } |
| |
| /** |
| * Returns the snapshot of the FTL stack trace at the time this exception was created. |
| */ |
| public String getFTLInstructionStack() { |
| synchronized (lock) { |
| if (ftlInstructionStackSnapshot != null || renderedFtlInstructionStackSnapshot != null) { |
| if (renderedFtlInstructionStackSnapshot == null) { |
| StringWriter sw = new StringWriter(); |
| PrintWriter pw = new PrintWriter(sw); |
| _CoreAPI.outputInstructionStack(ftlInstructionStackSnapshot, false, pw); |
| pw.close(); |
| if (renderedFtlInstructionStackSnapshot == null) { |
| renderedFtlInstructionStackSnapshot = sw.toString(); |
| deleteFTLInstructionStackSnapshotIfNotNeeded(); |
| } |
| } |
| return renderedFtlInstructionStackSnapshot; |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| private String getFTLInstructionStackTopFew() { |
| synchronized (lock) { |
| if (ftlInstructionStackSnapshot != null || renderedFtlInstructionStackSnapshotTop != null) { |
| if (renderedFtlInstructionStackSnapshotTop == null) { |
| int stackSize = ftlInstructionStackSnapshot.length; |
| String s; |
| if (stackSize == 0) { |
| s = ""; |
| } else { |
| StringWriter sw = new StringWriter(); |
| _CoreAPI.outputInstructionStack(ftlInstructionStackSnapshot, true, sw); |
| s = sw.toString(); |
| } |
| if (renderedFtlInstructionStackSnapshotTop == null) { |
| renderedFtlInstructionStackSnapshotTop = s; |
| deleteFTLInstructionStackSnapshotIfNotNeeded(); |
| } |
| } |
| return renderedFtlInstructionStackSnapshotTop.length() != 0 |
| ? renderedFtlInstructionStackSnapshotTop : null; |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| private void deleteFTLInstructionStackSnapshotIfNotNeeded() { |
| if (renderedFtlInstructionStackSnapshot != null && renderedFtlInstructionStackSnapshotTop != null |
| && (positionsCalculated || blamedExpression != null)) { |
| ftlInstructionStackSnapshot = null; |
| } |
| |
| } |
| |
| private String getDescription() { |
| synchronized (lock) { |
| if (description == null && descriptionBuilder != null) { |
| description = descriptionBuilder.toString( |
| getFailingInstruction(), |
| env != null ? env.getShowErrorTips() : true); |
| descriptionBuilder = null; |
| } |
| return description; |
| } |
| } |
| |
| private TemplateElement getFailingInstruction() { |
| if (ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length > 0) { |
| return ftlInstructionStackSnapshot[0]; |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * @return the execution environment in which the exception occurred. |
| * {@code null} if the exception was deserialized. |
| */ |
| public Environment getEnvironment() { |
| return env; |
| } |
| |
| /** |
| * Overrides {@link Throwable#printStackTrace(PrintStream)} so that it will include the FTL stack trace. |
| */ |
| @Override |
| public void printStackTrace(PrintStream out) { |
| printStackTrace(out, true, true, true); |
| } |
| |
| /** |
| * Overrides {@link Throwable#printStackTrace(PrintWriter)} so that it will include the FTL stack trace. |
| */ |
| @Override |
| public void printStackTrace(PrintWriter out) { |
| printStackTrace(out, true, true, true); |
| } |
| |
| /** |
| * @param heading should the heading at the top be printed |
| * @param ftlStackTrace should the FTL stack trace be printed |
| * @param javaStackTrace should the Java stack trace be printed |
| * |
| * @since 2.3.20 |
| */ |
| public void printStackTrace(PrintWriter out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) { |
| synchronized (out) { |
| printStackTrace(new PrintWriterStackTraceWriter(out), heading, ftlStackTrace, javaStackTrace); |
| } |
| } |
| |
| /** |
| * @param heading should the heading at the top be printed |
| * @param ftlStackTrace should the FTL stack trace be printed |
| * @param javaStackTrace should the Java stack trace be printed |
| * |
| * @since 2.3.20 |
| */ |
| public void printStackTrace(PrintStream out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) { |
| synchronized (out) { |
| printStackTrace(new PrintStreamStackTraceWriter(out), heading, ftlStackTrace, javaStackTrace); |
| } |
| } |
| |
| private void printStackTrace(StackTraceWriter out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) { |
| synchronized (out) { |
| if (heading) { |
| out.println("FreeMarker template error:"); |
| } |
| |
| if (ftlStackTrace) { |
| String stackTrace = getFTLInstructionStack(); |
| if (stackTrace != null) { |
| out.println(getMessageWithoutStackTop()); // Not getMessage()! |
| out.println(); |
| out.println(_CoreAPI.ERROR_MESSAGE_HR); |
| out.println(FTL_INSTRUCTION_STACK_TRACE_TITLE); |
| out.print(stackTrace); |
| out.println(_CoreAPI.ERROR_MESSAGE_HR); |
| } else { |
| ftlStackTrace = false; |
| javaStackTrace = true; |
| } |
| } |
| |
| if (javaStackTrace) { |
| if (ftlStackTrace) { // We are after an FTL stack trace |
| out.println(); |
| out.println("Java stack trace (for programmers):"); |
| out.println(_CoreAPI.ERROR_MESSAGE_HR); |
| synchronized (lock) { |
| if (messageWasAlreadyPrintedForThisTrace == null) { |
| messageWasAlreadyPrintedForThisTrace = new ThreadLocal(); |
| } |
| messageWasAlreadyPrintedForThisTrace.set(Boolean.TRUE); |
| } |
| |
| try { |
| out.printStandardStackTrace(this); |
| } finally { |
| messageWasAlreadyPrintedForThisTrace.set(Boolean.FALSE); |
| } |
| } else { // javaStackTrace only |
| out.printStandardStackTrace(this); |
| } |
| |
| if (getCause() != null) { |
| // Dirty hack to fight with ServletException class whose getCause() method doesn't work properly: |
| Throwable causeCause = getCause().getCause(); |
| if (causeCause == null) { |
| try { |
| // Reflection is used to prevent dependency on Servlet classes. |
| Method m = getCause().getClass().getMethod("getRootCause", CollectionUtils.EMPTY_CLASS_ARRAY); |
| Throwable rootCause = (Throwable) m.invoke(getCause(), CollectionUtils.EMPTY_OBJECT_ARRAY); |
| if (rootCause != null) { |
| out.println("ServletException root cause: "); |
| out.printStandardStackTrace(rootCause); |
| } |
| } catch (Throwable exc) { |
| ; // ignore |
| } |
| } |
| } |
| } // if (javaStackTrace) |
| } |
| } |
| |
| /** |
| * Prints the stack trace as if wasn't overridden by {@link TemplateException}. |
| * @since 2.3.20 |
| */ |
| public void printStandardStackTrace(PrintStream ps) { |
| super.printStackTrace(ps); |
| } |
| |
| /** |
| * Prints the stack trace as if wasn't overridden by {@link TemplateException}. |
| * @since 2.3.20 |
| */ |
| public void printStandardStackTrace(PrintWriter pw) { |
| super.printStackTrace(pw); |
| } |
| |
| @Override |
| public String getMessage() { |
| if (messageWasAlreadyPrintedForThisTrace != null |
| && messageWasAlreadyPrintedForThisTrace.get() == Boolean.TRUE) { |
| return "[... Exception message was already printed; see it above ...]"; |
| } else { |
| synchronized (lock) { |
| if (message == null) renderMessages(); |
| return message; |
| } |
| } |
| } |
| |
| /** |
| * Similar to {@link #getMessage()}, but it doesn't contain the position of the failing instruction at then end |
| * of the text. It might contains the position of the failing <em>expression</em> though as part of the expression |
| * quotation, as that's the part of the description. |
| */ |
| public String getMessageWithoutStackTop() { |
| synchronized (lock) { |
| if (messageWithoutStackTop == null) renderMessages(); |
| return messageWithoutStackTop; |
| } |
| } |
| |
| /** |
| * 1-based line number of the failing section, or {@code null} if the information is not available. |
| * |
| * @since 2.3.21 |
| */ |
| public Integer getLineNumber() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| calculatePosition(); |
| } |
| return lineNumber; |
| } |
| } |
| |
| /** |
| * Returns the name ({@link Template#getName()}) of the template where the error has occurred, or {@code null} if |
| * the information isn't available. This shouldn't be used for showing the error position; use |
| * {@link #getTemplateSourceName()} instead. |
| * |
| * @deprecated Use {@link #getTemplateSourceName()} instead, unless you are really sure that this is what you want. |
| * This method isn't really deprecated, it's just marked so to warn users about this. |
| * |
| * @since 2.3.21 |
| */ |
| @Deprecated |
| public String getTemplateName() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| calculatePosition(); |
| } |
| return templateName; |
| } |
| } |
| |
| /** |
| * Returns the source name ({@link Template#getSourceName()}) of the template where the error has occurred, or |
| * {@code null} if the information isn't available. This is what should be used for showing the error position. |
| * |
| * @since 2.3.22 |
| */ |
| public String getTemplateSourceName() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| calculatePosition(); |
| } |
| return templateSourceName; |
| } |
| } |
| |
| /** |
| * 1-based column number of the failing section, or {@code null} if the information is not available. |
| * |
| * @since 2.3.21 |
| */ |
| public Integer getColumnNumber() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| calculatePosition(); |
| } |
| return columnNumber; |
| } |
| } |
| |
| /** |
| * 1-based line number of the last line that contains the failing section, or {@code null} if the information is not |
| * available. |
| * |
| * @since 2.3.21 |
| */ |
| public Integer getEndLineNumber() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| calculatePosition(); |
| } |
| return endLineNumber; |
| } |
| } |
| |
| /** |
| * 1-based column number of the last character of the failing template section, or {@code null} if the information |
| * is not available. Note that unlike with Java string API-s, this column number is inclusive. |
| * |
| * @since 2.3.21 |
| */ |
| public Integer getEndColumnNumber() { |
| synchronized (lock) { |
| if (!positionsCalculated) { |
| calculatePosition(); |
| } |
| return endColumnNumber; |
| } |
| } |
| |
| /** |
| * If there was a blamed expression attached to this exception, it returns its canonical form, otherwise it returns |
| * {@code null}. This expression should always be inside the failing FTL instruction. |
| * |
| * <p>The typical application of this is getting the undefined expression from {@link InvalidReferenceException}-s. |
| * |
| * @since 2.3.21 |
| */ |
| public String getBlamedExpressionString() { |
| synchronized (lock) { |
| if (!blamedExpressionStringCalculated) { |
| if (blamedExpression != null) { |
| blamedExpressionString = blamedExpression.getCanonicalForm(); |
| } |
| blamedExpressionStringCalculated = true; |
| } |
| return blamedExpressionString; |
| } |
| } |
| |
| Expression getBlamedExpression() { |
| return blamedExpression; |
| } |
| |
| private void writeObject(ObjectOutputStream out) throws IOException, ClassNotFoundException { |
| // These are calculated from transient fields, so this is the last chance to calculate them: |
| getFTLInstructionStack(); |
| getFTLInstructionStackTopFew(); |
| getDescription(); |
| calculatePosition(); |
| getBlamedExpressionString(); |
| |
| out.defaultWriteObject(); |
| } |
| |
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { |
| lock = new Object(); |
| in.defaultReadObject(); |
| } |
| |
| /** Delegate to a {@link PrintWriter} or to a {@link PrintStream}. */ |
| private interface StackTraceWriter { |
| void print(Object obj); |
| void println(Object obj); |
| void println(); |
| void printStandardStackTrace(Throwable exception); |
| } |
| |
| private static class PrintStreamStackTraceWriter implements StackTraceWriter { |
| |
| private final PrintStream out; |
| |
| PrintStreamStackTraceWriter(PrintStream out) { |
| this.out = out; |
| } |
| |
| public void print(Object obj) { |
| out.print(obj); |
| } |
| |
| public void println(Object obj) { |
| out.println(obj); |
| } |
| |
| public void println() { |
| out.println(); |
| } |
| |
| public void printStandardStackTrace(Throwable exception) { |
| if (exception instanceof TemplateException) { |
| ((TemplateException) exception).printStandardStackTrace(out); |
| } else { |
| exception.printStackTrace(out); |
| } |
| } |
| |
| } |
| |
| private static class PrintWriterStackTraceWriter implements StackTraceWriter { |
| |
| private final PrintWriter out; |
| |
| PrintWriterStackTraceWriter(PrintWriter out) { |
| this.out = out; |
| } |
| |
| public void print(Object obj) { |
| out.print(obj); |
| } |
| |
| public void println(Object obj) { |
| out.println(obj); |
| } |
| |
| public void println() { |
| out.println(); |
| } |
| |
| public void printStandardStackTrace(Throwable exception) { |
| if (exception instanceof TemplateException) { |
| ((TemplateException) exception).printStandardStackTrace(out); |
| } else { |
| exception.printStackTrace(out); |
| } |
| } |
| |
| } |
| |
| } |