Java tutorial
/* * This file is part of HeavySpleef. * Copyright (c) 2014-2016 Matthias Werning * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.xaniox.heavyspleef.core; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import de.xaniox.heavyspleef.core.i18n.I18N; import de.xaniox.heavyspleef.core.i18n.I18NManager; import de.xaniox.heavyspleef.core.i18n.Messages; import de.xaniox.heavyspleef.core.persistence.MoreFutures; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import java.io.*; import java.net.URL; import java.net.URLConnection; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Updater implements Listener { private static final int THREAD_POOL_SIZE = 1; private static final int PROJECT_ID = 51622; private static final String SERVERMODS_API_URL = "https://api.curseforge.com/servermods/files?projectIds="; private static final Pattern VERSION_PATTERN = Pattern.compile("([0-9]+\\.)+[0-9]+"); private static final String USER_AGENT = "HeavySpleef-Updater"; private static final int BUFFER_SIZE = 1024; private final I18N i18n = I18NManager.getGlobal(); private final JSONParser parser = new JSONParser(); private final ListeningExecutorService service; private final Plugin plugin; private final PluginDescriptionFile desc; private final File updateFolder; private CheckResult result; public Updater(Plugin plugin) { ExecutorService execService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); this.service = MoreExecutors.listeningDecorator(execService); this.plugin = plugin; this.desc = plugin.getDescription(); this.updateFolder = plugin.getServer().getUpdateFolderFile(); Bukkit.getPluginManager().registerEvents(this, plugin); } public ListenableFuture<CheckResult> check(FutureCallback<CheckResult> callback) { Callable<CheckResult> callable = new CheckCallable(); ListenableFuture<CheckResult> future = service.submit(callable); if (callback != null) { MoreFutures.addBukkitSyncCallback(plugin, future, callback); } return future; } public ListenableFuture<Void> update(CommandSender progressionReceiver, FutureCallback<Void> callback) { if (result == null) { throw new IllegalArgumentException("Must check for updates with Updater#check(FutureCallback) first"); } //We assume that the caller checked if an update is available, so just execute the update Callable<Void> callable = new UpdateCallable(progressionReceiver); ListenableFuture<Void> future = service.submit(callable); if (callback != null) { MoreFutures.addBukkitSyncCallback(plugin, future, callback); } return future; } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); if (!player.hasPermission(Permissions.PERMISSION_UPDATE)) { return; } if (result == null || !result.isUpdateAvailable()) { return; } player.sendMessage(i18n.getVarString(Messages.Player.UPDATE_AVAILABLE) .setVariable("new-version", result.getVersion().toString()) .setVariable("this-version", plugin.getDescription().getVersion()).toString()); } public File getUpdateFolder() { return updateFolder; } public CheckResult getResult() { return result; } private class CheckCallable implements Callable<CheckResult> { @Override public CheckResult call() throws Exception { URL url = new URL(SERVERMODS_API_URL + PROJECT_ID); URLConnection connection = url.openConnection(); connection.setUseCaches(false); connection.setDoOutput(true); connection.setRequestProperty("User-Agent", USER_AGENT); String name; String fileName; String downloadUrl; try (InputStream in = connection.getInputStream()) { Reader reader = new InputStreamReader(in); JSONArray fileArray = (JSONArray) parser.parse(reader); //Search for the latest jar available boolean isJar = false; for (int i = 1; !isJar && i <= fileArray.size(); i++) { JSONObject latestJarObj = (JSONObject) fileArray.get(fileArray.size() - i); name = (String) latestJarObj.get("name"); fileName = (String) latestJarObj.get("fileName"); downloadUrl = (String) latestJarObj.get("downloadUrl"); if (fileName.toLowerCase().endsWith(".jar")) { isJar = true; } } JSONObject latestFileObj = (JSONObject) fileArray.get(fileArray.size() - 1); name = (String) latestFileObj.get("name"); fileName = (String) latestFileObj.get("fileName"); downloadUrl = (String) latestFileObj.get("downloadUrl"); } Matcher versionMatcher = VERSION_PATTERN.matcher(name); boolean found = versionMatcher.find(); if (!found) { throw new Version.VersionException("Update name '" + name + "' does not contain a version"); } String versionString = versionMatcher.group(); Version version = Version.parse(versionString); Version thisVersion = Version.parse(desc.getVersion()); boolean updateAvailable = thisVersion.compareTo(version) < 0; result = new CheckResult(updateAvailable, downloadUrl, fileName, version); return result; } } private class UpdateCallable implements Callable<Void> { private CommandSender messageReceiver; public UpdateCallable(CommandSender messageReceiver) { this.messageReceiver = messageReceiver; } @Override public Void call() throws Exception { String downloadUrl = result.getDownloadUrl(); if (!updateFolder.exists()) { updateFolder.mkdir(); } File file = new File(updateFolder, result.getFileName()); if (!file.exists()) { file.createNewFile(); } URL url = new URL(downloadUrl); URLConnection connection = url.openConnection(); connection.setDoInput(true); connection.setRequestProperty("User-Agent", USER_AGENT); final long size = connection.getContentLengthLong(); long downloaded = 0; int lastPercentagePrinted = 5; try (InputStream in = connection.getInputStream(); OutputStream out = new FileOutputStream(file)) { int read; byte[] buffer = new byte[BUFFER_SIZE]; while ((read = in.read(buffer, 0, BUFFER_SIZE)) > 0) { downloaded += read; out.write(buffer, 0, read); int percent = (int) (downloaded * 100D / size); if (percent % 5 == 0 && messageReceiver != null && percent != lastPercentagePrinted) { StringBuilder progressionBuilder = new StringBuilder(); progressionBuilder.append(ChatColor.GREEN); int partsDownloaded = percent / 5; int partsLeft = (100 - percent) / 5; for (int i = 0; i < partsDownloaded; i++) { progressionBuilder.append("|"); } progressionBuilder.append(ChatColor.RED); for (int i = 0; i < partsLeft; i++) { progressionBuilder.append("|"); } messageReceiver.sendMessage(ChatColor.DARK_GRAY + " [" + progressionBuilder + ChatColor.DARK_GRAY + "] " + ChatColor.GOLD + percent + "%"); lastPercentagePrinted = percent; } } } return null; } } public static class Version implements Comparable<Version> { private static final String SNAPSHOT_IDENTIFIER = "-SNAPSHOT"; private static final String SPLIT_BY_DOT = "\\."; private int[] components; private boolean snapshot; public Version(int[] components, boolean snapshot) { this.components = components; this.snapshot = snapshot; } public static Version parse(String versionString) throws VersionException { boolean snapshot; if ((snapshot = versionString.endsWith(SNAPSHOT_IDENTIFIER))) { versionString = versionString.substring(0, versionString.lastIndexOf(SNAPSHOT_IDENTIFIER)); } char[] chars = versionString.toCharArray(); StringBuilder versionBuilder = new StringBuilder(); for (char c : chars) { if (!Character.isDigit(c) && c != '.') { break; } versionBuilder.append(c); } versionString = versionBuilder.toString(); String[] strComponents = versionString.split(SPLIT_BY_DOT); final int deepness = strComponents.length; int[] components = new int[deepness]; try { for (int i = 0; i < deepness; i++) { int comp = Integer.parseInt(strComponents[i]); components[i] = comp; } } catch (NumberFormatException nfe) { throw new InvalidVersionException("Invalid version string '" + versionString + "'"); } return new Version(components, snapshot); } @Override public String toString() { int length = components.length; StringBuilder builder = new StringBuilder(); for (int i = 0; i < length; i++) { builder.append(components[i]); if (i + 1 < length) { builder.append('.'); } } if (snapshot) { builder.append(SNAPSHOT_IDENTIFIER); } return builder.toString(); } @Override public int compareTo(Version o) { int[] otherComponents = o.components; int thisDeepness = components.length; int otherDeepness = otherComponents.length; int deepness = Math.max(thisDeepness, otherDeepness); for (int i = 0; i < deepness; i++) { int comp = i < thisDeepness ? components[i] : 0; int otherComp = i < otherDeepness ? otherComponents[i] : 0; if (comp < otherComp) { return -1; } else if (comp > otherComp) { return 1; } } //Version are exact the same, try to check if one is snapshot if (snapshot && !o.snapshot) { return 1; } else if (!snapshot && o.snapshot) { return -1; } return 0; } private static class VersionException extends RuntimeException { private static final long serialVersionUID = 7300635845275692986L; public VersionException(String message) { super(message); } } private static class InvalidVersionException extends VersionException { private static final long serialVersionUID = 3202153428976606075L; public InvalidVersionException(String message) { super(message); } } } public class CheckResult { private boolean updateAvailable; private String downloadUrl; private String fileName; private Version version; public CheckResult(boolean updateAvailable, String downloadUrl, String fileName, Version version) { this.updateAvailable = updateAvailable; this.downloadUrl = downloadUrl; this.fileName = fileName; this.version = version; } public boolean isUpdateAvailable() { return updateAvailable; } public String getDownloadUrl() { return downloadUrl; } public String getFileName() { return fileName; } public Version getVersion() { return version; } } }