Skip to content

Commit e828c3a

Browse files
authored
feature(annotations) @ExceptionHandler methods (#538)
1 parent 4169901 commit e828c3a

7 files changed

Lines changed: 371 additions & 1 deletion

File tree

cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import cloud.commandframework.Command;
2727
import cloud.commandframework.CommandComponent;
2828
import cloud.commandframework.CommandManager;
29+
import cloud.commandframework.annotations.exception.ExceptionHandler;
30+
import cloud.commandframework.annotations.exception.ExceptionHandlerFactory;
2931
import cloud.commandframework.annotations.injection.ParameterInjectorRegistry;
3032
import cloud.commandframework.annotations.injection.RawArgs;
3133
import cloud.commandframework.annotations.parsers.MethodArgumentParser;
@@ -107,6 +109,7 @@ public final class AnnotationParser<C> {
107109
private FlagAssembler flagAssembler;
108110
private CommandExtractor commandExtractor;
109111
private SuggestionProviderFactory<C> suggestionProviderFactory;
112+
private ExceptionHandlerFactory<C> exceptionHandlerFactory;
110113

111114
/**
112115
* Construct a new annotation parser
@@ -170,6 +173,7 @@ public AnnotationParser(
170173
this.argumentAssembler = new ArgumentAssemblerImpl<>(this);
171174
this.commandExtractor = new CommandExtractorImpl(this);
172175
this.suggestionProviderFactory = SuggestionProviderFactory.defaultFactory();
176+
this.exceptionHandlerFactory = ExceptionHandlerFactory.defaultFactory();
173177
// TODO(City): Add mapper so that we can map this to rich descriptions easily.
174178
this.registerBuilderModifier(
175179
CommandDescription.class,
@@ -547,6 +551,28 @@ public void suggestionProviderFactory(final @NonNull SuggestionProviderFactory<C
547551
this.suggestionProviderFactory = suggestionProviderFactory;
548552
}
549553

554+
/**
555+
* Returns the exception provider factory.
556+
*
557+
* @return the exception provider factory
558+
* @since 2.0.0
559+
*/
560+
@API(status = API.Status.STABLE, since = "2.0.0")
561+
public @NonNull ExceptionHandlerFactory<C> exceptionHandlerFactory() {
562+
return this.exceptionHandlerFactory;
563+
}
564+
565+
/**
566+
* Sets the exception provider factory.
567+
*
568+
* @param exceptionHandlerFactory new exception provider factory
569+
* @since 2.0.0
570+
*/
571+
@API(status = API.Status.STABLE, since = "2.0.0")
572+
public void exceptionHandlerFactory(final @NonNull ExceptionHandlerFactory<C> exceptionHandlerFactory) {
573+
this.exceptionHandlerFactory = exceptionHandlerFactory;
574+
}
575+
550576
/**
551577
* Parses all known {@link cloud.commandframework.annotations.processing.CommandContainer command containers}.
552578
*
@@ -610,6 +636,7 @@ public void suggestionProviderFactory(final @NonNull SuggestionProviderFactory<C
610636
public <T> @NonNull Collection<@NonNull Command<C>> parse(final @NonNull T instance) {
611637
this.parseSuggestions(instance);
612638
this.parseParsers(instance);
639+
this.parseExceptionHandlers(instance);
613640

614641
final Collection<CommandDescriptor> commandDescriptors = this.commandExtractor.extractCommands(instance);
615642
final Collection<Command<C>> commands = this.construct(instance, commandDescriptors);
@@ -669,6 +696,28 @@ private <T> void parseSuggestions(final @NonNull T instance) {
669696
}
670697
}
671698

699+
@SuppressWarnings("unchecked")
700+
private <T> void parseExceptionHandlers(final @NonNull T instance) {
701+
for (final Method method : instance.getClass().getMethods()) {
702+
final ExceptionHandler exceptionHandler = method.getAnnotation(ExceptionHandler.class);
703+
if (exceptionHandler == null) {
704+
continue;
705+
}
706+
if (!method.isAccessible()) {
707+
method.setAccessible(true);
708+
}
709+
710+
try {
711+
this.manager.exceptionController().registerHandler(
712+
(Class<Throwable>) exceptionHandler.value(),
713+
this.exceptionHandlerFactory.createExceptionHandler(instance, method)
714+
);
715+
} catch (final Exception e) {
716+
throw new RuntimeException(e);
717+
}
718+
}
719+
}
720+
672721
@SuppressWarnings("deprecation")
673722
private <T> void parseParsers(final @NonNull T instance) {
674723
for (final Method method : instance.getClass().getMethods()) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2022 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.exception;
25+
26+
import cloud.commandframework.exceptions.handling.ExceptionContext;
27+
import java.lang.annotation.ElementType;
28+
import java.lang.annotation.Retention;
29+
import java.lang.annotation.RetentionPolicy;
30+
import java.lang.annotation.Target;
31+
import org.apiguardian.api.API;
32+
import org.checkerframework.checker.nullness.qual.NonNull;
33+
34+
/**
35+
* Annotation used to map methods to {@link cloud.commandframework.exceptions.handling.ExceptionHandler exception handlers}.
36+
* <p>
37+
* The method signature is not fixed, and the parameters can be any combination of: <ul>
38+
* <li>{@link cloud.commandframework.exceptions.handling.ExceptionContext}</li>
39+
* <li>{@link #value()}</li>
40+
* <li>{@link cloud.commandframework.context.CommandContext}</li>
41+
* <li>the command sender type</li>
42+
* <li>any type that can be injected using {@link cloud.commandframework.context.CommandContext#inject(Class)}</li>
43+
* </ul>
44+
* <p>
45+
* See {@link cloud.commandframework.exceptions.handling.ExceptionHandler#handle(ExceptionContext)} for information about
46+
* the exception handler behavior.
47+
*
48+
* @since 2.0.0
49+
*/
50+
@Retention(RetentionPolicy.RUNTIME)
51+
@Target(ElementType.METHOD)
52+
@API(status = API.Status.STABLE, since = "2.0.0")
53+
public @interface ExceptionHandler {
54+
55+
/**
56+
* Returns the type of exceptions caught by the exception handler.
57+
*
58+
* @return the exception type
59+
*/
60+
@NonNull Class<? extends Throwable> value();
61+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2022 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.exception;
25+
26+
import cloud.commandframework.exceptions.handling.ExceptionHandler;
27+
import java.lang.reflect.Method;
28+
import org.apiguardian.api.API;
29+
import org.checkerframework.checker.nullness.qual.NonNull;
30+
31+
@FunctionalInterface
32+
@API(status = API.Status.STABLE, since = "2.0.0")
33+
public interface ExceptionHandlerFactory<C> {
34+
35+
/**
36+
* Returns a factory that produces {@link MethodExceptionHandler} instances.
37+
*
38+
* @param <C> the command sender type
39+
* @return the created factory
40+
*/
41+
static <C> @NonNull ExceptionHandlerFactory<C> defaultFactory() {
42+
return MethodExceptionHandler::new;
43+
}
44+
45+
/**
46+
* Creates an exception handler using the given {@code method}.
47+
*
48+
* @param instance the parsed instance
49+
* @param method the exception handler method
50+
* @return the method handler
51+
*/
52+
@NonNull ExceptionHandler<C, Throwable> createExceptionHandler(@NonNull Object instance, @NonNull Method method);
53+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2022 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.exception;
25+
26+
import cloud.commandframework.exceptions.handling.ExceptionContext;
27+
import cloud.commandframework.exceptions.handling.ExceptionHandler;
28+
import java.lang.invoke.MethodHandle;
29+
import java.lang.invoke.MethodHandles;
30+
import java.lang.reflect.Method;
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import org.apiguardian.api.API;
34+
import org.checkerframework.checker.nullness.qual.NonNull;
35+
36+
/**
37+
* Represents a method annotated with {@link cloud.commandframework.annotations.exception.ExceptionHandler}.
38+
*
39+
* @param <C> the command sender type
40+
* @since 2.0.0
41+
*/
42+
@API(status = API.Status.STABLE, since = "2.0.0")
43+
public final class MethodExceptionHandler<C> implements ExceptionHandler<C, Throwable> {
44+
45+
private final Class<?>[] parameters;
46+
private final MethodHandle methodHandle;
47+
48+
/**
49+
* Creates a new handler.
50+
*
51+
* @param instance the parsed instance
52+
* @param method the method
53+
*/
54+
public MethodExceptionHandler(
55+
final @NonNull Object instance,
56+
final @NonNull Method method
57+
) {
58+
this.parameters = method.getParameterTypes();
59+
try {
60+
this.methodHandle = MethodHandles.lookup().unreflect(method).bindTo(instance);
61+
} catch (final IllegalAccessException e) {
62+
throw new RuntimeException(e);
63+
}
64+
}
65+
66+
@Override
67+
public void handle(final @NonNull ExceptionContext<C, Throwable> context) throws Throwable {
68+
final List<Object> arguments = new ArrayList<>(this.parameters.length);
69+
for (final Class<?> argument : this.parameters) {
70+
if (argument.isInstance(context)) {
71+
arguments.add(context);
72+
} else if (argument.isInstance(context.exception())) {
73+
arguments.add(context.exception());
74+
} else if (argument.isInstance(context.context())) {
75+
arguments.add(context.context());
76+
} else if (argument.isInstance(context.context().getSender())) {
77+
arguments.add(context.context().getSender());
78+
} else {
79+
arguments.add(context.context().inject(argument).orElseThrow(() ->
80+
new IllegalArgumentException("Can't map argument of type " + argument.getName())));
81+
}
82+
}
83+
this.methodHandle.invokeWithArguments(arguments);
84+
}
85+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Classes related to {@link cloud.commandframework.annotations.exception.ExceptionHandler}-annotated methods
3+
*/
4+
package cloud.commandframework.annotations.exception;

0 commit comments

Comments
 (0)