From fab21c3eeac5726e48509762f41e92887789a0a0 Mon Sep 17 00:00:00 2001 From: wizjany Date: Sat, 11 May 2019 21:06:23 -0400 Subject: [PATCH] Add AsyncCommandBuilder as replacement for AsyncCommandHelper. See full explanation at https://github.com/EngineHub/WorldGuard/pull/408 --- .../worldedit/command/WorldEditCommands.java | 15 +- .../command/util/AsyncCommandBuilder.java | 193 ++++++++++++++++++ .../command/util/AsyncCommandHelper.java | 144 ------------- .../util/paste/ActorCallbackPaste.java | 40 ++-- .../worldedit/util/paste/EngineHubPaste.java | 9 +- .../sk89q/worldedit/util/paste/Pastebin.java | 93 --------- .../sk89q/worldedit/util/paste/Paster.java | 5 +- 7 files changed, 217 insertions(+), 282 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandBuilder.java delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandHelper.java delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Pastebin.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/WorldEditCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/WorldEditCommands.java index 0d19d1538..dee15226e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/WorldEditCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/WorldEditCommands.java @@ -67,7 +67,7 @@ public class WorldEditCommands { aliases = { "ver" }, desc = "Get WorldEdit version" ) - public void version(Actor actor) throws WorldEditException { + public void version(Actor actor) { actor.print("WorldEdit version " + WorldEdit.getVersion()); actor.print("https://github.com/EngineHub/worldedit/"); @@ -90,7 +90,7 @@ public class WorldEditCommands { desc = "Reload configuration" ) @CommandPermissions("worldedit.reload") - public void reload(Actor actor) throws WorldEditException { + public void reload(Actor actor) { we.getPlatformManager().queryCapability(Capability.CONFIGURATION).reload(); we.getEventBus().post(new ConfigurationLoadEvent(we.getPlatformManager().queryCapability(Capability.CONFIGURATION).getConfiguration())); actor.print("Configuration reloaded!"); @@ -100,7 +100,7 @@ public class WorldEditCommands { name = "report", desc = "Writes a report on WorldEdit" ) - @CommandPermissions({"worldedit.report"}) + @CommandPermissions("worldedit.report") public void report(Actor actor, @Switch(name = 'p', desc = "Pastebins the report") boolean pastebin) throws WorldEditException { @@ -119,10 +119,7 @@ public class WorldEditCommands { if (pastebin) { actor.checkPermission("worldedit.report.pastebin"); - ActorCallbackPaste.pastebin( - we.getSupervisor(), actor, result, "WorldEdit report: %s.report", - WorldEdit.getInstance().getPlatformManager().getPlatformCommandManager().getExceptionConverter() - ); + ActorCallbackPaste.pastebin(we.getSupervisor(), actor, result, "WorldEdit report: %s.report"); } } @@ -130,7 +127,7 @@ public class WorldEditCommands { name = "cui", desc = "Complete CUI handshake (internal usage)" ) - public void cui(Player player, LocalSession session) throws WorldEditException { + public void cui(Player player, LocalSession session) { session.setCUISupport(true); session.dispatchCUISetup(player); } @@ -141,7 +138,7 @@ public class WorldEditCommands { ) public void tz(Player player, LocalSession session, @Arg(desc = "The timezone to set") - String timezone) throws WorldEditException { + String timezone) { try { ZoneId tz = ZoneId.of(timezone); session.setTimezone(tz); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandBuilder.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandBuilder.java new file mode 100644 index 000000000..5d10870b5 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandBuilder.java @@ -0,0 +1,193 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + + +package com.sk89q.worldedit.command.util; + +import com.google.common.base.Strings; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.internal.command.exception.ExceptionConverter; +import com.sk89q.worldedit.util.formatting.component.ErrorFormat; +import com.sk89q.worldedit.util.formatting.text.Component; +import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.util.formatting.text.format.TextColor; +import com.sk89q.worldedit.util.task.FutureForwardingTask; +import com.sk89q.worldedit.util.task.Supervisor; +import org.enginehub.piston.exception.CommandException; +import org.enginehub.piston.exception.CommandExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.concurrent.Callable; +import java.util.function.Consumer; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class AsyncCommandBuilder { + + private static final Logger logger = LoggerFactory.getLogger(AsyncCommandBuilder.class); + + private final Callable callable; + private final Actor sender; + + @Nullable + private Supervisor supervisor; + @Nullable + private String description; + @Nullable + private String delayMessage; + + @Nullable + private Component successMessage; + @Nullable + private Consumer consumer; + + @Nullable + private Component failureMessage; + @Nullable + private ExceptionConverter exceptionConverter; + + private AsyncCommandBuilder(Callable callable, Actor sender) { + checkNotNull(callable); + checkNotNull(sender); + this.callable = callable; + this.sender = sender; + } + + public static AsyncCommandBuilder wrap(Callable callable, Actor sender) { + return new AsyncCommandBuilder<>(callable, sender); + } + + public AsyncCommandBuilder registerWithSupervisor(Supervisor supervisor, String description) { + this.supervisor = checkNotNull(supervisor); + this.description = checkNotNull(description); + return this; + } + + public AsyncCommandBuilder sendMessageAfterDelay(String message) { + this.delayMessage = checkNotNull(message); + return this; + } + + public AsyncCommandBuilder onSuccess(@Nullable Component message, @Nullable Consumer consumer) { + checkArgument(message != null || consumer != null, "Can't have null message AND consumer"); + this.successMessage = message; + this.consumer = consumer; + return this; + } + + public AsyncCommandBuilder onSuccess(@Nullable String message, @Nullable Consumer consumer) { + checkArgument(message != null || consumer != null, "Can't have null message AND consumer"); + this.successMessage = message == null ? null : TextComponent.of(message, TextColor.LIGHT_PURPLE); + this.consumer = consumer; + return this; + } + + public AsyncCommandBuilder onFailure(@Nullable Component message, @Nullable ExceptionConverter exceptionConverter) { + checkArgument(message != null || exceptionConverter != null, "Can't have null message AND exceptionConverter"); + this.failureMessage = message; + this.exceptionConverter = exceptionConverter; + return this; + } + + public AsyncCommandBuilder onFailure(@Nullable String message, @Nullable ExceptionConverter exceptionConverter) { + checkArgument(message != null || exceptionConverter != null, "Can't have null message AND exceptionConverter"); + this.failureMessage = message == null ? null : ErrorFormat.wrap(message); + this.exceptionConverter = exceptionConverter; + return this; + } + + public ListenableFuture buildAndExec(ListeningExecutorService executor) { + final ListenableFuture future = checkNotNull(executor).submit(this::runTask); + if (delayMessage != null) { + FutureProgressListener.addProgressListener(future, sender, delayMessage); + } + if (supervisor != null && description != null) { + supervisor.monitor(FutureForwardingTask.create(future, description, sender)); + } + return future; + } + + private T runTask() { + T result = null; + try { + result = callable.call(); + if (consumer != null) { + consumer.accept(result); + } + if (successMessage != null) { + sender.print(successMessage); + } + } catch (Exception orig) { + Component failure = failureMessage != null ? failureMessage : TextComponent.of("An error occurred"); + try { + if (exceptionConverter != null) { + try { + exceptionConverter.convert(orig); + } catch (CommandException converted) { + Component message; + + // TODO remove this once WG migrates to piston and can use piston exceptions everywhere + message = tryExtractOldCommandException(converted); + + if (message == null) { + if (Strings.isNullOrEmpty(converted.getMessage())) { + message = TextComponent.of("Unknown error."); + } else { + message = converted.getRichMessage(); + } + } + sender.print(failure.append(TextComponent.of(": ")).append(message)); + } + } else { + throw orig; + } + } catch (Throwable unknown) { + sender.print(failure.append(TextComponent.of(": Unknown error. Please see console."))); + logger.error("Uncaught exception occurred in task: " + description, orig); + } + } + return result; + } + + // this is needed right now since worldguard is still on the 2011 command framework which throws and converts + // com.sk89q.minecraft.util.commands.CommandException. the ExceptionConverter currently expects converted + // exceptions to be org.enginehub.piston.CommandException, throw it wraps the resulting InvocationTargetException in + // a CommandExecutionException. here, we unwrap those layers to retrieve the original WG error message + private Component tryExtractOldCommandException(CommandException converted) { + Component message = null; + if (converted instanceof CommandExecutionException) { + Throwable parentCause = converted; + while ((parentCause = parentCause.getCause()) != null) { + if (parentCause instanceof com.sk89q.minecraft.util.commands.CommandException) { + final String msg = parentCause.getMessage(); + if (!Strings.isNullOrEmpty(msg)) { + message = TextComponent.of(msg); + } + break; + } + } + } + return message; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandHelper.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandHelper.java deleted file mode 100644 index 842cd5752..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/AsyncCommandHelper.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.command.util; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.sk89q.worldedit.extension.platform.Actor; -import com.sk89q.worldedit.internal.command.exception.ExceptionConverter; -import com.sk89q.worldedit.util.task.FutureForwardingTask; -import com.sk89q.worldedit.util.task.Supervisor; -import com.sk89q.worldedit.world.World; - -import javax.annotation.Nullable; -import java.util.concurrent.ForkJoinPool; - -public class AsyncCommandHelper { - - private final ListenableFuture future; - private final Supervisor supervisor; - private final Actor sender; - private final ExceptionConverter exceptionConverter; - @Nullable - private Object[] formatArgs; - - private AsyncCommandHelper(ListenableFuture future, Supervisor supervisor, Actor sender, ExceptionConverter exceptionConverter) { - checkNotNull(future); - checkNotNull(supervisor); - checkNotNull(sender); - checkNotNull(exceptionConverter); - - this.future = future; - this.supervisor = supervisor; - this.sender = sender; - this.exceptionConverter = exceptionConverter; - } - - public AsyncCommandHelper formatUsing(Object... args) { - this.formatArgs = args; - return this; - } - - private String format(String message) { - if (formatArgs != null) { - return String.format(message, formatArgs); - } else { - return message; - } - } - - public AsyncCommandHelper registerWithSupervisor(String description) { - supervisor.monitor( - FutureForwardingTask.create( - future, format(description), sender)); - return this; - } - - public AsyncCommandHelper sendMessageAfterDelay(String message) { - FutureProgressListener.addProgressListener(future, sender, format(message)); - return this; - } - - public AsyncCommandHelper thenRespondWith(String success, String failure) { - // Send a response message - Futures.addCallback( - future, - new MessageFutureCallback.Builder(sender) - .exceptionConverter(exceptionConverter) - .onSuccess(format(success)) - .onFailure(format(failure)) - .build(), - ForkJoinPool.commonPool()); - return this; - } - - public AsyncCommandHelper thenTellErrorsOnly(String failure) { - // Send a response message - Futures.addCallback( - future, - new MessageFutureCallback.Builder(sender) - .exceptionConverter(exceptionConverter) - .onFailure(format(failure)) - .build(), - ForkJoinPool.commonPool()); - return this; - } - - public AsyncCommandHelper forRegionDataLoad(World world, boolean silent) { - checkNotNull(world); - - formatUsing(world.getName()); - registerWithSupervisor("Loading region data for '%s'"); - if (silent) { - thenTellErrorsOnly("Failed to load regions '%s'"); - } else { - sendMessageAfterDelay("(Please wait... loading the region data for '%s')"); - thenRespondWith( - "Loaded region data for '%s'", - "Failed to load regions '%s'"); - } - - return this; - } - - public AsyncCommandHelper forRegionDataSave(World world, boolean silent) { - checkNotNull(world); - - formatUsing(world.getName()); - registerWithSupervisor("Saving region data for '%s'"); - if (silent) { - thenTellErrorsOnly("Failed to save regions '%s'"); - } else { - sendMessageAfterDelay("(Please wait... saving the region data for '%s')"); - thenRespondWith( - "Saved region data for '%s'", - "Failed to load regions '%s'"); - } - - return this; - } - - public static AsyncCommandHelper wrap(ListenableFuture future, Supervisor supervisor, Actor sender, ExceptionConverter exceptionConverter) { - return new AsyncCommandHelper(future, supervisor, sender, exceptionConverter); - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/ActorCallbackPaste.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/ActorCallbackPaste.java index 0509275f6..1eb51a21f 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/ActorCallbackPaste.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/ActorCallbackPaste.java @@ -19,22 +19,16 @@ package com.sk89q.worldedit.util.paste; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.sk89q.worldedit.command.util.AsyncCommandHelper; +import com.sk89q.worldedit.command.util.AsyncCommandBuilder; import com.sk89q.worldedit.extension.platform.Actor; -import com.sk89q.worldedit.internal.command.exception.ExceptionConverter; import com.sk89q.worldedit.util.task.Supervisor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.net.URL; -import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Callable; -public class ActorCallbackPaste { +public final class ActorCallbackPaste { - private static final Logger LOGGER = LoggerFactory.getLogger(ActorCallbackPaste.class); + private static final Paster paster = new EngineHubPaste(); private ActorCallbackPaste() { } @@ -48,25 +42,15 @@ public class ActorCallbackPaste { * @param content The content * @param successMessage The message, formatted with {@link String#format(String, Object...)} on success */ - public static void pastebin(Supervisor supervisor, final Actor sender, String content, final String successMessage, final ExceptionConverter exceptionConverter) { - ListenableFuture future = new EngineHubPaste().paste(content); + public static void pastebin(Supervisor supervisor, final Actor sender, String content, final String successMessage) { + Callable task = paster.paste(content); - AsyncCommandHelper.wrap(future, supervisor, sender, exceptionConverter) - .registerWithSupervisor("Submitting content to a pastebin service...") - .sendMessageAfterDelay("(Please wait... sending output to pastebin...)"); - - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(URL url) { - sender.print(String.format(successMessage, url)); - } - - @Override - public void onFailure(Throwable throwable) { - LOGGER.warn("Failed to submit pastebin", throwable); - sender.printError("Failed to submit to a pastebin. Please see console for the error."); - } - }, ForkJoinPool.commonPool()); + AsyncCommandBuilder.wrap(task, sender) + .registerWithSupervisor(supervisor, "Submitting content to a pastebin service.") + .sendMessageAfterDelay("(Please wait... sending output to pastebin...)") + .onSuccess((String) null, url -> sender.print(String.format(successMessage, url))) + .onFailure("Failed to submit paste", null) + .buildAndExec(Pasters.getExecutor()); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/EngineHubPaste.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/EngineHubPaste.java index da6b701fd..ac7484846 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/EngineHubPaste.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/EngineHubPaste.java @@ -19,7 +19,6 @@ package com.sk89q.worldedit.util.paste; -import com.google.common.util.concurrent.ListenableFuture; import com.sk89q.worldedit.util.net.HttpRequest; import org.json.simple.JSONValue; @@ -35,11 +34,11 @@ public class EngineHubPaste implements Paster { private static final Pattern URL_PATTERN = Pattern.compile("https?://.+$"); @Override - public ListenableFuture paste(String content) { - return Pasters.getExecutor().submit(new PasteTask(content)); + public Callable paste(String content) { + return new PasteTask(content); } - private final class PasteTask implements Callable { + private static final class PasteTask implements Callable { private final String content; private PasteTask(String content) { @@ -50,7 +49,7 @@ public class EngineHubPaste implements Paster { public URL call() throws IOException, InterruptedException { HttpRequest.Form form = HttpRequest.Form.create(); form.add("content", content); - form.add("from", "worldguard"); + form.add("from", "enginehub"); URL url = HttpRequest.url("http://paste.enginehub.org/paste"); String result = HttpRequest.post(url) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Pastebin.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Pastebin.java deleted file mode 100644 index dcbef09b0..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Pastebin.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.util.paste; - -import com.google.common.util.concurrent.ListenableFuture; -import com.sk89q.worldedit.util.net.HttpRequest; - -import java.io.IOException; -import java.net.URL; -import java.util.concurrent.Callable; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Pastebin implements Paster { - - private static final Pattern URL_PATTERN = Pattern.compile("https?://pastebin.com/([^/]+)$"); - - private boolean mungingLinks = true; - - public boolean isMungingLinks() { - return mungingLinks; - } - - public void setMungingLinks(boolean mungingLinks) { - this.mungingLinks = mungingLinks; - } - - @Override - public ListenableFuture paste(String content) { - if (mungingLinks) { - content = content.replaceAll("http://", "http_//"); - } - - return Pasters.getExecutor().submit(new PasteTask(content)); - } - - private final class PasteTask implements Callable { - private final String content; - - private PasteTask(String content) { - this.content = content; - } - - @Override - public URL call() throws IOException, InterruptedException { - HttpRequest.Form form = HttpRequest.Form.create(); - form.add("api_option", "paste"); - form.add("api_dev_key", "4867eae74c6990dbdef07c543cf8f805"); - form.add("api_paste_code", content); - form.add("api_paste_private", "0"); - form.add("api_paste_name", ""); - form.add("api_paste_expire_date", "1W"); - form.add("api_paste_format", "text"); - form.add("api_user_key", ""); - - URL url = HttpRequest.url("http://pastebin.com/api/api_post.php"); - String result = HttpRequest.post(url) - .bodyForm(form) - .execute() - .expectResponseCode(200) - .returnContent() - .asString("UTF-8").trim(); - - Matcher m = URL_PATTERN.matcher(result); - - if (m.matches()) { - return new URL("http://pastebin.com/raw.php?i=" + m.group(1)); - } else if (result.matches("^https?://.+")) { - return new URL(result); - } else { - throw new IOException("Failed to save paste; instead, got: " + result); - } - } - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Paster.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Paster.java index 7a7d74cac..a65ccd6c1 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Paster.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/paste/Paster.java @@ -19,12 +19,11 @@ package com.sk89q.worldedit.util.paste; -import com.google.common.util.concurrent.ListenableFuture; - import java.net.URL; +import java.util.concurrent.Callable; public interface Paster { - ListenableFuture paste(String content); + Callable paste(String content); }