diff --git a/pom.xml b/pom.xml index d8c3ecc..0318801 100644 --- a/pom.xml +++ b/pom.xml @@ -48,5 +48,18 @@ 1.0 provided + + + com.fasterxml.jackson.core + jackson-databind + 2.9.0 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.9.0 + + \ No newline at end of file diff --git a/src/main/java/com/redstoner/exceptions/InvalidModuleException.java b/src/main/java/com/redstoner/exceptions/InvalidModuleException.java new file mode 100644 index 0000000..930a2bb --- /dev/null +++ b/src/main/java/com/redstoner/exceptions/InvalidModuleException.java @@ -0,0 +1,35 @@ +package com.redstoner.exceptions; + +import com.redstoner.loader.ModuleLoader; + +public class InvalidModuleException extends Exception +{ + public InvalidModuleException() + { + } + + public InvalidModuleException(String message) + { + super(message); + } + + public InvalidModuleException(String message, Throwable cause) + { + super(message, cause); + } + + public InvalidModuleException(Throwable cause) + { + super(cause); + } + + public InvalidModuleException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } + + public static InvalidModuleException descriptorFileMissing(ModuleLoader loader) + { + return new InvalidModuleException("Descriptor file missing for module \"" + loader.getModuleName() + "\" in jar file \"" + loader.getJarFile().getPath() + "\""); + } +} diff --git a/src/main/java/com/redstoner/faucet/Faucet.java b/src/main/java/com/redstoner/faucet/Faucet.java index 6c37f15..946139a 100644 --- a/src/main/java/com/redstoner/faucet/Faucet.java +++ b/src/main/java/com/redstoner/faucet/Faucet.java @@ -64,4 +64,9 @@ public class Faucet extends JavaPlugin { } + + public ClassLoader getParentLoader() + { + return parentLoader; + } } diff --git a/src/main/java/com/redstoner/loader/ModuleDescriptor.java b/src/main/java/com/redstoner/loader/ModuleDescriptor.java new file mode 100644 index 0000000..a7a4809 --- /dev/null +++ b/src/main/java/com/redstoner/loader/ModuleDescriptor.java @@ -0,0 +1,119 @@ +package com.redstoner.loader; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public final class ModuleDescriptor +{ + private static final ObjectMapper yamlObjectMapper; + private String name; + private Version version; + private String mainClass; + private List dependencies; + private List softDependencies; + + public ModuleDescriptor() { + + } + + public ModuleDescriptor(String name, Version version, String mainClass, List dependencies, List softDependencies) + { + this.name = name; + this.version = version; + this.mainClass = mainClass; + this.dependencies = dependencies; + this.softDependencies = softDependencies; + } + + static { + yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + yamlObjectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(Version.class, new JsonDeserializer() + { + @Override + public Version deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + JsonNode node = p.readValueAsTree(); + String text = node.asText(); + Version version = Version.tryParse(text); + if (version == null) { + throw new IOException("Invalid version: " + text); + } + return version; + } + }); + } + + public static ModuleDescriptor load(String content) throws IOException + { + return yamlObjectMapper.readValue(content, ModuleDescriptor.class); + } + + public static ModuleDescriptor load(InputStream inputStream) throws IOException + { + return yamlObjectMapper.readValue(inputStream, ModuleDescriptor.class); + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public Version getVersion() + { + return version; + } + + public void setVersion(Version version) + { + this.version = version; + } + + public String getMainClass() + { + return mainClass; + } + + public void setMainClass(String mainClass) + { + this.mainClass = mainClass; + } + + public List getDependencies() + { + return dependencies; + } + + public void setDependencies(List dependencies) + { + this.dependencies = dependencies; + } + + public List getSoftDependencies() + { + return softDependencies; + } + + public void setSoftDependencies(List softDependencies) + { + this.softDependencies = softDependencies; + } + +} diff --git a/src/main/java/com/redstoner/loader/ModuleLoader.java b/src/main/java/com/redstoner/loader/ModuleLoader.java new file mode 100644 index 0000000..f7428cb --- /dev/null +++ b/src/main/java/com/redstoner/loader/ModuleLoader.java @@ -0,0 +1,152 @@ +package com.redstoner.loader; + +import com.redstoner.exceptions.InvalidModuleException; +import com.redstoner.faucet.Faucet; +import com.redstoner.modules.Module; +import com.redstoner.util.Checksum; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; + +public class ModuleLoader +{ + private final Faucet faucet; + private final String moduleName; + private final File jarFile; + private final URL jarFileURL; + private long jarFileLastModified; + private byte[] jarFileChecksum; + private ModuleDescriptor moduleDescriptor; + private ClassLoader classLoader; + private boolean isLoaded; + private Module loadedModule; + private boolean isEnabled; + private long loadTime; + + + public ModuleLoader(Faucet faucet, String moduleName, File jarFile) + { + if (!jarFile.isFile()) + { + throw new IllegalArgumentException("Expected a file"); + } + + this.faucet = faucet; + this.moduleName = moduleName; + this.jarFile = jarFile; + try + { + this.jarFileURL = jarFile.toURL(); + } catch (MalformedURLException e) + { + throw new IllegalArgumentException(e); + } + } + + public void load() throws InvalidModuleException + { + if (isLoaded) + { + throw new IllegalStateException("Cannot load a module when it is loaded"); + } + + ClassLoader classLoader = new URLClassLoader(new URL[]{jarFileURL}, faucet.getParentLoader()); + ModuleDescriptor descriptor; + try (InputStream descriptorStream = classLoader.getResourceAsStream("module-descriptor.yml")) + { + if (descriptorStream == null) + { + throw InvalidModuleException.descriptorFileMissing(this); + } + + descriptor = ModuleDescriptor.load(descriptorStream); + } catch (IOException e) + { + throw new InvalidModuleException(e); + } + + this.classLoader = classLoader; + this.moduleDescriptor = descriptor; + + // TODO + // needs to check if version is newer than the old one, if an old one is present + // etc + } + + public void reload() throws InvalidModuleException + { + if (!isOutdated()) + { + return; + } + unload(); + load(); + } + + public boolean isOutdated() + { + if (jarFileChecksum == null) + { + return true; + } + + if (jarFile.lastModified() != jarFileLastModified) + { + return true; + } + + byte[] checksum = computeChecksum(); + return !Arrays.equals(checksum, this.jarFileChecksum); + } + + public void unload() + { + + } + + private byte[] computeChecksum() + { + return Checksum.SHA1.compute(jarFile); + } + + public String getModuleName() + { + return moduleName; + } + + public File getJarFile() + { + return jarFile; + } + + public ModuleDescriptor getModuleDescriptor() + { + return moduleDescriptor; + } + + public ClassLoader getClassLoader() + { + return classLoader; + } + + public boolean isLoaded() + { + return isLoaded; + } + + public Module getLoadedModule() + { + return loadedModule; + } + + public boolean isEnabled() + { + return isEnabled; + } + +} diff --git a/src/main/java/com/redstoner/loader/Version.java b/src/main/java/com/redstoner/loader/Version.java new file mode 100644 index 0000000..6ddeae2 --- /dev/null +++ b/src/main/java/com/redstoner/loader/Version.java @@ -0,0 +1,465 @@ +package com.redstoner.loader; + +/** + * This class represents a version, as the name suggests. + *

+ * Based on: + * https://msdn.microsoft.com/en-us/library/system.version(v=vs.110).aspx Based + * on: + * https://github.com/mono/mono/blob/master/mcs/class/referencesource/mscorlib/system/version.cs + */ +public final class Version implements Comparable, Cloneable +{ + + private final int major; + private final int minor; + private final int build; + private final int revision; + + /** + * Initializes a new instance of the Version class, with major=0, minor=0, + * build=undefined(-1), revision=undefined(-1) + */ + public Version() + { + this.major = 0; + this.minor = 0; + this.build = -1; + this.revision = -1; + } + + /** + * Initializes a new instance of the Version class using the specified major + * and minor values. + * + * @param major major value + * @param minor minor value + * @throws IllegalArgumentException if major or minor is less than 0 + */ + public Version(int major, int minor) + { + this.major = validateRange("major", major); + this.minor = validateRange("minor", minor); + this.build = -1; + this.revision = -1; + } + + /** + * Validates the given component + * + * @param componentName the name of the component, used for exception message + * @param component the component value + * @return the component value + * @throws IllegalArgumentException if the component value is out of range // less than 0. + */ + private static int validateRange(String componentName, int component) + { + if (component < 0) + { + throw new IllegalArgumentException('\'' + componentName + "' component out of range: " + component); + } + return component; + } + + /** + * Initializes a new instance of the Version class using the specified + * major, minor, and build values. + * + * @param major major value + * @param minor minor value + * @param build build value + * @throws NullPointerException if major, minor or build is less than 0 + */ + public Version(int major, int minor, int build) + { + this.major = validateRange("major", major); + this.minor = validateRange("minor", minor); + this.build = validateRange("build", build); + this.revision = -1; + } + + /** + * Initializes a new instance of the Version class based on string + * + * @param version the string to parse + */ + public Version(String version) + { + Version v = parseVersion(version); + + this.major = v.major; + this.minor = v.minor; + this.build = v.build; + this.revision = v.revision; + } + + /** + * Gets a Version object from the given String representation + *

+ * A string representation of a Version must follow the regex: + * ([0-9]+\.){1-3}[0-9]+ without any numbers in it exceeding + * {@link Integer#MAX_VALUE} + *

+ * For a simpler representation format: major.minor[.build[.revision]] + * + * @param input The String representation of a Version + * @return a Version + * @throws IllegalArgumentException if the input does not follow a Version + * syntax. + * @throws NullPointerException if input is null + */ + public static Version parseVersion(String input) + { + int length = input.length(); + int[] result = new int[]{-1, -1, -1, -1}; + int index = 0; + int currentNumber = -1; + int resultIndex = 0; + + while (index < length) + { + if (resultIndex == 4) + { + + throw new IllegalArgumentException("Found a version component beyond the fourth at " + input + "[" + index + "]"); + } + + char c = input.charAt(index); + + // the ascii code for the character '0' is 0x30. + // used in favor of Character.digit(char, int) for performance. + int digit = c - 0x30; + + if (c == '.') + { + if (currentNumber < 0) + { + throw new IllegalArgumentException("Expected a digit at " + input + "[" + index + "], but found " + c); + } + + result[resultIndex] = currentNumber; + resultIndex++; + currentNumber = -1; + } else if (!(0 <= digit && digit <= 9)) + { + throw new IllegalArgumentException("Version components must be separated by a '.', but found " + c + " at " + input + "[" + index + "]"); + } else if (currentNumber < 0) + { + //Got digit + currentNumber = digit; + } else + { + // any digit subsequent to the first + if (currentNumber == 0) + { + // ensure we don't have a component that started with 0 and has any additional digits. + // The first 0 would be obsolete to represent its value, meaning that there would be multiple valid string representations for the same Version + throw new IllegalArgumentException("Unneeded 0 in version component number is illegal at " + input + "[" + index + "]"); + } + + //Got digit + currentNumber = currentNumber * 10 + digit; + } + + index++; + } + + if (currentNumber >= 0) + { + result[resultIndex] = currentNumber; + resultIndex++; + } + + switch (resultIndex) + { + default: + throw new IllegalArgumentException("Version requires major and minor value: " + input); + case 2: + return new Version(result[0], result[1]); + case 3: + return new Version(result[0], result[1], result[2]); + case 4: + return new Version(result[0], result[1], result[2], result[3]); + } + } + + /** + * Initializes a new instance of the Version class with the specified major, + * minor, build, and revision numbers. + * + * @param major major value + * @param minor minor value + * @param build build value + * @param revision revision value + * @throws IllegalArgumentException if major, minor, build or revision is + * less than 0 + */ + public Version(int major, int minor, int build, int revision) + { + this.major = validateRange("major", major); + this.minor = validateRange("minor", minor); + this.build = validateRange("build", build); + this.revision = validateRange("revision", revision); + } + + /** + * Tries to convert the string representation of a version number to an + * equivalent Version object, and returns the result value or null if parse + * failed. + * + * @param input The input to parse + * @return The parsed version or null if parse failed. + */ + public static Version tryParse(String input) + { + try + { + return parseVersion(input); + } catch (Exception ex) + { + //Simply ignore the error + return null; + } + } + + /** + * Gets the value of the major component of the version number for the + * current Version object. + * + * @return major value + */ + public int getMajor() + { + return major; + } + + /** + * Gets the value of the minor component of the version number for the + * current Version object. + * + * @return minor value + */ + public int getMinor() + { + return minor; + } + + /** + * Gets the value of the build component of the version number for the + * current Version object. + * + * @return build value, -1 if undefined + */ + public int getBuild() + { + return build; + } + + /** + * Gets the value of the revision component of the version number for the + * current Version object. + * + * @return revision value, -1 if undefined + */ + public int getRevision() + { + return revision; + } + + /** + * Gets the high 16 bits of the revision number. + * + * @return major revision value, or -1 if revision is undefined + */ + public int getMajorRevision() + { + return revision == -1 ? -1 : revision >>> 16 & 0xFFFF; + } + + /** + * Gets the low 16 bits of the revision number. + * + * @return minor revision value, or -1 if revision is undefined + */ + public int getMinorRevision() + { + return revision == -1 ? -1 : revision & 0xFFFF; + } + + /** + * Compares the current Version object to a specified Version object and + * returns an indication of their relative values. + * + * @param other The version object to compare to + * @return 1 if this version is later than other, 0 if this version is equal + * to other, and -1 otherwise + * @throws NullPointerException if other is null + */ + @Override + public int compareTo(Version other) + { + if (other == null) + { + return 1; + } + + if (this.major != other.major) + { + return this.major > other.major ? 1 : -1; + } + if (this.minor != other.minor) + { + return this.minor > other.minor ? 1 : -1; + } + if (this.build != other.build) + { + return this.build > other.build ? 1 : -1; + } + if (this.revision != other.revision) + { + return this.revision > other.revision ? 1 : -1; + } + return 0; + } + + @Override + public int hashCode() + { + // rotate each component to start from a different byte + int major = this.major; + int minor = (this.minor >>> 8 | this.minor << 24); + int build = (this.build >>> 16 | this.build << 16); + int revision = (this.revision >>> 24 | this.revision << 8); + return major ^ minor ^ build ^ revision; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + + if (o == null || !(o instanceof Version)) + { + return false; + } + + Version version = (Version) o; + + return major == version.major + && minor == version.minor + && build == version.build + && revision == version.revision; + } + + /** + * @return a new Version object whose value is the same as the current + * Version object. + */ + @Override + public Version clone() + { + try + { + return (Version) super.clone(); + } catch (Exception ex) + { + throw new InternalError("This should never happen", ex); + } + } + + /** + * Gets a String representation of this Version object. The String + * representation of two distinct Version objects is equal if and only if + * the two Version objects are equal as declared by + * {@link Object#equals(Object)} The returned representation includes at + * least 1 component of this version, but will include any components that + * are in use. + * + * @return a String representation of this Version object. + */ + @Override + public String toString() + { + return toString(getComponentCount()); + } + + /** + * Converts the value of the current Version object to its equivalent String + * representation. A specified count indicates the number of components to + * return. + * + * @param components the number of components to include in the string + * representation of this Version object. + * @return A string representation of this version object. + * @throws IllegalArgumentException if components is not between 0 and 4 + * inclusive, or components exceeds {@link #getComponentCount()} + */ + public String toString(int components) + { + if (!(0 <= components && components <= 4)) + { + throw new IllegalArgumentException(); + } + StringBuilder result = new StringBuilder(); + if (0 < components) + { + result.append(major); + } + if (1 < components) + { + result.append('.').append(minor); + } + if (2 < components) + { + result.append('.').append(build); + } + if (3 < components) + { + result.append('.').append(revision); + } + return result.toString(); + } + + /** + * Computes the amount of used components. That is, the number of components + * counted up to and including the last defined component. + * + * @return the amount of used components + */ + public int getComponentCount() + { + if (revision != -1) + { + return 4; + } + if (build != -1) + { + return 3; + } + return 2; + } + /* + / ** + * Tries to convert the string representation of a version number to an + * equivalent Version object, and returns a value that indicates whether the + * conversion succeeded. + * + * @param input The input to parse + * @param version The result version + * @return true if the input was parsed successfully and the result of + * parsing the input is equal to the given version + * @throws NullPointerException if input is null or version is null + * / + public static boolean tryParse(String input, InOutParam version) { + try { + version.setValue(parseVersion(input)); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + */ +} diff --git a/src/main/java/com/redstoner/util/Checksum.java b/src/main/java/com/redstoner/util/Checksum.java new file mode 100644 index 0000000..ecf710e --- /dev/null +++ b/src/main/java/com/redstoner/util/Checksum.java @@ -0,0 +1,47 @@ +package com.redstoner.util; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.MessageDigest; + +public enum Checksum +{ + MD5("MD5"), + SHA1("SHA1"), + SHA256("SHA-256"), + SHA512("SHA-512"); + + private String name; + + Checksum(String name) + { + this.name = name; + } + + public byte[] compute(File input) + { + try (InputStream in = new FileInputStream(input)) + { + MessageDigest digest = MessageDigest.getInstance(getName()); + byte[] block = new byte[4096]; + int length; + while ((length = in.read(block)) > 0) + { + digest.update(block, 0, length); + } + return digest.digest(); + } catch (Exception e) + { + e.printStackTrace(); + } + return null; + } + + public String getName() + { + return name; + } + +} \ No newline at end of file