First of all, I am absolutely surprised that this code even works. Originally what I did was create a version checking system for Minecraft plugins (specifically Bukkit, Craftbukkit, & Spigot).
What I wanted to do was refactor my Version
class in order to make it platform independent (where it doesn't depend or provide functionality for only Minecraft servers and plugins).
So instead of the constructors accepting Bukkit classes, I changed it to accept a generic String that would represent the version. Then I made Builder methods, which I deleted and move into a factory: VersionFactory
(this makes it easier for end-users to construct Version objects specific to Bukkit servers).
So, the main methods of Version.java are isCompatible()
and isSupported()
. However, the first thing these methods did was perform an isEnabled()
check on the Version object because any compatibility checks should fail (as intended) if the target plugin isn't even installed on the server.
However, any such checks on specific Bukkit classes and objects would restrict the use of my Version checking system to only Bukkit. And I wanted to make it platform independent.
So what I did was create a functional interface called Tester
, and asked for it in the constructor of Version (along with the object under test). The primary intent is to allow plugin.isEnabled()
to be run and return true or false BEFORE isCompatible()
or isSupported()
is executed. However, it does allow ANY custom test()
to be performed.
What is surprising is that I can pass type Object
into the constructor, which gets assigned to a field, and ultimately gets passed to tester.isEnabled()
which is able to call a plugin specific method on a generic Object type!
Version.java
package mc.euro.version;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The Version object: Capable of asking the important questions:. <br/><br/>
*
* Is the version that's currently installed on the server compatible/supported with a specified version ? <br/><br/>
*
* isCompatible(): Is the installed version greater than or equal to the minimum required version ? <br/><br/>
*
* isSupported(): Is the installed version less than or equal to the maximum required version ? <br/><br/>
*
* @author Europia79, BigTeddy98, Tux2, DSH105
*/
public class Version implements Comparable<Version> {
/**
* The Predicate tester preforms the job of checking if the plugin isEnabled(). <br/><br/>
*
* Because, if the plugin is disabled, then our compatibility check should fail.
*/
Tester tester;
Object object;
final String version;
String separator = "[_.-]";
/**
* VersionFactory methods getPluginVersion(), getServerVersion(), getNmsVersion() available for convenience. <br/>
* @param version The version of the plugin, server, or application that is currently running in the JVM. <br/>
*/
public Version(String version) {
this.version = version;
this.tester = TesterFactory.getDefaultTester();
this.object = new Object();
}
/**
* VersionFactory methods getPluginVersion(), getServerVersion(), getNmsVersion() available for convenience. <br/>
* @param version The version of the plugin, server, or application that is currently running in the JVM. <br/>
* @param tester isCompatible() & isSupported() will ask the tester if the object isEnabled() before proceeding. <br/>
* @param object The object to be tested. <br/>
*/
public Version(String version, Tester tester, Object object) {
this.version = version;
this.tester = tester;
this.object = object;
}
public boolean isEnabled() {
return tester.isEnabled(object);
}
/**
* @param minVersion - The absolute minimum version that's required to achieve compatibility.
* @return Return true, if the currently running/installed version is greater than or equal to minVersion.
*/
public boolean isCompatible(String minVersion) {
if (!this.isEnabled()) return false;
int x = compareTo(new Version(minVersion));
if (x >= 0) {
return true;
}
return false;
}
/**
* @param maxVersion - The absolute maximum version that's supported.
* @return Return true, if the currently running/installed version is less than or equal to maxVersion.
*/
public boolean isSupported(String maxVersion) {
if (!this.isEnabled()) return false;
int x = compareTo(new Version(maxVersion));
if (x <= 0) {
return true;
}
return false;
}
@Override
public int compareTo(Version whichVersion) {
int[] currentVersion = parseVersion(this.version);
int[] otherVersion = parseVersion(whichVersion.toString());
int length = (currentVersion.length >= otherVersion.length) ? currentVersion.length : otherVersion.length;
for (int index = 0; index <= (length - 1); index = index + 1) {
try {
if (currentVersion[index] != otherVersion[index]) {
return currentVersion[index] - otherVersion[index];
}
} catch (IndexOutOfBoundsException ex) {
if (currentVersion.length > otherVersion.length) {
return currentVersion[index] - 0;
} else if (currentVersion.length < otherVersion.length) {
return 0 - otherVersion[index];
}
}
}
return 0;
}
/**
* A typical version of 1.2.3.4-b567 will be broken down into an array. <br/><br/>
*
* [1] [2] [3] [4] [567]
*/
private int[] parseVersion(String version) {
String[] stringArray = version.split(separator);
int[] temp = new int[stringArray.length];
for (int index = 0; index <= (stringArray.length - 1); index = index + 1) {
String t = stringArray[index].replaceAll("\\D", "");
try {
temp[index] = Integer.valueOf(t);
} catch(NumberFormatException ex) {
temp[index] = 0;
}
}
return temp;
}
public Version setSeparator(String regex) {
this.separator = regex;
return this;
}
/**
* search() for possible Development builds.
*/
public boolean search(String regex) {
if (version == null) return false;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(this.version);
if (matcher.find()) {
return true;
}
return false;
}
/**
* Used to get a Sub-Version (or Development build). <br/><br/>
* @param regex
* @return A completely new Version object.
*/
public Version getSubVersion(String regex) {
if (version == null) return this;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(this.version);
String dev = this.version;
if (matcher.find()) {
dev = matcher.group();
}
return new Version(dev);
}
@Override
public String toString() {
String v = (this.version == null) ? "" : this.version;
return v;
}
}
Examples of using it
public class SomePlugin extends JavaPlugin {
private Version server;
public static final String MAX = "1.7.10-R9.9-SNAPSHOT";
public static final String MIN = "1.2.5";
public static final String NMS = VersionFactory.getNmsVersion().toString();
@Override
public void onEnable() {
server = VersionFactory.getServerVersion();
if (!server.isSupported(MAX) || !server.isCompatible(MIN)) {
getLogger().info("This plugin is not compatible with your server.");
getLogger().info("The maximum supported version is " + MAX);
getLogger().info("The minimum capatible version is " + MIN);
Bukkit.getServer().getPluginManager().disablePlugin(this);
return;
}
// ...
Version HD = VersionFactory.getPluginVersion("HolographicDisplays");
Version Holoapi = VersionFactory.getPluginVersion("HoloAPI");
debug.log("HolographicDisplays version = " + HD.toString());
debug.log("HoloAPI version = " + Holoapi.toString());
if (ShowHolograms && HD.isCompatible("1.8.5")) {
this.holograms = new HolographicDisplay(this);
debug.log("HolographicDisplays support is enabled.");
} else if (ShowHolograms && Holoapi.isEnabled()) {
this.holograms = new HolographicAPI(this);
debug.log("HoloAPI support is enabled.");
} else {
this.holograms = new HologramsOff();
debug.log("Hologram support is disabled.");
debug.log("Please download HoloAPI or HolographicDisplays to enable Hologram support.");
}
}
public static Class<?> getNmsClass(String clazz) throws Exception {
return Class.forName("com.yourdomain.yourproject.compat." + NMS + "." + clazz);
}
}
VersionFactory.java
package mc.euro.version;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
/**
* Handles the construction of new Version objects specific to Craftbukkit.
* @author Nikolai
*/
public class VersionFactory {
/**
* Factory method used when you want to construct a Version object via a Plugin object. <br/>
*/
public static Version getPluginVersion(Plugin plugin) {
String version = (plugin == null) ? null : plugin.getDescription().getVersion();
Tester tester = TesterFactory.getNewTester(plugin);
return new Version(version, tester, plugin);
}
/**
* Factory method used when you want to construct a Version object via pluginName. <br/>
*/
public static Version getPluginVersion(String pluginName) {
Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName);
String version = (plugin == null) ? null : plugin.getDescription().getVersion();
Tester tester = TesterFactory.getNewTester(plugin);
return new Version(version, tester, plugin);
}
/**
* Factory method to conveniently construct a Version object of the server. <br/>
*/
public static Version getServerVersion() {
String version = Bukkit.getServer().getBukkitVersion();
return new Version(version);
}
/**
* Factory method to conveniently construct a Version object of net.minecraft.server.v1_X_RY package. <br/>
*/
public static Version getNmsVersion() {
String NMS = null;
try {
NMS = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3];
} catch (ArrayIndexOutOfBoundsException ex) {
NMS = "pre";
}
return new Version(NMS);
}
}
TesterFactory.java
package mc.euro.version;
import org.bukkit.plugin.Plugin;
/**
* Provides less typing for the construction of Plugin Testers. <br/><br/>
*
* @author Nikolai
*/
public class TesterFactory {
public static Tester getNewTester(Plugin plugin) {
if (plugin == null) {
return new Tester<Plugin>() {
@Override
public boolean isEnabled(Plugin t) {
return false;
}
};
} else {
return new Tester<Plugin>() {
@Override
public boolean isEnabled(Plugin t) {
return t.isEnabled();
}
};
}
}
public static Tester getDefaultTester() {
return new Tester() {
@Override
public boolean isEnabled(Object t) {
return true;
}
};
}
}
Tester.java
package mc.euro.version;
/**
* The Tester interface is used to optionally pass an additional isEnabled() check to the Version object. <br/><br/>
*
* Notice that if the test returns false, then the compatibility check will stop, (as intended)
* because if the plugin is not enabled, then it's not compatible.
*
* @author Nikolai
*/
public interface Tester<T> {
public boolean isEnabled(T t);
}
All the code is here
The build tests are here
The javadocs are here
isEnabled()
is being invoked on type Object. I wrote this code after reading about Java 8 lambdas. And I wanted to mess around with the concept of passing behavior into a method, except with Java 6 (because Minecraft is compiled with Java6). \$\endgroup\$TesterFactory.getDefaultTester()
, which returns an unsafe unparameterizedTester
, notTesterFactory.getNewTester()
, which returns aTester<Plugin>
. (BTW, a good IDE should warn you about unparameterized generics.) \$\endgroup\$VersionFactory
. Which for the methodsgetPluginVersion()
are both usingTesterFactory.getNewTester()
. \$\endgroup\$getNewTester
returns aTester
, not aTester<Plugin>
, because of the declared return type. Basically, you're still using unparameterized generics and it's ignoring the extra<Plugin>
detail. \$\endgroup\$