3
\$\begingroup\$

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

\$\endgroup\$
5
  • \$\begingroup\$ Where exactly do you call a plugin-specific method that you believe you shouldn't be able to? \$\endgroup\$ Commented Oct 2, 2014 at 23:52
  • \$\begingroup\$ @raptortech97 github.com/Europia79/Version/blob/master/src/main/java/mc/euro/… accepts type Plugin. However github.com/Europia79/Version/blob/master/src/main/java/mc/euro/… provides type Object. So essentially, Plugin method 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\$
    – Nikolaii99
    Commented Oct 3, 2014 at 0:20
  • \$\begingroup\$ That's because you're calling TesterFactory.getDefaultTester(), which returns an unsafe unparameterized Tester, not TesterFactory.getNewTester(), which returns a Tester<Plugin>. (BTW, a good IDE should warn you about unparameterized generics.) \$\endgroup\$ Commented Oct 3, 2014 at 0:27
  • \$\begingroup\$ @raptortech97 No, look at the Examples section. They're all using the VersionFactory. Which for the methods getPluginVersion() are both using TesterFactory.getNewTester(). \$\endgroup\$
    – Nikolaii99
    Commented Oct 3, 2014 at 0:33
  • \$\begingroup\$ Ah! But getNewTester returns a Tester, not a Tester<Plugin>, because of the declared return type. Basically, you're still using unparameterized generics and it's ignoring the extra <Plugin> detail. \$\endgroup\$ Commented Oct 3, 2014 at 0:41

1 Answer 1

5
\$\begingroup\$
    if (x >= 0) {
        return true;
    } 
    return false;

    if (x <= 0) {
        return true;
    }
    return false;

    if (matcher.find()) {
        return true;
    }
    return false;

Are constructs I'd express as

return x >= 0;

and

return x <= 0;

and

return matcher.find();

instead.


@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;
}

index = index + 1 can be index++.

for return currentVersion[index] - 0;, it's silly to subtract 0 as it does nothing.

index <= (length - 1) can be index < length instead. In fact, that's how I write my for loops: for(int i = 0; i < iterations; i++)

for int length = (currentVersion.length >= otherVersion.length) ? currentVersion.length : otherVersion.length;, consider using Math.max(currentVersion.length, otherVersion.length) instead.


The reason I would express all these statements differently is because most of those things can simply be expressed in less code. The statement index <= (length - 1) is not easier to understand, nor does it provide any semantic value over index < length. By removing these overly complex syntactical structures, you make it easier to understand the code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.