Skip to content

Commit

Permalink
feat: allowing multiple exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
yaansz committed Aug 25, 2024
1 parent 3780fe8 commit 8794bdb
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 75 deletions.
4 changes: 2 additions & 2 deletions src/main/java/com/softawii/Main.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.softawii;

import com.softawii.curupira.example.controller.Foo;
import com.softawii.curupira.example.exceptions.ExceptionHandler;
import com.softawii.curupira.example.exceptions.FooExceptionHandler;
import com.softawii.curupira.v2.core.CurupiraBoot;
import com.softawii.curupira.v2.integration.BasicContextProvider;
import net.dv8tion.jda.api.JDA;
Expand All @@ -20,7 +20,7 @@ public static void main(String[] args) throws InterruptedException, NoSuchMethod
String pkg = "com.softawii.curupira.example";

context.registerInstance(Foo.class, new Foo());
context.registerInstance(ExceptionHandler.class, new ExceptionHandler());
context.registerInstance(FooExceptionHandler.class, new FooExceptionHandler());

JDABuilder builder = JDABuilder.createDefault(token);
JDA JDA = builder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public TextLocaleResponse charlie(
@RequestInfo Member member,
@DiscordParameter(name = "poll", description = "pool name") String name) {

throw new RuntimeException("This is a test exception");
throw new NullPointerException("This is a test exception");
// return new TextLocaleResponse("foo.bar.charlie.response.ok", name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,27 @@

import com.softawii.curupira.v2.annotations.DiscordException;
import com.softawii.curupira.v2.annotations.DiscordExceptions;
import com.softawii.curupira.v2.annotations.LocaleType;
import com.softawii.curupira.v2.localization.LocalizationManager;
import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.DiscordLocale;
import net.dv8tion.jda.api.interactions.Interaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;

@DiscordExceptions
public class ExceptionHandler {
@DiscordExceptions(packages = "com.softawii.curupira.example")
public class FooExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionHandler.class);
private static final Logger LOGGER = LoggerFactory.getLogger(FooExceptionHandler.class);

@DiscordException(NullPointerException.class)
public void handle(NullPointerException exception, Interaction interaction) {
LOGGER.error("An error occurred 1", exception);
}

@DiscordException(InvocationTargetException.class)
public void handle(InvocationTargetException exception, Interaction interaction) {
LOGGER.error("An error occurred 2, info, exception: {}, user id: {}, user name: {}", exception.getTargetException(), interaction.getUser().getIdLong(), interaction.getUser().getName());
public void handle(Throwable exception, Interaction interaction, LocalizationManager localization, @LocaleType DiscordLocale locale) {
LOGGER.error("An error occurred 2, info, exception: {}, user id: {}, user name: {}", exception, interaction.getUser().getIdLong(), interaction.getUser().getName());

if(interaction instanceof GenericCommandInteractionEvent event) {
event.reply(exception.getTargetException().getMessage()).queue();
event.reply(localization.getLocalizedString("foo.bar.nullpointer", locale)).queue();
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DiscordExceptions {

String[] packages() default {};
Class<?>[] classes() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,18 @@ public CommandHandler(JDA jda, Object instance, Method method, LocalizationFunct
register();
}

public Class<?> getControllerClass() {
return method.getDeclaringClass();
}

public List<OptionData> getOptions() {
return options;
}

public LocalizationManager getLocalization() {
return localization;
}

public String getFullCommandName() {
DiscordController controllerInfo = method.getDeclaringClass().getAnnotation(DiscordController.class);
DiscordCommand commandInfo = method.getAnnotation(DiscordCommand.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void onCommandInteractionReceived(GenericCommandInteractionEvent event) {
// TODO: Validate Environment
handler.execute(event);
} catch (InvocationTargetException | IllegalAccessException e) {
exceptionMapper.handle(e, event);
exceptionMapper.handle(handler.getControllerClass(), e, event, handler.getLocalization());
}
} else {
this.logger.error("Command not found: {}", event.getFullCommandName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.softawii.curupira.v2.core.exception;

import com.softawii.curupira.v2.annotations.DiscordException;
import com.softawii.curupira.v2.localization.LocalizationManager;
import com.softawii.curupira.v2.parser.DiscordToJavaParser;
import net.dv8tion.jda.api.interactions.Interaction;
import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

public class ExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionHandler.class);

private final Object instance;
private final Map<Class<? extends Throwable>, Method> handlers;

public ExceptionHandler(Object instance) {
this.instance = instance;
this.handlers = new HashMap<>();
scanMethods();
}

private void scanMethods() {
for (Method method : instance.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(DiscordException.class)) {
DiscordException annotation = method.getAnnotation(DiscordException.class);

if(Arrays.stream(method.getParameters()).noneMatch(parameter -> parameter.getType() == Throwable.class)) {
throw new RuntimeException("Invalid handler method signature: " + method.getName());
}

if(Arrays.stream(method.getParameters()).noneMatch(parameter -> parameter.getType() == Interaction.class)) {
throw new RuntimeException("Invalid handler method signature: " + method.getName());
}

for (Class<? extends Throwable> exception : annotation.value()) {
handlers.put(exception, method);
}
}
}
}

private Method getHandler(Class<?> exception) {
Method handler = handlers.get(exception);

if(handler == null) {
for (Class<?> superClass = exception.getSuperclass(); superClass != null; superClass = superClass.getSuperclass()) {
handler = handlers.get(superClass);
if (handler != null) {
break;
}
}
}

return handler;
}

private Object[] getParameters(Method method, Interaction event, LocalizationManager localization) {
List<Object> parameters = new ArrayList<>();

for(Parameter parameter : method.getParameters())
parameters.add(DiscordToJavaParser.getParameterFromEvent(event, parameter, localization));
return parameters.toArray();
}

public void handle(Throwable exception, Interaction interaction, LocalizationManager localization) {
if(exception instanceof InvocationTargetException invocation) {
exception = invocation.getTargetException();
}

Method handler = getHandler(exception.getClass());

if(handler != null) {
try {
handler.invoke(instance, getParameters(handler, interaction, localization));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to invoke handler method: " + handler.getName(), e);
}
} else {
LOGGER.error("Unhandled exception", exception);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.softawii.curupira.v2.annotations.DiscordException;
import com.softawii.curupira.v2.annotations.DiscordExceptions;
import com.softawii.curupira.v2.integration.ContextProvider;
import com.softawii.curupira.v2.localization.LocalizationManager;
import com.softawii.curupira.v2.utils.ScanUtils;
import net.dv8tion.jda.api.interactions.Interaction;
import org.slf4j.Logger;
Expand All @@ -19,13 +20,16 @@

public class ExceptionMapper {
private static final Logger logger = LoggerFactory.getLogger(ExceptionMapper.class);
private final Map<Class<? extends Throwable>, Method> handlers;
private final ContextProvider context;
private Object instance;
private final Map<String, ExceptionHandler> handlerByPackage;
private final Map<Class<?>, ExceptionHandler> handlerByClass;
private ExceptionHandler defaultHandler;

public ExceptionMapper(ContextProvider context, String ... packages) {
this.handlers = new HashMap<>();
this.context = context;
this.handlerByPackage = new HashMap<>();
this.handlerByClass = new HashMap<>();
this.defaultHandler = null;

scanPackages(packages);
}
Expand All @@ -38,60 +42,75 @@ private void scanPackages(String ... packages) {
classes.addAll(ScanUtils.getClassesInPackage(pkg).stream().filter(clazz -> clazz.isAnnotationPresent(DiscordExceptions.class)).toList());
}

if(classes.stream().count() > 1) {
throw new RuntimeException("Only one class can have the DiscordExceptions annotation");
}

if(classes.stream().count() == 1) {
scanClass(classes.stream().findFirst().get());
for(Class<?> clazz : classes) {
scanClass(clazz);
}
}

private void scanClass(Class<?> clazz) {
this.instance = context.getInstance(clazz);
DiscordExceptions annotation = clazz.getAnnotation(DiscordExceptions.class);
ExceptionHandler handler = new ExceptionHandler(context.getInstance(clazz));

for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(DiscordException.class)) {
DiscordException annotation = method.getAnnotation(DiscordException.class);
// define providers
if(annotation.classes().length == 0 && annotation.packages().length == 0) {
defaultHandler = handler;
} else {
for(Class<?> exception : annotation.classes()) {
handlerByClass.put(exception, handler);
}

if(method.getParameters().length != 2 && method.getParameters()[0].getType() != Throwable.class && method.getParameters()[1].getType() != Interaction.class) {
throw new RuntimeException("Invalid handler method signature: " + method.getName());
}
for(String pkg : annotation.packages()) {
handlerByPackage.put(pkg, handler);
}
}
}

private ExceptionHandler findHandlerByCaller(Class<?> caller) {
// 1. option 1 - check if the caller class is in the map
ExceptionHandler handler = handlerByClass.get(caller);

for (Class<? extends Throwable> exception : annotation.value()) {
handlers.put(exception, method);
// 2. option 2 - super classes
if(handler == null) {
for (Class<?> superClass = caller.getSuperclass(); superClass != null; superClass = superClass.getSuperclass()) {
handler = handlerByClass.get(superClass);
if (handler != null) {
break;
}
}
}
}

private Method findHandlerMethod(Class<?> exceptionClass) {
Method handlerMethod = handlers.get(exceptionClass);
if (handlerMethod == null) {
// Check for superclass handlers if a direct one is not found
for (Class<?> superClass = exceptionClass.getSuperclass(); superClass != null; superClass = superClass.getSuperclass()) {
handlerMethod = handlers.get(superClass);
if (handlerMethod != null) {
// 3. option 3 - check if the caller package is in the map
if (handler == null) {
String pkg = caller.getPackageName();
while (!pkg.isEmpty()) {
handler = handlerByPackage.get(pkg);
if (handler != null) {
break;
}

// Move up to the superpackage
int lastDotIndex = pkg.lastIndexOf('.');
if (lastDotIndex == -1) {
break;
}
pkg = pkg.substring(0, lastDotIndex);
}
}
return handlerMethod;

if (handler == null) {
handler = defaultHandler;
}

return handler;
}

public void handle(Throwable exception, Interaction interaction) {
Class<?> exceptionClass = exception.getClass();
Method handlerMethod = findHandlerMethod(exceptionClass);
public void handle(Class<?> caller, Throwable exception, Interaction interaction, LocalizationManager localization) {
ExceptionHandler handler = findHandlerByCaller(caller);

if (handlerMethod != null) {
try {
// Invoke the handler method
handlerMethod.invoke(this.instance, exception, interaction);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to invoke handler method: " + handlerMethod.getName(), e);
}
if(handler != null) {
handler.handle(exception, interaction, localization);
} else {
logger.error("Unhandled exception", exception);
throw new RuntimeException("No handler found for exception: " + exception.getClass().getName());
}
}

Expand Down
Loading

0 comments on commit 8794bdb

Please sign in to comment.