diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index a147a0e358e..85ad37f6057 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -137,8 +137,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; import java.util.logging.Filter; import java.util.logging.Level; import java.util.regex.Matcher; @@ -528,15 +526,9 @@ public void onEnable() { new DefaultFunctions(); ChatMessages.registerListeners(); - - try { - getAddonInstance().loadClasses("ch.njol.skript", - "conditions", "effects", "events", "expressions", "entity", "sections", "structures"); - } catch (final Exception e) { - exception(e, "Could not load required .class files: " + e.getLocalizedMessage()); - setEnabled(false); - return; - } + + getAddonInstance().loadClasses("ch.njol.skript", "conditions", "effects", "events", "expressions", "entity", "sections", "structures") + .loadModules("org.skriptlang.skript", "common", "bukkit"); Commands.registerListeners(); @@ -545,50 +537,32 @@ public void onEnable() { final long tick = testing() ? Bukkit.getWorlds().get(0).getFullTime() : 0; Bukkit.getScheduler().scheduleSyncDelayedTask(this, new Runnable() { - @SuppressWarnings("synthetic-access") @Override + @SuppressWarnings("synthetic-access") public void run() { assert Bukkit.getWorlds().get(0).getFullTime() == tick; - + // Load hooks from Skript jar - try { - try (JarFile jar = new JarFile(getFile())) { - for (JarEntry e : new EnumerationIterable<>(jar.entries())) { - if (e.getName().startsWith("ch/njol/skript/hooks/") && e.getName().endsWith("Hook.class") && StringUtils.count("" + e.getName(), '/') <= 5) { - final String c = e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length()); - try { - Class hook = Class.forName(c, true, getClassLoader()); - if (Hook.class.isAssignableFrom(hook) && !Modifier.isAbstract(hook.getModifiers()) && isHookEnabled((Class>) hook)) { - hook.getDeclaredConstructor().setAccessible(true); - hook.getDeclaredConstructor().newInstance(); - } - } catch (ClassNotFoundException ex) { - Skript.exception(ex, "Cannot load class " + c); - } catch (ExceptionInInitializerError err) { - Skript.exception(err.getCause(), "Class " + c + " generated an exception while loading"); - } catch (Exception ex) { - Skript.exception(ex, "Exception initializing hook: " + c); - } - } + getAddonInstance().loadClasses("ch.njol.skript.hooks", false, false, hook -> { + //noinspection unchecked - We made sure that it's a valid hook + if (Hook.class.isAssignableFrom(hook) && !hook.isInterface() && !Modifier.isAbstract(hook.getModifiers()) && isHookEnabled((Class>) hook)) { + try { + hook.getDeclaredConstructor().setAccessible(true); + hook.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + //noinspection ThrowableNotThrown + Skript.exception(ex, "Failed to load hook class " + hook); } } - } catch (IOException e) { - error("Error while loading plugin hooks" + (e.getLocalizedMessage() == null ? "" : ": " + e.getLocalizedMessage())); - Skript.exception(e); - } + }); finishedLoadingHooks = true; if (TestMode.ENABLED) { info("Preparing Skript for testing..."); tainted = true; - try { - getAddonInstance().loadClasses("ch.njol.skript", "tests"); - } catch (IOException e) { - Skript.exception("Failed to load testing environment."); - Bukkit.getServer().shutdown(); - } + getAddonInstance().loadClasses("ch.njol.skript", "tests"); } - + stopAcceptingRegistrations(); @@ -1218,10 +1192,14 @@ public static void checkAcceptRegistrations() { private static void stopAcceptingRegistrations() { acceptRegistrations = false; - + Converters.createMissingConverters(); Classes.onRegistrationsStop(); + + // Clear each cache + getAddonInstance().resetEntryCache(); // The SkriptAddon representing Skript is not part of the addons list + getAddons().forEach(SkriptAddon::resetEntryCache); } // ================ ADDONS ================ diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index d32f90d1a5c..45d51b75625 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -21,16 +21,19 @@ import ch.njol.skript.localization.Language; import ch.njol.skript.util.Utils; import ch.njol.skript.util.Version; -import ch.njol.util.coll.iterator.EnumerationIterable; +import ch.njol.util.StringUtils; import org.bukkit.plugin.java.JavaPlugin; import org.eclipse.jdt.annotation.Nullable; +import org.skriptlang.skript.registration.Module; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Matcher; @@ -38,110 +41,184 @@ /** * Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. - * - * @author Peter Güttinger */ public final class SkriptAddon { public final JavaPlugin plugin; public final Version version; - private final String name; /** * Package-private constructor. Use {@link Skript#registerAddon(JavaPlugin)} to get a SkriptAddon for your plugin. * - * @param p + * @param plugin The plugin representing the SkriptAddon to be registered. */ - SkriptAddon(final JavaPlugin p) { - plugin = p; - name = "" + p.getName(); - Version v; + SkriptAddon(JavaPlugin plugin) { + this.plugin = plugin; + + Version version; + String descriptionVersion = plugin.getDescription().getVersion(); try { - v = new Version("" + p.getDescription().getVersion()); + version = new Version(descriptionVersion); } catch (final IllegalArgumentException e) { - final Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(p.getDescription().getVersion()); + Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(descriptionVersion); if (!m.find()) - throw new IllegalArgumentException("The version of the plugin " + p.getName() + " does not contain any numbers: " + p.getDescription().getVersion()); - v = new Version(Utils.parseInt("" + m.group(1)), m.group(2) == null ? 0 : Utils.parseInt("" + m.group(2)), m.group(3) == null ? 0 : Utils.parseInt("" + m.group(3))); - Skript.warning("The plugin " + p.getName() + " uses a non-standard version syntax: '" + p.getDescription().getVersion() + "'. Skript will use " + v + " instead."); + throw new IllegalArgumentException("The version of the plugin " + plugin.getName() + " does not contain any numbers: " + descriptionVersion); + version = new Version(Utils.parseInt("" + m.group(1)), m.group(2) == null ? 0 : Utils.parseInt("" + m.group(2)), m.group(3) == null ? 0 : Utils.parseInt("" + m.group(3))); + Skript.warning("The plugin " + plugin.getName() + " uses a non-standard version syntax: '" + descriptionVersion + "'. Skript will use " + version + " instead."); } - version = v; + this.version = version; } - + + /** + * @return The name of the {@link JavaPlugin} responsible for this addon. + */ + public String getName() { + return plugin.getName(); + } + @Override - public final String toString() { - return name; + public String toString() { + return getName(); } - - public String getName() { - return name; + + /** + * Loads classes of the plugin by package. Useful for registering many syntax elements like Skript. + * + * @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). + * @param subPackages Specific subpackages to search in (e.g. 'conditions') + * If no subpackages are provided, all subpackages of the base package will be searched. + * @return This SkriptAddon. + */ + public SkriptAddon loadClasses(String basePackage, String... subPackages) { + return loadClasses(basePackage, true, true, null, subPackages); } - + + private JarEntry @Nullable [] entryCache = null; + /** - * Loads classes of the plugin by package. Useful for registering many syntax elements like Skript does it. - * - * @param basePackage The base package to add to all sub packages, e.g. "ch.njol.skript". - * @param subPackages Which subpackages of the base package should be loaded, e.g. "expressions", "conditions", "effects". Subpackages of these packages will be loaded - * as well. Use an empty array to load all subpackages of the base package. - * @throws IOException If some error occurred attempting to read the plugin's jar file. + * Internal method for clearing an addon's entry cache. + */ + void resetEntryCache() { + entryCache = null; + } + + /** + * Loads classes of the plugin by package. Useful for registering many syntax elements like Skript. + * + * @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). + * @param initialize Whether classes found in the package search should be initialized. + * @param recursive Whether to recursively search through the subpackages provided. + * @param withClass A consumer that will run with each found class. + * @param subPackages Specific subpackages to search in (e.g. 'conditions') + * If no subpackages are provided, all subpackages of the base package will be searched. * @return This SkriptAddon */ - public SkriptAddon loadClasses(String basePackage, String... subPackages) throws IOException { - assert subPackages != null; - JarFile jar = new JarFile(getFile()); + @SuppressWarnings("ThrowableNotThrown") + public SkriptAddon loadClasses(String basePackage, boolean initialize, boolean recursive, @Nullable Consumer> withClass, String... subPackages) { for (int i = 0; i < subPackages.length; i++) subPackages[i] = subPackages[i].replace('.', '/') + "/"; basePackage = basePackage.replace('.', '/') + "/"; - try { - List classNames = new ArrayList<>(); - for (JarEntry e : new EnumerationIterable<>(jar.entries())) { - if (e.getName().startsWith(basePackage) && e.getName().endsWith(".class")) { - boolean load = subPackages.length == 0; - for (String sub : subPackages) { - if (e.getName().startsWith(sub, basePackage.length())) { + // Used for tracking valid classes if a non-recursive search is done + // Depth is the measure of how "deep" from the head package of 'basePackage' a class is + int initialDepth = !recursive ? StringUtils.count(basePackage, '/') + 1 : 0; + + File file = getFile(); + if (file == null) { + Skript.error("Unable to retrieve file from addon '" + getName() + "'. Classes will not be loaded."); + return this; + } + + if (entryCache == null) { + try (JarFile jar = new JarFile(file)) { + entryCache = jar.stream().toArray(JarEntry[]::new); + } catch (IOException e) { + Skript.exception(e, "Failed to load classes for addon: " + plugin.getName()); + return this; + } + } + + List classNames = new ArrayList<>(); + for (JarEntry entry : entryCache) { + if (entry == null) // This entry has already been loaded before + continue; + + String name = entry.getName(); + if (name.startsWith(basePackage) && name.endsWith(".class")) { + boolean load = subPackages.length == 0; + + if (load) { // No subpackages provided + load = recursive || StringUtils.count(name, '/') <= initialDepth; + } else { + for (String subPackage : subPackages) { + if ( + // We also need to account for subpackage depths when not doing a recursive search + (recursive || StringUtils.count(name, '/') <= initialDepth + StringUtils.count(subPackage, '/')) + && name.startsWith(subPackage, basePackage.length()) + ) { load = true; break; } } - - if (load) - classNames.add(e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length())); } + + if (load) + classNames.add(name.replace('/', '.').substring(0, name.length() - ".class".length())); } + } - classNames.sort(String::compareToIgnoreCase); + classNames.sort(String::compareToIgnoreCase); - for (String c : classNames) { - try { - Class.forName(c, true, plugin.getClass().getClassLoader()); - } catch (ClassNotFoundException ex) { - Skript.exception(ex, "Cannot load class " + c + " from " + this); - } catch (ExceptionInInitializerError err) { - Skript.exception(err.getCause(), this + "'s class " + c + " generated an exception while loading"); - } - } - } finally { + for (String className : classNames) { try { - jar.close(); - } catch (IOException e) {} + Class clazz = Class.forName(className, initialize, plugin.getClass().getClassLoader()); + if (withClass != null) + withClass.accept(clazz); + } catch (ClassNotFoundException ex) { + Skript.exception(ex, "Cannot load class " + className); + } catch (ExceptionInInitializerError err) { + Skript.exception(err.getCause(), this + "'s class " + className + " generated an exception while loading"); + } } + return this; } + + /** + * Loads all module classes found in the package search. + * @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). + * @param subPackages Specific subpackages to search in (e.g. 'conditions'). + * If no subpackages are provided, all subpackages will be searched. + * Note that the search will go no further than the first layer of subpackages. + * @return This SkriptAddon. + */ + @SuppressWarnings("ThrowableNotThrown") + public SkriptAddon loadModules(String basePackage, String... subPackages) { + return loadClasses(basePackage, false, false, c -> { + if (Module.class.isAssignableFrom(c) && !c.isInterface() && !Modifier.isAbstract(c.getModifiers())) { + try { + ((Module) c.getConstructor().newInstance()).register(this); + } catch (Exception e) { + Skript.exception(e, "Failed to load module " + c); + } + } + }, subPackages); + } @Nullable private String languageFileDirectory = null; /** - * Makes Skript load language files from the specified directory, e.g. "lang" or "skript lang" if you have a lang folder yourself. Localised files will be read from the - * plugin's jar and the plugin's data folder, but the default English file is only taken from the jar and must exist! + * Loads language files from the specified directory (e.g. "lang") into Skript. + * Localized files will be read from the plugin's jar and the plugin's data file, + * but the default.lang file is only taken from the jar and must exist! * - * @param directory Directory name - * @return This SkriptAddon + * @param directory The directory containing language files. + * @return This SkriptAddon. */ public SkriptAddon setLanguageFileDirectory(String directory) { if (languageFileDirectory != null) - throw new IllegalStateException(); + throw new IllegalStateException("The language file directory may only be set once."); directory = "" + directory.replace('\\', '/'); if (directory.endsWith("/")) directory = "" + directory.substring(0, directory.length() - 1); @@ -149,7 +226,11 @@ public SkriptAddon setLanguageFileDirectory(String directory) { Language.loadDefault(this); return this; } - + + /** + * @return The language file directory set for this addon. + * Null if not yet set using {@link #setLanguageFileDirectory(String)}. + */ @Nullable public String getLanguageFileDirectory() { return languageFileDirectory; @@ -159,27 +240,25 @@ public String getLanguageFileDirectory() { private File file = null; /** - * @return The jar file of the plugin. The first invocation of this method uses reflection to invoke the protected method {@link JavaPlugin#getFile()} to get the plugin's jar - * file. The file is then cached and returned upon subsequent calls to this method to reduce usage of reflection. + * @return The jar file of the plugin. + * After this method is first called, the file will be cached for future use. */ @Nullable public File getFile() { if (file != null) return file; try { - final Method getFile = JavaPlugin.class.getDeclaredMethod("getFile"); + Method getFile = JavaPlugin.class.getDeclaredMethod("getFile"); getFile.setAccessible(true); file = (File) getFile.invoke(plugin); return file; - } catch (final NoSuchMethodException e) { - Skript.outdatedError(e); - } catch (final IllegalArgumentException e) { + } catch (NoSuchMethodException | IllegalArgumentException e) { Skript.outdatedError(e); - } catch (final IllegalAccessException e) { + } catch (IllegalAccessException e) { assert false; - } catch (final SecurityException e) { + } catch (SecurityException e) { throw new RuntimeException(e); - } catch (final InvocationTargetException e) { + } catch (InvocationTargetException e) { throw new RuntimeException(e.getCause()); } return null; diff --git a/src/main/java/org/skriptlang/skript/registration/Module.java b/src/main/java/org/skriptlang/skript/registration/Module.java new file mode 100644 index 00000000000..158deffc009 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/Module.java @@ -0,0 +1,61 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package org.skriptlang.skript.registration; + +import ch.njol.skript.SkriptAddon; + +/** + * A module is a part of a {@link SkriptAddon} containing related syntax, classinfos, converters, etc. + * They are intended for providing organization and structure. + * Modules can be loaded using {@link SkriptAddon#loadModules(String, String...)}. + * Note that when loading 'org.skriptlang.skript.X', the module class should be placed at 'org.skriptlang.skript.X.ModuleClassHere' + * as the mentioned method will not search deeper than the provided subpackages. + * The example below is a possible organization structure that a project using Modules could use. + *
+ * potions
+ * |- elements
+ *    |- PotionsExpr.java
+ * |- PotionsModule.java
+ * math
+ * |- elements
+ *    |- MathExpr.java
+ * |- MathModule.java
+ * MyPlugin.java
+ * 
+ */ +public interface Module { + + /** + * @param addon The addon responsible for registering this module. + * To be used for registering syntax, classinfos, etc. + */ + void register(SkriptAddon addon); + + /** + * Loads syntax elements for this module. + * @param loader The SkriptAddon to load syntax with. + * @param subPackageName The location of syntax elements (ex: "elements") + * Elements should not be contained within the main module package. + * They should be within a subpackage of the package containing the Module class. + */ + default void loadSyntax(SkriptAddon loader, String subPackageName) { + loader.loadClasses(getClass().getPackage().getName() + "." + subPackageName); + } + +}