blob: d8cd92d222f367d3e69573cc7cc089b345ade388 [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 org.apache.commons.jexl3;
import org.apache.commons.jexl3.internal.Debugger;
import org.apache.commons.jexl3.parser.JavaccError;
import org.apache.commons.jexl3.parser.JexlNode;
import org.apache.commons.jexl3.parser.ParseException;
import org.apache.commons.jexl3.parser.TokenMgrError;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.List;
/**
* Wraps any error that might occur during interpretation of a script or expression.
*
* @since 2.0
*/
public class JexlException extends RuntimeException {
/** The point of origin for this exception. */
private final transient JexlNode mark;
/** The debug info. */
private final transient JexlInfo info;
/** Maximum number of characters around exception location. */
private static final int MAX_EXCHARLOC = 42;
/**
* Creates a new JexlException.
*
* @param node the node causing the error
* @param msg the error message
*/
public JexlException(JexlNode node, String msg) {
this(node, msg, null);
}
/**
* Creates a new JexlException.
*
* @param node the node causing the error
* @param msg the error message
* @param cause the exception causing the error
*/
public JexlException(JexlNode node, String msg, Throwable cause) {
super(msg != null ? msg : "", unwrap(cause));
if (node != null) {
mark = node;
info = node.jexlInfo();
} else {
mark = null;
info = null;
}
}
/**
* Creates a new JexlException.
*
* @param jinfo the debugging information associated
* @param msg the error message
* @param cause the exception causing the error
*/
public JexlException(JexlInfo jinfo, String msg, Throwable cause) {
super(msg != null ? msg : "", unwrap(cause));
mark = null;
info = jinfo;
}
/**
* Gets the specific information for this exception.
*
* @return the information
*/
public JexlInfo getInfo() {
return getInfo(mark, info);
}
/**
* Creates a string builder pre-filled with common error information (if possible).
*
* @param node the node
* @return a string builder
*/
private static StringBuilder errorAt(JexlNode node) {
JexlInfo info = node != null? getInfo(node, node.jexlInfo()) : null;
StringBuilder msg = new StringBuilder();
if (info != null) {
msg.append(info.toString());
} else {
msg.append("?:");
}
msg.append(' ');
return msg;
}
/**
* Gets the most specific information attached to a node.
*
* @param node the node
* @param info the information
* @return the information or null
*/
public static JexlInfo getInfo(JexlNode node, JexlInfo info) {
if (info != null && node != null) {
final Debugger dbg = new Debugger();
if (dbg.debug(node)) {
return new JexlInfo(info) {
@Override
public JexlInfo.Detail getDetail() {
return dbg;
}
};
}
}
return info;
}
/**
* Cleans a JexlException from any org.apache.commons.jexl3.internal stack trace element.
*
* @return this exception
*/
public JexlException clean() {
return clean(this);
}
/**
* Cleans a Throwable from any org.apache.commons.jexl3.internal stack trace element.
*
* @param <X> the throwable type
* @param xthrow the thowable
* @return the throwable
*/
private static <X extends Throwable> X clean(X xthrow) {
if (xthrow != null) {
List<StackTraceElement> stackJexl = new ArrayList<StackTraceElement>();
for (StackTraceElement se : xthrow.getStackTrace()) {
String className = se.getClassName();
if (!className.startsWith("org.apache.commons.jexl3.internal")
&& !className.startsWith("org.apache.commons.jexl3.parser")) {
stackJexl.add(se);
}
}
xthrow.setStackTrace(stackJexl.toArray(new StackTraceElement[stackJexl.size()]));
}
return xthrow;
}
/**
* Unwraps the cause of a throwable due to reflection.
*
* @param xthrow the throwable
* @return the cause
*/
private static Throwable unwrap(Throwable xthrow) {
if (xthrow instanceof InvocationTargetException) {
return xthrow.getCause();
} else if (xthrow instanceof UndeclaredThrowableException) {
return xthrow.getCause();
} else {
return xthrow;
}
}
/**
* Merge the node info and the cause info to obtain best possible location.
*
* @param info the node
* @param cause the cause
* @return the info to use
*/
private static JexlInfo merge(JexlInfo info, JavaccError cause) {
JexlInfo dbgn = info != null ? info : null;
if (cause == null) {
return dbgn;
} else if (dbgn == null) {
return new JexlInfo("", cause.getLine(), cause.getColumn());
} else {
return new JexlInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
}
}
/**
* Accesses detailed message.
*
* @return the message
*/
protected String detailedMessage() {
return super.getMessage();
}
/**
* Formats an error message from the parser.
*
* @param prefix the prefix to the message
* @param expr the expression in error
* @return the formatted message
*/
protected String parserError(String prefix, String expr) {
int length = expr.length();
if (length < MAX_EXCHARLOC) {
return prefix + " error in '" + expr + "'";
} else {
int begin = info.getColumn();
int end = begin + (MAX_EXCHARLOC / 2);
begin -= (MAX_EXCHARLOC / 2);
if (begin < 0) {
end -= begin;
begin = 0;
}
return prefix + " error near '... "
+ expr.substring(begin, end > length ? length : end) + " ...'";
}
}
/**
* Thrown when tokenization fails.
*
* @since 3.0
*/
public static class Tokenization extends JexlException {
/**
* Creates a new Tokenization exception instance.
* @param info the location info
* @param cause the javacc cause
*/
public Tokenization(JexlInfo info, TokenMgrError cause) {
super(merge(info, cause), cause.getAfter(), null);
}
/**
* @return the specific detailed message
*/
public String getDetail() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return parserError("tokenization", getDetail());
}
}
/**
* Thrown when parsing fails.
*
* @since 3.0
*/
public static class Parsing extends JexlException {
/**
* Creates a new Parsing exception instance.
*
* @param info the location information
* @param cause the javacc cause
*/
public Parsing(JexlInfo info, ParseException cause) {
super(merge(info, cause), cause.getAfter(), null);
}
/**
* Creates a new Parsing exception instance.
*
* @param info the location information
* @param msg the message
*/
public Parsing(JexlInfo info, String msg) {
super(info, msg, null);
}
/**
* @return the specific detailed message
*/
public String getDetail() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return parserError("parsing", getDetail());
}
}
/**
* Thrown when parsing fails due to an ambiguous statement.
*
* @since 3.0
*/
public static class Ambiguous extends Parsing {
/**
* Creates a new Ambiguous statement exception instance.
* @param info the location information
* @param expr the source expression line
*/
public Ambiguous(JexlInfo info, String expr) {
super(info, expr);
}
@Override
protected String detailedMessage() {
return parserError("ambiguous statement", getDetail());
}
}
/**
* Thrown when parsing fails due to an invalid assigment.
*
* @since 3.0
*/
public static class Assignment extends Parsing {
/**
* Creates a new Assignment statement exception instance.
*
* @param info the location information
* @param expr the source expression line
*/
public Assignment(JexlInfo info, String expr) {
super(info, expr);
}
@Override
protected String detailedMessage() {
return parserError("assignment", getDetail());
}
}
/**
* Thrown when a variable is unknown.
*
* @since 3.0
*/
public static class Variable extends JexlException {
/**
* Undefined variable flag.
*/
private final boolean undefined;
/**
* Creates a new Variable exception instance.
*
* @param node the offending ASTnode
* @param var the unknown variable
* @param undef whether the variable is undefined or evaluated as null
*/
public Variable(JexlNode node, String var, boolean undef) {
super(node, var, null);
undefined = undef;
}
/**
* Whether the variable causing an error is undefined or evaluated as null.
*
* @return true if undefined, false otherwise
*/
public boolean isUndefined() {
return undefined;
}
/**
* @return the variable name
*/
public String getVariable() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return (undefined? "undefined" : "null value") + " variable " + getVariable();
}
}
/**
* Generates a message for a variable error.
*
* @param node the node where the error occurred
* @param variable the variable
* @param undef whether the variable is null or undefined
* @return the error message
*/
public static String variableError(JexlNode node, String variable, boolean undef) {
StringBuilder msg = errorAt(node);
if (undef) {
msg.append("undefined");
} else {
msg.append("null value");
}
msg.append(" variable ");
msg.append(variable);
return msg.toString();
}
/**
* Thrown when a property is unknown.
*
* @since 3.0
*/
public static class Property extends JexlException {
/**
* Creates a new Property exception instance.
*
* @param node the offending ASTnode
* @param var the unknown variable
* @param cause the exception causing the error
*/
public Property(JexlNode node, String var, Throwable cause) {
super(node, var, cause);
}
/**
* @return the property name
*/
public String getProperty() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return "unsolvable property '" + getProperty() + "'";
}
}
/**
* Generates a message for an unsolvable property error.
*
* @param node the node where the error occurred
* @param var the variable
* @return the error message
*/
public static String propertyError(JexlNode node, String var) {
StringBuilder msg = errorAt(node);
msg.append("unsolvable property '");
msg.append(var);
msg.append('\'');
return msg.toString();
}
/**
* Thrown when a method or ctor is unknown, ambiguous or inaccessible.
*
* @since 3.0
*/
public static class Method extends JexlException {
/**
* Creates a new Method exception instance.
*
* @param node the offending ASTnode
* @param name the method name
*/
public Method(JexlNode node, String name) {
super(node, name);
}
/**
* Creates a new Method exception instance.
*
* @param info the location information
* @param name the unknown method
* @param cause the exception causing the error
*/
public Method(JexlInfo info, String name, Throwable cause) {
super(info, name, cause);
}
/**
* @return the method name
*/
public String getMethod() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return "unsolvable function/method '" + getMethod() + "'";
}
}
/**
* Generates a message for a unsolvable method error.
*
* @param node the node where the error occurred
* @param method the method name
* @return the error message
*/
public static String methodError(JexlNode node, String method) {
StringBuilder msg = errorAt(node);
msg.append("unsolvable function/method '");
msg.append(method);
msg.append('\'');
return msg.toString();
}
/**
* Thrown when an operator fails.
*
* @since 3.0
*/
public static class Operator extends JexlException {
/**
* Creates a new Operator exception instance.
*
* @param node the location information
* @param symbol the operator name
* @param cause the exception causing the error
*/
public Operator(JexlNode node, String symbol, Throwable cause) {
super(node, symbol, cause);
}
/**
* @return the method name
*/
public String getSymbol() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return "error calling operator '" + getSymbol() + "'";
}
}
/**
* Generates a message for an operator error.
*
* @param node the node where the error occurred
* @param symbol the operator name
* @return the error message
*/
public static String operatorError(JexlNode node, String symbol) {
StringBuilder msg = errorAt(node);
msg.append("error calling operator '");
msg.append(symbol);
msg.append('\'');
return msg.toString();
}
/**
* Thrown when an annotation handler throws an exception.
*
* @since 3.1
*/
public static class Annotation extends JexlException {
/**
* Creates a new Annotation exception instance.
*
* @param node the annotated statement node
* @param name the annotation name
* @param cause the exception causing the error
*/
public Annotation(JexlNode node, String name, Throwable cause) {
super(node, name, cause);
}
/**
* @return the annotation name
*/
public String getAnnotation() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return "error processing annotation '" + getAnnotation() + "'";
}
}
/**
* Generates a message for an annotation error.
*
* @param node the node where the error occurred
* @param annotation the annotation name
* @return the error message
*/
public static String annotationError(JexlNode node, String annotation) {
StringBuilder msg = errorAt(node);
msg.append("error processing annotation '");
msg.append(annotation);
msg.append('\'');
return msg.toString();
}
/**
* Thrown to return a value.
*
* @since 3.0
*/
public static class Return extends JexlException {
/** The returned value. */
private final Object result;
/**
* Creates a new instance of Return.
*
* @param node the return node
* @param msg the message
* @param value the returned value
*/
public Return(JexlNode node, String msg, Object value) {
super(node, msg, null);
this.result = value;
}
/**
* @return the returned value
*/
public Object getValue() {
return result;
}
}
/**
* Thrown to cancel a script execution.
*
* @since 3.0
*/
public static class Cancel extends JexlException {
/**
* Creates a new instance of Cancel.
*
* @param node the node where the interruption was detected
*/
public Cancel(JexlNode node) {
super(node, "execution cancelled", null);
}
}
/**
* Thrown to break a loop.
*
* @since 3.0
*/
public static class Break extends JexlException {
/**
* Creates a new instance of Break.
*
* @param node the break
*/
public Break(JexlNode node) {
super(node, "break loop", null);
}
}
/**
* Thrown to continue a loop.
*
* @since 3.0
*/
public static class Continue extends JexlException {
/**
* Creates a new instance of Continue.
*
* @param node the continue
*/
public Continue(JexlNode node) {
super(node, "continue loop", null);
}
}
/**
* Detailed info message about this error.
* Format is "debug![begin,end]: string \n msg" where:
* <ul></ul>
* - debug is the debugging information if it exists (@link JexlEngine.setDebug)
* - begin, end are character offsets in the string for the precise location of the error
* - string is the string representation of the offending expression
* - msg is the actual explanation message for this error
*
* @return this error as a string
*/
@Override
public String getMessage() {
StringBuilder msg = new StringBuilder();
if (info != null) {
msg.append(info.toString());
} else {
msg.append("?:");
}
msg.append(' ');
msg.append(detailedMessage());
Throwable cause = getCause();
if (cause instanceof JexlArithmetic.NullOperand) {
msg.append(" caused by null operand");
}
return msg.toString();
}
}