diff --git a/common/src/main/java/com/dfsek/terra/addons/loading/AddonClassLoader.java b/common/src/main/java/com/dfsek/terra/addons/loading/AddonClassLoader.java index 5cd20fd75..58093e6cf 100644 --- a/common/src/main/java/com/dfsek/terra/addons/loading/AddonClassLoader.java +++ b/common/src/main/java/com/dfsek/terra/addons/loading/AddonClassLoader.java @@ -51,7 +51,7 @@ public class AddonClassLoader extends URLClassLoader { set.add((Class) clazz); } catch(ClassNotFoundException e) { - e.printStackTrace(); + throw new IllegalStateException(e); // this should literally never happen, if it does something is very wrong } } diff --git a/common/src/main/java/com/dfsek/terra/addons/loading/AddonLoadException.java b/common/src/main/java/com/dfsek/terra/addons/loading/AddonLoadException.java index 45e8aa1b2..867987ec2 100644 --- a/common/src/main/java/com/dfsek/terra/addons/loading/AddonLoadException.java +++ b/common/src/main/java/com/dfsek/terra/addons/loading/AddonLoadException.java @@ -1,6 +1,8 @@ package com.dfsek.terra.addons.loading; public class AddonLoadException extends Exception { + private static final long serialVersionUID = -4949084729296580176L; + public AddonLoadException(String message) { super(message); } diff --git a/common/src/main/java/com/dfsek/terra/addons/loading/pre/AddonPool.java b/common/src/main/java/com/dfsek/terra/addons/loading/pre/AddonPool.java new file mode 100644 index 000000000..2264b49d6 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/addons/loading/pre/AddonPool.java @@ -0,0 +1,32 @@ +package com.dfsek.terra.addons.loading.pre; + +import com.dfsek.terra.addons.loading.AddonLoadException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class AddonPool { + private final Map pool = new HashMap<>(); + + public void add(PreLoadAddon addon) throws AddonLoadException { + if(pool.containsKey(addon.getId())) + throw new AddonLoadException("Duplicate addon ID: " + addon.getId()); + pool.put(addon.getId(), addon); + } + + public PreLoadAddon get(String id) { + return pool.get(id); + } + + public void buildAll() throws AddonLoadException { + for(PreLoadAddon value : pool.values()) { + value.rebuildDependencies(this, value, true); + } + } + + public Set getAddons() { + return new HashSet<>(pool.values()); + } +} diff --git a/common/src/main/java/com/dfsek/terra/addons/loading/pre/CircularDependencyException.java b/common/src/main/java/com/dfsek/terra/addons/loading/pre/CircularDependencyException.java new file mode 100644 index 000000000..f262f7285 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/addons/loading/pre/CircularDependencyException.java @@ -0,0 +1,15 @@ +package com.dfsek.terra.addons.loading.pre; + +import com.dfsek.terra.addons.loading.AddonLoadException; + +public class CircularDependencyException extends AddonLoadException { + private static final long serialVersionUID = 7398510879124125121L; + + public CircularDependencyException(String message) { + super(message); + } + + public CircularDependencyException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/common/src/main/java/com/dfsek/terra/addons/loading/pre/DependencyMissingException.java b/common/src/main/java/com/dfsek/terra/addons/loading/pre/DependencyMissingException.java new file mode 100644 index 000000000..936c6f480 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/addons/loading/pre/DependencyMissingException.java @@ -0,0 +1,15 @@ +package com.dfsek.terra.addons.loading.pre; + +import com.dfsek.terra.addons.loading.AddonLoadException; + +public class DependencyMissingException extends AddonLoadException { + private static final long serialVersionUID = -8419489102208521583L; + + public DependencyMissingException(String message) { + super(message); + } + + public DependencyMissingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/common/src/main/java/com/dfsek/terra/addons/loading/pre/PreLoadAddon.java b/common/src/main/java/com/dfsek/terra/addons/loading/pre/PreLoadAddon.java new file mode 100644 index 000000000..08e006e21 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/addons/loading/pre/PreLoadAddon.java @@ -0,0 +1,48 @@ +package com.dfsek.terra.addons.loading.pre; + +import com.dfsek.terra.addons.addon.TerraAddon; +import com.dfsek.terra.addons.annotations.Addon; +import com.dfsek.terra.addons.annotations.Depends; +import com.dfsek.terra.addons.loading.AddonLoadException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PreLoadAddon { + private final List depends = new ArrayList<>(); + private final Class addonClass; + private final String id; + private final String[] dependencies; + + public PreLoadAddon(Class addonClass) { + this.addonClass = addonClass; + this.id = addonClass.getAnnotation(Addon.class).value(); + Depends depends = addonClass.getAnnotation(Depends.class); + this.dependencies = depends == null ? new String[] {} : depends.value(); + } + + public List getDepends() { + return depends; + } + + public void rebuildDependencies(AddonPool pool, PreLoadAddon origin, boolean levelG1) throws AddonLoadException { + if(this.equals(origin) && !levelG1) + throw new CircularDependencyException("Detected circular dependency in addon \"" + id + "\", dependencies: " + Arrays.toString(dependencies)); + + for(String dependency : dependencies) { + PreLoadAddon preLoadAddon = pool.get(dependency); + if(preLoadAddon == null) throw new DependencyMissingException("Dependency " + dependency + " was not found."); + depends.add(preLoadAddon); + preLoadAddon.rebuildDependencies(pool, origin, false); + } + } + + public String getId() { + return id; + } + + public Class getAddonClass() { + return addonClass; + } +} diff --git a/common/src/main/java/com/dfsek/terra/registry/AddonRegistry.java b/common/src/main/java/com/dfsek/terra/registry/AddonRegistry.java index 17667d75d..892e48d87 100644 --- a/common/src/main/java/com/dfsek/terra/registry/AddonRegistry.java +++ b/common/src/main/java/com/dfsek/terra/registry/AddonRegistry.java @@ -1,19 +1,16 @@ package com.dfsek.terra.registry; import com.dfsek.terra.addons.addon.TerraAddon; -import com.dfsek.terra.addons.annotations.Addon; -import com.dfsek.terra.addons.annotations.Depends; import com.dfsek.terra.addons.loading.AddonClassLoader; import com.dfsek.terra.addons.loading.AddonLoadException; +import com.dfsek.terra.addons.loading.pre.AddonPool; +import com.dfsek.terra.addons.loading.pre.PreLoadAddon; import com.dfsek.terra.api.core.TerraPlugin; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; public class AddonRegistry extends TerraRegistry { private final TerraPlugin main; @@ -30,67 +27,61 @@ public class AddonRegistry extends TerraRegistry { @Override public boolean add(String name, TerraAddon addon) { + if(contains(name)) throw new IllegalArgumentException("Addon " + name + " is already registered."); addon.initialize(); main.getLogger().info("Loaded addon " + addon.getName() + " v" + addon.getVersion() + ", by " + addon.getAuthor()); return super.add(name, addon); } + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + public boolean loadAll() { boolean valid = true; File addonsFolder = new File(main.getDataFolder(), "addons"); addonsFolder.mkdirs(); - Map> addonIDs = new HashMap<>(); + AddonPool pool = new AddonPool(); try { for(File jar : addonsFolder.listFiles(file -> file.getName().endsWith(".jar"))) { - main.getLogger().info("Loading Addon(s) from: " + jar.getName()); - - Set> addonClasses = AddonClassLoader.fetchAddonClasses(jar); - - for(Class addonClass : addonClasses) { - String id = addonClass.getAnnotation(Addon.class).value(); - if(addonIDs.containsKey(id)) - throw new AddonLoadException("Duplicate addon ID: " + id); - addonIDs.put(id, addonClass); + for(Class addonClass : AddonClassLoader.fetchAddonClasses(jar)) { + pool.add(new PreLoadAddon(addonClass)); } } - for(Map.Entry> entry : addonIDs.entrySet()) { - Class addonClass = entry.getValue(); - - Depends dependencies = addonClass.getAnnotation(Depends.class); - - if(dependencies != null) { - for(String dependency : dependencies.value()) { - if(!addonIDs.containsKey(dependency)) - throw new AddonLoadException("Addon " + entry.getKey() + " specifies dependency " + dependency + ", which is not loaded. Please install " + dependency + " to use " + entry.getKey()); - } - } + pool.buildAll(); + for(PreLoadAddon addon : pool.getAddons()) { + Class addonClass = addon.getAddonClass(); Constructor constructor; + try { constructor = addonClass.getConstructor(); } catch(NoSuchMethodException e) { throw new AddonLoadException("Addon class has no valid constructor: " + addonClass.getCanonicalName(), e); } - TerraAddon addon; + TerraAddon loadedAddon; try { - addon = constructor.newInstance(); + loadedAddon = constructor.newInstance(); } catch(InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new AddonLoadException("Failed to instantiate addon: " + addonClass.getCanonicalName(), e); + throw new AddonLoadException("Failed to load addon \" + " + addon.getId() + "\": ", e); } try { - addChecked(addon.getName(), addon); + addChecked(loadedAddon.getName(), loadedAddon); } catch(IllegalArgumentException e) { - throw new AddonLoadException("Duplicate addon ID; addon with ID " + addon.getName() + " is already loaded."); + valid = false; + main.getLogger().severe("Duplicate addon ID; addon with ID " + loadedAddon.getName() + " is already loaded."); + main.getLogger().severe("Existing addon class: " + get(loadedAddon.getName()).getClass().getCanonicalName()); + main.getLogger().severe("Duplicate addon class: " + addonClass.getCanonicalName()); } } - } catch(IOException | AddonLoadException e) { + } catch(AddonLoadException | IOException e) { e.printStackTrace(); valid = false; - main.getLogger().severe("Addons failed to load. Please ensure all addons are properly installed."); } return valid; diff --git a/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/TerraBukkitPlugin.java b/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/TerraBukkitPlugin.java index f79e3722f..7e62cd637 100644 --- a/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/TerraBukkitPlugin.java +++ b/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/TerraBukkitPlugin.java @@ -141,7 +141,12 @@ public class TerraBukkitPlugin extends JavaPlugin implements TerraPlugin { LangUtil.load(config.getLanguage(), this); // Load language. debugLogger.setDebug(isDebug()); - addonRegistry.loadAll(); + if(!addonRegistry.loadAll()) { + getLogger().severe("Failed to load addons. Please correct addon installations to continue."); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + registry.loadAll(this); // Load all config packs. PluginCommand c = Objects.requireNonNull(getCommand("terra")); diff --git a/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/population/SerializationUtil.java b/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/population/SerializationUtil.java index 0a0267b6a..a297259c5 100644 --- a/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/population/SerializationUtil.java +++ b/platforms/bukkit/src/main/java/com/dfsek/terra/bukkit/population/SerializationUtil.java @@ -41,7 +41,7 @@ public final class SerializationUtil { try { if(result.getName().contains(oldNameSpace)) { String newClassName = result.getName().replace(oldNameSpace, newNameSpace); - Class localClass = Class.forName(newClassName); + Class localClass = Class.forName(newClassName); Field nameField = ObjectStreamClass.class.getDeclaredField("name"); nameField.setAccessible(true);