Java tutorial
/* * This file is part of Skript. * * Skript 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. * * Skript 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 Skript. If not, see <http://www.gnu.org/licenses/>. * * * Copyright 2011-2014 Peter Gttinger * */ package ch.njol.skript; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.events.XMLEvent; import org.apache.commons.lang.StringEscapeUtils; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.eclipse.jdt.annotation.Nullable; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import ch.njol.skript.localization.FormattedMessage; import ch.njol.skript.localization.Message; import ch.njol.skript.util.Date; import ch.njol.skript.util.ExceptionUtils; import ch.njol.skript.util.FileUtils; import ch.njol.skript.util.Task; import ch.njol.skript.util.Timespan; import ch.njol.skript.util.Version; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * @author Peter Gttinger */ public final class Updater { @SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUALS") public final static class VersionInfo implements Comparable<VersionInfo> { final String name; // exact name, e.g. "2.0 (jar only)" final Version version; final String downloadURL; @Nullable @SuppressFBWarnings(value = "URF_UNREAD_FIELD", justification = "used in SkriptCommand") String changelog; @Nullable @SuppressFBWarnings(value = "URF_UNREAD_FIELD", justification = "used in SkriptCommand") Date date; VersionInfo(final String name, final Version version, final String downloadURL) { this.name = name; this.version = version; this.downloadURL = downloadURL; } @Override public String toString() { return version.toString(); } @Override public int compareTo(final @Nullable VersionInfo o) { return version.compareTo(o == null ? null : o.version); } } /** * Used to check for updates & get the file download links as required by the Bukkit guidelines */ private final static String filesURL = "https://api.curseforge.com/servermods/files?projectIds=32084"; /** * Used to get the changelogs and release dates of the newest files */ private final static String RSSURL = "http://dev.bukkit.org/server-mods/skript/files.rss"; private final static DateFormat RFC2822 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH); public static enum UpdateState { NOT_STARTED, CHECK_IN_PROGRESS, CHECK_ERROR, CHECKED_FOR_UPDATE, DOWNLOAD_IN_PROGRESS, DOWNLOAD_ERROR, DOWNLOADED; } public final static ReentrantReadWriteLock stateLock = new ReentrantReadWriteLock(); /** * must be synchronised with {@link #stateLock} */ public static volatile UpdateState state = UpdateState.NOT_STARTED; final static AtomicReference<String> error = new AtomicReference<String>(); public final static List<VersionInfo> infos = new ArrayList<VersionInfo>(); public final static AtomicReference<VersionInfo> latest = new AtomicReference<VersionInfo>(); // must be down here as they reference 'error' and 'latest' which are defined above public final static Message m_not_started = new Message("updater.not started"); public final static Message m_checking = new Message("updater.checking"); public final static Message m_check_in_progress = new Message("updater.check in progress"); public final static FormattedMessage m_check_error = new FormattedMessage("updater.check error", error); public final static Message m_running_latest_version = new Message("updater.running latest version"); public final static Message m_running_latest_version_beta = new Message( "updater.running latest version (beta)"); public final static FormattedMessage m_update_available = new FormattedMessage("updater.update available", latest, Skript.getVersion()); public final static FormattedMessage m_downloading = new FormattedMessage("updater.downloading", latest); public final static Message m_download_in_progress = new Message("updater.download in progress"); public final static FormattedMessage m_download_error = new FormattedMessage("updater.download error", error); public final static FormattedMessage m_downloaded = new FormattedMessage("updater.downloaded", latest); public final static Message m_internal_error = new Message("updater.internal error"); @Nullable static Task checkerTask = null; static void start() { checkerTask = new Task(Skript.getInstance(), 0, true) { @SuppressWarnings("null") @Override public void run() { if (!SkriptConfig.checkForNewVersion.value()) return; check(Bukkit.getConsoleSender(), SkriptConfig.automaticallyDownloadNewVersion.value(), true); final Timespan t = SkriptConfig.updateCheckInterval.value(); if (t.getTicks() != 0) setNextExecution(t.getTicks()); } }; } /** * @param sender Sender to receive messages * @param download Whether to directly download the newest version if one is found * @param isAutomatic */ static void check(final CommandSender sender, final boolean download, final boolean isAutomatic) { stateLock.writeLock().lock(); try { if (state == UpdateState.CHECK_IN_PROGRESS || state == UpdateState.DOWNLOAD_IN_PROGRESS) return; state = UpdateState.CHECK_IN_PROGRESS; } finally { stateLock.writeLock().unlock(); } if (!isAutomatic || Skript.logNormal()) Skript.info(sender, "" + m_checking); Skript.newThread(new Runnable() { @Override public void run() { infos.clear(); InputStream in = null; try { final URLConnection conn = new URL(filesURL).openConnection(); conn.setRequestProperty("User-Agent", "Skript/v" + Skript.getVersion() + " (by Njol)"); in = conn.getInputStream(); final BufferedReader reader = new BufferedReader(new InputStreamReader(in, conn.getContentEncoding() == null ? "UTF-8" : conn.getContentEncoding())); try { final String line = reader.readLine(); if (line != null) { final JSONArray a = (JSONArray) JSONValue.parse(line); for (final Object o : a) { final Object name = ((JSONObject) o).get("name"); if (!(name instanceof String) || !((String) name).matches("\\d+\\.\\d+(\\.\\d+)?( \\(jar( only)?\\))?"))// not the default version pattern to not match beta/etc. versions continue; final Object url = ((JSONObject) o).get("downloadUrl"); if (!(url instanceof String)) continue; final Version version = new Version(((String) name).contains(" ") ? "" + ((String) name).substring(0, ((String) name).indexOf(' ')) : ((String) name)); if (version.compareTo(Skript.getVersion()) > 0) { infos.add(new VersionInfo((String) name, version, (String) url)); } } } } finally { reader.close(); } if (!infos.isEmpty()) { Collections.sort(infos); latest.set(infos.get(0)); } else { latest.set(null); } getChangelogs(sender); final String message = infos.isEmpty() ? (Skript.getVersion().isStable() ? "" + m_running_latest_version : "" + m_running_latest_version_beta) : "" + m_update_available; if (isAutomatic && !infos.isEmpty()) { Skript.adminBroadcast(message); } else { Skript.info(sender, message); } if (download && !infos.isEmpty()) { stateLock.writeLock().lock(); try { state = UpdateState.DOWNLOAD_IN_PROGRESS; } finally { stateLock.writeLock().unlock(); } download_i(sender, isAutomatic); } else { stateLock.writeLock().lock(); try { state = UpdateState.CHECKED_FOR_UPDATE; } finally { stateLock.writeLock().unlock(); } } } catch (final IOException e) { stateLock.writeLock().lock(); try { state = UpdateState.CHECK_ERROR; error.set(ExceptionUtils.toString(e)); if (sender != null) Skript.error(sender, m_check_error.toString()); } finally { stateLock.writeLock().unlock(); } } catch (final Exception e) { if (sender != null) Skript.error(sender, m_internal_error.toString()); Skript.exception(e, "Unexpected error while checking for a new version of Skript"); stateLock.writeLock().lock(); try { state = UpdateState.CHECK_ERROR; error.set(e.getClass().getSimpleName() + ": " + e.getLocalizedMessage()); } finally { stateLock.writeLock().unlock(); } } finally { if (in != null) { try { in.close(); } catch (final IOException e) { } } } } }, "Skript update thread").start(); } /** * Gets the changelogs and release dates of the newest versions * * @param sender */ final static void getChangelogs(final CommandSender sender) { InputStream in = null; InputStreamReader r = null; try { final URLConnection conn = new URL(RSSURL).openConnection(); conn.setRequestProperty("User-Agent", "Skript/v" + Skript.getVersion() + " (by Njol)"); // Bukkit returns a 403 (forbidden) if no user agent is set in = conn.getInputStream(); r = new InputStreamReader(in, conn.getContentEncoding() == null ? "UTF-8" : conn.getContentEncoding()); final XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(r); infos.clear(); VersionInfo current = null; outer: while (reader.hasNext()) { XMLEvent e = reader.nextEvent(); if (e.isStartElement()) { final String element = e.asStartElement().getName().getLocalPart(); if (element.equalsIgnoreCase("title")) { final String name = reader.nextEvent().asCharacters().getData().trim(); for (final VersionInfo i : infos) { if (name.equals(i.name)) { current = i; continue outer; } } current = null; } else if (element.equalsIgnoreCase("description")) { if (current == null) continue; final StringBuilder cl = new StringBuilder(); while ((e = reader.nextEvent()).isCharacters()) cl.append(e.asCharacters().getData()); current.changelog = "- " + StringEscapeUtils.unescapeHtml("" + cl).replace("<br>", "") .replace("<p>", "").replace("</p>", "").replaceAll("\n(?!\n)", "\n- "); } else if (element.equalsIgnoreCase("pubDate")) { if (current == null) continue; synchronized (RFC2822) { // to make FindBugs shut up current.date = new Date( RFC2822.parse(reader.nextEvent().asCharacters().getData()).getTime()); } } } } } catch (final IOException e) { stateLock.writeLock().lock(); try { state = UpdateState.CHECK_ERROR; error.set(ExceptionUtils.toString(e)); Skript.error(sender, m_check_error.toString()); } finally { stateLock.writeLock().unlock(); } } catch (final Exception e) { Skript.error(sender, m_internal_error.toString()); Skript.exception(e, "Unexpected error while checking for a new version of Skript"); stateLock.writeLock().lock(); try { state = UpdateState.CHECK_ERROR; error.set(e.getClass().getSimpleName() + ": " + e.getLocalizedMessage()); } finally { stateLock.writeLock().unlock(); } } finally { if (in != null) { try { in.close(); } catch (final IOException e) { } } if (r != null) { try { r.close(); } catch (final IOException e) { } } } } /** * Must set {@link #state} to {@link UpdateState#DOWNLOAD_IN_PROGRESS} prior to calling this * * @param sender * @param isAutomatic */ static void download_i(final CommandSender sender, final boolean isAutomatic) { assert sender != null; stateLock.readLock().lock(); try { if (state != UpdateState.DOWNLOAD_IN_PROGRESS) throw new IllegalStateException(); } finally { stateLock.readLock().unlock(); } Skript.info(sender, "" + m_downloading); // boolean hasJar = false; // ZipInputStream zip = null; InputStream in = null; try { final URLConnection conn = new URL(latest.get().downloadURL).openConnection(); in = conn.getInputStream(); assert in != null; FileUtils.save(in, new File(Bukkit.getUpdateFolderFile(), "Skript.jar")); // zip = new ZipInputStream(conn.getInputStream()); // ZipEntry entry; //// boolean hasAliases = false; // while ((entry = zip.getNextEntry()) != null) { // if (entry.getName().endsWith("Skript.jar")) { // assert !hasJar; // save(zip, new File(Bukkit.getUpdateFolderFile(), "Skript.jar")); // hasJar = true; // }// else if (entry.getName().endsWith("aliases.sk")) { //// assert !hasAliases; //// saveZipEntry(zip, new File(Skript.getInstance().getDataFolder(), "aliases-" + latest.get().version + ".sk")); //// hasAliases = true; //// } // zip.closeEntry(); // if (hasJar)// && hasAliases) // break; // } if (isAutomatic) Skript.adminBroadcast("" + m_downloaded); else Skript.info(sender, "" + m_downloaded); stateLock.writeLock().lock(); try { state = UpdateState.DOWNLOADED; } finally { stateLock.writeLock().unlock(); } } catch (final IOException e) { stateLock.writeLock().lock(); try { state = UpdateState.DOWNLOAD_ERROR; error.set(ExceptionUtils.toString(e)); Skript.error(sender, m_download_error.toString()); } finally { stateLock.writeLock().unlock(); } } catch (final Exception e) { Skript.exception(e, "Error while downloading the latest version of Skript"); stateLock.writeLock().lock(); try { state = UpdateState.DOWNLOAD_ERROR; error.set(e.getClass().getSimpleName() + ": " + e.getLocalizedMessage()); } finally { stateLock.writeLock().unlock(); } } finally { if (in != null) { try { in.close(); } catch (final IOException e) { } } } } /** * Must only be called if {@link #state} == {@link UpdateState#CHECKED_FOR_UPDATE} or {@link UpdateState#DOWNLOAD_ERROR} * * @param sender */ public static void download(final CommandSender sender, final boolean isAutomatic) { assert sender != null; stateLock.writeLock().lock(); try { if (state != UpdateState.CHECKED_FOR_UPDATE && state != UpdateState.DOWNLOAD_ERROR) throw new IllegalStateException("Must check for an update first"); state = UpdateState.DOWNLOAD_IN_PROGRESS; } finally { stateLock.writeLock().unlock(); } Skript.newThread(new Runnable() { @Override public void run() { download_i(sender, isAutomatic); } }, "Skript download thread").start(); } }