diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 8b03eaca40b..182fea04729 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -62,7 +62,6 @@ import ch.njol.skript.variables.Variables; import ch.njol.util.Closeable; import ch.njol.util.Kleenean; -import ch.njol.util.NullableChecker; import ch.njol.util.StringUtils; import ch.njol.util.coll.iterator.CheckedIterator; import ch.njol.util.coll.iterator.EnumerationIterable; @@ -87,11 +86,15 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.junit.After; import org.junit.runner.JUnitCore; import org.junit.runner.Result; +import org.skriptlang.skript.bukkit.registration.BukkitRegistryKeys; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.junit.runner.notification.Failure; import org.skriptlang.skript.bukkit.SkriptMetrics; import org.skriptlang.skript.bukkit.breeding.BreedingModule; @@ -105,6 +108,9 @@ import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.structure.Structure; import org.skriptlang.skript.lang.structure.StructureInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.registration.SyntaxInfo; import java.io.File; import java.io.IOException; @@ -138,6 +144,8 @@ import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; @@ -176,14 +184,24 @@ public final class Skript extends JavaPlugin implements Listener { @Nullable private static Skript instance = null; + static org.skriptlang.skript.@UnknownNullability Skript skript = null; + private static org.skriptlang.skript.@UnknownNullability Skript unmodifiableSkript = null; + private static boolean disabled = false; private static boolean partDisabled = false; public static Skript getInstance() { - final Skript i = instance; - if (i == null) + if (instance == null) throw new IllegalStateException(); - return i; + return instance; + } + + @ApiStatus.Experimental + public static org.skriptlang.skript.Skript instance() { + if (unmodifiableSkript == null) { + throw new SkriptAPIException("Skript is still initializing"); + } + return unmodifiableSkript; } /** @@ -400,8 +418,6 @@ public void onEnable() { } catch (Exception e) { Skript.exception(e, "Update checker could not be initialized."); } - experimentRegistry = new ExperimentRegistry(this); - Feature.registerAll(getAddonInstance(), experimentRegistry); if (!getDataFolder().isDirectory()) getDataFolder().mkdirs(); @@ -476,9 +492,17 @@ public void onEnable() { } } - // initialize the Skript addon instance + // initialize the modern Skript instance + skript = org.skriptlang.skript.Skript.of(getClass(), getName()); + unmodifiableSkript = skript.unmodifiableView(); + skript.localizer().setSourceDirectories("lang", + getDataFolder().getAbsolutePath() + "lang"); + // initialize the old Skript SkriptAddon instance getAddonInstance(); + experimentRegistry = new ExperimentRegistry(this); + Feature.registerAll(getAddonInstance(), experimentRegistry); + // Load classes which are always safe to use new JavaClasses(); // These may be needed in configuration @@ -618,7 +642,6 @@ public void run() { stopAcceptingRegistrations(); - Documentation.generate(); // TODO move to test classes? // Variable loading @@ -1225,7 +1248,6 @@ private boolean isServerRunning() { private void beforeDisable() { partDisabled = true; EvtSkript.onSkriptStop(); // TODO [code style] warn user about delays in Skript stop events - ScriptLoader.unloadScripts(ScriptLoader.getLoadedScripts()); } @@ -1337,194 +1359,232 @@ public static void checkAcceptRegistrations() { private static void stopAcceptingRegistrations() { Converters.createChainedConverters(); - acceptRegistrations = false; - Classes.onRegistrationsStop(); } // ================ ADDONS ================ - private final static HashMap addons = new HashMap<>(); + @Deprecated + private static final Set addons = new HashSet<>(); /** * Registers an addon to Skript. This is currently not required for addons to work, but the returned {@link SkriptAddon} provides useful methods for registering syntax elements * and adding new strings to Skript's localization system (e.g. the required "types.[type]" strings for registered classes). - * - * @param p The plugin + * + * @param plugin The plugin */ - public static SkriptAddon registerAddon(final JavaPlugin p) { + public static SkriptAddon registerAddon(JavaPlugin plugin) { checkAcceptRegistrations(); - if (addons.containsKey(p.getName())) - throw new IllegalArgumentException("The plugin " + p.getName() + " is already registered"); - final SkriptAddon addon = new SkriptAddon(p); - addons.put(p.getName(), addon); + SkriptAddon addon = new SkriptAddon(plugin); + addons.add(addon); return addon; } - @Nullable - public static SkriptAddon getAddon(final JavaPlugin p) { - return addons.get(p.getName()); + public static @Nullable SkriptAddon getAddon(JavaPlugin plugin) { + if (plugin == Skript.getInstance()) { + return Skript.getAddonInstance(); + } + for (SkriptAddon addon : getAddons()) { + if (addon.plugin == plugin) { + return addon; + } + } + return null; } - @Nullable - public static SkriptAddon getAddon(final String name) { - return addons.get(name); + public static @Nullable SkriptAddon getAddon(String name) { + if (name.equals(Skript.getInstance().getName())) { + return Skript.getAddonInstance(); + } + for (SkriptAddon addon : getAddons()) { + if (addon.getName().equals(name)) { + return addon; + } + } + return null; } - @SuppressWarnings("null") - public static Collection getAddons() { - return Collections.unmodifiableCollection(addons.values()); + public static @Unmodifiable Collection getAddons() { + Set addons = new HashSet<>(Skript.addons); + addons.addAll(instance().addons().stream() + .filter(addon -> addons.stream().noneMatch(oldAddon -> oldAddon.name().equals(addon.name()))) + .map(SkriptAddon::fromModern) + .collect(Collectors.toSet()) + ); + return Collections.unmodifiableCollection(addons); } - @Nullable - private static SkriptAddon addon; + @Deprecated + private static @Nullable SkriptAddon addon; /** * @return A {@link SkriptAddon} representing Skript. */ public static SkriptAddon getAddonInstance() { if (addon == null) { - addon = new SkriptAddon(Skript.getInstance()); - addon.setLanguageFileDirectory("lang"); + addon = SkriptAddon.fromModern(instance()); } return addon; } // ================ CONDITIONS & EFFECTS & SECTIONS ================ - private static final List> conditions = new ArrayList<>(50); - private static final List> effects = new ArrayList<>(50); - private static final List> statements = new ArrayList<>(100); - private static final List> sections = new ArrayList<>(50); + private static final class BukkitOrigin implements SyntaxOrigin { - public static Collection> getStatements() { - return statements; - } + private final String name; - public static Collection> getEffects() { - return effects; - } + private BukkitOrigin(Plugin plugin) { + this.name = plugin.getName(); + } - public static Collection> getSections() { - return sections; - } + @Override + public String name() { + return name; + } - // ================ CONDITIONS ================ - public static Collection> getConditions() { - return conditions; } - private final static int[] conditionTypesStartIndices = new int[ConditionType.values().length]; + private static SyntaxOrigin getSyntaxOrigin(JavaPlugin plugin) { + SkriptAddon addon = getAddon(plugin); + if (addon != null) { + return SyntaxOrigin.of(addon); + } + return new BukkitOrigin(plugin); + } /** - * registers a {@link Condition}. - * - * @param condition The condition's class + * Registers a {@link Condition}. + * + * @param conditionClass The condition's class * @param patterns Skript patterns to match this condition */ - public static void registerCondition(Class condition, String... patterns) throws IllegalArgumentException { - registerCondition(condition, ConditionType.COMBINED, patterns); + public static void registerCondition(Class conditionClass, String... patterns) throws IllegalArgumentException { + registerCondition(conditionClass, ConditionType.COMBINED, patterns); } /** - * registers a {@link Condition}. - * - * @param condition The condition's class - * @param type The conditions {@link ConditionType type}. This is used to determine in which order to try to parse conditions. + * Registers a {@link Condition}. + * + * @param conditionClass The condition's class + * @param type The type of condition which affects its priority in the parsing search * @param patterns Skript patterns to match this condition */ - public static void registerCondition(Class condition, ConditionType type, String... patterns) throws IllegalArgumentException { + public static void registerCondition(Class conditionClass, ConditionType type, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - final SyntaxElementInfo info = new SyntaxElementInfo<>(patterns, condition, originClassPath); - conditions.add(conditionTypesStartIndices[type.ordinal()], info); - statements.add(conditionTypesStartIndices[type.ordinal()], info); - for (int i = type.ordinal(); i < ConditionType.values().length; i++) - conditionTypesStartIndices[i]++; + skript.syntaxRegistry().register(SyntaxRegistry.CONDITION, SyntaxInfo.builder(conditionClass) + .priority(type.priority()) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(conditionClass))) + .addPatterns(patterns) + .build() + ); } /** * Registers an {@link Effect}. - * - * @param effect The effect's class + * + * @param effectClass The effect's class * @param patterns Skript patterns to match this effect */ - public static void registerEffect(final Class effect, final String... patterns) throws IllegalArgumentException { + public static void registerEffect(Class effectClass, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - final SyntaxElementInfo info = new SyntaxElementInfo<>(patterns, effect, originClassPath); - effects.add(info); - statements.add(info); + skript.syntaxRegistry().register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(effectClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(effectClass))) + .addPatterns(patterns) + .build() + ); } /** * Registers a {@link Section}. * - * @param section The section's class + * @param sectionClass The section's class * @param patterns Skript patterns to match this section * @see Section */ - public static void registerSection(Class section, String... patterns) throws IllegalArgumentException { + public static void registerSection(Class sectionClass, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - SyntaxElementInfo info = new SyntaxElementInfo<>(patterns, section, originClassPath); - sections.add(info); + skript.syntaxRegistry().register(SyntaxRegistry.SECTION, SyntaxInfo.builder(sectionClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(sectionClass))) + .addPatterns(patterns) + .build() + ); } - // ================ EXPRESSIONS ================ + public static @Unmodifiable Collection> getStatements() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.STATEMENT).stream() + .map(SyntaxElementInfo::, Statement>fromModern) + .collect(Collectors.toUnmodifiableList()); + } + + public static @Unmodifiable Collection> getConditions() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.CONDITION).stream() + .map(SyntaxElementInfo::, Condition>fromModern) + .collect(Collectors.toUnmodifiableList()); + } + + public static @Unmodifiable Collection> getEffects() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.EFFECT).stream() + .map(SyntaxElementInfo::, Effect>fromModern) + .collect(Collectors.toUnmodifiableList()); + } - private final static List> expressions = new ArrayList<>(100); + public static @Unmodifiable Collection> getSections() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.SECTION).stream() + .map(SyntaxElementInfo::, Section>fromModern) + .collect(Collectors.toUnmodifiableList()); + } - private final static int[] expressionTypesStartIndices = new int[ExpressionType.values().length]; + // ================ EXPRESSIONS ================ /** * Registers an expression. - * - * @param c The expression's class + * + * @param expressionType The expression's class * @param returnType The superclass of all values returned by the expression * @param type The expression's {@link ExpressionType type}. This is used to determine in which order to try to parse expressions. * @param patterns Skript patterns that match this expression * @throws IllegalArgumentException if returnType is not a normal class */ - public static , T> void registerExpression(final Class c, final Class returnType, final ExpressionType type, final String... patterns) throws IllegalArgumentException { + public static , T> void registerExpression( + Class expressionType, Class returnType, ExpressionType type, String... patterns + ) throws IllegalArgumentException { checkAcceptRegistrations(); - if (returnType.isAnnotation() || returnType.isArray() || returnType.isPrimitive()) - throw new IllegalArgumentException("returnType must be a normal type"); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - final ExpressionInfo info = new ExpressionInfo<>(patterns, returnType, c, originClassPath, type); - expressions.add(expressionTypesStartIndices[type.ordinal()], info); - for (int i = type.ordinal(); i < ExpressionType.values().length; i++) { - expressionTypesStartIndices[i]++; - } + skript.syntaxRegistry().register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(expressionType) + .returnType(returnType) + .priority(type.priority()) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(expressionType))) + .addPatterns(patterns) + .build() + ); } - @SuppressWarnings("null") public static Iterator> getExpressions() { - return expressions.iterator(); - } - - public static Iterator> getExpressions(final Class... returnTypes) { - return new CheckedIterator<>(getExpressions(), new NullableChecker>() { - @Override - public boolean check(final @Nullable ExpressionInfo i) { - if (i == null || i.returnType == Object.class) + List> list = new ArrayList<>(); + for (SyntaxInfo.Expression info : instance().syntaxRegistry().syntaxes(SyntaxRegistry.EXPRESSION)) + list.add((ExpressionInfo) SyntaxElementInfo.fromModern(info)); + return list.iterator(); + } + + public static Iterator> getExpressions(Class... returnTypes) { + return new CheckedIterator<>(getExpressions(), info -> { + if (info == null || info.returnType == Object.class) + return true; + for (Class returnType : returnTypes) { + assert returnType != null; + if (Converters.converterExists(info.returnType, returnType)) return true; - for (final Class returnType : returnTypes) { - assert returnType != null; - if (Converters.converterExists(i.returnType, returnType)) - return true; - } - return false; } + return false; }); } // ================ EVENTS ================ - private static final List> events = new ArrayList<>(50); - private static final List> structures = new ArrayList<>(10); - /** * Registers an event. * @@ -1544,51 +1604,66 @@ public static SkriptEventInfo registerEvent(String na * Registers an event. * * @param name The name of the event, used for error messages - * @param c The event's class + * @param eventClass The event's class * @param events The Bukkit events this event applies to * @param patterns Skript patterns to match this event * @return A SkriptEventInfo representing the registered event. Used to generate Skript's documentation. */ - public static SkriptEventInfo registerEvent(String name, Class c, Class[] events, String... patterns) { + @SuppressWarnings("ConstantConditions") // caused by bad array annotations + public static SkriptEventInfo registerEvent( + String name, Class eventClass, Class[] events, String... patterns + ) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - - String[] transformedPatterns = new String[patterns.length]; for (int i = 0; i < patterns.length; i++) - transformedPatterns[i] = SkriptEvent.fixPattern(patterns[i]); - - SkriptEventInfo r = new SkriptEventInfo<>(name, transformedPatterns, c, originClassPath, events); - Skript.events.add(r); - return r; + patterns[i] = BukkitSyntaxInfos.fixPattern(patterns[i]); + var legacy = new SkriptEventInfo.ModernSkriptEventInfo<>(name, patterns, eventClass, "", events); + skript.syntaxRegistry().register(BukkitRegistryKeys.EVENT, legacy); + return legacy; } - public static void registerStructure(Class c, String... patterns) { + public static void registerStructure(Class structureClass, String... patterns) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath); - structures.add(structureInfo); + skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .addPatterns(patterns) + .build() + ); } - public static void registerSimpleStructure(Class c, String... patterns) { + public static void registerSimpleStructure(Class structureClass, String... patterns) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath, true); - structures.add(structureInfo); + skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .addPatterns(patterns) + .nodeType(SyntaxInfo.Structure.NodeType.SIMPLE) + .build() + ); } - public static void registerStructure(Class c, EntryValidator entryValidator, String... patterns) { + public static void registerStructure( + Class structureClass, EntryValidator entryValidator, String... patterns + ) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath, entryValidator); - structures.add(structureInfo); - } - - public static Collection> getEvents() { - return events; - } - - public static List> getStructures() { - return structures; + skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .addPatterns(patterns) + .entryValidator(entryValidator) + .build() + ); + } + + public static @Unmodifiable Collection> getEvents() { + return instance().syntaxRegistry() + .syntaxes(BukkitRegistryKeys.EVENT).stream() + .map(SyntaxElementInfo::, SkriptEvent>fromModern) + .collect(Collectors.toUnmodifiableList()); + } + + public static @Unmodifiable List> getStructures() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.STRUCTURE).stream() + .map(SyntaxElementInfo::, Structure>fromModern) + .collect(Collectors.toUnmodifiableList()); } // ================ COMMANDS ================ diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index 3a1cf894d35..01819d3ac28 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -20,49 +20,60 @@ import java.io.File; import java.io.IOException; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Nullable; -import ch.njol.skript.localization.Language; import ch.njol.skript.util.Utils; import ch.njol.skript.util.Version; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; /** * Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. */ -public final class SkriptAddon { +public final class SkriptAddon implements org.skriptlang.skript.addon.SkriptAddon { public final JavaPlugin plugin; public final Version version; private final String name; + private final org.skriptlang.skript.addon.SkriptAddon addon; + /** * Package-private constructor. Use {@link Skript#registerAddon(JavaPlugin)} to get a SkriptAddon for your plugin. - * - * @param p */ - SkriptAddon(final JavaPlugin p) { - plugin = p; - name = "" + p.getName(); - Version v; + SkriptAddon(JavaPlugin plugin) { + this(plugin, Skript.skript.registerAddon(plugin.getClass(), plugin.getName())); + } + + SkriptAddon(JavaPlugin plugin, org.skriptlang.skript.addon.SkriptAddon addon) { + this.addon = addon; + this.plugin = plugin; + this.name = plugin.getName(); + Version version; try { - v = new Version("" + p.getDescription().getVersion()); - } catch (final IllegalArgumentException e) { - final Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(p.getDescription().getVersion()); + version = new Version(plugin.getDescription().getVersion()); + } catch (IllegalArgumentException e) { + final Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(plugin.getDescription().getVersion()); 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 " + name + " does not contain any numbers: " + plugin.getDescription().getVersion()); + 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 " + name + " uses a non-standard version syntax: '" + plugin.getDescription().getVersion() + "'. Skript will use " + version + " instead."); } - version = v; + this.version = version; } @Override public final String toString() { - return name; + return getName(); } public String getName() { @@ -83,9 +94,6 @@ public SkriptAddon loadClasses(String basePackage, String... subPackages) throws return this; } - @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! @@ -94,19 +102,13 @@ public SkriptAddon loadClasses(String basePackage, String... subPackages) throws * @return This SkriptAddon */ public SkriptAddon setLanguageFileDirectory(String directory) { - if (languageFileDirectory != null) - throw new IllegalStateException(); - directory = "" + directory.replace('\\', '/'); - if (directory.endsWith("/")) - directory = "" + directory.substring(0, directory.length() - 1); - languageFileDirectory = directory; - Language.loadDefault(this); + localizer().setSourceDirectories(directory, plugin.getDataFolder().getAbsolutePath() + directory); return this; } @Nullable public String getLanguageFileDirectory() { - return languageFileDirectory; + return localizer().languageFileDirectory(); } @Nullable @@ -126,4 +128,67 @@ public File getFile() { return file; } + // + // Modern SkriptAddon Compatibility + // + + @ApiStatus.Experimental + static SkriptAddon fromModern(org.skriptlang.skript.addon.SkriptAddon addon) { + return new SkriptAddon(JavaPlugin.getProvidingPlugin(addon.source()), addon); + } + + @Override + @ApiStatus.Experimental + public Class source() { + return addon.source(); + } + + @Override + @ApiStatus.Experimental + public String name() { + return addon.name(); + } + + @Override + @ApiStatus.Experimental + public > void storeRegistry(Class registryClass, R registry) { + addon.storeRegistry(registryClass, registry); + } + + @Override + @ApiStatus.Experimental + public void removeRegistry(Class> registryClass) { + addon.removeRegistry(registryClass); + } + + @Override + @ApiStatus.Experimental + public boolean hasRegistry(Class> registryClass) { + return addon.hasRegistry(registryClass); + } + + @Override + @ApiStatus.Experimental + public > R registry(Class registryClass) { + return addon.registry(registryClass); + } + + @Override + @ApiStatus.Experimental + public > R registry(Class registryClass, Supplier putIfAbsent) { + return addon.registry(registryClass, putIfAbsent); + } + + @Override + @ApiStatus.Experimental + public SyntaxRegistry syntaxRegistry() { + return addon.syntaxRegistry(); + } + + @Override + @ApiStatus.Experimental + public Localizer localizer() { + return addon.localizer(); + } + } diff --git a/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java b/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java index 9a71bbb79b3..c32b5bb3007 100644 --- a/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java +++ b/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java @@ -28,6 +28,10 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Checker; import ch.njol.util.Kleenean; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Priority; /** * This class can be used for an easier writing of conditions that contain only one type in the pattern, @@ -54,6 +58,14 @@ */ public abstract class PropertyCondition extends Condition implements Checker { + /** + * A priority for {@link PropertyCondition}s. + * They will be registered before {@link SyntaxInfo#PATTERN_MATCHES_EVERYTHING} expressions + * but after {@link SyntaxInfo#COMBINED} expressions. + */ + @ApiStatus.Experimental + public static final Priority DEFAULT_PRIORITY = Priority.before(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + /** * See {@link PropertyCondition} for more info */ @@ -83,14 +95,53 @@ public enum PropertyType { WILL } - private Expression expr; + /** + * @param registry The SyntaxRegistry to register with. + * @param condition The class to register + * @param property The property name, for example fly in players can fly + * @param type Must be plural, for example players in players can fly + * @param The Condition type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static SyntaxInfo register(SyntaxRegistry registry, Class condition, String property, String type) { + return register(registry, condition, PropertyType.BE, property, type); + } + + /** + * @param registry The SyntaxRegistry to register with. + * @param condition The class to register + * @param propertyType The property type, see {@link PropertyType} + * @param property The property name, for example fly in players can fly + * @param type Must be plural, for example players in players can fly + * @param The Condition type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static SyntaxInfo register(SyntaxRegistry registry, Class condition, PropertyType propertyType, String property, String type) { + if (type.contains("%")) + throw new SkriptAPIException("The type argument must not contain any '%'s"); + SyntaxInfo.Builder builder = SyntaxInfo.builder(condition).priority(DEFAULT_PRIORITY); + switch (propertyType) { + case BE -> builder.addPatterns("%" + type + "% (is|are) " + property, + "%" + type + "% (isn't|is not|aren't|are not) " + property); + case CAN -> builder.addPatterns("%" + type + "% can " + property, + "%" + type + "% (can't|cannot|can not) " + property); + case HAVE -> builder.addPatterns("%" + type + "% (has|have) " + property, + "%" + type + "% (doesn't|does not|do not|don't) have " + property); + case WILL -> builder.addPatterns("%" + type + "% will " + property, + "%" + type + "% (will (not|neither)|won't) " + property); + } + SyntaxInfo info = builder.build(); + registry.register(SyntaxRegistry.CONDITION, info); + return info; + } /** * @param condition the class to register * @param property the property name, for example fly in players can fly * @param type must be plural, for example players in players can fly */ - public static void register(Class condition, String property, String type) { register(condition, PropertyType.BE, property, type); } @@ -101,7 +152,6 @@ public static void register(Class condition, String propert * @param property the property name, for example fly in players can fly * @param type must be plural, for example players in players can fly */ - public static void register(Class condition, PropertyType propertyType, String property, String type) { if (type.contains("%")) throw new SkriptAPIException("The type argument must not contain any '%'s"); @@ -132,6 +182,8 @@ public static void register(Class condition, PropertyType p } } + private Expression expr; + @Override @SuppressWarnings("unchecked") public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { diff --git a/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java b/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java index 574969ce4c4..f2caf0b4f35 100644 --- a/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java +++ b/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java @@ -42,7 +42,11 @@ import ch.njol.skript.util.Getter; import ch.njol.skript.util.Utils; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Priority; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** @@ -65,6 +69,37 @@ */ public class EventValueExpression extends SimpleExpression implements DefaultExpression { + /** + * A priority for {@link EventValueExpression}s. + * They will be registered before {@link SyntaxInfo#COMBINED} expressions + * but after {@link SyntaxInfo#SIMPLE} expressions. + */ + @ApiStatus.Experimental + public static final Priority DEFAULT_PRIORITY = Priority.before(SyntaxInfo.COMBINED); + + /** + * Registers an event value expression with the provided pattern. + * The syntax info will be forced to use the {@link #DEFAULT_PRIORITY} priority. + * This also adds '[the]' to the start of the pattern. + * + * @param registry The SyntaxRegistry to register with. + * @param expressionClass The EventValueExpression class being registered. + * @param returnType The class representing the expression's return type. + * @param pattern The pattern to match for creating this expression. + * @param The return type. + * @param The Expression type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static , T> SyntaxInfo.Expression register(SyntaxRegistry registry, Class expressionClass, Class returnType, String pattern) { + SyntaxInfo.Expression info = SyntaxInfo.Expression.builder(expressionClass) + .returnType(returnType) + .priority(DEFAULT_PRIORITY) + .addPattern("[the] " + pattern) + .build(); + registry.register(SyntaxRegistry.EXPRESSION, info); + return info; + } /** * Registers an expression as {@link ExpressionType#EVENT} with the provided pattern. diff --git a/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java b/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java index 772559136fc..a0020ddf759 100644 --- a/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java +++ b/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java @@ -2,15 +2,20 @@ import ch.njol.skript.Skript; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.lang.converter.Converter; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.SyntaxElement; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Priority; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.Converters; import java.util.Arrays; @@ -26,18 +31,15 @@ public abstract class PropertyExpression extends SimpleExpression { /** * A helper method to get the property patterns given a property, type, and default expression parameter. - * - * @param property the property - * @param fromType the type(s) that the property should apply to - * @param defaultExpr whether the type(s) should be optional - * + * @param property the property + * @param fromType the type(s) that the property should apply to + * @param defaultExpr whether the type(s) should be optional * @return an array of strings representing the patterns of the given property and type(s) * @throws IllegalArgumentException if property or fromType is null */ private static String[] patternsOf(String property, String fromType, boolean defaultExpr) { - if (property == null || fromType == null) - throw new IllegalArgumentException("'property' or 'fromType' was null."); - + Preconditions.checkNotNull(property, "property must be present"); + Preconditions.checkNotNull(fromType, "fromType must be present"); String types = defaultExpr ? "[of %" + fromType + "%]" : "of %" + fromType + "%"; return new String[]{"[the] " + property + " " + types, "%" + fromType + "%'[s] " + property}; } @@ -70,6 +72,37 @@ public static String[] getDefaultPatterns(String property, String fromType) { return patternsOf(property, fromType, true); } + /** + * A priority for {@link PropertyExpression}s. + * They will be registered before {@link SyntaxInfo#PATTERN_MATCHES_EVERYTHING} expressions + * but after {@link SyntaxInfo#COMBINED} expressions. + */ + @ApiStatus.Experimental + public static final Priority DEFAULT_PRIORITY = Priority.before(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + + /** + * Registers an expression with the two default property patterns "property of %types%" and "%types%'[s] property" + * + * @param registry The SyntaxRegistry to register with. + * @param expressionClass The PropertyExpression class being registered. + * @param returnType The class representing the expression's return type. + * @param property The name of the property. + * @param fromType Should be plural to support multiple objects but doesn't have to be. + * @param The return type. + * @param The Expression type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static , T> SyntaxInfo.Expression register(SyntaxRegistry registry, Class expressionClass, Class returnType, String property, String fromType) { + SyntaxInfo.Expression info = SyntaxInfo.Expression.builder(expressionClass) + .returnType(returnType) + .priority(DEFAULT_PRIORITY) + .addPatterns(getPatterns(property, fromType)) + .build(); + registry.register(SyntaxRegistry.EXPRESSION, info); + return info; + } + /** * Registers an expression as {@link ExpressionType#PROPERTY} with the two default property patterns "property of %types%" and "%types%'[s] property" * @@ -82,6 +115,30 @@ public static void register(Class> expressionClass, Skript.registerExpression(expressionClass, type, ExpressionType.PROPERTY, getPatterns(property, fromType)); } + /** + * Registers an expression with the two default property patterns "property [of %types%]" and "%types%'[s] property" + * This method also makes the expression type optional to force a default expression on the property expression. + * + * @param registry The SyntaxRegistry to register with. + * @param expressionClass The PropertyExpression class being registered. + * @param returnType The class representing the expression's return type. + * @param property The name of the property. + * @param fromType Should be plural to support multiple objects but doesn't have to be. + * @param The return type. + * @param The Expression type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static , T> SyntaxInfo.Expression registerDefault(SyntaxRegistry registry, Class expressionClass, Class returnType, String property, String fromType) { + SyntaxInfo.Expression info = SyntaxInfo.Expression.builder(expressionClass) + .returnType(returnType) + .priority(DEFAULT_PRIORITY) + .addPatterns(getDefaultPatterns(property, fromType)) + .build(); + registry.register(SyntaxRegistry.EXPRESSION, info); + return info; + } + /** * Registers an expression as {@link ExpressionType#PROPERTY} with the two default property patterns "property [of %types%]" and "%types%'[s] property" * This method also makes the expression type optional to force a default expression on the property expression. diff --git a/src/main/java/ch/njol/skript/lang/Condition.java b/src/main/java/ch/njol/skript/lang/Condition.java index 9ba8c40e853..03e425c562c 100644 --- a/src/main/java/ch/njol/skript/lang/Condition.java +++ b/src/main/java/ch/njol/skript/lang/Condition.java @@ -19,10 +19,14 @@ package ch.njol.skript.lang; import ch.njol.skript.Skript; +import ch.njol.skript.conditions.base.PropertyCondition; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Checker; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.util.Priority; import java.util.Iterator; @@ -39,17 +43,34 @@ public enum ConditionType { * * @see #PROPERTY */ - COMBINED, + COMBINED(SyntaxInfo.COMBINED), /** * Property conditions, e.g. "%properties% is/are data value[s]" */ - PROPERTY, + PROPERTY(PropertyCondition.DEFAULT_PRIORITY), /** * Conditions whose pattern matches (almost) everything or should be last checked. */ - PATTERN_MATCHES_EVERYTHING; + PATTERN_MATCHES_EVERYTHING(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + + @ApiStatus.Experimental + private final Priority priority; + + @ApiStatus.Experimental + ConditionType(Priority priority) { + this.priority = priority; + } + + /** + * @return The Priority equivalent of this ConditionType. + */ + @ApiStatus.Experimental + public Priority priority() { + return this.priority; + } + } private boolean negated; diff --git a/src/main/java/ch/njol/skript/lang/ExpressionType.java b/src/main/java/ch/njol/skript/lang/ExpressionType.java index 7f16051ee57..cdd957e1f41 100644 --- a/src/main/java/ch/njol/skript/lang/ExpressionType.java +++ b/src/main/java/ch/njol/skript/lang/ExpressionType.java @@ -20,6 +20,10 @@ import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.expressions.base.PropertyExpression; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.util.Priority; /** * Used to define in which order to parse expressions. @@ -29,32 +33,63 @@ public enum ExpressionType { /** * Expressions that only match simple text, e.g. "[the] player" */ - SIMPLE, + SIMPLE(SyntaxInfo.SIMPLE), /** * Expressions that are related to the Event that are typically simple. * * @see EventValueExpression */ - EVENT, + EVENT(EventValueExpression.DEFAULT_PRIORITY), /** * Expressions that contain other expressions, e.g. "[the] distance between %location% and %location%" * * @see #PROPERTY */ - COMBINED, + COMBINED(SyntaxInfo.COMBINED), /** * Property expressions, e.g. "[the] data value[s] of %items%"/"%items%'[s] data value[s]" * * @see PropertyExpression */ - PROPERTY, + PROPERTY(PropertyExpression.DEFAULT_PRIORITY), /** * Expressions whose pattern matches (almost) everything. Typically when using regex. Example: "[the] [loop-]<.+>" */ - PATTERN_MATCHES_EVERYTHING; + PATTERN_MATCHES_EVERYTHING(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + + @ApiStatus.Experimental + private final Priority priority; + + @ApiStatus.Experimental + ExpressionType(Priority priority) { + this.priority = priority; + } + + /** + * @return The Priority equivalent of this ExpressionType. + */ + @ApiStatus.Experimental + public Priority priority() { + return priority; + } + + @ApiStatus.Experimental + public static @Nullable ExpressionType fromModern(Priority priority) { + if (priority == SyntaxInfo.SIMPLE) + return ExpressionType.SIMPLE; + if (priority == EventValueExpression.DEFAULT_PRIORITY) + return ExpressionType.EVENT; + if (priority == SyntaxInfo.COMBINED) + return ExpressionType.COMBINED; + if (priority == PropertyExpression.DEFAULT_PRIORITY) + return ExpressionType.PROPERTY; + if (priority == SyntaxInfo.PATTERN_MATCHES_EVERYTHING) + return ExpressionType.PATTERN_MATCHES_EVERYTHING; + return null; + } } diff --git a/src/main/java/ch/njol/skript/lang/SkriptEvent.java b/src/main/java/ch/njol/skript/lang/SkriptEvent.java index ec85f9fd266..6a73ee32471 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptEvent.java +++ b/src/main/java/ch/njol/skript/lang/SkriptEvent.java @@ -26,10 +26,11 @@ import ch.njol.skript.events.EvtClick; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.structures.StructEvent.EventData; -import ch.njol.skript.util.Utils; -import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.EventPriority; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; +import ch.njol.skript.util.Utils; +import org.bukkit.event.Cancellable; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.entry.EntryContainer; import org.skriptlang.skript.lang.script.Script; @@ -256,29 +257,7 @@ public boolean canExecuteAsynchronously() { * to be nullable. */ public static String fixPattern(String pattern) { - char[] chars = pattern.toCharArray(); - StringBuilder stringBuilder = new StringBuilder(); - - boolean inType = false; - for (int i = 0; i < chars.length; i++) { - char character = chars[i]; - stringBuilder.append(character); - - if (character == '%') { - // toggle inType - inType = !inType; - - // add the dash character if it's not already present - // a type specification can have two prefix characters for modification - if (inType && i + 2 < chars.length && chars[i + 1] != '-' && chars[i + 2] != '-') - stringBuilder.append('-'); - } else if (character == '\\' && i + 1 < chars.length) { - // Make sure we don't toggle inType for escape percentage signs - stringBuilder.append(chars[i + 1]); - i++; - } - } - return stringBuilder.toString(); + return BukkitSyntaxInfos.fixPattern(pattern); } @Nullable diff --git a/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java b/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java index 78d32c9b023..fe337eaa30a 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java +++ b/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java @@ -18,17 +18,29 @@ */ package ch.njol.skript.lang; +import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.SkriptConfig; import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import ch.njol.skript.lang.SkriptEventInfo.ModernSkriptEventInfo; import org.bukkit.event.Event; import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.skriptlang.skript.lang.structure.StructureInfo; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; +import org.skriptlang.skript.util.Priority; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.List; import java.util.Locale; -public final class SkriptEventInfo extends StructureInfo { +public sealed class SkriptEventInfo extends StructureInfo permits ModernSkriptEventInfo { public Class[] events; public final String name; @@ -200,4 +212,125 @@ public ListeningBehavior getListeningBehavior() { return documentationID; } + /* + * Registration API Compatibility + */ + + /** + * Internal wrapper class for providing compatibility with the new Registration API. + */ + @ApiStatus.Internal + @ApiStatus.Experimental + public static final class ModernSkriptEventInfo + extends SkriptEventInfo + implements BukkitSyntaxInfos.Event { + + private final SyntaxOrigin origin; + + public ModernSkriptEventInfo(String name, String[] patterns, Class eventClass, String originClassPath, Class[] events) { + super(name, patterns, eventClass, originClassPath, events); + origin = SyntaxOrigin.of(Skript.getAddon(JavaPlugin.getProvidingPlugin(eventClass))); + } + + @Override + public Builder, E> builder() { + return BukkitSyntaxInfos.Event.builder(type(), name()) + .origin(origin) + .addPatterns(patterns()) + .priority(priority()) + .listeningBehavior(listeningBehavior()) + .since(since()) + .documentationId(id()) + .addDescription(description()) + .addExamples(examples()) + .addKeywords(keywords()) + .addRequiredPlugins(requiredPlugins()) + .addEvents(events()); + } + + @Override + public SyntaxOrigin origin() { + return origin; + } + + @Override + public Class type() { + return getElementClass(); + } + + @Override + public E instance() { + try { + return type().getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Override + public @Unmodifiable Collection patterns() { + return List.of(getPatterns()); + } + + @Override + public Priority priority() { + return SyntaxInfo.COMBINED; + } + + @Override + public ListeningBehavior listeningBehavior() { + return getListeningBehavior(); + } + + @Override + public String name() { + return getName(); + } + + @Override + public String id() { + return getId(); + } + + @Override + public @Nullable String since() { + return getSince(); + } + + @Override + public @Nullable String documentationId() { + return getDocumentationID(); + } + + @Override + public Collection description() { + String[] description = getDescription(); + return description != null ? List.of(description) : List.of(); + } + + @Override + public Collection examples() { + String[] examples = getExamples(); + return examples != null ? List.of(examples) : List.of(); + } + + @Override + public Collection keywords() { + String[] keywords = getKeywords(); + return keywords != null ? List.of(keywords) : List.of(); + } + + @Override + public Collection requiredPlugins() { + String[] requiredPlugins = getRequiredPlugins(); + return requiredPlugins != null ? List.of(requiredPlugins) : List.of(); + } + + @Override + public Collection> events() { + return List.of(events); + } + } + } diff --git a/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java b/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java index 3945d2eb02b..abf8e8e5265 100644 --- a/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java +++ b/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java @@ -18,13 +18,19 @@ */ package ch.njol.skript.lang; +import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.lang.structure.StructureInfo; + import ch.njol.skript.SkriptAPIException; import java.lang.reflect.Modifier; import java.util.Arrays; /** - * @author Peter Güttinger * @param the syntax element this info is for */ public class SyntaxElementInfo { @@ -74,4 +80,50 @@ public String[] getPatterns() { public String getOriginClassPath() { return originClassPath; } + + @Contract("_ -> new") + @ApiStatus.Experimental + @SuppressWarnings("unchecked") + public static , E extends SyntaxElement> I fromModern(SyntaxInfo info) { + if (info instanceof BukkitSyntaxInfos.Event event) { + // We must first go back to the raw input + String rawName = event.name().startsWith("On ") + ? event.name().substring(3) + : "*" + event.name(); + SkriptEventInfo eventInfo = new SkriptEventInfo<>( + rawName, event.patterns().toArray(new String[0]), + event.type(), event.origin().name(), + (Class[]) event.events().toArray(new Class[0])); + String since = event.since(); + if (since != null) + eventInfo.since(since); + String documentationId = event.documentationId(); + if (documentationId != null) + eventInfo.documentationID(documentationId); + eventInfo.listeningBehavior(event.listeningBehavior()) + .description(event.description().toArray(new String[0])) + .examples(event.examples().toArray(new String[0])) + .keywords(event.keywords().toArray(new String[0])) + .requiredPlugins(event.requiredPlugins().toArray(new String[0])); + + return (I) eventInfo; + } else if (info instanceof SyntaxInfo.Structure structure) { + return (I) new StructureInfo<>(structure.patterns().toArray(new String[0]), structure.type(), + structure.origin().name(), structure.entryValidator(), structure.nodeType()); + } else if (info instanceof SyntaxInfo.Expression expression) { + return (I) fromModernExpression(expression); + } + + return (I) new SyntaxElementInfo<>(info.patterns().toArray(new String[0]), info.type(), info.origin().name()); + } + + @Contract("_ -> new") + @ApiStatus.Experimental + private static , R> ExpressionInfo fromModernExpression(SyntaxInfo.Expression info) { + return new ExpressionInfo<>( + info.patterns().toArray(new String[0]), info.returnType(), + info.type(), info.origin().name(), ExpressionType.fromModern(info.priority()) + ); + } + } diff --git a/src/main/java/ch/njol/skript/localization/Language.java b/src/main/java/ch/njol/skript/localization/Language.java index d6fdf217b7a..ffacdff9cb2 100644 --- a/src/main/java/ch/njol/skript/localization/Language.java +++ b/src/main/java/ch/njol/skript/localization/Language.java @@ -19,11 +19,12 @@ package ch.njol.skript.localization; import ch.njol.skript.Skript; -import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.Config; import ch.njol.skript.util.ExceptionUtils; import ch.njol.skript.util.FileUtils; import ch.njol.skript.util.Version; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.localization.Localizer; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Nullable; @@ -64,7 +65,7 @@ public class Language { @Nullable private static HashMap localizedLanguage = null; - private static final HashMap langVersion = new HashMap<>(); + private static final HashMap langVersion = new HashMap<>(); public static String getName() { return name; @@ -183,46 +184,80 @@ public static boolean isInitialized() { return !defaultLanguage.isEmpty(); } + @Nullable + private static String getSanitizedLanguageDirectory(SkriptAddon addon) { + Localizer localizer = addon.localizer(); + if (localizer == null) { + return null; + } + String languageFileDirectory = localizer.languageFileDirectory(); + if (languageFileDirectory == null) { + return null; + } + // sanitization + languageFileDirectory = languageFileDirectory.replace('\\', '/'); + if (languageFileDirectory.startsWith("/")) { + languageFileDirectory = languageFileDirectory.substring(1); + } + if (languageFileDirectory.endsWith("/")) { + languageFileDirectory = languageFileDirectory.substring(0, languageFileDirectory.length() - 1); + } + return languageFileDirectory; + } + public static void loadDefault(SkriptAddon addon) { - if (addon.getLanguageFileDirectory() == null) + String languageFileDirectory = getSanitizedLanguageDirectory(addon); + if (languageFileDirectory == null) { return; + } - InputStream defaultIs = addon.plugin.getResource(addon.getLanguageFileDirectory() + "/default.lang"); - InputStream englishIs = addon.plugin.getResource(addon.getLanguageFileDirectory() + "/english.lang"); + Class source = addon.source(); + assert source != null; // getSanitizedLanguageDirectory call means source should not be null + try ( + InputStream defaultIs = source.getResourceAsStream("/" + languageFileDirectory + "/default.lang"); + InputStream englishIs = source.getResourceAsStream("/" + languageFileDirectory + "/english.lang") + ) { - if (defaultIs == null) { - if (englishIs == null) { - throw new IllegalStateException(addon + " is missing the required default.lang file!"); - } else { - defaultIs = englishIs; - englishIs = null; + InputStream defaultLangIs = defaultIs; + InputStream englishLangIs = englishIs; + if (defaultLangIs == null) { + if (englishLangIs == null) { + throw new IllegalStateException(addon + " is missing the required default.lang file!"); + } else { + defaultLangIs = englishLangIs; + englishLangIs = null; + } } - } - HashMap def = load(defaultIs, "default", false); - HashMap en = load(englishIs, "english", addon == Skript.getAddonInstance()); - String v = def.get("version"); - if (v == null) - Skript.warning("Missing version in default.lang"); + Map def = load(defaultLangIs, "default", false); + Map en = load(englishLangIs, "english", addon instanceof org.skriptlang.skript.Skript); - langVersion.put(addon.plugin, v == null ? Skript.getVersion() : new Version(v)); - def.remove("version"); - defaultLanguage.putAll(def); + String v = def.get("version"); + if (v == null) + Skript.warning("Missing version in default.lang"); - if (localizedLanguage == null) - localizedLanguage = new HashMap<>(); - localizedLanguage.putAll(en); + langVersion.put(addon.name(), v == null ? Skript.getVersion() : new Version(v)); + def.remove("version"); + defaultLanguage.putAll(def); - for (LanguageChangeListener l : listeners) - l.onLanguageChange(); + if (localizedLanguage == null) + localizedLanguage = new HashMap<>(); + localizedLanguage.putAll(en); + + for (LanguageChangeListener l : listeners) + l.onLanguageChange(); + + } catch (IOException e) { + throw new RuntimeException(e); + } } public static boolean load(String name) { name = "" + name.toLowerCase(Locale.ENGLISH); localizedLanguage = new HashMap<>(); - boolean exists = load(Skript.getAddonInstance(), name, true); - for (SkriptAddon addon : Skript.getAddons()) { + boolean exists = load(Skript.instance(), name, true); + for (SkriptAddon addon : Skript.instance().addons()) { exists |= load(addon, name, false); } if (!exists) { @@ -243,20 +278,42 @@ public static boolean load(String name) { } private static boolean load(SkriptAddon addon, String name, boolean tryUpdate) { - if (addon.getLanguageFileDirectory() == null) + String languageFileDirectory = getSanitizedLanguageDirectory(addon); + if (languageFileDirectory == null) { return false; + } + + Class source = addon.source(); + // Backwards addon compatibility - if (name.equals("english") && addon.plugin.getResource(addon.getLanguageFileDirectory() + "/default.lang") == null) - return true; + if (name.equals("english")) { + try (InputStream is = source.getResourceAsStream("/" + languageFileDirectory + "/default.lang")) { + if (is == null) { + return true; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } - HashMap l = load(addon.plugin.getResource(addon.getLanguageFileDirectory() + "/" + name + ".lang"), name, tryUpdate); - File file = new File(addon.plugin.getDataFolder(), addon.getLanguageFileDirectory() + File.separator + name + ".lang"); - try { - if (file.exists()) - l.putAll(load(new FileInputStream(file), name, tryUpdate)); - } catch (FileNotFoundException e) { - assert false; + HashMap l; + try (InputStream is = source.getResourceAsStream("/" + languageFileDirectory + "/" + name + ".lang")) { + l = load(is, name, tryUpdate); + } catch (IOException e) { + throw new RuntimeException(e); } + + String dataFileDirectory = addon.localizer().dataFileDirectory(); + if (dataFileDirectory != null) { // attempt to load language files from disk + File file = new File(dataFileDirectory, File.separator + name + ".lang"); + try { + if (file.exists()) + l.putAll(load(new FileInputStream(file), name, tryUpdate)); + } catch (FileNotFoundException e) { + assert false; + } + } + if (l.isEmpty()) return false; if (!l.containsKey("version")) { @@ -264,7 +321,7 @@ private static boolean load(SkriptAddon addon, String name, boolean tryUpdate) { } else { try { Version v = new Version("" + l.get("version")); - Version lv = langVersion.get(addon.plugin); + Version lv = langVersion.get(addon.name()); assert lv != null; // set in loadDefault() if (v.isSmallerThan(lv)) Skript.warning(addon + "'s language file " + name + ".lang is outdated, some messages will be english."); diff --git a/src/main/java/ch/njol/skript/util/Utils.java b/src/main/java/ch/njol/skript/util/Utils.java index 2cda7d3109b..b99c2c97c51 100644 --- a/src/main/java/ch/njol/skript/util/Utils.java +++ b/src/main/java/ch/njol/skript/util/Utils.java @@ -58,6 +58,7 @@ import net.md_5.bungee.api.ChatColor; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.util.ClassLoader; /** * Utility class. @@ -208,49 +209,25 @@ public static Pair getAmount(String s) { * 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. * @return This SkriptAddon + * @deprecated Use {@link org.skriptlang.skript.util.ClassLoader}. */ + @Deprecated public static Class[] getClasses(Plugin plugin, String basePackage, String... subPackages) throws IOException { - assert subPackages != null; - JarFile jar = new JarFile(getFile(plugin)); - for (int i = 0; i < subPackages.length; i++) - subPackages[i] = subPackages[i].replace('.', '/') + "/"; - basePackage = basePackage.replace('.', '/') + "/"; List> classes = new ArrayList<>(); - try { - List classNames = new ArrayList<>(); - - for (JarEntry e : new EnumerationIterable<>(jar.entries())) { - if (e.getName().startsWith(basePackage) && e.getName().endsWith(".class") && !e.getName().endsWith("package-info.class")) { - boolean load = subPackages.length == 0; - for (String sub : subPackages) { - if (e.getName().startsWith(sub, basePackage.length())) { - load = true; - break; - } - } - - if (load) - classNames.add(e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length())); - } - } - - classNames.sort(String::compareToIgnoreCase); - - for (String c : classNames) { - try { - classes.add(Class.forName(c, true, plugin.getClass().getClassLoader())); - } catch (ClassNotFoundException | NoClassDefFoundError ex) { - Skript.exception(ex, "Cannot load class " + c); - } catch (ExceptionInInitializerError err) { - Skript.exception(err.getCause(), "class " + c + " generated an exception while loading"); - } - } - } finally { - try { - jar.close(); - } catch (IOException e) {} + ClassLoader loader = ClassLoader.builder() + .basePackage(basePackage) + .addSubPackages(subPackages) + .deep(true) + .initialize(true) + .forEachClass(classes::add) + .build(); + File jarFile = getFile(plugin); + if (jarFile != null) { + loader.loadClasses(plugin.getClass(), jarFile); + } else { + loader.loadClasses(plugin.getClass()); } - return classes.toArray(new Class[classes.size()]); + return classes.toArray(new Class[0]); } /** diff --git a/src/main/java/org/skriptlang/skript/Skript.java b/src/main/java/org/skriptlang/skript/Skript.java new file mode 100644 index 00000000000..25604cf5cbf --- /dev/null +++ b/src/main/java/org/skriptlang/skript/Skript.java @@ -0,0 +1,56 @@ +package org.skriptlang.skript; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.addon.SkriptAddon; + +import java.util.Collection; + +/** + * The main class for everything related to Skript. + */ +@ApiStatus.Experimental +public interface Skript extends SkriptAddon { + + /** + * Constructs a default implementation of a Skript. + * It makes use of the default implementations of required components. + * @param source The main class of the application creating this Skript. + * Typically, this can be the class invoking this method. + * @param name The name for the Skript to use. + * @return A Skript. + */ + @Contract("_, _ -> new") + static Skript of(Class source, String name) { + return new SkriptImpl(source, name); + } + + /** + * Registers the provided addon with this Skript and loads the provided modules. + * @param source The main class of the application registering this addon. + * Typically, this can be the class invoking this method. + * @param name The name of the addon to register. + */ + @Contract("_, _ -> new") + SkriptAddon registerAddon(Class source, String name); + + /** + * @return An unmodifiable snapshot of addons currently registered with this Skript. + */ + @Unmodifiable Collection addons(); + + /** + * Constructs an unmodifiable view of this Skript. + * That is, the returned Skript will be unable to register new addons + * and the individual addons from {@link #addons()} will be unmodifiable. + * Additionally, it will return unmodifiable views of its inherited {@link SkriptAddon} components. + * @return An unmodifiable view of this Skript. + */ + @Override + @Contract("-> new") + default Skript unmodifiableView() { + return new SkriptImpl.UnmodifiableSkript(this, SkriptAddon.super.unmodifiableView()); + } + +} diff --git a/src/main/java/org/skriptlang/skript/SkriptImpl.java b/src/main/java/org/skriptlang/skript/SkriptImpl.java new file mode 100644 index 00000000000..03b96f503a5 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/SkriptImpl.java @@ -0,0 +1,263 @@ +package org.skriptlang.skript; + +import ch.njol.skript.SkriptAPIException; +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +final class SkriptImpl implements Skript { + + /** + * The addon instance backing this Skript. + */ + private final SkriptAddon addon; + + SkriptImpl(Class source, String name) { + addon = new SkriptAddonImpl(this, source, name, Localizer.of(this)); + storeRegistry(SyntaxRegistry.class, SyntaxRegistry.empty()); + } + + /* + * Registry Management + */ + + private static final Map, Registry> registries = new ConcurrentHashMap<>(); + + @Override + public > void storeRegistry(Class registryClass, R registry) { + registries.put(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + registries.remove(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return registries.containsKey(registryClass); + } + + @Override + public > R registry(Class registryClass) { + //noinspection unchecked + R registry = (R) registries.get(registryClass); + if (registry == null) + throw new NullPointerException("Registry not present for " + registryClass); + return registry; + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + //noinspection unchecked + return (R) registries.computeIfAbsent(registryClass, key -> putIfAbsent.get()); + } + + /* + * SkriptAddon Management + */ + + private static final Map addons = new HashMap<>(); + + @Override + public SkriptAddon registerAddon(Class source, String name) { + // make sure an addon is not already registered with this name + SkriptAddon existing = addons.get(name); + if (existing != null) { + throw new SkriptAPIException( + "An addon (provided by '" + existing.source().getName() + "') with the name '" + name + "' is already registered" + ); + } + + SkriptAddon addon = new SkriptAddonImpl(this, source, name, null); + addons.put(name, addon); + return addon; + } + + @Override + public @Unmodifiable Collection addons() { + return ImmutableSet.copyOf(addons.values()); + } + + /* + * SkriptAddon Implementation + */ + + @Override + public Class source() { + return addon.source(); + } + + @Override + public String name() { + return addon.name(); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return registry(SyntaxRegistry.class); + } + + @Override + public Localizer localizer() { + return addon.localizer(); + } + + @Override + public void loadModules(AddonModule... modules) { + addon.loadModules(modules); + } + + private static final class SkriptAddonImpl implements SkriptAddon { + + private final Skript skript; + private final Class source; + private final String name; + private final Localizer localizer; + + SkriptAddonImpl(Skript skript, Class source, String name, @Nullable Localizer localizer) { + this.skript = skript; + this.source = source; + this.name = name; + this.localizer = localizer == null ? Localizer.of(this) : localizer; + } + + @Override + public Class source() { + return source; + } + + @Override + public String name() { + return name; + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + skript.storeRegistry(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + skript.removeRegistry(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return skript.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + return skript.registry(registryClass); + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + return skript.registry(registryClass, putIfAbsent); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return skript.syntaxRegistry(); + } + + @Override + public Localizer localizer() { + return localizer; + } + + } + + /* + * ViewProvider Implementation + */ + + static final class UnmodifiableSkript implements Skript { + + private final Skript skript; + private final SkriptAddon unmodifiableAddon; + + UnmodifiableSkript(Skript skript, SkriptAddon unmodifiableAddon) { + this.skript = skript; + this.unmodifiableAddon = unmodifiableAddon; + } + + @Override + public SkriptAddon registerAddon(Class source, String name) { + throw new UnsupportedOperationException("Cannot register addons using an unmodifiable Skript"); + } + + @Override + public @Unmodifiable Collection addons() { + ImmutableSet.Builder addons = ImmutableSet.builder(); + skript.addons().stream() + .map(SkriptAddon::unmodifiableView) + .forEach(addons::add); + return addons.build(); + } + + @Override + public Class source() { + return skript.source(); + } + + @Override + public String name() { + return skript.name(); + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + unmodifiableAddon.storeRegistry(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + unmodifiableAddon.removeRegistry(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return unmodifiableAddon.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + return unmodifiableAddon.registry(registryClass); + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + return unmodifiableAddon.registry(registryClass, putIfAbsent); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return unmodifiableAddon.syntaxRegistry(); + } + + @Override + public Localizer localizer() { + return unmodifiableAddon.localizer(); + } + + @Override + public void loadModules(AddonModule... modules) { + unmodifiableAddon.loadModules(modules); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/addon/AddonModule.java b/src/main/java/org/skriptlang/skript/addon/AddonModule.java new file mode 100644 index 00000000000..808a0b08eed --- /dev/null +++ b/src/main/java/org/skriptlang/skript/addon/AddonModule.java @@ -0,0 +1,21 @@ +package org.skriptlang.skript.addon; + +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.Skript; +import org.skriptlang.skript.registration.SyntaxRegistry; + +/** + * A module is a component of a {@link SkriptAddon} used for registering syntax and other {@link Skript} components. + */ +@FunctionalInterface +@ApiStatus.Experimental +public interface AddonModule { + + /** + * Used for loading the components (e.g. syntax) of this module. + * Typically triggered through {@link SkriptAddon#loadModules(AddonModule...)}. + * @param addon The addon this module belongs to. + */ + void load(SkriptAddon addon); + +} diff --git a/src/main/java/org/skriptlang/skript/addon/SkriptAddon.java b/src/main/java/org/skriptlang/skript/addon/SkriptAddon.java new file mode 100644 index 00000000000..4b7b6813a05 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/addon/SkriptAddon.java @@ -0,0 +1,108 @@ +package org.skriptlang.skript.addon; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.skriptlang.skript.Skript; +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; +import org.skriptlang.skript.util.ViewProvider; + +import java.util.function.Supplier; + +/** + * A Skript addon is an extension to Skript that expands its features. + * Typically, an addon instance may be obtained through {@link Skript#registerAddon(Class, String)}. + */ +@ApiStatus.Experimental +public interface SkriptAddon extends ViewProvider { + + /** + * @return A class from the application that registered this addon. + * Typically, this is the main class or the specific class in which registration occurred. + */ + Class source(); + + /** + * @return The name of this addon. + */ + String name(); + + /** + * Stores a registry under registryClass. + * If a registry is already stored under registryClass, it will be replaced. + * @param registryClass The class (key) to store registry under. + * @param registry The registry to store. + * @param The type of registry. + */ + > void storeRegistry(Class registryClass, R registry); + + /** + * Removes the registry stored under registryClass. + * It is safe to call this method even if a registry is not stored under registryClass. + * @param registryClass The class (key) that the registry to remove is under. + */ + void removeRegistry(Class> registryClass); + + /** + * Determines whether a registry has been stored under registryClass. + * @param registryClass The class (key) to search for a registry under. + * @return Whether a registry is stored under registryClass. + */ + boolean hasRegistry(Class> registryClass); + + /** + * Obtains the registry stored under registryClass. + * This method will never return null, meaning it may be necessary to call {@link #hasRegistry(Class)} + * if you are not sure whether the registry you need exists. + * @param registryClass The class (key) that the registry is stored under. + * @return The registry stored under registryClass. + * @param The type of registry. + */ + > R registry(Class registryClass); + + /** + * Searches for a registry stored under registryClass. + * If the search fails, putIfAbsent will be used to get, store, and return a registry of the requested type. + * @param registryClass The class (key) to search for a registry under. + * @param putIfAbsent A supplier to use for creating an instance of the desired type of registry if one + * is not already stored under registryClass. + * @return The registry stored under registryClass or created from putIfAbsent. + * @param The type of registry. + */ + > R registry(Class registryClass, Supplier putIfAbsent); + + /** + * @return A syntax registry for this addon's syntax. + */ + SyntaxRegistry syntaxRegistry(); + + /** + * @return A localizer for this addon's localizations. + */ + Localizer localizer(); + + /** + * A helper method for loading addon modules. + * @param modules The modules to load. + */ + default void loadModules(AddonModule... modules) { + for (AddonModule module : modules) { + module.load(this); + } + } + + /** + * Constructs an unmodifiable view of this addon. + * That is, the returned addon will return unmodifiable views of its {@link #syntaxRegistry()} and {@link #localizer()}. + * @return An unmodifiable view of this addon. + * @see SyntaxRegistry#unmodifiableView() + * @see Localizer#unmodifiableView() + */ + @Override + @Contract("-> new") + default SkriptAddon unmodifiableView() { + return new SkriptAddonImpl.UnmodifiableAddon(this); + } + +} diff --git a/src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java b/src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java new file mode 100644 index 00000000000..33af5c65b74 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java @@ -0,0 +1,79 @@ +package org.skriptlang.skript.addon; + +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; +import org.skriptlang.skript.util.ViewProvider; + +import java.util.function.Supplier; + +class SkriptAddonImpl { + + static class UnmodifiableAddon implements SkriptAddon { + + private final SkriptAddon addon; + private final Localizer unmodifiableLocalizer; + + UnmodifiableAddon(SkriptAddon addon) { + this.addon = addon; + this.unmodifiableLocalizer = addon.localizer().unmodifiableView(); + } + + @Override + public Class source() { + return addon.source(); + } + + @Override + public String name() { + return addon.name(); + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + throw new UnsupportedOperationException("Cannot store registries on an unmodifiable addon"); + } + + @Override + public void removeRegistry(Class> registryClass) { + throw new UnsupportedOperationException("Cannot remove registries from an unmodifiable addon"); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return addon.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + R registry = addon.registry(registryClass); + if (registry instanceof ViewProvider) { + //noinspection unchecked + registry = ((ViewProvider) registry).unmodifiableView(); + } + return registry; + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + throw new UnsupportedOperationException("Cannot store registries on an unmodifiable addon"); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return addon.syntaxRegistry().unmodifiableView(); + } + + @Override + public Localizer localizer() { + return unmodifiableLocalizer; + } + + @Override + public void loadModules(AddonModule... modules) { + throw new UnsupportedOperationException("Cannot load modules using an unmodifiable addon"); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java new file mode 100644 index 00000000000..336b84a2361 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java @@ -0,0 +1,22 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.SkriptEvent; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos.Event; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.registration.SyntaxRegistry.Key; + +/** + * A class containing {@link SyntaxRegistry} keys for Bukkit-specific syntax elements. + */ +@ApiStatus.Experimental +public final class BukkitRegistryKeys { + + private BukkitRegistryKeys() { } + + /** + * A key representing the Bukkit-specific {@link SkriptEvent} syntax element. + */ + public static final Key> EVENT = Key.of("event"); + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java new file mode 100644 index 00000000000..6a70409b82a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java @@ -0,0 +1,315 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfosImpl.EventImpl; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxInfo.Builder; + +import java.util.Collection; + +/** + * A class containing the interfaces representing Bukkit-specific SyntaxInfo implementations. + */ +@ApiStatus.Experimental +public final class BukkitSyntaxInfos { + + private BukkitSyntaxInfos() { } + + /** + * A syntax info to be used for {@link SkriptEvent}s. + * It contains additional details including the Bukkit events represented along with documentation data. + * @param The class providing the implementation of the SkriptEvent this info represents. + */ + public interface Event extends SyntaxInfo { + + /** + * @param eventClass The Structure class the info will represent. + * @param name The name of the SkriptEvent. + * @return A Structure-specific builder for creating a syntax info representing type. + */ + static Builder, E> builder( + Class eventClass, String name + ) { + return new EventImpl.BuilderImpl<>(eventClass, name); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E> builder(); + + /** + * @return The listening behavior for the SkriptEvent. Determines when the event should trigger. + */ + ListeningBehavior listeningBehavior(); + + /** + * @return The name of the {@link SkriptEvent}. + */ + String name(); + + /** + * @return A documentation-friendly version of {@link #name()}. + */ + String id(); + + /** + * @return Documentation data. Represents the version of the plugin in which a syntax was added. + * @see ch.njol.skript.doc.Since + */ + @Nullable String since(); + + /** + * @return Documentation data. Used for identifying specific syntaxes in documentation. + * @see ch.njol.skript.doc.DocumentationId + */ + @Nullable String documentationId(); + + /** + * @return Documentation data. A description of a syntax. + * @see ch.njol.skript.doc.Description + */ + Collection description(); + + /** + * @return Documentation data. Examples for using a syntax. + * @see ch.njol.skript.doc.Examples + */ + Collection examples(); + + /** + * @return Documentation data. Keywords are used by the search engine to provide relevant results. + * @see ch.njol.skript.doc.Keywords + */ + Collection keywords(); + + /** + * @return Documentation data. Plugins other than Skript that are required by a syntax. + * @see ch.njol.skript.doc.RequiredPlugins + */ + Collection requiredPlugins(); + + /** + * @return A collection of the classes representing the Bukkit events the {@link SkriptEvent} listens for. + */ + Collection> events(); + + /** + * An Event-specific builder is used for constructing a new Event syntax info. + * @see #builder(Class, String) + * @param The type of builder being used. + * @param The SkriptEvent class providing the implementation of the syntax info being built. + */ + interface Builder, E extends SkriptEvent> extends SyntaxInfo.Builder { + + /** + * Sets the listening behavior the event will use. + * This determines when the event should trigger. + * By default, this is {@link ListeningBehavior#UNCANCELLED}. + * @param listeningBehavior The listening behavior to use. + * @return This builder. + * @see Event#listeningBehavior() + */ + @Contract("_ -> this") + B listeningBehavior(ListeningBehavior listeningBehavior); + + /** + * Sets the "since" value the event's documentation will use. + * @param since The "since" value to use. + * @return This builder. + * @see Event#since() + */ + @Contract("_ -> this") + B since(String since); + + /** + * Sets the documentation identifier the event's documentation will use. + * @param documentationId The documentation identifier to use. + * @return This builder. + * @see Event#documentationId() + */ + @Contract("_ -> this") + B documentationId(String documentationId); + + /** + * Adds a description line to the event's documentation. + * @param description The description line to add. + * @return This builder. + * @see Event#description() + */ + @Contract("_ -> this") + B addDescription(String description); + + /** + * Adds lines of description to the event's documentation. + * @param description The description lines to add. + * @return This builder. + * @see Event#description() + */ + @Contract("_ -> this") + B addDescription(String... description); + + /** + * Adds lines of description to the event's documentation. + * @param description The description lines to add. + * @return This builder. + * @see Event#description() + */ + @Contract("_ -> this") + B addDescription(Collection description); + + /** + * Adds an example to the event's documentation. + * @param example The example to add. + * @return This builder. + * @see Event#examples() + */ + @Contract("_ -> this") + B addExample(String example); + + /** + * Adds examples to the event's documentation. + * @param examples The examples to add. + * @return This builder. + * @see Event#examples() + */ + @Contract("_ -> this") + B addExamples(String... examples); + + /** + * Adds examples to the event's documentation. + * @param examples The examples to add. + * @return This builder. + * @see Event#examples() + */ + @Contract("_ -> this") + B addExamples(Collection examples); + + /** + * Adds a keyword to the event's documentation. + * @param keyword The keyword to add. + * @return This builder. + * @see Event#keywords() + */ + @Contract("_ -> this") + B addKeyword(String keyword); + + /** + * Adds keywords to the event's documentation. + * @param keywords The keywords to add. + * @return This builder. + * @see Event#keywords() + */ + @Contract("_ -> this") + B addKeywords(String... keywords); + + /** + * Adds keywords to the event's documentation. + * @param keywords The keywords to add. + * @return This builder. + * @see Event#keywords() + */ + @Contract("_ -> this") + B addKeywords(Collection keywords); + + /** + * Adds a required plugin to event's documentation. + * @param plugin The required plugin to add. + * @return This builder. + * @see Event#requiredPlugins() + */ + @Contract("_ -> this") + B addRequiredPlugin(String plugin); + + /** + * Adds required plugins to the event's documentation. + * @param plugins The required plugins to add. + * @return This builder. + * @see Event#requiredPlugins() + */ + @Contract("_ -> this") + B addRequiredPlugins(String... plugins); + + /** + * Adds required plugins to the event's documentation. + * @param plugins The required plugins to add. + * @return This builder. + * @see Event#requiredPlugins() + */ + @Contract("_ -> this") + B addRequiredPlugins(Collection plugins); + + /** + * Adds an event to the event's documentation. + * @param event The event to add. + * @return This builder. + * @see Event#events() + */ + @Contract("_ -> this") + B addEvent(Class event); + + /** + * Adds events to the event's documentation. + * @param events The events to add. + * @return This builder. + * @see Event#events() + */ + @Contract("_ -> this") + B addEvents(Class... events); + + /** + * Adds events to the event's documentation. + * @param events The events to add. + * @return This builder. + * @see Event#events() + */ + @Contract("_ -> this") + B addEvents(Collection> events); + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Event build(); + + } + + } + + /** + * Fixes patterns in event by modifying every {@link ch.njol.skript.patterns.TypePatternElement} to be nullable. + */ + public static String fixPattern(String pattern) { + char[] chars = pattern.toCharArray(); + StringBuilder stringBuilder = new StringBuilder(); + + boolean inType = false; + for (int i = 0; i < chars.length; i++) { + char character = chars[i]; + stringBuilder.append(character); + + if (character == '%') { + // toggle inType + inType = !inType; + + // add the dash character if it's not already present + // a type specification can have two prefix characters for modification + if (inType && i + 2 < chars.length && chars[i + 1] != '-' && chars[i + 2] != '-') + stringBuilder.append('-'); + } else if (character == '\\' && i + 1 < chars.length) { + // Make sure we don't toggle inType for escape percentage signs + stringBuilder.append(chars[i + 1]); + i++; + } + } + return stringBuilder.toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java new file mode 100644 index 00000000000..0a7f1a74a6a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java @@ -0,0 +1,383 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos.Event; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; +import org.skriptlang.skript.util.Priority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Supplier; + +final class BukkitSyntaxInfosImpl { + + static final class EventImpl implements Event { + + private final SyntaxInfo defaultInfo; + private final ListeningBehavior listeningBehavior; + private final String name; + private final String id; + private final @Nullable String since; + private final @Nullable String documentationId; + private final Collection description; + private final Collection examples; + private final Collection keywords; + private final Collection requiredPlugins; + private final Collection> events; + + EventImpl( + SyntaxInfo defaultInfo, ListeningBehavior listeningBehavior, String name, + @Nullable String since, @Nullable String documentationId, Collection description, Collection examples, + Collection keywords, Collection requiredPlugins, Collection> events + ) { + this.defaultInfo = defaultInfo; + this.listeningBehavior = listeningBehavior; + this.name = name.startsWith("*") ? name.substring(1) : "On " + name; + this.id = name.toLowerCase(Locale.ENGLISH) + .replaceAll("[#'\"<>/&]", "") + .replaceAll("\\s+", "_"); + this.since = since; + this.documentationId = documentationId; + this.description = ImmutableList.copyOf(description); + this.examples = ImmutableList.copyOf(examples); + this.keywords = ImmutableList.copyOf(keywords); + this.requiredPlugins = ImmutableList.copyOf(requiredPlugins); + this.events = ImmutableList.copyOf(events); + } + + @Override + public Builder, E> builder() { + var builder = new BuilderImpl<>(type(), name); + defaultInfo.builder().applyTo(builder); + builder.listeningBehavior(listeningBehavior); + builder.documentationId(id); + if (since != null) { + builder.since(since); + } + if (documentationId != null) { + builder.documentationId(documentationId); + } + builder.addDescription(description); + builder.addExamples(examples); + builder.addKeywords(keywords); + builder.addRequiredPlugins(requiredPlugins); + builder.addEvents(events); + return builder; + } + + @Override + public ListeningBehavior listeningBehavior() { + return listeningBehavior; + } + + @Override + public String name() { + return name; + } + + @Override + public String id() { + return id; + } + + @Override + @Nullable + public String since() { + return since; + } + + @Override + @Nullable + public String documentationId() { + return documentationId; + } + + @Override + public Collection description() { + return description; + } + + @Override + public Collection examples() { + return examples; + } + + @Override + public Collection keywords() { + return keywords; + } + + @Override + public Collection requiredPlugins() { + return requiredPlugins; + } + + @Override + public Collection> events() { + return events; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return (other instanceof Event event) && + Objects.equals(defaultInfo, other) && + Objects.equals(name(), event.name()) && + Objects.equals(events(), event.events()); + } + + @Override + public int hashCode() { + return Objects.hash(defaultInfo, name(), events()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .add("name", name()) + .add("events", events()) + .toString(); + } + + // + // default methods + // + + @Override + public SyntaxOrigin origin() { + return defaultInfo.origin(); + } + + @Override + public Class type() { + return defaultInfo.type(); + } + + @Override + public E instance() { + return defaultInfo.instance(); + } + + @Override + @Unmodifiable + public Collection patterns() { + return defaultInfo.patterns(); + } + + @Override + public Priority priority() { + return defaultInfo.priority(); + } + + @SuppressWarnings("unchecked") + static final class BuilderImpl, E extends SkriptEvent> implements Event.Builder { + + private final SyntaxInfo.Builder defaultBuilder; + private ListeningBehavior listeningBehavior = ListeningBehavior.UNCANCELLED; + private final String name; + private @Nullable String since; + private @Nullable String documentationId; + private final List description = new ArrayList<>(); + private final List examples = new ArrayList<>(); + private final List keywords = new ArrayList<>(); + private final List requiredPlugins = new ArrayList<>(); + private final List> events = new ArrayList<>(); + + BuilderImpl(Class type, String name) { + this.defaultBuilder = SyntaxInfo.builder(type); + this.name = name; + } + + @Override + public B listeningBehavior(ListeningBehavior listeningBehavior) { + this.listeningBehavior = listeningBehavior; + return (B) this; + } + + @Override + public B since(String since) { + this.since = since; + return (B) this; + } + + @Override + public B documentationId(String documentationId) { + this.documentationId = documentationId; + return (B) this; + } + + @Override + public B addDescription(String description) { + this.description.add(description); + return (B) this; + } + + @Override + public B addDescription(String... description) { + Collections.addAll(this.description, description); + return (B) this; + } + + @Override + public B addDescription(Collection description) { + this.description.addAll(description); + return (B) this; + } + + @Override + public B addExample(String example) { + this.examples.add(example); + return (B) this; + } + + @Override + public B addExamples(String... examples) { + Collections.addAll(this.examples, examples); + return (B) this; + } + + @Override + public B addExamples(Collection examples) { + this.examples.addAll(examples); + return (B) this; + } + + @Override + public B addKeyword(String keyword) { + this.keywords.add(keyword); + return (B) this; + } + + @Override + public B addKeywords(String... keywords) { + Collections.addAll(this.keywords, keywords); + return (B) this; + } + + @Override + public B addKeywords(Collection keywords) { + this.keywords.addAll(keywords); + return (B) this; + } + + @Override + public B addRequiredPlugin(String plugin) { + this.requiredPlugins.add(plugin); + return (B) this; + } + + @Override + public B addRequiredPlugins(String... plugins) { + Collections.addAll(this.requiredPlugins, plugins); + return (B) this; + } + + @Override + public B addRequiredPlugins(Collection plugins) { + this.requiredPlugins.addAll(plugins); + return (B) this; + } + + @Override + public B addEvent(Class event) { + this.events.add(event); + return (B) this; + } + + @Override + public B addEvents(Class... events) { + Collections.addAll(this.events, events); + return (B) this; + } + + @Override + public B addEvents(Collection> events) { + this.events.addAll(events); + return (B) this; + } + + @Override + public B origin(SyntaxOrigin origin) { + defaultBuilder.origin(origin); + return (B) this; + } + + @Override + public B supplier(Supplier supplier) { + defaultBuilder.supplier(supplier); + return (B) this; + } + + @Override + public B addPattern(String pattern) { + defaultBuilder.addPattern(pattern); + return (B) this; + } + + @Override + public B addPatterns(String... patterns) { + defaultBuilder.addPatterns(patterns); + return (B) this; + } + + @Override + public B addPatterns(Collection patterns) { + defaultBuilder.addPatterns(patterns); + return (B) this; + } + + @Override + public B priority(Priority priority) { + defaultBuilder.priority(priority); + return (B) this; + } + + @Override + public Event build() { + return new EventImpl<>( + defaultBuilder.build(), listeningBehavior, name, + since, documentationId, description, examples, keywords, requiredPlugins, events + ); + } + + @Override + public void applyTo(SyntaxInfo.Builder builder) { + defaultBuilder.applyTo(builder); + //noinspection rawtypes - Should be safe, generics will not influence this + if (builder instanceof Event.Builder eventBuilder) { + eventBuilder.listeningBehavior(listeningBehavior); + if (since != null) { + eventBuilder.since(since); + } + if (documentationId != null) { + eventBuilder.documentationId(documentationId); + } + eventBuilder.addDescription(description); + eventBuilder.addExamples(examples); + eventBuilder.addKeywords(keywords); + eventBuilder.addRequiredPlugins(requiredPlugins); + eventBuilder.addEvents(events); + } + } + + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/structure/Structure.java b/src/main/java/org/skriptlang/skript/lang/structure/Structure.java index f56cffcfbf0..296a6ee3043 100644 --- a/src/main/java/org/skriptlang/skript/lang/structure/Structure.java +++ b/src/main/java/org/skriptlang/skript/lang/structure/Structure.java @@ -113,7 +113,7 @@ public final boolean init(Expression[] expressions, int matchedPattern, Kleen StructureInfo structureInfo = structureData.structureInfo; assert structureInfo != null; - if (structureInfo.simple) { // simple structures do not have validators + if (structureData.node instanceof SimpleNode) { // simple structures do not have validators return init(literals, matchedPattern, parseResult, null); } @@ -206,10 +206,10 @@ public static Structure parse(String expr, Node node, @Nullable String defaultEr throw new IllegalArgumentException("only simple or section nodes may be parsed as a structure"); ParserInstance.get().getData(StructureData.class).node = node; - if (node instanceof SimpleNode) { // only allow simple structures for simple nodes - iterator = new CheckedIterator<>(iterator, item -> item != null && item.simple); - } else { // only allow non-simple structures for section nodes - iterator = new CheckedIterator<>(iterator, item -> item != null && !item.simple); + if (node instanceof SimpleNode) { // filter out section only structures + iterator = new CheckedIterator<>(iterator, item -> item != null && item.nodeType.canBeSimple()); + } else { // filter out simple only structures + iterator = new CheckedIterator<>(iterator, item -> item != null && item.nodeType.canBeSection()); } iterator = new ConsumingIterator<>(iterator, elementInfo -> ParserInstance.get().getData(StructureData.class).structureInfo = elementInfo); diff --git a/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java b/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java index 773c3212af3..285f98200f2 100644 --- a/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java +++ b/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java @@ -19,8 +19,10 @@ package org.skriptlang.skript.lang.structure; import ch.njol.skript.lang.SyntaxElementInfo; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.registration.SyntaxInfo; /** * Special {@link SyntaxElementInfo} for {@link Structure}s that may contain information such as the {@link EntryValidator}. @@ -35,20 +37,28 @@ public class StructureInfo extends SyntaxElementInfo { */ public final boolean simple; + @ApiStatus.Experimental + public final SyntaxInfo.Structure.NodeType nodeType; + public StructureInfo(String[] patterns, Class c, String originClassPath) throws IllegalArgumentException { this(patterns, c, originClassPath, false); } - public StructureInfo(String[] patterns, Class c, String originClassPath, boolean simple) throws IllegalArgumentException { - super(patterns, c, originClassPath); - this.entryValidator = null; - this.simple = simple; + public StructureInfo(String[] patterns, Class elementClass, String originClassPath, boolean simple) throws IllegalArgumentException { + this(patterns, elementClass, originClassPath, null, simple ? SyntaxInfo.Structure.NodeType.SIMPLE : SyntaxInfo.Structure.NodeType.SECTION); + } + + public StructureInfo(String[] patterns, Class elementClass, String originClassPath, @Nullable EntryValidator entryValidator) throws IllegalArgumentException { + this(patterns, elementClass, originClassPath, entryValidator, SyntaxInfo.Structure.NodeType.SECTION); } - public StructureInfo(String[] patterns, Class c, String originClassPath, EntryValidator entryValidator) throws IllegalArgumentException { - super(patterns, c, originClassPath); + @ApiStatus.Experimental + public StructureInfo(String[] patterns, Class elementClass, String originClassPath, + @Nullable EntryValidator entryValidator, SyntaxInfo.Structure.NodeType nodeType) throws IllegalArgumentException { + super(patterns, elementClass, originClassPath); this.entryValidator = entryValidator; - this.simple = false; + this.nodeType = nodeType; + this.simple = nodeType.canBeSimple(); } } diff --git a/src/main/java/org/skriptlang/skript/localization/Localizer.java b/src/main/java/org/skriptlang/skript/localization/Localizer.java new file mode 100644 index 00000000000..c237dd20a83 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/localization/Localizer.java @@ -0,0 +1,66 @@ +package org.skriptlang.skript.localization; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.util.ViewProvider; + +/** + * A Localizer is used for the localization of translatable strings. + * + * This API is highly experimental and will be subject to change due to pending localization reworks. + * In its current state, it acts as a bridge between old and new API. + * + * @see ch.njol.skript.localization.Language + */ +@ApiStatus.Experimental +public interface Localizer extends ViewProvider { + + /** + * @param addon The addon this localizer is localizing for. + * @return A localizer with no default translations. + */ + @Contract("_ -> new") + static Localizer of(SkriptAddon addon) { + return new LocalizerImpl(addon); + } + + /** + * Sets the language file directories for this localizer. + * This method will initiate a loading of any language files in the provided directories. + * @param languageFileDirectory The path to the directory on the jar containing language files. + * @param dataFileDirectory The path to the directory on the disk containing language files. + * For example, this may include language files that have been saved to enable user customization. + */ + void setSourceDirectories(String languageFileDirectory, @Nullable String dataFileDirectory); + + /** + * @return The path to the directory on the jar containing language files. + */ + @Nullable String languageFileDirectory(); + + /** + * @return The path to the directory on the disk containing language files. + */ + @Nullable String dataFileDirectory(); + + /** + * Used for obtaining the translation of a language key. + * @param key The key of the translation to obtain. + * @return The translation represented by the provided key, or null if no translation exists. + */ + @Nullable String translate(String key); + + /** + * Constructs an unmodifiable view of this localizer. + * That is, no new translations may be added. + * @return An unmodifiable view of this localizer. + */ + @Override + @Contract("-> new") + default Localizer unmodifiableView() { + return new LocalizerImpl.UnmodifiableLocalizer(this); + } + +} diff --git a/src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java b/src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java new file mode 100644 index 00000000000..f35a9ee8eef --- /dev/null +++ b/src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java @@ -0,0 +1,74 @@ +package org.skriptlang.skript.localization; + +import ch.njol.skript.SkriptAPIException; +import ch.njol.skript.localization.Language; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; + +final class LocalizerImpl implements Localizer { + + private final SkriptAddon addon; + + LocalizerImpl(SkriptAddon addon) { + this.addon = addon; + } + + private String languageFileDirectory; + private String dataFileDirectory; + + @Override + public void setSourceDirectories(String languageFileDirectory, @Nullable String dataFileDirectory) { + if (this.languageFileDirectory != null) { + throw new SkriptAPIException("A localizer's source directories may only be set once."); + } + this.languageFileDirectory = languageFileDirectory; + this.dataFileDirectory = dataFileDirectory; + Language.loadDefault(addon); + } + + @Override + public @Nullable String languageFileDirectory() { + return languageFileDirectory; + } + + @Override + public @Nullable String dataFileDirectory() { + return dataFileDirectory; + } + + @Override + public @Nullable String translate(String key) { + return Language.get_(key); + } + + static final class UnmodifiableLocalizer implements Localizer { + + private final Localizer localizer; + + UnmodifiableLocalizer(Localizer localizer) { + this.localizer = localizer; + } + + @Override + public void setSourceDirectories(String languageFileDirectory, @Nullable String dataFileDirectory) { + throw new UnsupportedOperationException("Cannot set the source directories of an unmodifiable Localizer."); + } + + @Override + public @Nullable String languageFileDirectory() { + return localizer.languageFileDirectory(); + } + + @Override + public @Nullable String dataFileDirectory() { + return localizer.dataFileDirectory(); + } + + @Override + public @Nullable String translate(String key) { + return localizer.translate(key); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java new file mode 100644 index 00000000000..a937ff706e9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java @@ -0,0 +1,189 @@ +package org.skriptlang.skript.registration; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.registration.DefaultSyntaxInfosImpl.ExpressionImpl; +import org.skriptlang.skript.registration.DefaultSyntaxInfosImpl.StructureImpl; + +/** + * This class is not safe to be directly referenced. + * Use {@link SyntaxInfo} instead. + */ +@ApiStatus.Internal +@ApiStatus.Experimental +public interface DefaultSyntaxInfos { + + /** + * A syntax info to be used for {@link ch.njol.skript.lang.Expression}s. + * It differs from a typical info in that it also has a return type. + * @param The class providing the implementation of the Expression this info represents. + * @param The type of the return type of the Expression. + */ + @ApiStatus.Experimental + interface Expression, R> extends SyntaxInfo { + + /** + * Constructs a builder for an expression syntax info. + * @param expressionClass The Expression class the info will represent. + * @return An Expression-specific builder for creating a syntax info representing expressionClass. + * @param The class providing the implementation of the Expression this info represents. + */ + @Contract("_ -> new") + static , R> Builder, E, R> builder(Class expressionClass) { + return new ExpressionImpl.BuilderImpl<>(expressionClass); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E, R> builder(); + + /** + * @return The class representing the supertype of all values the Expression may return. + */ + Class returnType(); + + /** + * An Expression-specific builder is used for constructing a new Expression syntax info. + * @see #builder(Class) + * @param The type of builder being used. + * @param The Expression class providing the implementation of the syntax info being built. + * @param The type of the return type of the Expression. + */ + interface Builder, E extends ch.njol.skript.lang.Expression, R> extends SyntaxInfo.Builder { + + /** + * Sets the class representing the supertype of all values the Expression may return. + * @param returnType The class to use as the return type. + * @return This builder. + * @see Expression#returnType() + */ + @Contract("_ -> this") + B returnType(Class returnType); + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Expression build(); + + } + + } + + /** + * A syntax info to be used for {@link org.skriptlang.skript.lang.structure.Structure}s. + * It contains additional details including the {@link EntryValidator} to use, if any. + * @param The class providing the implementation of the Structure this info represents. + */ + @ApiStatus.Experimental + interface Structure extends SyntaxInfo { + + /** + * Represents type of {@link ch.njol.skript.config.Node}s that can represent a Structure. + */ + enum NodeType { + + /** + * For Structures that can be represented using a {@link ch.njol.skript.config.SimpleNode}. + */ + SIMPLE, + + /** + * For Structures that can be represented using a {@link ch.njol.skript.config.SectionNode}. + */ + SECTION, + + /** + * For Structures that can be represented using a + * {@link ch.njol.skript.config.SimpleNode} or {@link ch.njol.skript.config.SectionNode}. + */ + BOTH; + + /** + * @return Whether a Structure of this type can be represented using a {@link ch.njol.skript.config.SimpleNode}. + */ + public boolean canBeSimple() { + return this != SECTION; + } + + /** + * @return Whether a Structure of this type can be represented using a {@link ch.njol.skript.config.SectionNode}. + */ + public boolean canBeSection() { + return this != SIMPLE; + } + + } + + /** + * Constructs a builder for a structure syntax info. + * @param structureClass The Structure class the info will represent. + * @return A Structure-specific builder for creating a syntax info representing structureClass. + * @param The class providing the implementation of the Structure this info represents. + */ + @Contract("_ -> new") + static Builder, E> builder(Class structureClass) { + return new StructureImpl.BuilderImpl<>(structureClass); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E> builder(); + + /** + * @return The entry validator to use for handling the Structure's entries. + * If null, the Structure is expected to manually handle any entries. + */ + @Nullable EntryValidator entryValidator(); + + /** + * @return The type of {@link ch.njol.skript.config.Node}s that can represent the Structure. + */ + NodeType nodeType(); + + /** + * A Structure-specific builder is used for constructing a new Structure syntax info. + * @see #builder(Class) + * @param The type of builder being used. + * @param The Structure class providing the implementation of the syntax info being built. + */ + interface Builder, E extends org.skriptlang.skript.lang.structure.Structure> extends SyntaxInfo.Builder { + + /** + * Sets the entry validator the Structure will use for handling entries. + * @param entryValidator The entry validator to use. + * @return This builder. + * @see Structure#entryValidator() + */ + @Contract("_ -> this") + B entryValidator(EntryValidator entryValidator); + + /** + * Sets the type of {@link ch.njol.skript.config.Node}s that can represent the Structure. + * By default, this is typically {@link NodeType#SECTION}. + * @return This builder. + * @see Structure#type() + */ + B nodeType(NodeType type); + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Structure build(); + + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java new file mode 100644 index 00000000000..faf556f6539 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java @@ -0,0 +1,220 @@ +package org.skriptlang.skript.registration; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.registration.SyntaxInfo.Builder; +import org.skriptlang.skript.registration.SyntaxInfoImpl.BuilderImpl; +import org.skriptlang.skript.util.Priority; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +final class DefaultSyntaxInfosImpl { + + /** + * {@inheritDoc} + */ + static class ExpressionImpl, R> + extends SyntaxInfoImpl implements DefaultSyntaxInfos.Expression { + + private final Class returnType; + + ExpressionImpl( + SyntaxOrigin origin, Class type, @Nullable Supplier supplier, + Collection patterns, Priority priority, @Nullable Class returnType + ) { + super(origin, type, supplier, patterns, priority); + Preconditions.checkNotNull(returnType, "An expression syntax info must have a return type."); + this.returnType = returnType; + } + + @Override + public Expression.Builder, E, R> builder() { + var builder = new BuilderImpl<>(type()); + super.builder().applyTo(builder); + builder.returnType(returnType); + return builder; + } + + @Override + public Class returnType() { + return returnType; + } + + @Override + public boolean equals(Object other) { + return other instanceof Expression expression && + super.equals(other) && + returnType() == expression.returnType(); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), returnType()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .add("returnType", returnType()) + .toString(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + static final class BuilderImpl, E extends ch.njol.skript.lang.Expression, R> + extends SyntaxInfoImpl.BuilderImpl + implements Expression.Builder { + + private @Nullable Class returnType; + + BuilderImpl(Class expressionClass) { + super(expressionClass); + } + + @Override + public B returnType(Class returnType) { + this.returnType = returnType; + return (B) this; + } + + public Expression build() { + return new ExpressionImpl<>(origin, type, supplier, patterns, priority, returnType); + } + + @Override + public void applyTo(SyntaxInfo.Builder builder) { + super.applyTo(builder); + //noinspection rawtypes - Might be unsafe, hopefully the return types match + if (builder instanceof Expression.Builder expressionBuilder) { + if (returnType != null) { + expressionBuilder.returnType(returnType); + } + } + } + } + + } + + /** + * {@inheritDoc} + */ + static class StructureImpl + extends SyntaxInfoImpl implements DefaultSyntaxInfos.Structure { + + private final @Nullable EntryValidator entryValidator; + private final NodeType nodeType; + + StructureImpl( + SyntaxOrigin origin, Class type, @Nullable Supplier supplier, + Collection patterns, Priority priority, + @Nullable EntryValidator entryValidator, NodeType nodeType + ) { + super(origin, type, supplier, patterns, priority); + if (!nodeType.canBeSection() && entryValidator != null) + throw new IllegalArgumentException("Simple Structures cannot have an EntryValidator"); + this.entryValidator = entryValidator; + this.nodeType = nodeType; + } + + @Override + public Structure.Builder, E> builder() { + var builder = new BuilderImpl<>(type()); + super.builder().applyTo(builder); + if (entryValidator != null) { + builder.entryValidator(entryValidator); + } + builder.nodeType(nodeType); + return builder; + } + + @Override + public @Nullable EntryValidator entryValidator() { + return entryValidator; + } + + @Override + public NodeType nodeType() { + return nodeType; + } + + @Override + public boolean equals(Object other) { + return other instanceof Structure structure && + super.equals(other) && + Objects.equals(entryValidator(), structure.entryValidator()) && + Objects.equals(nodeType(), structure.nodeType()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entryValidator(), nodeType()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .add("entryValidator", entryValidator()) + .toString(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + static final class BuilderImpl, E extends org.skriptlang.skript.lang.structure.Structure> + extends SyntaxInfoImpl.BuilderImpl + implements Structure.Builder { + + private @Nullable EntryValidator entryValidator; + private NodeType nodeType = NodeType.SECTION; + + BuilderImpl(Class structureClass) { + super(structureClass); + } + + @Override + public B entryValidator(EntryValidator entryValidator) { + this.entryValidator = entryValidator; + return (B) this; + } + + @Override + public B nodeType(NodeType nodeType) { + this.nodeType = nodeType; + return (B) this; + } + + public Structure build() { + return new StructureImpl<>(origin, type, supplier, patterns, priority, entryValidator, nodeType); + } + + @Override + public void applyTo(SyntaxInfo.Builder builder) { + super.applyTo(builder); + //noinspection rawtypes - Should be safe, generics will not influence this + if (builder instanceof Structure.Builder structureBuilder) { + if (entryValidator != null) { + structureBuilder.entryValidator(entryValidator); + structureBuilder.nodeType(nodeType); + } + } + } + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java b/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java new file mode 100644 index 00000000000..039d60bc49a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java @@ -0,0 +1,165 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.SyntaxElement; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.registration.SyntaxInfoImpl.BuilderImpl; +import org.skriptlang.skript.util.Builder.Buildable; +import org.skriptlang.skript.util.Priority; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * A syntax info contains the details of a syntax, including its origin and patterns. + * @param The class providing the implementation of the syntax this info represents. + */ +@ApiStatus.Experimental +public interface SyntaxInfo extends Buildable, SyntaxInfo>, DefaultSyntaxInfos { + + /** + * A priority for infos with patterns that only match simple text (they do not have any {@link Expression}s). + * Example: "[the] console" + */ + Priority SIMPLE = Priority.base(); + + /** + * A priority for infos with patterns that contain at least one {@link Expression}. + * This is typically the default priority of an info. + * Example: "[the] first %number% characters of %strings%" + */ + Priority COMBINED = Priority.after(SIMPLE); + + /** + * A priority for infos with patterns that can match almost anything. + * This is likely the case when using regex or multiple expressions next to each other in a pattern. + * Example: "[the] [loop-]<.+>" + */ + Priority PATTERN_MATCHES_EVERYTHING = Priority.after(COMBINED); + + /** + * Constructs a builder for a syntax info. + * @param type The syntax class the info will represent. + * @return A builder for creating a syntax info representing type. + */ + @Contract("_ -> new") + static Builder, E> builder(Class type) { + return new BuilderImpl<>(type); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E> builder(); + + /** + * @return The origin of this syntax. + */ + SyntaxOrigin origin(); + + /** + * @return The class providing the implementation of this syntax. + */ + Class type(); + + /** + * @return A new instance of the class providing the implementation of this syntax. + */ + @Contract("-> new") + E instance(); + + /** + * @return The patterns of this syntax. + */ + @Unmodifiable Collection patterns(); + + /** + * @return The priority of this syntax, which dictates its position for matching during parsing. + */ + Priority priority(); + + /** + * A builder is used for constructing a new syntax info. + * @see #builder(Class) + * @param The type of builder being used. + * @param The class providing the implementation of the syntax info being built. + */ + interface Builder, E extends SyntaxElement> extends org.skriptlang.skript.util.Builder, SyntaxInfo> { + + /** + * Sets the origin the syntax info will use. + * @param origin The origin to use. + * @return This builder. + * @see SyntaxInfo#origin() + */ + @Contract("_ -> this") + B origin(SyntaxOrigin origin); + + /** + * Sets the supplier the syntax info will use to create new instances of the implementing class. + * @param supplier The supplier to use. + * @return This builder. + * @see SyntaxInfo#instance() + */ + @Contract("_ -> this") + B supplier(Supplier supplier); + + /** + * Adds a new pattern for the syntax info. + * @param pattern The pattern to add. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("_ -> this") + B addPattern(String pattern); + + /** + * Adds new patterns for the syntax info. + * @param patterns The patterns to add. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("_ -> this") + B addPatterns(String... patterns); + + /** + * Adds new patterns for the syntax info. + * @param patterns The patterns to add. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("_ -> this") + B addPatterns(Collection patterns); + + /** + * Sets the priority the syntax info will use, which dictates its position for matching during parsing. + * @param priority The priority to use. + * @return This builder. + */ + @Contract("_ -> this") + B priority(Priority priority); + + /** + * Builds a new syntax info from the set details. + * @return A syntax info representing the class providing the syntax's implementation. + */ + @Contract("-> new") + SyntaxInfo build(); + + /** + * Applies the values of this builder onto builder. + * When using this method, it is possible that some values are not safe to copy over. + * For example, when applying a SyntaxInfo for some type to a SyntaxInfo of another type, + * it is *not* safe to copy over {@link #supplier(Supplier)}, but that operation will occur anyway. + * In cases like this, you are expected to correct the values. + * @param builder The builder to apply values onto. + */ + @Override + void applyTo(Builder builder); + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java b/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java new file mode 100644 index 00000000000..fca551484cb --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java @@ -0,0 +1,192 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.SyntaxElement; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.util.ClassUtils; +import org.skriptlang.skript.util.Priority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +class SyntaxInfoImpl implements SyntaxInfo { + + private final SyntaxOrigin origin; + private final Class type; + private final @Nullable Supplier supplier; + private final Collection patterns; + private final Priority priority; + + protected SyntaxInfoImpl( + SyntaxOrigin origin, Class type, @Nullable Supplier supplier, + Collection patterns, Priority priority + ) { + Preconditions.checkArgument(supplier != null || ClassUtils.isNormalClass(type), + "Failed to register a syntax info for '" + type.getName() + "'." + + " Element classes must be a normal type unless a supplier is provided."); + Preconditions.checkArgument(!patterns.isEmpty(), + "Failed to register a syntax info for '" + type.getName() + "'." + + " There must be at least one pattern."); + this.origin = origin; + this.type = type; + this.supplier = supplier; + this.patterns = ImmutableList.copyOf(patterns); + this.priority = priority; + } + + @Override + public Builder, T> builder() { + var builder = new BuilderImpl<>(type); + builder.origin(origin); + if (supplier != null) { + builder.supplier(supplier); + } + builder.addPatterns(patterns); + builder.priority(priority); + return builder; + } + + @Override + public SyntaxOrigin origin() { + return origin; + } + + @Override + public Class type() { + return type; + } + + @Override + public T instance() { + try { + return supplier == null ? type.getDeclaredConstructor().newInstance() : supplier.get(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Override + public @Unmodifiable Collection patterns() { + return patterns; + } + + @Override + public Priority priority() { + return priority; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return other instanceof SyntaxInfo info && + Objects.equals(origin(), info.origin()) && + Objects.equals(type(), info.type()) && + Objects.equals(patterns(), info.patterns()) && + Objects.equals(priority(), info.priority()); + } + + @Override + public int hashCode() { + return Objects.hash(origin(), type(), patterns(), priority()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .toString(); + } + + @SuppressWarnings("unchecked") + static class BuilderImpl, E extends SyntaxElement> implements Builder { + + /** + * A default origin that describes the class of a syntax. + */ + private static final class ClassOrigin implements SyntaxOrigin { + + private final String name; + + ClassOrigin(Class clazz) { + this.name = clazz.getName(); + } + + @Override + public String name() { + return name; + } + + } + + final Class type; + SyntaxOrigin origin; + @Nullable Supplier supplier; + final List patterns = new ArrayList<>(); + Priority priority = SyntaxInfo.COMBINED; + + BuilderImpl(Class type) { + this.type = type; + origin = new ClassOrigin(type); + } + + public B origin(SyntaxOrigin origin) { + this.origin = origin; + return (B) this; + } + + public B supplier(Supplier supplier) { + this.supplier = supplier; + return (B) this; + } + + public B addPattern(String pattern) { + this.patterns.add(pattern); + return (B) this; + } + + public B addPatterns(String... patterns) { + Collections.addAll(this.patterns, patterns); + return (B) this; + } + + public B addPatterns(Collection patterns) { + this.patterns.addAll(patterns); + return (B) this; + } + + @Override + public B priority(Priority priority) { + this.priority = priority; + return (B) this; + } + + public SyntaxInfo build() { + return new SyntaxInfoImpl<>(origin, type, supplier, patterns, priority); + } + + @Override + public void applyTo(Builder builder) { + builder.origin(origin); + if (supplier != null) { + //noinspection rawtypes - Let's hope the user knows what they are doing... + builder.supplier((Supplier) supplier); + } + builder.addPatterns(patterns); + builder.priority(priority); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java b/src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java new file mode 100644 index 00000000000..b5e8aa9d552 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java @@ -0,0 +1,60 @@ +package org.skriptlang.skript.registration; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.skriptlang.skript.addon.SkriptAddon; + +/** + * The origin of a syntax, currently only used for documentation purposes. + */ +@FunctionalInterface +@ApiStatus.Experimental +public interface SyntaxOrigin { + + /** + * Constructs a syntax origin from an addon. + * @param addon The addon to construct this origin from. + * @return An origin pointing to the provided addon. + */ + @Contract("_ -> new") + static SyntaxOrigin of(SkriptAddon addon) { + return new AddonOrigin(addon); + } + + /** + * A basic origin describing the addon a syntax has originated from. + * @see SyntaxOrigin#of(SkriptAddon) + */ + final class AddonOrigin implements SyntaxOrigin { + + private final SkriptAddon addon; + + private AddonOrigin(SkriptAddon addon) { + this.addon = addon.unmodifiableView(); + } + + /** + * @return A string representing the name of the addon this origin describes. + * Equivalent to {@link SkriptAddon#name()}. + */ + @Override + public String name() { + return addon.name(); + } + + /** + * @return An unmodifiable view of the addon this origin describes. + * @see SkriptAddon#unmodifiableView() + */ + public SkriptAddon addon() { + return addon; + } + + } + + /** + * @return A string representing this origin. + */ + String name(); + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java new file mode 100644 index 00000000000..e8ab2bb3b0b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java @@ -0,0 +1,41 @@ +package org.skriptlang.skript.registration; + +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * A syntax register is a collection of registered {@link SyntaxInfo}s of a common type. + * @param The type of syntax in this register. + */ +final class SyntaxRegister> { + + private static final Comparator> SET_COMPARATOR = (a,b) -> { + if (a == b) { // only considered equal if registering the same infos + return 0; + } + int result = a.priority().compareTo(b.priority()); + // when elements have the same priority, the oldest element comes first + return result != 0 ? result : 1; + }; + + final Set syntaxes = new ConcurrentSkipListSet<>(SET_COMPARATOR); + + public Collection syntaxes() { + synchronized (syntaxes) { + return ImmutableSet.copyOf(syntaxes); + } + } + + public void add(I info) { + syntaxes.add(info); + } + + public void remove(I info) { + syntaxes.remove(info); + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java new file mode 100644 index 00000000000..acde00a843f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java @@ -0,0 +1,158 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.Statement; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.util.Registry; +import org.skriptlang.skript.util.ViewProvider; + +import java.util.Collection; + +/** + * A syntax registry manages all {@link SyntaxRegister}s for syntax registration. + */ +@ApiStatus.Experimental +public interface SyntaxRegistry extends ViewProvider, Registry> { + + /** + * A key representing the built-in {@link Structure} syntax element. + */ + Key> STRUCTURE = Key.of("structure"); + + /** + * A key representing the built-in {@link Section} syntax element. + */ + Key> SECTION = Key.of("section"); + + /** + * A key representing all {@link Statement} syntax elements. + * By default, this includes {@link #EFFECT} and {@link #CONDITION}. + */ + Key> STATEMENT = Key.of("statement"); + + /** + * A key representing the built-in {@link Effect} syntax element. + */ + Key> EFFECT = ChildKey.of(STATEMENT, "effect"); + + /** + * A key representing the built-in {@link Condition} syntax element. + */ + Key> CONDITION = ChildKey.of(STATEMENT, "condition"); + + /** + * A key representing the built-in {@link Expression} syntax element. + */ + Key> EXPRESSION = Key.of("expression"); + + /** + * Constructs a default implementation of a syntax registry. + * This implementation is practically a wrapper around {@code Map, SyntaxRegistry>}. + * @return A syntax registry containing no elements. + */ + @Contract("-> new") + static SyntaxRegistry empty() { + return new SyntaxRegistryImpl(); + } + + /** + * A method to obtain all syntaxes registered under a certain key. + * @param key The key to obtain syntaxes from. + * @return An unmodifiable snapshot of all syntaxes registered under key. + * @param The syntax type. + */ + > @Unmodifiable Collection syntaxes(Key key); + + /** + * Registers a new syntax under a provided key. + * @param key The key to register info under. + * @param info The syntax info to register. + * @param The syntax type. + */ + > void register(Key key, I info); + + /** + * Unregisters a syntax registered under a provided key. + * @param key The key the info is registered under. + * @param info The syntax info to unregister. + * @param The syntax type. + */ + > void unregister(Key key, I info); + + /** + * Constructs an unmodifiable view of this syntax registry. + * That is, the returned registry will not allow registrations. + * @return An unmodifiable view of this syntax registry. + */ + @Override + @Contract("-> new") + default SyntaxRegistry unmodifiableView() { + return new SyntaxRegistryImpl.UnmodifiableRegistry(this); + } + + /** + * {@inheritDoc} + * There are no guarantees on the ordering of the returned collection. + * @return An unmodifiable snapshot of all syntaxes registered. + */ + @Override + @Unmodifiable Collection> elements(); + + /** + * Represents a syntax element type. + * @param The syntax type. + */ + @ApiStatus.Experimental + interface Key> { + + /** + * @param name The name of this key. + * @return A default key implementation. + * @param The syntax type. + */ + @Contract("_ -> new") + static > Key of(String name) { + return new SyntaxRegistryImpl.KeyImpl<>(name); + } + + /** + * @return The name of the syntax element this key represents. + */ + String name(); + + } + + /** + * Like a {@link Key}, but it has a parent which causes elements to be registered to itself and its parent. + * @param The child key's syntax type. + * @param

The parent key's syntax type. + */ + @ApiStatus.Experimental + interface ChildKey> extends Key { + + /** + * @param parent The parent of this key. + * @param name The name of this key. + * @return A default child key implementation. + * @param The child key's syntax type. + * @param

The parent key's syntax type. + */ + @Contract("_, _ -> new") + static > Key of(Key

parent, String name) { + return new SyntaxRegistryImpl.ChildKeyImpl<>(parent, name); + } + + /** + * @return The parent key of this child key. + */ + Key

parent(); + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java new file mode 100644 index 00000000000..843a1061697 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java @@ -0,0 +1,156 @@ +package org.skriptlang.skript.registration; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +final class SyntaxRegistryImpl implements SyntaxRegistry { + + private final Map, SyntaxRegister> registers = new ConcurrentHashMap<>(); + + @Override + @Unmodifiable + public > Collection syntaxes(Key key) { + return register(key).syntaxes(); + } + + @Override + public > void register(Key key, I info) { + register(key).add(info); + if (key instanceof ChildKey) { + register(((ChildKey) key).parent(), info); + } + } + + @Override + public > void unregister(Key key, I info) { + register(key).remove(info); + if (key instanceof ChildKey) { + unregister(((ChildKey) key).parent(), info); + } + } + + @SuppressWarnings("unchecked") + private > SyntaxRegister register(Key key) { + return (SyntaxRegister) registers.computeIfAbsent(key, k -> new SyntaxRegister<>()); + } + + @Override + public Collection> elements() { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + registers.values().forEach(register -> { + synchronized (register.syntaxes) { + builder.addAll(register.syntaxes); + } + }); + return builder.build(); + } + + static final class UnmodifiableRegistry implements SyntaxRegistry { + + private final SyntaxRegistry registry; + + UnmodifiableRegistry(SyntaxRegistry registry) { + this.registry = registry; + } + + @Override + public @Unmodifiable Collection> elements() { + return registry.elements(); + } + + @Override + public @Unmodifiable > Collection syntaxes(Key key) { + return registry.syntaxes(key); + } + + @Override + public > void register(Key key, I info) { + throw new UnsupportedOperationException("Cannot register syntax infos with an unmodifiable syntax registry."); + } + + @Override + public > void unregister(Key key, I info) { + throw new UnsupportedOperationException("Cannot unregister syntax infos from an unmodifiable syntax registry."); + } + + } + + static class KeyImpl> implements Key { + + protected final String name; + + KeyImpl(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return other instanceof Key key && + name().equals(key.name()); + } + + @Override + public int hashCode() { + return name().hashCode(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .toString(); + } + + } + + static final class ChildKeyImpl> extends KeyImpl implements ChildKey { + + private final Key

parent; + + ChildKeyImpl(Key

parent, String name) { + super(name); + this.parent = parent; + } + + @Override + public Key

parent() { + return parent; + } + + @Override + public boolean equals(Object other) { + return other instanceof ChildKey key && + super.equals(other) && + parent().equals(key.parent()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), parent()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .add("parent", parent()) + .toString(); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/Builder.java b/src/main/java/org/skriptlang/skript/util/Builder.java new file mode 100644 index 00000000000..e3280fbdf8e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Builder.java @@ -0,0 +1,42 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; + +/** + * An interface providing common methods to be implemented for any builder. + * + * @param The type of builder being used. + * @param The type of object being built. + */ +@ApiStatus.Experimental +public interface Builder, T> { + + /** + * Represents an object that can be converted back into a builder. + * @param The type of builder being used. + * @param The type of object being built. + */ + interface Buildable, T> { + + /** + * @return A builder representing this object. + */ + @Contract("-> new") + B builder(); + + } + + /** + * @return An object of T built from the values specified by this builder. + */ + @Contract("-> new") + T build(); + + /** + * Applies the values of this builder onto builder. + * @param builder The builder to apply values onto. + */ + void applyTo(B builder); + +} diff --git a/src/main/java/org/skriptlang/skript/util/ClassLoader.java b/src/main/java/org/skriptlang/skript/util/ClassLoader.java new file mode 100644 index 00000000000..030f6538e37 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/ClassLoader.java @@ -0,0 +1,279 @@ +package org.skriptlang.skript.util; + +import ch.njol.skript.Skript; +import ch.njol.util.StringUtils; +import com.google.common.reflect.ClassPath; +import com.google.common.reflect.ClassPath.ResourceInfo; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +/** + * A utility class for loading classes contained in specific packages. + */ +public class ClassLoader { + + /** + * @return A builder for creating a loader. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A utility method for loading and initializing all classes within the base package/subpackages. + * This method will perform a deep search, meaning classes within subpackages of subpackages will be initialized too. + * @param source A class within the resource classes should be loaded from. + * @param jarFile A file representing the jar to search for classes. + * @param basePackage The package within the jar to load classes from. + * @param subPackages The specific subpackages within basePackage to load. + * If not provided, classes within basePackage and all of its subpackages will be loaded. + */ + public static void loadClasses(Class source, File jarFile, String basePackage, String... subPackages) { + builder() + .basePackage(basePackage) + .addSubPackages(subPackages) + .initialize(true) + .deep(true) + .build() + .loadClasses(source, jarFile); + } + + private final String basePackage; + private final Collection subPackages; + private final boolean initialize; + private final boolean deep; + private final @Nullable Consumer> forEachClass; + + private ClassLoader(String basePackage, Collection subPackages, boolean initialize, + boolean deep, @Nullable Consumer> forEachClass) { + if (basePackage.isEmpty()) { + throw new IllegalArgumentException("The base package must be set"); + } + this.basePackage = basePackage.replace('.', '/') + "/"; + this.subPackages = subPackages.stream() + .map(subPackage -> subPackage.replace('.', '/') + "/") + .collect(Collectors.toSet()); + this.initialize = initialize; + this.deep = deep; + this.forEachClass = forEachClass; + } + + /** + * Loads all classes (from the provided source) meeting the criteria set by this loader. + * It is recommended to use one of the methods that also accept a [jar] file + * ({@link #loadClasses(Class, File)} and {@link #loadClasses(Class, JarFile)}) for increased reliability. + * @param source A class within the resource classes should be loaded from. + */ + public void loadClasses(Class source) { + loadClasses(source, (JarFile) null); + } + + /** + * Loads all classes (from the provided source) meeting the criteria set by this loader. + * @param source A class within the resource classes should be loaded from. + * @param jarFile A file representing the jar to search for classes. While it is possible to load the classes without a jar, + * it is recommended to provide one for reliability. + * @see #loadClasses(Class, JarFile) + */ + public void loadClasses(Class source, File jarFile) { + try (JarFile jar = new JarFile(jarFile)) { + loadClasses(source, jar); + } catch (IOException e) { + // TODO better logging + Skript.warning("Failed to access jar file: " + e); + loadClasses(source); // try to load using just the source class + } + } + + /** + * Loads all classes (from the provided source) meeting the criteria set by this loader. + * @param source A class within the resource classes should be loaded from. + * @param jar A jar to search for classes. While it is possible to load the classes without this jar, + * it is recommended to provide one for reliability. + * @see #loadClasses(Class, File) + */ + public void loadClasses(Class source, @Nullable JarFile jar) { + final Collection classPaths; + try { + if (jar != null) { // load from jar if available + classPaths = jar.stream() + .map(JarEntry::getName) + .collect(Collectors.toSet()); + } else { + classPaths = ClassPath.from(source.getClassLoader()).getResources().stream() + .map(ResourceInfo::getResourceName) + .collect(Collectors.toSet()); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load classes: " + e); + } + + // 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 + final int expectedDepth = !this.deep ? StringUtils.count(this.basePackage, '/') : 0; + final int offset = this.basePackage.length(); + + // classes will be loaded in alphabetical order + Collection classNames = new TreeSet<>(String::compareToIgnoreCase); + for (String name : classPaths) { + if (!name.startsWith(this.basePackage) || !name.endsWith(".class") || name.endsWith("package-info.class")) + continue; + boolean load; + if (this.subPackages.isEmpty()) { + // loaded only if within base package when deep searches are forbidden + load = this.deep || StringUtils.count(name, '/') == expectedDepth; + } else { + load = false; + for (String subPackage : this.subPackages) { + // if the entry is within the subpackage, ensure it is not any deeper if not permitted + if (name.startsWith(subPackage, offset) + && (this.deep || StringUtils.count(name, '/') == expectedDepth + StringUtils.count(subPackage, '/'))) { + load = true; + break; + } + } + } + + if (load) { + // replace separators and .class extension + classNames.add(name.replace('/', '.').substring(0, name.length() - 6)); + } + } + + java.lang.ClassLoader loader = source.getClassLoader(); + for (String className : classNames) { + try { + Class clazz = Class.forName(className, this.initialize, loader); + if (this.forEachClass != null) + this.forEachClass.accept(clazz); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("Failed to load class: " + className, ex); + } catch (ExceptionInInitializerError err) { + throw new RuntimeException(className + " generated an exception while loading", err.getCause()); + } + } + } + + /** + * A builder for constructing a {@link ClassLoader}. + */ + public static final class Builder { + + private String basePackage = ""; + private final Collection subPackages = new HashSet<>(); + private boolean initialize; + private boolean deep; + private @Nullable Consumer> forEachClass; + + private Builder() { } + + /** + * Sets the package the loader should start loading classes from. + * This is required. + * @param basePackage A string representing package to start loading classes from. + * @return This builder. + */ + @Contract("_ -> this") + public Builder basePackage(String basePackage) { + this.basePackage = basePackage; + return this; + } + + /** + * Adds a subpackage the loader should start loading classes from. + * This is useful for when you may want to load from some, but not all, of the subpackages of the base package. + * @param subPackage A string representing a subpackage to load from. + * @return This builder. + * @see #addSubPackages(String...) + * @see #addSubPackages(Collection) + */ + @Contract("_ -> this") + public Builder addSubPackage(String subPackage) { + this.subPackages.add(subPackage); + return this; + } + + /** + * Adds subpackages the loader should start loading classes from. + * This is useful for when you may want to load from some, but not all, of the subpackages of the base package. + * @param subPackages Strings representing subpackages to load from. + * @return This builder. + * @see #addSubPackage(String) + * @see #addSubPackages(Collection) + */ + @Contract("_ -> this") + public Builder addSubPackages(String... subPackages) { + Collections.addAll(this.subPackages, subPackages); + return this; + } + + /** + * Adds subpackages the loader should start loading classes from. + * This is useful for when you may want to load from some, but not all, of the subpackages of the base package. + * @param subPackages Strings representing subpackages to load from. + * @return This builder. + * @see #addSubPackage(String) + * @see #addSubPackages(String...) + */ + @Contract("_ -> this") + public Builder addSubPackages(Collection subPackages) { + this.subPackages.addAll(subPackages); + return this; + } + + /** + * Sets whether the loader will initialize found classes. + * @param initialize Whether classes should be initialized when found. + * @return This builder. + */ + @Contract("_ -> this") + public Builder initialize(boolean initialize) { + this.initialize = initialize; + return this; + } + + /** + * Sets whether the loader will perform a deep search. + * @param deep Whether subpackages of the provided base package (or subpackages) should be searched. + * @return This builder. + */ + @Contract("_ -> this") + public Builder deep(boolean deep) { + this.deep = deep; + return this; + } + + /** + * Sets a consumer to be run for each found class. + * @param forEachClass A consumer to run for each found class. + * @return This builder. + */ + @Contract("_ -> this") + public Builder forEachClass(Consumer> forEachClass) { + this.forEachClass = forEachClass; + return this; + } + + /** + * Builds a new loader from the set details. + * @return A loader for loading classes through the manner outlined by this builder. + */ + @Contract("-> new") + public ClassLoader build() { + return new ClassLoader(basePackage, subPackages, initialize, deep, forEachClass); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/ClassUtils.java b/src/main/java/org/skriptlang/skript/util/ClassUtils.java new file mode 100644 index 00000000000..9315f9b14b3 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/ClassUtils.java @@ -0,0 +1,19 @@ +package org.skriptlang.skript.util; + +import java.lang.reflect.Modifier; + +/** + * Utilities for interacting with classes. + */ +public final class ClassUtils { + + /** + * @param clazz The class to check. + * @return True if clazz does not represent an annotation, array, primitive, interface, or abstract class. + */ + public static boolean isNormalClass(Class clazz) { + return !clazz.isAnnotation() && !clazz.isArray() && !clazz.isPrimitive() + && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()); + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/Priority.java b/src/main/java/org/skriptlang/skript/util/Priority.java new file mode 100644 index 00000000000..a5f71a5c932 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Priority.java @@ -0,0 +1,55 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collection; + +/** + * Priorities are used for things like ordering syntax and loading structures in a specific order. + */ +@ApiStatus.Experimental +public interface Priority extends Comparable { + + /** + * @return A base priority for other priorities to build relationships off of. + */ + @Contract("-> new") + static Priority base() { + return new PriorityImpl(); + } + + /** + * Constructs a new priority that is before priority. + * Note that this method will not make any changes to the {@link #after()} of priority. + * @param priority The priority that will be after the returned priority. + * @return A priority that is before priority. + */ + @Contract("_ -> new") + static Priority before(Priority priority) { + return new PriorityImpl(priority, true); + } + + /** + * Constructs a new priority that is after priority. + * Note that this method will not make any changes to the {@link #before()} of priority. + * @param priority The priority that will be before the returned priority. + * @return A priority that is after priority. + */ + @Contract("_ -> new") + static Priority after(Priority priority) { + return new PriorityImpl(priority, false); + } + + /** + * @return A collection of all priorities this priority is known to be after. + */ + @Unmodifiable Collection after(); + + /** + * @return A collection of all priorities this priority is known to be before. + */ + @Unmodifiable Collection before(); + +} diff --git a/src/main/java/org/skriptlang/skript/util/PriorityImpl.java b/src/main/java/org/skriptlang/skript/util/PriorityImpl.java new file mode 100644 index 00000000000..b342cf1e982 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/PriorityImpl.java @@ -0,0 +1,82 @@ +package org.skriptlang.skript.util; + +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +class PriorityImpl implements Priority { + + private final Set after; + + private final Set before; + + PriorityImpl() { + this.after = ImmutableSet.of(); + this.before = ImmutableSet.of(); + } + + PriorityImpl(Priority priority, boolean isBefore) { + Set after = new HashSet<>(); + Set before = new HashSet<>(); + if (isBefore) { + before.add(priority); + } else { + after.add(priority); + } + after.addAll(priority.after()); + before.addAll(priority.before()); + + this.after = ImmutableSet.copyOf(after); + this.before = ImmutableSet.copyOf(before); + } + + @Override + public int compareTo(Priority other) { + if (this == other) { + return 0; + } + + Collection ourBefore = this.before(); + Collection otherAfter = other.after(); + + // check whether this is known to be before other and whether other is known to be after this + if (ourBefore.contains(other) || otherAfter.contains(this)) { + return -1; + } + + Collection ourAfter = this.after(); + Collection otherBefore = other.before(); + + // check whether this is known to be after other and whether other is known to be before this + if (ourAfter.contains(other) || otherBefore.contains(this)) { + return 1; + } + + // check whether the set of items we are before has common elements with the set of items other is after + if (ourBefore.stream().anyMatch(otherAfter::contains)) { + return -1; + } + + // check whether the set of items we are after has common elements with the set of items other is before + if (ourAfter.stream().anyMatch(otherBefore::contains)) { + return 1; + } + + // there is no meaningful relationship, we consider ourselves the same + // however, in cases of a custom implementation, we defer to them to determine the relationship + return (other instanceof PriorityImpl) ? 0 : (other.compareTo(this) * -1); + } + + @Override + public Collection after() { + return after; + } + + @Override + public Collection before() { + return before; + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/Registry.java b/src/main/java/org/skriptlang/skript/util/Registry.java new file mode 100644 index 00000000000..46e4f859785 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Registry.java @@ -0,0 +1,31 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A registry maintains a collection of elements. + * It is up to individual implementations as to how they may be modified. + * @param The type of elements stored in a registry. + */ +@ApiStatus.Experimental +public interface Registry extends Iterable { + + /** + * @return A collection of all elements in this registry. + */ + Collection elements(); + + /** + * By default, this is a wrapper for elements().iterator(). + * @return An iterator over all elements in this registry. + * @see Collection#iterator() + */ + @Override + default Iterator iterator() { + return elements().iterator(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/ViewProvider.java b/src/main/java/org/skriptlang/skript/util/ViewProvider.java new file mode 100644 index 00000000000..7a9748ad1bb --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/ViewProvider.java @@ -0,0 +1,22 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; + +/** + * For objects that can provide an unmodifiable view of themselves. + * An unmodifiable view means that the object may only be used in a read-only manner (its values may not be changed). + * Since it is a view, it will reflect any changes made to the object it was created from. + * @param The type being viewed. + */ +@ApiStatus.Experimental +public interface ViewProvider { + + /** + * Constructs an unmodifiable view of this. + * @return An unmodifiable view of this. + */ + @Contract("-> new") + T unmodifiableView(); + +}