package com.laytonsmith.core.exceptions;
import com.laytonsmith.PureUtilities.Common.ReflectionUtils;
import com.laytonsmith.PureUtilities.Common.StackTraceUtils;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import com.laytonsmith.PureUtilities.TermColors;
import com.laytonsmith.abstraction.MCPlayer;
import com.laytonsmith.abstraction.enums.MCChatColor;
import com.laytonsmith.core.CHLog;
import com.laytonsmith.core.LogLevel;
import com.laytonsmith.core.ObjectGenerator;
import com.laytonsmith.core.Prefs;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.constructs.CArray;
import com.laytonsmith.core.constructs.CClosure;
import com.laytonsmith.core.constructs.CInt;
import com.laytonsmith.core.constructs.CNull;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.environments.CommandHelperEnvironment;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.environments.GlobalEnv;
import com.laytonsmith.core.exceptions.CRE.AbstractCREException;
import com.laytonsmith.core.exceptions.CRE.CRECausedByWrapper;
import com.laytonsmith.core.exceptions.CRE.CREThrowable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A ConfigRuntimeException is the base class for user level exceptions.
*/
public class ConfigRuntimeException extends RuntimeException {
/**
* Creates a new instance of <code>ConfigRuntimeException</code> without
* detail message.
*/
protected ConfigRuntimeException() {
}
/**
* Sets the environment of the exception.
*
* @param env
*/
public void setEnv(Environment env) {
this.env = env;
}
/**
* This returns the environment that was set when the exception was thrown.
* It may be null, though that's due to an incomplete swapover, and should
* be fixed.
*/
public Environment getEnv() {
return this.env;
}
/**
* A reaction is a pre-programmed response to the exception bubbling all the
* way up. One of these reaction types must be set by user code (or defaults
* to REPORT), and the correct action will occur.
*/
public static enum Reaction {
/**
* This exception should be ignored, because a handler dealt with it as
* desired. The plugin is no longer responsible for dealing with this
* exception
*/
IGNORE,
/**
* No handler knew how to deal with this exception, or they chose not to
* handle it. The plugin should handle it by using the default action
* for an uncaught exception
*/
REPORT,
/**
* A handler knew how to deal with this exception, and furthermore, it
* escalated it to a more serious category. Though the behavior may be
* undefined, the plugin should pass the exception up further.
*/
FATAL
}
/**
* If a exception bubbles all the way up to the top, this should be called
* first, to see what reaction the plugin should take. Generally speaking,
* you'll want to use {@link #HandleUncaughtException} instead of this,
* though if you need to take custom action, you can determine the user's
* preferred reaction with this method.
*
* @param e
* @return
*/
public static Reaction GetReaction(ConfigRuntimeException e, Environment env) {
//If there is an exception handler, call it to see what it says.
Reaction reaction = Reaction.REPORT;
if (env.getEnv(GlobalEnv.class).GetExceptionHandler() != null) {
CClosure c = env.getEnv(GlobalEnv.class).GetExceptionHandler();
CArray ex = ObjectGenerator.GetGenerator().exception(e, env, Target.UNKNOWN);
Construct ret = CNull.NULL;
try {
c.execute(new Construct[]{ex});
} catch (FunctionReturnException retException) {
ret = retException.getReturn();
}
if (ret instanceof CNull || Prefs.ScreamErrors()) {
reaction = Reaction.REPORT;
} else {
if (Static.getBoolean(ret)) {
reaction = Reaction.IGNORE;
} else {
reaction = Reaction.FATAL;
}
}
}
return reaction;
}
/**
* Compile errors are always handled with the default mechanism, but to
* standardize error handling, this method must be used.
*
* @param e
* @param optionalMessage
* @param player
*/
public static void HandleUncaughtException(ConfigCompileException e, String optionalMessage, MCPlayer player) {
if (optionalMessage != null) {
DoWarning(optionalMessage);
}
DoReport(e, player);
}
public static void HandleUncaughtException(ConfigCompileGroupException e, MCPlayer player) {
for (ConfigCompileException ce : e.getList()) {
HandleUncaughtException(ce, null, player);
}
}
public static void HandleUncaughtException(ConfigCompileGroupException e, String optionalMessage, MCPlayer player) {
DoWarning(optionalMessage);
HandleUncaughtException(e, player);
}
/**
* If there's nothing special you want to do with the exception, you can
* send it here, and it will take the default action for an uncaught
* exception.
*
* @param e
* @param r
*/
public static void HandleUncaughtException(ConfigRuntimeException e, Environment env) {
HandleUncaughtException(e, env, GetReaction(e, env));
}
/**
* If there's nothing special you want to do with the exception, you can
* send it here, and it will take the default action for an uncaught
* exception.
*
* @param e
* @param r
*/
private static void HandleUncaughtException(ConfigRuntimeException e, Environment env, Reaction r) {
if (r == Reaction.IGNORE) {
//Welp, you heard the man.
CHLog.GetLogger().Log(CHLog.Tags.RUNTIME, LogLevel.DEBUG, "An exception bubbled to the top, but was instructed by an event handler to not cause output.", e.getTarget());
} else if (r == ConfigRuntimeException.Reaction.REPORT) {
ConfigRuntimeException.DoReport(e, env);
} else if (r == ConfigRuntimeException.Reaction.FATAL) {
ConfigRuntimeException.DoReport(e, env);
//Well, here goes nothing
throw e;
}
}
/**
* If the Reaction returned by GetReaction is to report the exception, this
* function should be used to standardize the report format. If the error
* message wouldn't be very useful by itself, or if a hint is desired, an
* optional message may be provided (null otherwise).
*
* @param e
* @param optionalMessage
*/
@SuppressWarnings("ThrowableResultIgnored")
private static void DoReport(String message, String exceptionType, ConfigRuntimeException ex, List<StackTraceElement> stacktrace, MCPlayer currentPlayer) {
String type = exceptionType;
if (exceptionType == null) {
type = "FATAL";
}
List<StackTraceElement> st = new ArrayList<>(stacktrace);
if (message == null) {
message = "";
}
if (!"".equals(message.trim())) {
message = ": " + message;
}
Target top = Target.UNKNOWN;
for (StackTraceElement e : st) {
Target t = e.getDefinedAt();
if (top == Target.UNKNOWN) {
top = t;
}
}
StringBuilder log = new StringBuilder();
StringBuilder console = new StringBuilder();
StringBuilder player = new StringBuilder();
PrintMessage(log, console, player, type, message, ex, st);
if(ex != null){
// Otherwise, a CCE
if(ex.getCause() != null && ex.getCause() instanceof ConfigRuntimeException){
ex = (ConfigRuntimeException) ex.getCause();
}
while(ex instanceof CRECausedByWrapper) {
Target t = ex.getTarget();
log.append("Caused by:\n");
console.append(TermColors.CYAN).append("Caused by:\n");
player.append(MCChatColor.AQUA).append("Caused by:\n");
CArray exception = ((CRECausedByWrapper) ex).getException();
CArray stackTrace = Static.getArray(exception.get("stackTrace", t), t);
List<StackTraceElement> newSt = new ArrayList<>();
for(Construct consElement : stackTrace.asList()){
CArray element = Static.getArray(consElement, t);
int line = Static.getInt32(element.get("line", t), t);
File file = new File(element.get("file", t).val());
int col = 0; // This will need updating eventually
Target stElementTarget = new Target(line, file, col);
newSt.add(new StackTraceElement(element.get("id", t).val(), stElementTarget));
}
String nType = exception.get("classType", t).val();
String nMessage = exception.get("message", t).val();
if (!"".equals(nMessage.trim())) {
nMessage = ": " + nMessage;
}
PrintMessage(log, console, player, nType, nMessage, ex, newSt);
ex = (ConfigRuntimeException) ex.getCause();
}
}
//Log
//Don't log to screen though, since we're ALWAYS going to do that ourselves.
CHLog.GetLogger().Log("COMPILE ERROR".equals(exceptionType) ? CHLog.Tags.COMPILER : CHLog.Tags.RUNTIME,
LogLevel.ERROR, log.toString(), top, false);
//Console
StreamUtils.GetSystemOut().println(console.toString() + TermColors.reset());
//Player
if (currentPlayer != null) {
currentPlayer.sendMessage(player.toString());
}
}
private static void PrintMessage(StringBuilder log, StringBuilder console, StringBuilder player, String type, String message, Throwable ex, List<StackTraceElement> st){
log.append(type).append(message).append("\n");
console.append(TermColors.RED).append(type).append(TermColors.WHITE).append(message).append("\n");
player.append(MCChatColor.RED).append(type).append(MCChatColor.WHITE).append(message).append("\n");
for (StackTraceElement e : st) {
Target t = e.getDefinedAt();
String proc = e.getProcedureName();
File file = t.file();
int line = t.line();
int column = t.col();
String filepath;
String simplepath;
if (file == null) {
filepath = simplepath = "Unknown Source";
} else {
filepath = file.getPath();
simplepath = file.getName();
}
log.append("\t").append(proc).append(":").append(filepath).append(":")
.append(line)/*.append(".")
.append(column)*/.append("\n");
console.append("\t").append(TermColors.GREEN).append(proc)
.append(TermColors.WHITE).append(":")
.append(TermColors.YELLOW).append(filepath)
.append(TermColors.WHITE).append(":")
.append(TermColors.CYAN).append(line)/*.append(".").append(column)*/.append("\n");
player.append("\t").append(MCChatColor.GREEN).append(proc)
.append(MCChatColor.WHITE).append(":")
.append(MCChatColor.YELLOW).append(simplepath)
.append(MCChatColor.WHITE).append(":")
.append(MCChatColor.AQUA).append(line)/*.append(".").append(column)*/.append("\n");
}
}
private static void DoReport(ConfigRuntimeException e, Environment env) {
MCPlayer p = null;
if (e.getEnv() != null && e.getEnv().getEnv(CommandHelperEnvironment.class).GetPlayer() != null) {
p = e.getEnv().getEnv(CommandHelperEnvironment.class).GetPlayer();
}
List<StackTraceElement> st = new ArrayList<>();
if(e instanceof AbstractCREException){
st = ((AbstractCREException)e).getCREStackTrace();
}
DoReport(e.getMessage(), AbstractCREException.getExceptionName(e), e, st, p);
if (Prefs.DebugMode()) {
if (e.getCause() != null && !(e.getCause() instanceof CRECausedByWrapper)) {
//This is more of a system level exception, so if debug mode is on, we also want to print this stack trace
StreamUtils.GetSystemErr().println("The previous MethodScript error had an attached cause:");
e.getCause().printStackTrace(StreamUtils.GetSystemErr());
}
if (e.getTarget().equals(Target.UNKNOWN)) {
//This should never happen, but there are still some hard to track
//down bugs that cause this. If it does happen, we want to print out
//a stacktrace from here, which *might* assist in fixing the error
//messages to provide a proper target.
StreamUtils.GetSystemErr().println("Since the exception has an unknown code target, here is additional information that may help:");
StreamUtils.GetSystemErr().println(StackTraceUtils.GetStacktrace(new Exception()));
}
}
}
private static void DoReport(ConfigCompileException e, MCPlayer player) {
List<StackTraceElement> st = new ArrayList<StackTraceElement>();
st.add(0, new StackTraceElement("", e.getTarget()));
DoReport(e.getMessage(), "COMPILE ERROR", null, st, player);
}
/**
* Shorthand for DoWarning(exception, null, true);
*
* @param e
*/
public static void DoWarning(Exception e) {
DoWarning(e, null, true);
}
/**
* Shorthand for DoWarning(null, message, true);
*
* @param optionalMessage
*/
public static void DoWarning(String optionalMessage) {
DoWarning(null, optionalMessage, true);
}
/**
* To standardize the warning messages displayed, this function should be
* used. It checks the preference setting for warnings to see if the warning
* should be shown to begin with, if checkPref is true. The exception is
* simply used to get an error message, and is otherwise unused. If the
* exception is a ConfigRuntimeException, it is displayed specially
* (including line number and file)
*
* @param e
* @param optionalMessage
* @throws NullPointerException If both the exception and message are null
* (or empty)
*/
public static void DoWarning(Exception e, String optionalMessage, boolean checkPrefs) {
if (e == null && (optionalMessage == null || optionalMessage.isEmpty())) {
throw new NullPointerException("Both the exception and the message cannot be empty");
}
if (!checkPrefs || Prefs.ShowWarnings()) {
String exceptionMessage = "";
Target t = Target.UNKNOWN;
if (e instanceof ConfigRuntimeException) {
ConfigRuntimeException cre = (ConfigRuntimeException) e;
exceptionMessage = MCChatColor.YELLOW + cre.getMessage()
+ MCChatColor.WHITE + " :: " + MCChatColor.GREEN
+ AbstractCREException.getExceptionName(cre) + MCChatColor.WHITE + ":"
+ MCChatColor.YELLOW + cre.target.file() + MCChatColor.WHITE + ":"
+ MCChatColor.AQUA + cre.target.line();
t = cre.getTarget();
} else if (e != null) {
exceptionMessage = MCChatColor.YELLOW + e.getMessage();
}
String message = exceptionMessage + MCChatColor.WHITE + optionalMessage;
CHLog.GetLogger().Log(CHLog.Tags.GENERAL, LogLevel.WARNING, Static.MCToANSIColors(message) + TermColors.reset(), t);
//Warnings are not shown to players ever
}
}
private Environment env;
private Target target;
/**
* Creates a new ConfigRuntimeException. If the exception is intended to be
* uncatchable, use {@link #CreateUncatchableException} instead.
*
* @param msg The message to be displayed
* @param t The code target this exception is being thrown from
* @deprecated Use the {@link #BuildException(java.lang.String, com.laytonsmith.core.functions.Exceptions.ExceptionType, com.laytonsmith.core.constructs.Target) } method instead.
*/
@Deprecated
public ConfigRuntimeException(String msg, Target t) {
this(msg, t, null);
}
/**
* Creates a new ConfigRuntimeException. If the exception is intended to be
* uncatchable, use {@link #CreateUncatchableException(java.lang.String, com.laytonsmith.core.constructs.Target, java.lang.Throwable) }
* instead.
* @param msg The message to be displayed
* @param t The code target this exception is being thrown from
* @param cause The chained cause. This is not used for normal execution, but is helpful
* when debugging errors. Where exceptions are triggered by Java code (as opposed to organic
* MethodScript errors) this version should always be preferred.
* @deprecated Use the {@link #BuildException(java.lang.String, com.laytonsmith.core.functions.Exceptions.ExceptionType, com.laytonsmith.core.constructs.Target, java.lang.Throwable) } method instead.
*/
@Deprecated
public ConfigRuntimeException(String msg, Target t, Throwable cause) {
super(msg, cause);
createException(t);
}
private void createException(Target t) {
this.target = t;
}
public void setTarget(Target t){
this.target = t;
}
// public ConfigRuntimeException(String msg, ExceptionType ex, int line_num){
// this(msg, ex, line_num, null);
// }
// public ConfigRuntimeException(String msg, int line_num){
// this(msg, null, line_num, null);
// }
/**
* Creates an uncatchable exception. This should rarely be used. An
* uncatchable exception is one where the user code is unable to catch the
* exception, and the type of the exception is null. Generally, this should
* only be used for completely fatal errors, for instance, a break/continue
* being used in top level code, or other user errors that would be compile
* errors, except the facilities aren't there yet for catching such an error
* in the compiler, or for errors in MethodScript itself.
*
* @param msg
* @param t
* @return Returns an uncatchable exception.
*/
public static ConfigRuntimeException CreateUncatchableException(String msg, Target t) {
return new ConfigRuntimeException(msg, t, null);
}
/**
* Creates an uncatchable exception with a cause. This should rarely be
* used. An uncatchable exception is one where the user code is unable to
* catch the exception, and the type of the exception is null. Generally,
* this should only be used for completely fatal errors, for instance, a
* break/continue being used in top level code, or other user errors that
* would be compile errors, except the facilities aren't there yet for
* catching such an error in the compiler, or for errors in MethodScript
* itself.
*
* @param msg
* @param t
* @param cause
* @return Returns an uncatchable exception
*/
public static ConfigRuntimeException CreateUncatchableException(String msg, Target t, Throwable cause) {
return new ConfigRuntimeException(msg, t, cause);
}
/**
* Gets the code target for this exception.
*
* @return
*/
public Target getTarget() {
return this.target;
}
/**
* Gets the shorter name of the file, or null if no file has been set.
*
* @return
*/
public String getSimpleFile() {
if (this.target.file() != null) {
return this.target.file().getName();
} else {
return null;
}
}
/**
* A stacktrace contains 1 or more stack trace elements. A new stacktrace
* element is added each time an exception bubbles up past a procedure.
*/
public static class StackTraceElement {
private final String procedureName;
private Target definedAt;
/**
* Creates a new StackTraceElement.
*
* @param procedureName The name of the procedure
* @param definedAt The code target where the procedure is defined at.
*/
public StackTraceElement(String procedureName, Target definedAt) {
this.procedureName = procedureName;
this.definedAt = definedAt;
}
/**
* Gets the name of the procedure.
*
* @return
*/
public String getProcedureName() {
return procedureName;
}
/**
* Gets the code target where the procedure is defined at.
*
* @return
*/
public Target getDefinedAt() {
return definedAt;
}
@Override
public String toString() {
return procedureName + " (Defined at " + definedAt + ")";
}
public CArray getObjectFor(){
CArray element = new CArray(Target.UNKNOWN);
element.set("id", getProcedureName());
try {
String name = "Unknown file";
if(getDefinedAt().file() != null){
name = getDefinedAt().file().getCanonicalPath();
}
element.set("file", name);
} catch (IOException ex) {
// This shouldn't happen, but if it does, we want to fall back to something marginally useful
String name = "Unknown file";
if(getDefinedAt().file() != null){
name = getDefinedAt().file().getAbsolutePath();
}
element.set("file", getDefinedAt().file().getAbsolutePath());
}
element.set("line", new CInt(getDefinedAt().line(), Target.UNKNOWN), Target.UNKNOWN);
return element;
}
/**
* In general, only the core elements should change this
* @param target
*/
void setDefinedAt(Target target) {
definedAt = target;
}
}
}