/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.royaldev.royalcommands;
import org.apache.commons.lang.StringUtils;
import org.bukkit.plugin.Plugin;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class that gets VU update data from the CurseForge API. {@link #getUpdateInfo(String)} is the main method of this
* class.
*/
public final class VUUpdater {
/**
* Matches "PluginName vA.B.C (V1V2V3)" according to the spec.
*/
private static final Pattern vuPattern = Pattern.compile("^(.+)\\s+v(\\d+(\\.\\d){2}).*\\((([0-9a-f]{2}){1,3})\\)$", Pattern.CASE_INSENSITIVE);
/**
* Checks for an update for the given plugin and plugin ID. This will check if the plugin version contains
* "-SNAPSHOT" and set the updater to check for development versions if it does. See
* {@link #checkForUpdate(org.bukkit.plugin.Plugin, String, boolean)} if that is not the behavior you desire.
*
* @param p Plugin to check for an update for
* @param pluginID ID of the plugin to get the information from. The ID comes from CurseForge.
* @return {@link VUUpdater.UpdateStatus}
*/
public static VUUpdater.UpdateStatus checkForUpdate(Plugin p, String pluginID) {
final String pluginVersion = p.getDescription().getVersion();
return VUUpdater.checkForUpdate(p, pluginID, pluginVersion.toUpperCase().contains("-SNAPSHOT"));
}
/**
* Checks for an update for the given plugin and plugin ID.
*
* @param p Plugin to check for an update for
* @param pluginID ID of the plugin to get the information from. The ID comes from CurseForge.
* @param useDev Should development versions be checked?
* @return {@link VUUpdater.UpdateStatus}
*/
public static UpdateStatus checkForUpdate(Plugin p, String pluginID, boolean useDev) {
final VUUpdateInfo vuui;
try {
vuui = VUUpdater.getUpdateInfo(pluginID);
} catch (IOException e) {
return UpdateStatus.ERROR;
}
final String pluginVersion = p.getDescription().getVersion();
if (useDev && !pluginVersion.contains(vuui.getDevelopment())) return UpdateStatus.UPDATE_FOUND;
if (!useDev && !pluginVersion.contains(vuui.getStable())) return UpdateStatus.UPDATE_FOUND;
return UpdateStatus.NO_UPDATE;
}
/**
* Gets the update information from the title of the latest file available from the CurseForge API.
*
* @param pluginID ID of the plugin to get the information from. The ID comes from CurseForge.
* @return {@link org.royaldev.royalcommands.VUUpdater.VUUpdateInfo}
* @throws IOException If any errors occur
*/
public static VUUpdateInfo getUpdateInfo(String pluginID) throws IOException {
final URLConnection conn = new URL("https://api.curseforge.com/servermods/files?projectIds=" + pluginID).openConnection();
conn.setConnectTimeout(5000);
conn.setRequestProperty("User-Agent", "VU/1.0");
conn.setDoOutput(true);
final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
final String response = reader.readLine();
final JSONArray array = (JSONArray) JSONValue.parse(response);
final Matcher m = VUUpdater.vuPattern.matcher((String) ((JSONObject) array.get(array.size() - 1)).get("name"));
if (!m.matches()) throw new IllegalArgumentException("No match found!");
final byte[] vuBytes = VUUpdater.hexStringToByteArray(m.group(4));
return new VUUpdateInfo(m.group(2), vuBytes);
}
/**
* Turns a hexadecimal string into an array of bytes.
*
* @param s Hexadecimal string (like FF00FF00)
* @return Byte array
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* Possible statuses for the update checker to return.
*/
public static enum UpdateStatus {
/**
* An error occurred getting the update information.
*/
ERROR,
/**
* No update was found.
*/
NO_UPDATE,
/**
* An update was found.
*/
UPDATE_FOUND
}
/**
* Class that holds and processes VU update information for plugins.
*/
public static class VUUpdateInfo {
/**
* VU bytes in the title
*/
private final byte[] vuBytes;
/**
* Version string.
*/
private String stable = null, devel = null;
/**
* Processes VU update information based on the given parameters. After construction, {@link #getStable()} and
* {@link #getDevelopment()} are available.
*
* @param versionOfFile Source version (version of the file with VU bytes on it). e.g. "1.0.0"
* @param vuBytes Array of VU bytes obtained from source file title
* @throws IllegalArgumentException If the length of vuBytes is less than one.
*/
public VUUpdateInfo(String versionOfFile, byte[] vuBytes) throws IllegalArgumentException {
if (vuBytes.length < 1) throw new IllegalArgumentException("Not enough VU bytes");
this.vuBytes = vuBytes;
int vuForStb = 0, vuForDev = 0;
if (this.hasFlag(VUFlag.VU_FOR_STABLE_FOLLOW)) {
vuForStb = this.vuBytes[this.hasFlag(VUFlag.VU_FOR_DEV_FIRST) ? 2 : 1];
}
if (this.hasFlag(VUFlag.VU_FOR_DEV_FOLLOW)) {
vuForDev = this.vuBytes[this.hasFlag(VUFlag.VU_FOR_DEV_FIRST) ? 1 : this.hasFlag(VUFlag.VU_FOR_STABLE_FOLLOW) ? 2 : 1];
}
this.stable = versionOfFile;
this.devel = versionOfFile;
if (vuForStb != 0) this.stable = vuToVersion(versionToVU(this.stable) + vuForStb);
if (vuForDev != 0) this.devel = vuToVersion(versionToVU(this.devel) + vuForDev);
if (this.hasFlag(VUFlag.STABLE_IS_SAME)) {
this.stable = versionOfFile; // override previous values
}
if (this.hasFlag(VUFlag.DEV_IS_STABLE)) {
this.devel = this.stable; // override previous values
}
}
/**
* Converts a version string to version units.
* <p>
* Converts the following:
* </p>
* <p>
* <code>3.2.0</code> to <code>320</code><br/>
* <code>10.2.0</code> to <code>1020</code>
* </p>
*
* @param version Version string
* @return Version units
*/
private int versionToVU(String version) {
version = version.replace(".", "");
try {
return Integer.parseInt(version);
} catch (NumberFormatException ex) {
return -1;
}
}
/**
* Converts version units to a version string.
* <p>
* Converts the following:
* </p>
* <p>
* <code>320</code> to <code>3.2.0</code><br/>
* <code>1020</code> to <code>10.2.0</code>
* </p>
*
* @param vu Version units
* @return Version string
*/
private String vuToVersion(int vu) {
String version = String.valueOf(vu).replaceAll(".(?!$)", "$0.");
while (StringUtils.countMatches(version, ".") > 2) version = version.replaceFirst("\\.", "");
return version;
}
/**
* Gets the development version string obtained from the VU bytes.
*
* @return Development version string
*/
public String getDevelopment() {
return this.devel;
}
/**
* Gets the stable version string obtained from the VU bytes.
*
* @return Stable version string
*/
public String getStable() {
return this.stable;
}
/**
* Returns if the head byte has the given flag set.
*
* @param flag Flag to check
* @return true if set, false if otherwise
*/
public boolean hasFlag(VUFlag flag) {
return (this.vuBytes[0] & (1 << flag.getBitNumber())) > 0;
}
/**
* Flags possible to have set on the head byte of VU bytes.
*/
private enum VUFlag {
/**
* Bit 7 of head VU byte.
* <p/>
* The stable version is the same as the source version if this is set.
*/
STABLE_IS_SAME((byte) 7),
/**
* Bit 6 of head VU byte.
* <p/>
* The development version is the same as the stable version if this is set.
*/
DEV_IS_STABLE((byte) 6),
/**
* Bit 5 of head VU byte.
* <p/>
* The VU for the development version will follow this byte if this is set.
*/
VU_FOR_DEV_FOLLOW((byte) 5),
/**
* Bit 4 of head VU byte.
* <p/>
* The VU for the stable version will follow this byte if this is set.
*/
VU_FOR_STABLE_FOLLOW((byte) 4),
/**
* Bit 3 of head VU byte.
* <p/>
* The VU for the development version will appear first after this byte if this is set.
* <p/>
* This should only be set if bit 3 or 4 are set.
*/
VU_FOR_DEV_FIRST((byte) 3);
/**
* Number that would be set for this flag in the VU head byte.
*/
private final byte bitNumber;
/**
* Constructs a VUFlag with the given bit number.
*
* @param bitNumber Bit number
*/
VUFlag(byte bitNumber) {
this.bitNumber = bitNumber;
}
/**
* Gets the bit number that would be set for this flag.
*
* @return Bit number
*/
public byte getBitNumber() {
return bitNumber;
}
}
}
}