/*
* Copyright 2016 Nabarun Mondal
* Licensed 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 com.noga.njexl.lang;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import com.noga.njexl.lang.parser.JexlNode;
import com.noga.njexl.lang.parser.ParseException;
import com.noga.njexl.lang.parser.TokenMgrError;
/**
* 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. */
protected final transient JexlNode mark;
/** The debug info. */
protected final transient JexlInfo info;
/** A marker to use in NPEs stating a null operand error. */
public static final String NULL_OPERAND = "jexl.null";
/** Minimum number of characters around exception location. */
private static final int MIN_EXCHARLOC = 5;
/** Maximum number of characters around exception location. */
private static final int MAX_EXCHARLOC = 10;
public String getFaultyCode(){
Throwable cause = getCause();
String myCause = "" ;
if ( cause != null ) {
if (cause instanceof JexlException) {
myCause = ((JexlException) cause).getFaultyCode();
} else {
myCause = cause.toString();
if ( myCause.contains(".njexl.")){
myCause = "" ; // ignore
}
}
}
if ( !myCause.isEmpty() ){
myCause = "\nCaused By : " + myCause ;
}
Throwable e = info.debugInfo().error() ;
if ( e == null && mark!= null ) {
return detailedMessage() + " : " + mark.locationInfo() + myCause ;
}
if ( e != null ){
return e.getMessage() + myCause ;
}
return info.debugInfo().toString();
}
/**
* Creates a new JexlException.
* @param node the node causing the error
* @param msg the error message
*/
public JexlException(JexlNode node, String msg) {
super(msg);
mark = node;
info = node != null ? node.debugInfo() : 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, unwrap(cause));
mark = node;
info = node != null ? node.debugInfo() : null;
}
/**
* Creates a new JexlException.
* @param dbg the debugging information associated
* @param msg the error message
*/
public JexlException(JexlInfo dbg, String msg) {
super(msg);
mark = null;
info = dbg;
}
/**
* Creates a new JexlException.
* @param dbg the debugging information associated
* @param msg the error message
* @param cause the exception causing the error
*/
public JexlException(JexlInfo dbg, String msg, Throwable cause) {
super(msg, unwrap(cause));
mark = null;
info = dbg;
}
/**
* 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 ((InvocationTargetException) xthrow).getTargetException();
} else if (xthrow instanceof UndeclaredThrowableException) {
return ((UndeclaredThrowableException) xthrow).getUndeclaredThrowable();
} else {
return xthrow;
}
}
/**
* Accesses detailed message.
* @return the message
* @since 2.1
*/
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
* @since 2.1
*/
protected String parserError(String prefix, String expr) {
int begin = info.debugInfo().getColumn();
int end = begin + MIN_EXCHARLOC;
begin -= MIN_EXCHARLOC;
if (begin < 0) {
end += MIN_EXCHARLOC;
begin = 0;
}
int length = expr.length();
// the begin check was missing : fixed that
if (length < MAX_EXCHARLOC || begin >= length ) {
return prefix + " error in '" + expr + "'";
} else {
return prefix + " error near '... "
+ expr.substring(begin, end > length ? length : end) + " ...'";
}
}
/**
* Thrown when tokenization fails.
* @since 2.1
*/
public static class Tokenization extends JexlException {
/**
* Creates a new Tokenization exception instance.
* @param node the location info
* @param expr the expression
* @param cause the javacc cause
*/
public Tokenization(JexlInfo node, CharSequence expr, TokenMgrError cause) {
super(merge(node, cause), expr.toString(), cause);
}
/**
* Merge the node info and the cause info to obtain best possible location.
* @param node the node
* @param cause the cause
* @return the info to use
*/
private static DebugInfo merge(JexlInfo node, TokenMgrError cause) {
DebugInfo dbgn = node != null ? node.debugInfo() : null;
if (cause == null) {
return dbgn;
} else if (dbgn == null) {
return new DebugInfo("", cause.getLine(), cause.getColumn(),cause);
} else {
return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn(),cause);
}
}
/**
* @return the expression
*/
public String getExpression() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return parserError("tokenization", getExpression());
}
}
/**
* Thrown when parsing fails.
* @since 2.1
*/
public static class Parsing extends JexlException {
/**
* Creates a new Variable exception instance.
* @param node the offending ASTnode
* @param expr the offending source
* @param cause the javacc cause
*/
public Parsing(JexlInfo node, CharSequence expr, ParseException cause) {
super(merge(node, cause), expr.toString(), cause);
}
/**
* Merge the node info and the cause info to obtain best possible location.
* @param node the node
* @param cause the cause
* @return the info to use
*/
private static DebugInfo merge(JexlInfo node, ParseException cause) {
DebugInfo dbgn = node != null ? node.debugInfo() : null;
if (cause == null) {
return dbgn;
} else if (dbgn == null) {
return new DebugInfo("", cause.getLine(), cause.getColumn());
} else {
return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn(),cause);
}
}
/**
* @return the expression
*/
public String getExpression() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return parserError("parsing", getExpression());
}
}
/**
* Thrown when a variable is unknown.
* @since 2.1
*/
public static class Variable extends JexlException {
/**
* Creates a new Variable exception instance.
* @param node the offending ASTnode
* @param var the unknown variable
*/
public Variable(JexlNode node, String var) {
super(node, var);
}
/**
* @return the variable name
*/
public String getVariable() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return String.format("undefined variable : '%s' ", getVariable());
}
}
/**
* Thrown when a property is unknown.
* @since 2.1
*/
public static class Property extends JexlException {
/**
* Creates a new Property exception instance.
* @param node the offending ASTnode
* @param var the unknown variable
*/
public Property(JexlNode node, String var) {
super(node, var);
}
/**
* @return the property name
*/
public String getProperty() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
if ( mark == null ){
return "";
}
Debugger d = new Debugger();
String a = d.data(mark);
String m = " inaccessible or unknown property access at : '%s' " ;
return String.format( m , a );
}
}
/**
* Thrown when a method or ctor is unknown, ambiguous or inaccessible.
* @since 2.1
*/
public static class Method extends JexlException {
/**
* Creates a new Method exception instance.
* @param node the offending ASTnode
* @param name the unknown method
*/
public Method(JexlNode node, String name) {
super(node, name);
}
/**
* @return the method name
*/
public String getMethod() {
return super.detailedMessage();
}
@Override
protected String detailedMessage() {
return "unknown, ambiguous or inaccessible method " + getMethod();
}
}
/**
* Thrown to return a value.
* @since 2.1
*/
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);
this.result = value;
}
/**
* @return the returned value
*/
public Object getValue() {
return result;
}
}
/**
* Thrown to cancel a script execution.
* @since 2.1
*/
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 continue in a loop
* @since 0.1
*/
public static class Continue extends JexlException {
public final Object value;
public final boolean hasValue;
/**
* Creates a new instance of Continue.
* @param node the node where the Continue was detected
* @param value the value of the continue
*/
public Continue(JexlNode node, Object value) {
super(node, "loop continued", null);
this.value = value ;
this.hasValue = true ;
}
/**
* Creates a new instance of Continue.
* @param node the node where the Continue was detected
*/
public Continue(JexlNode node) {
super(node, "loop continued", null);
this.value = null ;
this.hasValue = false ;
}
}
/**
* Thrown to break from a loop
* @since 0.1
*/
public static class Break extends JexlException {
public final Object value;
public final boolean hasValue;
/**
* Creates a new instance of Break.
* @param node the node where the Break was detected
* @param value the value of the break
*/
public Break(JexlNode node, Object value) {
super(node, "loop broken", null);
this.value = value ;
this.hasValue = true ;
}
/**
* Creates a new instance of Break.
* @param node the node where the Break was detected
*/
public Break(JexlNode node) {
super(node, "loop broken", null);
this.value = null ;
this.hasValue = false ;
}
}
/**
* Thrown to jump from a location of script to another
* @since 0.1
*/
public static class Jump extends JexlException {
public final int location;
public final boolean jump;
/**
* Creates a new instance of Jump.
* @param node the node from where the jump should take place
* @param location the location of the jump
*/
public Jump(JexlNode node, int location) {
super(node, "Jump Took Place", null);
this.location = location ;
this.jump = true ;
}
/**
* Creates a new instance of Jump.
* @param node the node where the Jump was detected
*/
public Jump(JexlNode node) {
super(node, "Jump Did not take Place!", null);
this.location = -1 ;
this.jump = false ;
}
}
/**
* Gets information about the cause of this error.
* <p>
* The returned string represents the outermost expression in error.
* The info parameter, an int[2] optionally provided by the caller, will be filled with the begin/end offset
* characters of the precise error's trigger.
* </p>
* @param offsets character offset interval of the precise node triggering the error
* @return a string representation of the offending expression, the empty string if it could not be determined
*/
public String getInfo(int[] offsets) {
Debugger dbg = new Debugger();
if (dbg.debug(mark)) {
if (offsets != null && offsets.length >= 2) {
offsets[0] = dbg.start();
offsets[1] = dbg.end();
}
return dbg.data();
}
return "";
}
/**
* Detailed info message about this error.
* Format is "debug![begin,end]: string \n msg" where:
* - 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() {
Debugger dbg = new Debugger();
StringBuilder msg = new StringBuilder();
if (info != null) {
msg.append(info.debugString());
}
if (dbg.debug(mark)) {
msg.append("![");
msg.append(dbg.start());
msg.append(",");
msg.append(dbg.end());
msg.append("]: '");
msg.append(dbg.data());
msg.append("'");
}
msg.append(' ');
msg.append(detailedMessage());
Throwable cause = getCause();
if (cause != null && NULL_OPERAND == cause.getMessage()) {
msg.append(" caused by null operand");
}
return msg.toString();
}
}