Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler Maven / Gradle / Ivy
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.cloud.minecraft.extras;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.ComponentMessageThrowable;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.returnsreceiver.qual.This;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.caption.CaptionVariable;
import org.incendo.cloud.caption.StandardCaptionKeys;
import org.incendo.cloud.exception.ArgumentParseException;
import org.incendo.cloud.exception.CommandExecutionException;
import org.incendo.cloud.exception.InvalidCommandSenderException;
import org.incendo.cloud.exception.InvalidSyntaxException;
import org.incendo.cloud.exception.NoPermissionException;
import org.incendo.cloud.exception.handling.ExceptionContext;
import org.incendo.cloud.exception.parsing.ParserException;
import org.incendo.cloud.minecraft.extras.caption.ComponentCaptionFormatter;
import org.incendo.cloud.minecraft.extras.caption.RichVariable;
import org.incendo.cloud.util.TypeUtils;
import static net.kyori.adventure.text.Component.newline;
import static net.kyori.adventure.text.Component.text;
/**
* Creates and registers {@link org.incendo.cloud.exception.handling.ExceptionHandler ExceptionHandlers} that send a {@link Component} to the
* command sender.
*
* @param command sender type
*/
public final class MinecraftExceptionHandler {
private static final Component NULL = text("null");
/**
* The default {@link InvalidSyntaxException} handler.
*
* @param sender type
* @return {@link InvalidSyntaxException} handler function
* @see #defaultInvalidSyntaxHandler()
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MessageFactory createDefaultInvalidSyntaxHandler() {
return (formatter, ctx) -> text()
.color(NamedTextColor.RED)
.append(ctx.context().formatCaption(
formatter,
StandardCaptionKeys.EXCEPTION_INVALID_SYNTAX,
RichVariable.of("syntax", ComponentHelper.highlight(text(String.format("/%s", ctx.exception().correctSyntax()),
NamedTextColor.GRAY), NamedTextColor.WHITE))
));
}
/**
* The default {@link InvalidCommandSenderException} handler.
*
* @param sender type
* @return {@link InvalidCommandSenderException} handler function
* @see #defaultInvalidSenderHandler()
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MessageFactory createDefaultInvalidSenderHandler() {
return (formatter, ctx) -> text()
.color(NamedTextColor.RED)
.append(ctx.context().formatCaption(
formatter,
StandardCaptionKeys.EXCEPTION_INVALID_SENDER,
RichVariable.of("actual", text(TypeUtils.simpleName(ctx.context().sender().getClass()), NamedTextColor.GRAY)),
RichVariable.of("expected", text(TypeUtils.simpleName(ctx.exception().requiredSender()), NamedTextColor.GRAY))
));
}
/**
* The default {@link NoPermissionException} handler.
*
* @param sender type
* @return {@link NoPermissionException} handler function
* @see #defaultNoPermissionHandler()
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MessageFactory createDefaultNoPermissionHandler() {
return (formatter, ctx) -> text()
.color(NamedTextColor.RED)
.append(ctx.context().formatCaption(
formatter,
StandardCaptionKeys.EXCEPTION_NO_PERMISSION,
CaptionVariable.of("permission", ctx.exception().permissionResult().permission().permissionString())
));
}
/**
* The default {@link ArgumentParseException} handler.
*
* @param sender type
* @return {@link ArgumentParseException} handler function
* @see #defaultArgumentParsingHandler()
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MessageFactory createDefaultArgumentParsingHandler() {
return (formatter, ctx) -> text()
.color(NamedTextColor.RED)
.append(ctx.context().formatCaption(
formatter,
StandardCaptionKeys.EXCEPTION_INVALID_ARGUMENT,
RichVariable.of("cause", getMessage(formatter, ctx.exception().getCause()).colorIfAbsent(NamedTextColor.GRAY))
));
}
/**
* Default logger for {@link #createDefaultCommandExecutionHandler(Consumer)}, using
* {@link Throwable#printStackTrace()} on the {@link CommandExecutionException}'s cause.
*
* @param sender type
* @return default logger
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static Consumer> createDefaultCommandExecutionLogger() {
return ctx -> ctx.exception().getCause().printStackTrace();
}
/**
* The default {@link CommandExecutionException} handler, using {@link #createDefaultCommandExecutionLogger()}.
*
* @param sender type
* @return {@link CommandExecutionException} handler function
* @see #defaultCommandExecutionHandler()
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MessageFactory createDefaultCommandExecutionHandler() {
return createDefaultCommandExecutionHandler(createDefaultCommandExecutionLogger());
}
/**
* The default {@link CommandExecutionException} handler, with a specific logger.
*
* @param logger logger
* @param sender type
* @return {@link CommandExecutionException} handler function
* @see #defaultCommandExecutionHandler(Consumer)
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MessageFactory createDefaultCommandExecutionHandler(
final Consumer> logger
) {
return (formatter, ctx) -> {
logger.accept(ctx);
final Throwable cause = ctx.exception().getCause();
final StringWriter writer = new StringWriter();
cause.printStackTrace(new PrintWriter(writer));
final String stackTrace = writer.toString().replaceAll("\t", " ");
final HoverEvent hover = HoverEvent.showText(
text()
.append(getMessage(formatter, cause))
.append(newline())
.append(text(stackTrace))
.append(newline())
.append(text(
" Click to copy",
NamedTextColor.GRAY,
TextDecoration.ITALIC
))
);
final ClickEvent click = ClickEvent.copyToClipboard(stackTrace);
return text()
.append(ctx.context().formatCaption(formatter, StandardCaptionKeys.EXCEPTION_UNEXPECTED))
.color(NamedTextColor.RED)
.hoverEvent(hover)
.clickEvent(click);
};
}
private final Map, MessageFactory> componentBuilders = new HashMap<>();
private final AudienceProvider audienceProvider;
private Decorator decorator = (formatter, ctx, msg) -> msg;
private ComponentCaptionFormatter captionFormatter = ComponentCaptionFormatter.placeholderReplacing();
private MinecraftExceptionHandler(final AudienceProvider audienceProvider) {
this.audienceProvider = audienceProvider;
}
/**
* Create a new {@link MinecraftExceptionHandler} using {@code audienceProvider}.
*
* @param audienceProvider audience provider
* @param sender type
* @return new {@link MinecraftExceptionHandler}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MinecraftExceptionHandler create(final AudienceProvider audienceProvider) {
return new MinecraftExceptionHandler<>(audienceProvider);
}
/**
* Create a new {@link MinecraftExceptionHandler} using {@link AudienceProvider#nativeAudience()}.
*
* @param sender type
* @return new {@link MinecraftExceptionHandler}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public static MinecraftExceptionHandler createNative() {
return create(AudienceProvider.nativeAudience());
}
/**
* Use the default {@link InvalidSyntaxException} handler.
*
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultInvalidSyntaxHandler() {
return this.handler(InvalidSyntaxException.class, createDefaultInvalidSyntaxHandler());
}
/**
* Use the default {@link InvalidCommandSenderException} handler.
*
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultInvalidSenderHandler() {
return this.handler(InvalidCommandSenderException.class, createDefaultInvalidSenderHandler());
}
/**
* Use the default {@link NoPermissionException} handler.
*
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultNoPermissionHandler() {
return this.handler(NoPermissionException.class, createDefaultNoPermissionHandler());
}
/**
* Use the default {@link ArgumentParseException} handler.
*
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultArgumentParsingHandler() {
return this.handler(ArgumentParseException.class, createDefaultArgumentParsingHandler());
}
/**
* Use the default {@link CommandExecutionException} handler with {@link #createDefaultCommandExecutionLogger()}.
*
* @return {@code this}
* @see #defaultCommandExecutionHandler(Consumer)
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultCommandExecutionHandler() {
return this.defaultCommandExecutionHandler(createDefaultCommandExecutionLogger());
}
/**
* Use the default {@link CommandExecutionException} handler with a custom logger.
*
* @param logger logger
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultCommandExecutionHandler(
final @NonNull Consumer> logger
) {
return this.handler(CommandExecutionException.class, createDefaultCommandExecutionHandler(logger));
}
/**
* Use all the default exception handlers.
*
* @return {@code this}
* @see #defaultArgumentParsingHandler()
* @see #defaultInvalidSenderHandler()
* @see #defaultInvalidSyntaxHandler()
* @see #defaultNoPermissionHandler()
* @see #defaultCommandExecutionHandler()
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler defaultHandlers() {
return this
.defaultArgumentParsingHandler()
.defaultInvalidSenderHandler()
.defaultInvalidSyntaxHandler()
.defaultNoPermissionHandler()
.defaultCommandExecutionHandler();
}
/**
* Sets the exception handler for the given {@code type}. A handler
* can return {@code null} to indicate no message should be sent.
*
* @param exception type
* @param type the exception type to handle
* @param componentFactory the factory that produces the components
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
@SuppressWarnings({"unchecked", "rawtypes"})
public @This @NonNull MinecraftExceptionHandler handler(
final @NonNull Class type,
final @NonNull MessageFactory componentFactory
) {
this.componentBuilders.put(type, componentFactory);
return this;
}
/**
* Sets the decorator that acts on a component before it's sent to the sender.
*
* @param decorator the component decorator
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler decorator(
final @NonNull Decorator decorator
) {
this.decorator = decorator;
return this;
}
/**
* Sets the caption formatter that is responsible for turning {@link org.incendo.cloud.caption.Caption captions} into Adventure {@link Component components}.
*
* @param captionFormatter caption formatter
* @return {@code this}
*/
public @This @NonNull MinecraftExceptionHandler captionFormatter(final @NonNull ComponentCaptionFormatter captionFormatter) {
this.captionFormatter = Objects.requireNonNull(captionFormatter, "captionFormatter");
return this;
}
/**
* Sets the decorator that acts on a component before it's sent to the sender.
*
* @param decorator the component decorator
* @return {@code this}
* @since 2.0.0
*/
@API(status = API.Status.STABLE, since = "2.0.0")
public @This @NonNull MinecraftExceptionHandler decorator(
final @NonNull Function<@NonNull Component, @NonNull ComponentLike> decorator
) {
return this.decorator((formatter, ctx, message) -> decorator.apply(message));
}
/**
* Registers configured handlers to the {@link org.incendo.cloud.exception.handling.ExceptionController}.
*
* @param manager the manager instance
* @since 2.0.0
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@API(status = API.Status.STABLE, since = "2.0.0")
public void registerTo(final @NonNull CommandManager manager) {
this.componentBuilders.forEach((type, formatter) -> {
manager.exceptionController().registerHandler(type, ctx -> {
final @Nullable ComponentLike message = formatter.message(this.captionFormatter, (ExceptionContext) ctx);
if (message != null) {
this.audienceProvider.apply(ctx.context().sender()).sendMessage(
this.decorator.decorate(this.captionFormatter, ctx, message.asComponent()));
}
});
});
}
private static Component getMessage(final ComponentCaptionFormatter formatter, final Throwable throwable) {
if (throwable instanceof ParserException) {
return ((ParserException) throwable).formatCaption(formatter);
}
final Component msg = ComponentMessageThrowable.getOrConvertMessage(throwable);
return msg == null ? NULL : msg;
}
@FunctionalInterface
public interface MessageFactory {
/**
* Formats the exception info into a {@link Component} message, or {@code null} to send no message.
*
* @param formatter formatter to create components with
* @param exceptionContext exception context
* @return message or {@code null}
*/
@Nullable ComponentLike message(@NonNull ComponentCaptionFormatter formatter, @NonNull ExceptionContext exceptionContext);
}
@FunctionalInterface
public interface Decorator {
/**
* Decorates a message before sending.
*
* @param formatter caption formatter
* @param exceptionContext exception context
* @param message message
* @return decorated message
*/
@NonNull ComponentLike decorate(
@NonNull ComponentCaptionFormatter formatter,
@NonNull ExceptionContext exceptionContext,
@NonNull Component message
);
}
}