blob: 96fb5b73410609a54ad1ac61bc90e809d665b4b4 [file] [log] [blame]
/*
* 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);
}
}
}
}