-
-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add API to use custom error handling when Argument parsing fails See #370 for the basis of these changes New changes here: - Arguments can only have an ArgumentParseExceptionHandler attached if they implement ArgumentParseExceptionArgument - The substitute value from ArgumentParseExceptionHandler doesn't have to be returned directly - ExceptionInformation can be provided by arguments - New NMS method to extract translation keys from CommandSyntaxExceptions
- Loading branch information
1 parent
5e546d7
commit da16444
Showing
59 changed files
with
2,488 additions
and
580 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
commandapi-core/src/main/java/dev/jorel/commandapi/SafeStaticOneParameterMethodHandle.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package dev.jorel.commandapi; | ||
|
||
import java.lang.invoke.MethodHandle; | ||
import java.lang.invoke.MethodHandles; | ||
import java.lang.invoke.MethodType; | ||
|
||
/** | ||
* A wrapper around MethodHandle with better type safety using generics and a | ||
* toggleable underlying implementation depending on whether we're using mojang | ||
* mappings or non-mojang mappings. This implementation only works for static | ||
* methods that have one parameter. | ||
* | ||
* @param <ReturnType> | ||
* @param <ParameterType> | ||
*/ | ||
public class SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> { | ||
|
||
private final MethodHandle handle; | ||
|
||
private SafeStaticOneParameterMethodHandle(MethodHandle handle) { | ||
this.handle = handle; | ||
} | ||
|
||
private static <ReturnType, ParameterType> SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> of( | ||
Class<?> classType, | ||
String methodName, String mojangMappedMethodName, | ||
Class<? super ReturnType> returnType, | ||
Class<? super ParameterType> parameterType | ||
) throws ReflectiveOperationException { | ||
return new SafeStaticOneParameterMethodHandle<>(MethodHandles.privateLookupIn(classType, MethodHandles.lookup()).findStatic(classType, SafeVarHandle.USING_MOJANG_MAPPINGS ? mojangMappedMethodName : methodName, MethodType.methodType(returnType, parameterType))); | ||
} | ||
|
||
public static <ReturnType, ParameterType> SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> ofOrNull( | ||
Class<?> classType, | ||
String methodName, String mojangMappedMethodName, | ||
Class<? super ReturnType> returnType, | ||
Class<? super ParameterType> parameterType | ||
) { | ||
try { | ||
return of(classType, methodName, mojangMappedMethodName, returnType, parameterType); | ||
} catch (ReflectiveOperationException e) { | ||
e.printStackTrace(); | ||
return null; | ||
} | ||
} | ||
|
||
public ReturnType invoke(ParameterType parameter) throws Throwable { | ||
return (ReturnType) handle.invoke(parameter); | ||
} | ||
|
||
public ReturnType invokeOrNull(ParameterType parameter) { | ||
try { | ||
return invoke(parameter); | ||
} catch (Throwable e) { | ||
e.printStackTrace(); | ||
return null; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
...api-core/src/main/java/dev/jorel/commandapi/arguments/ArgumentParseExceptionArgument.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package dev.jorel.commandapi.arguments; | ||
|
||
import com.mojang.brigadier.context.CommandContext; | ||
import com.mojang.brigadier.exceptions.CommandSyntaxException; | ||
import dev.jorel.commandapi.ChainableBuilder; | ||
import dev.jorel.commandapi.CommandAPIHandler; | ||
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException; | ||
import dev.jorel.commandapi.executors.CommandArguments; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
/** | ||
* An interface that indicates an argument can have an {@link ArgumentParseExceptionHandler} attached to it. | ||
* | ||
* @param <T> The class of the object that can be substituted instead of an exception when the Argument fails to parse. | ||
* @param <Raw> The class of the object returned by the initial Brigadier parse for the Argument. | ||
* @param <ExceptionInformation> The class that holds information about the exception. | ||
* @param <Impl> The class extending this class, used as the return type in chained calls. | ||
* @param <CommandSender> The CommandSender class used by the class extending this class. | ||
*/ | ||
public interface ArgumentParseExceptionArgument<T, Raw, ExceptionInformation, Impl extends AbstractArgument<?, Impl, ?, CommandSender>, CommandSender> extends ChainableBuilder<Impl> { | ||
/** | ||
* A map that links Arguments to their ExceptionHandlers. This is basically | ||
* equivalent to putting one instance variable in this interface, but Java | ||
* doesn't let you put instance variables in interfaces, so we have to do | ||
* this instead if we want to provide default implementations of the methods, | ||
* overall avoiding the code duplication that comes from implementing these | ||
* methods in the inheriting classes. | ||
*/ | ||
// TODO: Maybe this can be a WeakHashMap, so once the Argument objects aren't being used anywhere else we can forget | ||
// about them and not store them anymore. I'm not entirely sure that is what WeakHashMap does though. Are Arguments | ||
// ever GC'd anyway, or do they stick around somewhere? | ||
Map<ArgumentParseExceptionArgument<?, ?, ?, ?, ?>, ArgumentParseExceptionHandler<?, ?, ?, ?>> exceptionHandlers = new HashMap<>(); | ||
|
||
/** | ||
* Sets the {@link ArgumentParseExceptionHandler} this Argument should use when it fails to parse. | ||
* | ||
* @param exceptionHandler The new {@link ArgumentParseExceptionHandler} this argument should use | ||
* @return this current argument | ||
*/ | ||
default Impl withArgumentParseExceptionHandler( | ||
ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> exceptionHandler | ||
) { | ||
exceptionHandlers.put(this, exceptionHandler); | ||
return instance(); | ||
} | ||
|
||
/** | ||
* Returns the {@link ArgumentParseExceptionHandler} this argument is using | ||
* @return The {@link ArgumentParseExceptionHandler} this argument is using | ||
*/ | ||
default Optional<ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender>> getArgumentParseExceptionHandler() { | ||
return Optional.ofNullable( | ||
(ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender>) exceptionHandlers.get(this) | ||
); | ||
} | ||
|
||
default <Source, A extends AbstractArgument<?, ?, A, CommandSender>> | ||
T handleArgumentParseException( | ||
CommandContext<Source> cmdCtx, String key, CommandArguments previousArgs, | ||
CommandSyntaxException original, ExceptionInformation exceptionInformation | ||
) throws CommandSyntaxException { | ||
ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> exceptionHandler = | ||
getArgumentParseExceptionHandler().orElseThrow(() -> original); | ||
|
||
try { | ||
return exceptionHandler.handleException(new ArgumentParseExceptionContext<>( | ||
new WrapperCommandSyntaxException(original), | ||
exceptionInformation, | ||
CommandAPIHandler.<A, CommandSender, Source>getInstance().getPlatform() | ||
.getCommandSenderFromCommandSource(cmdCtx.getSource()).getSource(), | ||
(Raw) cmdCtx.getArgument(key, Object.class), | ||
previousArgs | ||
)); | ||
} catch (WrapperCommandSyntaxException newException) { | ||
throw newException.getException(); | ||
} | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
...dapi-core/src/main/java/dev/jorel/commandapi/arguments/ArgumentParseExceptionContext.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package dev.jorel.commandapi.arguments; | ||
|
||
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException; | ||
import dev.jorel.commandapi.executors.CommandArguments; | ||
|
||
/** | ||
* A record containing information on why an Argument failed to parse. | ||
* | ||
* @param exception The CommandSyntaxException that was thrown when the Argument failed to parse. | ||
* | ||
* @param <ExceptionInformation> The class that holds information about the exception. | ||
* @param exceptionInformation Extra information about the exception. | ||
* | ||
* @param <CommandSender> The CommandSender class being used. | ||
* @param sender The CommandSender who sent the command that caused the exception. | ||
* | ||
* @param <Raw> The class that is returned by the initial Brigadier parse for the Argument. | ||
* @param input The raw object returned by the initial Brigadier parse for the Argument. | ||
* | ||
* @param previousArguments A {@link CommandArguments} object holding previously declared (and parsed) arguments. This can | ||
* be used as if it were arguments in a command executor method. | ||
*/ | ||
public record ArgumentParseExceptionContext<Raw, ExceptionInformation, CommandSender>( | ||
/** | ||
* @param exception The CommandSyntaxException that was thrown when the Argument failed to parse. | ||
*/ | ||
WrapperCommandSyntaxException exception, | ||
/** | ||
* @param exceptionInformation Extra information about the exception. | ||
*/ | ||
ExceptionInformation exceptionInformation, | ||
/** | ||
* @param sender The CommandSender who sent the command that caused the exception. | ||
*/ | ||
CommandSender sender, | ||
/** | ||
* @param input The raw object returned by the initial Brigadier parse for the Argument. | ||
*/ | ||
Raw input, | ||
/** | ||
* @param previousArguments A {@link CommandArguments} object holding previously declared (and parsed) arguments. | ||
* This can be used as if it were arguments in a command executor method. | ||
*/ | ||
CommandArguments previousArguments) { | ||
} |
26 changes: 26 additions & 0 deletions
26
...dapi-core/src/main/java/dev/jorel/commandapi/arguments/ArgumentParseExceptionHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package dev.jorel.commandapi.arguments; | ||
|
||
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException; | ||
|
||
/** | ||
* A FunctionalInterface for defining custom behavior when an Argument fails to parse. | ||
* See {@link ArgumentParseExceptionHandler#handleException(ArgumentParseExceptionContext)}. | ||
* | ||
* @param <T> The class of the object that can be substituted instead of an exception when the Argument fails to parse. | ||
* @param <Raw> The class of the object returned by the initial Brigadier parse for the Argument. | ||
* @param <ExceptionInformation> The class that holds information about the exception. | ||
* @param <CommandSender> The CommandSender class being used. | ||
*/ | ||
@FunctionalInterface | ||
public interface ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> { | ||
/** | ||
* A method that handles when an Argument fails to parse. | ||
* It can either return an object or throw a different exception. | ||
* | ||
* @param context a {@link ArgumentParseExceptionContext} record that holds information | ||
* about why and when the Argument failed to parse | ||
* @return A new object in place of the failed parse | ||
* @throws WrapperCommandSyntaxException A new exception to pass on | ||
*/ | ||
T handleException(ArgumentParseExceptionContext<Raw, ExceptionInformation, CommandSender> context) throws WrapperCommandSyntaxException; | ||
} |
72 changes: 72 additions & 0 deletions
72
...dapi-core/src/main/java/dev/jorel/commandapi/arguments/ExceptionHandlingArgumentType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package dev.jorel.commandapi.arguments; | ||
|
||
import com.mojang.brigadier.StringReader; | ||
import com.mojang.brigadier.arguments.ArgumentType; | ||
import com.mojang.brigadier.context.CommandContext; | ||
import com.mojang.brigadier.exceptions.CommandSyntaxException; | ||
import com.mojang.brigadier.suggestion.Suggestions; | ||
import com.mojang.brigadier.suggestion.SuggestionsBuilder; | ||
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException; | ||
import dev.jorel.commandapi.wrappers.WrapperStringReader; | ||
import org.apache.commons.lang3.function.TriFunction; | ||
|
||
import java.util.Collection; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.function.BiFunction; | ||
|
||
/** | ||
* An {@link ArgumentType} that wraps another {@link ArgumentType} and intercepts any | ||
* {@link CommandSyntaxException} to send to a developer-specified {@link InitialParseExceptionHandler} | ||
* | ||
* @param baseType The {@link ArgumentType} this object is wrapping. | ||
* @param exceptionHandler The {@link InitialParseExceptionHandler} that handles intercepted {@link CommandSyntaxException}. | ||
* @param exceptionParser A function that parses the information in a {@link CommandSyntaxException} to create an | ||
* {@link ExceptionInformation} object. | ||
* | ||
* @param <T> The object returned when the wrapped {@link ArgumentType} is parsed. | ||
* @param <BaseType> The class of the {@link ArgumentType} this object is wrapping. | ||
* @param <ExceptionInformation> The class that holds information about the exception. | ||
*/ | ||
public record ExceptionHandlingArgumentType<T, BaseType extends ArgumentType<T>, ExceptionInformation>( | ||
/** | ||
* @param baseType The {@link ArgumentType} this object is wrapping | ||
*/ | ||
BaseType baseType, | ||
/** | ||
* @param exceptionHandler The {@link InitialParseExceptionHandler} that handles intercepted {@link CommandSyntaxException} | ||
*/ | ||
InitialParseExceptionHandler<T, ExceptionInformation> exceptionHandler, | ||
/** | ||
* @param exceptionParser A function that parses the information in a {@link CommandSyntaxException} to create an | ||
* {@link ExceptionInformation} object. | ||
*/ | ||
TriFunction<CommandSyntaxException, StringReader, BaseType, ExceptionInformation> exceptionParser | ||
) implements ArgumentType<T> { | ||
|
||
@Override | ||
public T parse(StringReader stringReader) throws CommandSyntaxException { | ||
try { | ||
return baseType.parse(stringReader); | ||
} catch (CommandSyntaxException original) { | ||
try { | ||
return exceptionHandler.handleException(new InitialParseExceptionContext<>( | ||
new WrapperCommandSyntaxException(original), | ||
exceptionParser.apply(original, stringReader, baseType), | ||
new WrapperStringReader(stringReader) | ||
)); | ||
} catch (WrapperCommandSyntaxException newException) { | ||
throw newException.getException(); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public Collection<String> getExamples() { | ||
return baseType.getExamples(); | ||
} | ||
|
||
@Override | ||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) { | ||
return baseType.listSuggestions(context, builder); | ||
} | ||
} |
Oops, something went wrong.