-
-
Notifications
You must be signed in to change notification settings - Fork 373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Module System #4573
Closed
APickledWalrus
wants to merge
19
commits into
SkriptLang:dev/feature
from
APickledWalrus:feature/modules
Closed
Add Module System #4573
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
adaa688
Implement initial design
APickledWalrus 0328f2b
Merge branch 'master' into feature/modules
APickledWalrus 0e44435
Merge branch 'master' into feature/modules
APickledWalrus 78b61ca
Implement entry cache
APickledWalrus d2f75b7
Use an array instead for the cache
APickledWalrus 72e5797
Merge branch 'master' into feature/modules
APickledWalrus 9a51f9f
It should actually build now
APickledWalrus 3767632
Further documentation updates
APickledWalrus 7305653
Merge branch 'master' into feature/modules
APickledWalrus d4e01a4
Merge branch 'master' into feature/modules
APickledWalrus 2a3f6d0
Improvements and fixes from review
APickledWalrus 803eeb9
Only open JarFile if necessary
APickledWalrus c08c4a6
Make entry cache not contain null elements
APickledWalrus 3038c51
Merge remote-tracking branch 'upstream/master' into feature/modules
APickledWalrus 10973c4
Change default syntax locations
APickledWalrus d6206e2
Fix loading
APickledWalrus 4ac2d16
Remove throws clause
APickledWalrus 2712285
Convert Module to Interface
APickledWalrus 66de7cd
Change default syntax locations
APickledWalrus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,123 +21,195 @@ | |
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; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* 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; | ||
} | ||
|
||
@Override | ||
public final String toString() { | ||
return name; | ||
public String toString() { | ||
return plugin.getName(); | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
return plugin.getName(); | ||
} | ||
|
||
/** | ||
* 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. <tt>"ch.njol.skript"</tt>. | ||
* @param subPackages Which subpackages of the base package should be loaded, e.g. <tt>"expressions", "conditions", "effects"</tt>. 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. | ||
* Loads classes of the plugin by package. Useful for registering many syntax elements like Skript. | ||
* | ||
* Please note that if you need to load the same class multiple times, | ||
* you should call {@link #resetEntryCache()} each time you call this method. | ||
* | ||
* @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(null, true, basePackage, true, subPackages); | ||
} | ||
|
||
@Nullable | ||
private JarEntry @Nullable [] entryCache; | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* This method resets the cache of jar entries used in {@link #loadClasses(Consumer, boolean, String, boolean, String...)}. | ||
* This method is meant for internal use, so you <i>probably</i> don't need it! | ||
* However, if you need loadClasses to load the same class multiple times, you <b>should</b> use this method. | ||
* | ||
* Note that this cache will be cleared when Skript stops accepting registrations. | ||
*/ | ||
public void resetEntryCache() { | ||
entryCache = null; | ||
} | ||
|
||
/** | ||
* Loads classes of the plugin by package. Useful for registering many syntax elements like Skript. | ||
* | ||
* Please note that if you need to load the same class multiple times, | ||
* you should call {@link #resetEntryCache()} each time you call this method. | ||
* | ||
* @param withClass A consumer that will run with each found class. | ||
* @param initialize Whether classes found in the package search should be initialized. | ||
* @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). | ||
* @param recursive Whether to recursively search through the subpackages provided. | ||
* @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(@Nullable Consumer<Class<?>> withClass, boolean initialize, String basePackage, boolean recursive, String... subPackages) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (int i = 0; i < subPackages.length; i++) | ||
subPackages[i] = subPackages[i].replace('.', '/') + "/"; | ||
basePackage = basePackage.replace('.', '/') + "/"; | ||
try { | ||
List<String> classNames = new ArrayList<>(); | ||
|
||
for (JarEntry e : new EnumerationIterable<>(jar.entries())) { | ||
if (e.getName().startsWith(basePackage) && e.getName().endsWith(".class")) { | ||
int depth = !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; | ||
} | ||
|
||
try (JarFile jar = new JarFile(file)) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
List<String> classNames = new ArrayList<>(); | ||
boolean hasWithClass = withClass != null; | ||
if (entryCache == null) | ||
entryCache = jar.stream().toArray(JarEntry[]::new); | ||
for (int i = 0; i < entryCache.length; i++) { | ||
JarEntry e = entryCache[i]; | ||
if (e == null) // This entry has already been loaded before | ||
continue; | ||
String name = e.getName(); | ||
if (name.startsWith(basePackage) && name.endsWith(".class") && (recursive || StringUtils.count(name, '/') <= depth)) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
boolean load = subPackages.length == 0; | ||
for (String sub : subPackages) { | ||
if (e.getName().startsWith(sub, basePackage.length())) { | ||
for (String subPackage : subPackages) { | ||
if (e.getName().startsWith(subPackage, basePackage.length())) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
load = true; | ||
break; | ||
} | ||
} | ||
|
||
if (load) | ||
if (load) { | ||
classNames.add(e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length())); | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
entryCache[i] = null; // Remove this item from the entry cache as this method will only load it once | ||
} | ||
} | ||
} | ||
|
||
classNames.sort(String::compareToIgnoreCase); | ||
|
||
for (String c : classNames) { | ||
try { | ||
Class.forName(c, true, plugin.getClass().getClassLoader()); | ||
Class<?> clazz = Class.forName(c, initialize, plugin.getClass().getClassLoader()); | ||
if (hasWithClass) | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
withClass.accept(clazz); | ||
} catch (ClassNotFoundException ex) { | ||
Skript.exception(ex, "Cannot load class " + c + " from " + this); | ||
Skript.exception(ex, "Cannot load class " + c); | ||
} catch (ExceptionInInitializerError err) { | ||
Skript.exception(err.getCause(), this + "'s class " + c + " generated an exception while loading"); | ||
} | ||
} | ||
} finally { | ||
try { | ||
jar.close(); | ||
} catch (IOException e) {} | ||
} catch (IOException e) { | ||
Skript.exception(e, "Failed to load classes for addon: " + plugin.getName()); | ||
} | ||
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. | ||
* Note that this method will also clear the entry cache of ALL checked classes, | ||
* even those that are not actually a Module. | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @return This SkriptAddon. | ||
*/ | ||
@SuppressWarnings("ThrowableNotThrown") | ||
public SkriptAddon loadModules(String basePackage, String... subPackages) { | ||
return loadClasses(c -> { | ||
if (Module.class.isAssignableFrom(c) && !c.isInterface() && !Modifier.isAbstract(c.getModifiers())) { | ||
try { | ||
((Module) c.getConstructor().newInstance()).register(this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, we may be able to find a better option for this. |
||
} catch (Exception e) { | ||
Skript.exception(e, "Failed to load registration " + c); | ||
} | ||
} | ||
}, false, basePackage, false, 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 <b>must</b> 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 <b>default.lang</b> file is only taken from the jar and <b>must</b> 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) | ||
|
@@ -149,7 +221,11 @@ public SkriptAddon setLanguageFileDirectory(String directory) { | |
Language.loadDefault(this); | ||
return this; | ||
} | ||
|
||
|
||
/** | ||
* @return The language file directory set for this addon. | ||
* It must first be set using {@link #setLanguageFileDirectory(String)}. | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
@Nullable | ||
public String getLanguageFileDirectory() { | ||
return languageFileDirectory; | ||
|
@@ -159,27 +235,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; | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this actually for? It's a test-only action but I can't see a comment explaining it.