diff --git a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/ChestsPlusPlus.java b/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/ChestsPlusPlus.java index 98286f4..eb7eff6 100644 --- a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/ChestsPlusPlus.java +++ b/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/ChestsPlusPlus.java @@ -23,7 +23,7 @@ import com.jamesdpeters.minecraft.chests.serialize.LocationInfo; import com.jamesdpeters.minecraft.chests.serialize.MaterialSerializer; import com.jamesdpeters.minecraft.chests.serialize.RecipeSerializable; import com.jamesdpeters.minecraft.chests.serialize.SpigotConfig; -import com.jamesdpeters.minecraft.chests.versionchecker.UpdateCheck; +import com.jamesdpeters.minecraft.chests.versionchecker.UpdateChecker; import fr.minuskube.inv.InventoryManager; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; @@ -109,44 +109,36 @@ public class ChestsPlusPlus extends JavaPlugin { if(isBeta) getLogger().warning("You are currently running a Beta build - update checker disabled! Build: "+BuildConstants.VERSION); if(Settings.isUpdateCheckEnabled() && !isDev && !isBeta) { - String SPIGOT_URL = "https://www.spigotmc.org/resources/chests-chest-linking-hopper-filtering-remote-chests-menus.71355/"; String BUKKIT_URL = "https://dev.bukkit.org/projects/chests-plus-plus/files"; - UpdateCheck updateChecker = UpdateCheck - .of(this) - .resourceId(71355) - .currentVersion(getDescription().getVersion()) - .handleResponse((versionResponse, version) -> { - switch (versionResponse) { - case FOUND_NEW: - getLogger().warning("New version of the plugin has been found: " + version); - getLogger().warning("Download at: "+SPIGOT_URL); - Bukkit.broadcastMessage(ChatColor.RED + "[Chests++] New version of the plugin was found: " + version); - Bukkit.broadcastMessage(ChatColor.RED + "[Chests++] Download at: " +ChatColor.WHITE+ BUKKIT_URL); - break; - case LATEST: - if(!boot) getLogger().info("Plugin is up to date! Thank you for supporting Chests++!"); - break; - case UNAVAILABLE: - Bukkit.broadcastMessage("Unable to perform an update check."); - } - boot = true; - }); - Bukkit.getScheduler().scheduleSyncRepeatingTask(this, updateChecker::check,0,Settings.getUpdateCheckerPeriodTicks()); + UpdateChecker.init(this, 71355, UpdateChecker.VERSION_SCHEME_DECIMAL); + Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> { + UpdateChecker.get().requestUpdateCheck().whenCompleteAsync((updateResult, throwable) -> { + switch (updateResult.getReason()) { + case NEW_UPDATE: + Bukkit.broadcastMessage(ChatColor.RED + "[Chests++] New version of the plugin was found: " + updateResult.getNewestVersion()); + Bukkit.broadcastMessage(ChatColor.RED + "[Chests++] Download at: " + ChatColor.WHITE + BUKKIT_URL); + break; + case UP_TO_DATE: + if (!boot) getLogger().info("Plugin is up to date! Thank you for supporting Chests++!"); + break; + } + boot = true; + }); + }, 0, Settings.getUpdateCheckerPeriodTicks()); } - getLogger().info("Chests++ enabled!"); - //Load storages after load. Bukkit.getScheduler().scheduleSyncDelayedTask(this, () ->{ Crafting.load(); new Config(); + getLogger().info("Chests++ Successfully Loaded Config and Recipes"); //Register event listeners getServer().getPluginManager().registerEvents(new StorageListener(),this); getServer().getPluginManager().registerEvents(new InventoryListener(),this); getServer().getPluginManager().registerEvents(new HopperListener(),this); getServer().getPluginManager().registerEvents(new WorldListener(),this); - getLogger().info("Chests++ Successfully Loaded Config and Recipes"); + getLogger().info("Chests++ enabled!"); },1); } diff --git a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/UpdateCheck.java b/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/UpdateCheck.java deleted file mode 100644 index eb7392b..0000000 --- a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/UpdateCheck.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.jamesdpeters.minecraft.chests.versionchecker; - -import com.google.common.base.Preconditions; -import com.google.common.io.Resources; -import com.google.common.net.HttpHeaders; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.Charset; -import java.util.Objects; -import java.util.function.BiConsumer; -import javax.net.ssl.HttpsURLConnection; -import org.bukkit.Bukkit; -import org.bukkit.plugin.java.JavaPlugin; - - -public class UpdateCheck { - - private static final String SPIGOT_URL = "https://api.spigotmc.org/legacy/update.php?resource=%d"; - - private final JavaPlugin javaPlugin; - - private String currentVersion; - private int resourceId = -1; - private BiConsumer versionResponse; - - private UpdateCheck(JavaPlugin javaPlugin) { - this.javaPlugin = Objects.requireNonNull(javaPlugin, "javaPlugin"); - this.currentVersion = javaPlugin.getDescription().getVersion(); - } - - public static UpdateCheck of(JavaPlugin javaPlugin) { - return new UpdateCheck(javaPlugin); - } - - public UpdateCheck currentVersion(String currentVersion) { - this.currentVersion = currentVersion; - return this; - } - - public UpdateCheck resourceId(int resourceId) { - this.resourceId = resourceId; - return this; - } - - public UpdateCheck handleResponse(BiConsumer versionResponse) { - this.versionResponse = versionResponse; - return this; - } - - public void check() { - Objects.requireNonNull(this.javaPlugin, "javaPlugin"); - Objects.requireNonNull(this.currentVersion, "currentVersion"); - Preconditions.checkState(this.resourceId != -1, "resource id not set"); - Objects.requireNonNull(this.versionResponse, "versionResponse"); - - Bukkit.getScheduler().runTaskAsynchronously(this.javaPlugin, () -> { - try { - HttpURLConnection httpURLConnection = (HttpsURLConnection) new URL(String.format(SPIGOT_URL, this.resourceId)).openConnection(); - httpURLConnection.setRequestMethod("GET"); - httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, "Mozilla/5.0"); - - String fetchedVersion = Resources.toString(httpURLConnection.getURL(), Charset.defaultCharset()); - - boolean latestVersion = fetchedVersion.equalsIgnoreCase(this.currentVersion); - - Bukkit.getScheduler().runTask(this.javaPlugin, () -> this.versionResponse.accept(latestVersion ? VersionResponse.LATEST : VersionResponse.FOUND_NEW, latestVersion ? this.currentVersion : fetchedVersion)); - } catch (IOException exception) { - exception.printStackTrace(); - Bukkit.getScheduler().runTask(this.javaPlugin, () -> this.versionResponse.accept(VersionResponse.UNAVAILABLE, null)); - } - }); - } -} \ No newline at end of file diff --git a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/UpdateChecker.java b/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/UpdateChecker.java new file mode 100644 index 0000000..a41cbfa --- /dev/null +++ b/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/UpdateChecker.java @@ -0,0 +1,309 @@ +package com.jamesdpeters.minecraft.chests.versionchecker; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Preconditions; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import org.apache.commons.lang.math.NumberUtils; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * A utility class to assist in checking for updates for plugins uploaded to + * SpigotMC. Before any members of this + * class are accessed, {@link #init(JavaPlugin, int)} must be invoked by the plugin, + * preferrably in its {@link JavaPlugin#onEnable()} method, though that is not a + * requirement. + *

+ * This class performs asynchronous queries to SpiGet, + * an REST server which is updated periodically. If the results of {@link #requestUpdateCheck()} + * are inconsistent with what is published on SpigotMC, it may be due to SpiGet's cache. + * Results will be updated in due time. + * + * @author Parker Hawke - 2008Choco + */ +public final class UpdateChecker { + + public static final VersionScheme VERSION_SCHEME_DECIMAL = (first, second) -> { + String[] firstSplit = splitVersionInfo(first), secondSplit = splitVersionInfo(second); + if (firstSplit == null || secondSplit == null) return null; + + for (int i = 0; i < Math.min(firstSplit.length, secondSplit.length); i++) { + int currentValue = NumberUtils.toInt(firstSplit[i]), newestValue = NumberUtils.toInt(secondSplit[i]); + + if (newestValue > currentValue) { + return second; + } else if (newestValue < currentValue) { + return first; + } + } + + return (secondSplit.length > firstSplit.length) ? second : first; + }; + + private static final String USER_AGENT = "CHOCO-update-checker"; + private static final String UPDATE_URL = "https://api.spigotmc.org/simple/0.1/index.php?action=getResource&id=%d"; + private static final Pattern DECIMAL_SCHEME_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)*"); + + private static UpdateChecker instance; + + private UpdateResult lastResult = null; + + private final JavaPlugin plugin; + private final int pluginID; + private final VersionScheme versionScheme; + + private UpdateChecker(JavaPlugin plugin, int pluginID, VersionScheme versionScheme) { + this.plugin = plugin; + this.pluginID = pluginID; + this.versionScheme = versionScheme; + } + + /** + * Request an update check to SpiGet. This request is asynchronous and may not complete + * immediately as an HTTP GET request is published to the SpiGet API. + * + * @return a future update result + */ + public CompletableFuture requestUpdateCheck() { + return CompletableFuture.supplyAsync(() -> { + int responseCode = -1; + try { + URL url = new URL(String.format(UPDATE_URL, pluginID)); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("User-Agent", USER_AGENT); + + InputStreamReader reader = new InputStreamReader(connection.getInputStream()); + responseCode = connection.getResponseCode(); + + JsonElement element = new JsonParser().parse(reader); + reader.close(); + + JsonObject versionObject = element.getAsJsonObject(); + String current = plugin.getDescription().getVersion(), newest = versionObject.get("current_version").getAsString(); + String latest = versionScheme.compareVersions(current, newest); + + if (latest == null) { + return new UpdateResult(UpdateReason.UNSUPPORTED_VERSION_SCHEME); + } else if (latest.equals(current)) { + return new UpdateResult(current.equals(newest) ? UpdateReason.UP_TO_DATE : UpdateReason.UNRELEASED_VERSION); + } else if (latest.equals(newest)) { + return new UpdateResult(UpdateReason.NEW_UPDATE, latest); + } + } catch (IOException e) { + return new UpdateResult(UpdateReason.COULD_NOT_CONNECT); + } catch (JsonSyntaxException e) { + return new UpdateResult(UpdateReason.INVALID_JSON); + } + + return new UpdateResult(responseCode == 401 ? UpdateReason.UNAUTHORIZED_QUERY : UpdateReason.UNKNOWN_ERROR); + }); + } + + /** + * Get the last update result that was queried by {@link #requestUpdateCheck()}. If no update + * check was performed since this class' initialization, this method will return null. + * + * @return the last update check result. null if none. + */ + public UpdateResult getLastResult() { + return lastResult; + } + + private static String[] splitVersionInfo(String version) { + Matcher matcher = DECIMAL_SCHEME_PATTERN.matcher(version); + if (!matcher.find()) return null; + + return matcher.group().split("\\."); + } + + /** + * Initialize this update checker with the specified values and return its instance. If an instance + * of UpdateChecker has already been initialized, this method will act similarly to {@link #get()} + * (which is recommended after initialization). + * + * @param plugin the plugin for which to check updates. Cannot be null + * @param pluginID the ID of the plugin as identified in the SpigotMC resource link. For example, + * "https://www.spigotmc.org/resources/veinminer.12038/" would expect "12038" as a value. The + * value must be greater than 0 + * @param versionScheme a custom version scheme parser. Cannot be null + * + * @return the UpdateChecker instance + */ + public static UpdateChecker init(JavaPlugin plugin, int pluginID, VersionScheme versionScheme) { + Preconditions.checkArgument(plugin != null, "Plugin cannot be null"); + Preconditions.checkArgument(pluginID > 0, "Plugin ID must be greater than 0"); + Preconditions.checkArgument(versionScheme != null, "null version schemes are unsupported"); + + return (instance == null) ? instance = new UpdateChecker(plugin, pluginID, versionScheme) : instance; + } + + /** + * Initialize this update checker with the specified values and return its instance. If an instance + * of UpdateChecker has already been initialized, this method will act similarly to {@link #get()} + * (which is recommended after initialization). + * + * @param plugin the plugin for which to check updates. Cannot be null + * @param pluginID the ID of the plugin as identified in the SpigotMC resource link. For example, + * "https://www.spigotmc.org/resources/veinminer.12038/" would expect "12038" as a value. The + * value must be greater than 0 + * + * @return the UpdateChecker instance + */ + public static UpdateChecker init(JavaPlugin plugin, int pluginID) { + return init(plugin, pluginID, VERSION_SCHEME_DECIMAL); + } + + /** + * Get the initialized instance of UpdateChecker. If {@link #init(JavaPlugin, int)} has not yet been + * invoked, this method will throw an exception. + * + * @return the UpdateChecker instance + */ + public static UpdateChecker get() { + Preconditions.checkState(instance != null, "Instance has not yet been initialized. Be sure #init() has been invoked"); + return instance; + } + + /** + * Check whether the UpdateChecker has been initialized or not (if {@link #init(JavaPlugin, int)} + * has been invoked) and {@link #get()} is safe to use. + * + * @return true if initialized, false otherwise + */ + public static boolean isInitialized() { + return instance != null; + } + + + /** + * A functional interface to compare two version Strings with similar version schemes. + */ + @FunctionalInterface + public static interface VersionScheme { + + /** + * Compare two versions and return the higher of the two. If null is returned, it is assumed + * that at least one of the two versions are unsupported by this version scheme parser. + * + * @param first the first version to check + * @param second the second version to check + * + * @return the greater of the two versions. null if unsupported version schemes + */ + public String compareVersions(String first, String second); + + } + + /** + * A constant reason for the result of {@link UpdateResult}. + */ + public static enum UpdateReason { + + /** + * A new update is available for download on SpigotMC. + */ + NEW_UPDATE, // The only reason that requires an update + + /** + * A successful connection to the SpiGet API could not be established. + */ + COULD_NOT_CONNECT, + + /** + * The JSON retrieved from SpiGet was invalid or malformed. + */ + INVALID_JSON, + + /** + * A 401 error was returned by the SpiGet API. + */ + UNAUTHORIZED_QUERY, + + /** + * The version of the plugin installed on the server is greater than the one uploaded + * to SpigotMC's resources section. + */ + UNRELEASED_VERSION, + + /** + * An unknown error occurred. + */ + UNKNOWN_ERROR, + + /** + * The plugin uses an unsupported version scheme, therefore a proper comparison between + * versions could not be made. + */ + UNSUPPORTED_VERSION_SCHEME, + + /** + * The plugin is up to date with the version released on SpigotMC's resources section. + */ + UP_TO_DATE; + + } + + /** + * Represents a result for an update query performed by {@link UpdateChecker#requestUpdateCheck()}. + */ + public final class UpdateResult { + + private final UpdateReason reason; + private final String newestVersion; + + { // An actual use for initializer blocks. This is madness! + UpdateChecker.this.lastResult = this; + } + + private UpdateResult(UpdateReason reason, String newestVersion) { + this.reason = reason; + this.newestVersion = newestVersion; + } + + private UpdateResult(UpdateReason reason) { + Preconditions.checkArgument(reason != UpdateReason.NEW_UPDATE, "Reasons that require updates must also provide the latest version String"); + this.reason = reason; + this.newestVersion = plugin.getDescription().getVersion(); + } + + /** + * Get the constant reason of this result. + * + * @return the reason + */ + public UpdateReason getReason() { + return reason; + } + + /** + * Check whether or not this result requires the user to update. + * + * @return true if requires update, false otherwise + */ + public boolean requiresUpdate() { + return reason == UpdateReason.NEW_UPDATE; + } + + /** + * Get the latest version of the plugin. This may be the currently installed version, it + * may not be. This depends entirely on the result of the update. + * + * @return the newest version of the plugin + */ + public String getNewestVersion() { + return newestVersion; + } + + } + +} diff --git a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/VersionResponse.java b/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/VersionResponse.java deleted file mode 100644 index cd472d8..0000000 --- a/ChestsPlusPlus_Main/src/main/java/com/jamesdpeters/minecraft/chests/versionchecker/VersionResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.jamesdpeters.minecraft.chests.versionchecker; - -public enum VersionResponse { - LATEST, - FOUND_NEW, - UNAVAILABLE -}