Class PluginManager
-
Field Summary
FieldsModifier and TypeFieldDescriptionprivate ClassLoaderConnected External Plugin API consumers, plugin name to scan package (#462): added whenregisterExternal(ExternalPluginAdapter, Class[])completes, removed byunregisterExternal(ExternalPluginAdapter).private intModule JARs the last discovery tried and could not turn into a module class (18-41): refused while its declared main class was read -- nomain:, not anUltiToolsPlugin, a main class from another JAR, a linkage failure, a failed JAR validation.private intCopies skipped by discovery because their module's classes load from another JAR.Entity class -> owning plugin name, populated every time aDataScopeis minted (both internal modules and external plugins).External plugin data folder (canonical path) -> theDataScopeminted for it, populated byregisterExternal(ExternalPluginAdapter, Class[]).private static final StringThe framework's own package, in both the dotted and the internal (slash) spelling.private static final StringThe framework-owned types whoseScheduledmethods this manager registers.private static final PatternA bare JVM internal (slash-separated) binary class name: one or more identifier segments joined by/, each starting with a letter,_or$.private static final StringThe name of the JVM system property that opts back into the pre-6.3.0 degraded load order (D-10): every module in discovery order (file-name order, #476), with no dependency resolution at all.(package private) static final StringFramework i18n key for the one line logged when a module cannot load because a plugin itsplugin.ymllists underdepend:is not installed or not enabled (#554).private final ThreadLocal<Map.Entry<String, Class<? extends UltiToolsPlugin>>> The module being registered on each thread, name and main class (#483); seegetModuleBeingRegistered().private final ModuleJarIndexWhat the start-up scan read: per declared main class, every JAR in the modules folder that declares it, and the JAR its class was loaded from (#516).private PlayerCacheManagerprivate final List<Class<? extends UltiToolsPlugin>> private final List<UltiToolsPlugin> The loaded modules, in load order.(package private) static final StringFramework i18n key: one partially reloaded module and the parts that did not reload (#529).(package private) static final StringFramework i18n key: the/ul reloadsummary when every module reloaded (#509).(package private) static final StringFramework i18n key: the/ul reloadsummary naming the modules that failed (#509).(package private) static final StringFramework i18n key: the line afterRELOAD_SUMMARY_FAILED_KEYwhen, besides the failed modules, some reloaded only partly; the partial modules' own lines follow it (#529; Codex review of #564, round 2).(package private) static final StringFramework i18n key: the/ul reloadsummary when no module failed but some reloaded only partly (#529).private final Map<UltiToolsPlugin, List<Runnable>> Incoming copy -> the actions that give the copies it supersedes back their registrations (#562).private TaskManager -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate static StringadditionalEntityRefusalMessage(String declaringPluginName, Class<?> entityClass, String owner) Builds the refusal message for anadditionalEntitiesdeclaration that does not live on the declarer's own classpath -- the same "confirmed owner" vs.voidapplyReloadedConfigBindings(UltiToolsPlugin, ReloadReport)with a report nobody reads: the values not applied are only logged.voidapplyReloadedConfigBindings(UltiToolsPlugin plugin, ReloadReport report) Applies a module's reloaded configuration to its config-bound@Scheduledtasks and@CmdCDcooldowns (#531).private voidassemblePluginContainer(SimpleContainer pluginContext, UltiToolsPlugin plugin, Class<? extends UltiToolsPlugin> pluginClass, ClassLoader loader) Assemble a plugin's container.private booleanprivate static List<BaseCommandExecutor> baseCommandExecutors(SimpleContainer context) private static voidbeginLoadScopes(UltiToolsPlugin plugin) Opens, on this thread, the registration scopes of the three registries a module files registrations in by name -- tab-completion completers, EventBus handlers, panel responders -- so that what the module registers while the framework loads it (its container refresh and itsregisterSelf()) is recorded against this instance (#506; Codex review of #564, round 3).private Map.Entry<String, Class<? extends UltiToolsPlugin>> beginRegistration(String name, Class<? extends UltiToolsPlugin> pluginClass) Records the module being registered on this thread (#483) and returns the record it replaces (a registration started from inside another module's registration), forendRegistration(java.util.Map.Entry<java.lang.String, java.lang.Class<? extends com.ultikits.ultitools.abstracts.UltiToolsPlugin>>)to restore.private booleanbelongsToAnotherKnownPlugin(File entityJarFile, Class<?> declaringClass) WhetherentityJarFileis itself the own jar of some plugin OTHER thandeclaringClass-- checked against every internal moduleinit()has already discovered (pluginClassList, regardless of load order) and every Bukkit plugin currently loaded (Bukkit.getPluginManager().getPlugins(), regardless of whether it has calledUltiToolsAPI.connect(...)yet).private static StringcanonicalPath(File folder) private static StringclassNameForEntityScan(JarEntry entry) Converts oneJarEntryfromscanEntitiesInJar(java.io.File)into a dotted class name, ornullwhen the entry is not a class file the entity scan cares about (aMETA-INFentry, or anything not ending in.class).voidclose()Unregister all plugins.cooldownDeclarations(BaseCommandExecutor executor) Every@CmdCDthe executor's validator can meet, each paired with a display owner for messages: the class-level annotation, and for each@CmdMappingmethod the annotationCooldownValidatoritself resolves (most-derived-wins), in the same way.private List<UltiToolsPlugin> copiesSupersededBy(UltiToolsPlugin plugin) The loaded copiespluginsupersedes: same main class, older version.private static StringdescribeBindingKey(String bindingKey) Renderscom.example.EssentialsConfig#features.wild.cooldownasEssentialsConfig key 'features.wild.cooldown'.private voidincomingactivated: the registrations released from the copies it supersedes stay released, and those copies are unloaded next (#562).(package private) booleandiscoverModuleClasses(File pluginFolder) Lists the modules folder once and reads each module JAR's declared main class, adding every class that loads topluginClassList.private static voidCloses the scopesbeginLoadScopes(UltiToolsPlugin)opened on this thread.private voidendRegistration(Map.Entry<String, Class<? extends UltiToolsPlugin>> previous) Restores the recordbeginRegistration(java.lang.String, java.lang.Class<? extends com.ultikits.ultitools.abstracts.UltiToolsPlugin>)replaced, or clears it; runs in afinally.private static StringextractMissingClassName(Throwable failure) Extraction of the internal (dot-qualified) name of the classfailurereports missing -- only for aNoClassDefFoundErrorwhose message is a well-formed bare internal class name; every other case (a differentLinkageErrorsubtype, a blank message, or a message shaped like the "wrong name" mismatch detail) returnsnullrather than guess, perINTERNAL_CLASS_NAME_PATTERN's own javadoc.findOwningPlugin(Class<?> entityClass) Looks up which loaded plugin ownsentityClass, perentityOwnership.findRegistrationOwner(String moduleName) The module copy a registration filed undermoduleNameonly belongs to when it is made outside every module's load scope -- for example from a module's scheduled task after it loaded (#562 item 1): the copy listed as loaded under that name; when more than one copy is listed under it, the first listed, the one loaded earliest.findScopeForDataFolder(File dataFolder) Resolves a data folder back to the registered external plugin scope that owns it (D-18).private UltiToolsPluginfinishInitializingPlugin(ClassLoader classLoader, Class<? extends UltiToolsPlugin> pluginClass, UltiToolsPlugin plugin) Run the construction-independent second half afterinitializePlugincompletes construction: the compatibility gate, then container assembly.private static StringfirstBinding(Collection<Object> beans, List<BaseCommandExecutor> executors) private static StringformatParameterTypes(Class<?>[] paramTypes) FormatsparamTypesas a Java-source-like parameter list, e.g.private static StringframeworkApiMismatchRefusal(File pluginJar, PluginYmlReader.PluginYmlInfo ymlInfo, String mainClassName, Throwable failure) The one SEVERE line for a module refused byframeworkLinkageFailure(java.lang.Throwable): which JAR, what it declares itself to be (only the keys its plugin.yml declares), why it cannot load, what to do, and the JVM's own message for whoever investigates further.(package private) static ThrowableframeworkLinkageFailure(Throwable failure) Decides whether a failure to load a module's declared main class means the module was built against a different UltiTools-API than the one installed -- usually an older one, the case an operator meets after upgrading the framework but not the module (6.3.0 madeUltiToolsPlugin.unregisterSelf()andreloadSelf()final, and every released module overrides at least the first).The live instances corresponding toFRAMEWORK_SCHEDULED_OWNER_TYPES, in the same order.The connected External Plugin API consumers, plugin name to scan package (#462) -- a copy, safe to read from any thread.Map.Entry<String, Class<? extends UltiToolsPlugin>> The module being registered on the calling thread, as name and main class, ornull(#483).Returns the modules currently loaded, in load order, as an unmodifiable snapshot.String[]getPluginScanPackages(Class<? extends UltiToolsPlugin> pluginClass) Get scan packages for a plugin class.private booleanhasNewerVersionLoaded(UltiToolsPlugin plugin) Determines whether a newer version of the same module is already loaded.voidinit(ClassLoader classLoader) Initialize plugin manager.private UltiToolsPlugininitializePlugin(ClassLoader classLoader, Class<? extends UltiToolsPlugin> pluginClass) Initialize module using its default (zero-argument) constructor.private static IntegerThe installed framework's API level, ornullwhen it cannot be read (no env.yml, malformed version).private static booleanisAbsentSoftDependClasspathGap(Throwable failure, List<String> softDepend) UltiEconomy#20: decides whetherfailure, caught while loading a class during a module's entity scan, is fully explained by an optional integration the module itself declared -- a class whose declared type hierarchy references a plugin API type, where that plugin (a) is named in the module's ownplugin.ymlsoftdepend:list and (b) is not currently installed, or installed but not enabled, on this server.private static booleanisClassAvailableFromAnyEnabledPlugin(String missingClassName) WhethermissingClassNameis physically present in any currently installed AND enabled plugin's own jar -- i.e.private static booleanisClassNotFoundKind(Throwable thrown) private static booleanisDuplicateCopy(File pluginJar, File supplier, String mainClassName) Whether a JAR whose declared main class was loaded from another JAR is another copy of that module: the other JAR is in the same modules folder and its ownplugin.ymldeclares the samemain:.private booleanWhether a loaded module other thanpluginitself carriesplugin's name.private static booleanisPluginAbsentOrDisabled(String pluginName) private booleanprivate static voidkeepRunning(UltiToolsPlugin plugin, ReloadReport report, String bindingKey, Integer before, Map<String, Integer> next, Set<String> reported, String why) (package private) voidThe part ofinit(ClassLoader)after discovery: orders the discovered module classes by their dependencies, registers each, and logs the load summary.private Class<? extends UltiToolsPlugin> loadPluginMainClass(ClassLoader classLoader, File pluginJar) Load module main class.private voidlogCycleDiagnosis(List<List<String>> cyclePaths) Logs the cycle detection line, then one Loop line and one author-hint line per cycle path.private static voidlogMissingRequiredPlugins(String moduleName, List<String> missing) Logs the oneMISSING_REQUIRED_PLUGIN_LOG_KEYline, without a trace (#554).(package private) static voidlogPluginInitializationFailure(Class<?> pluginClass, String moduleName, Throwable thrown) Logs a module's initialization failure (#554): when the failure is a class-not-found kind (NoClassDefFoundErrororClassNotFoundExceptionanywhere in the cause chain) and at least one plugin the module lists underdepend:is not installed or not enabled, one WARNING names the module and those plugins, without the raw trace -- the missing class belongs to the missing plugin and the trace says nothing more.(package private) static voidlogPluginInitializationFailure(String moduleName, Throwable thrown) Logs one refusal WARNING for a module that failed to initialize, surfacing the innermostUltiToolsException's message (module/file/field/value/constraint, per e.g.(package private) static voidlogPluginUnregistrationFailure(String moduleName, Throwable thrown) Logs one WARNING for a module whoseunregister(UltiToolsPlugin)call threw, mirroringlogPluginInitializationFailure(String, Throwable)'s message shape so both refusal classes (load-time, unload-time) read the same way in the console.private voidonPluginRegistered(UltiToolsPlugin plugin) private static StringparameterList(Method method) (Player, String)-- simple parameter type names, for a readable message owner.private static IntegerA plugin.ymlapi-versionas an integer API level, ornullwhen absent or not an integer.private booleanCompatibility gates that must run before the module's bean graph is built.private voidrecordNewEntityOwnership(DataScope scope, List<Class<?>> added) Recordsscope's owned entities inentityOwnershipwhere no owner is recorded yet, adding each entity it recorded toadded-- the records a refused registration may undo (#537, gate 1).private static voidrefreshCooldownBindings(UltiToolsPlugin plugin, ReloadReport report) private static voidrefreshOne(UltiToolsPlugin plugin, ReloadReport report, String bindingKey, Supplier<Long> source, Integer before, Map<String, Integer> next, Set<String> reported) Re-reads one bound cooldown intonext: the new value when it is valid, otherwise the running one with one WARNING per binding key -- also when the config's own reload failed.(package private) static voidRefuses config-bound@Scheduled/@CmdCDdeclarations in an external plugin's container, which has no module config registry to bind against (#531).private static booleanrefusesForUninstalledRequiredPlugin(PluginYmlReader.PluginYmlInfo pluginYml, String fallbackName) Refuses a module before anything of it runs when a plugin itsplugin.ymllists underdepend:is not installed on this server (#554): logs the one WARNING naming the module and those plugins and returnstrue; the caller returns without initializing, constructing, extracting or scanning anything of the module, so a class that belongs to the missing plugin is never loaded and never logs a trace.booleanregister(UltiToolsPlugin plugin) booleanregister(Class<? extends UltiToolsPlugin> pluginClass) Register plugin.private voidregisterBukkit(UltiToolsPlugin plugin) Register bukkit commands or listeners.private static <T> voidregisterByOwnType(SimpleContainer context, T instance) Bindsinstanceto its own class incontext's type registry, without assembling it, sogetBean(Class)and by-type@Autowiredresolve it.private booleanregisterClass(Class<? extends UltiToolsPlugin> pluginClass) private voidregisterEntityOwnership(DataScope scope) Recordsscope's owned entities inentityOwnership, so a later ownership refusal (D-15) can name the actual owning module instead of only saying "not registered to you".voidregisterExternal(ExternalPluginAdapter adapter) Register an external Bukkit plugin adapter with the framework.voidregisterExternal(ExternalPluginAdapter adapter, Class<?>[] additionalEntities) Register an external Bukkit plugin adapter with the framework, declaring entity classes that legitimately live outside the plugin's own JAR (D-19), in addition to the entities found by scanning the plugin's own JAR for@Tableclasses.private voidregisterExternalScope(File dataFolder, DataScope scope) Records an external plugin'sDataScopeunder its data folder's canonical path, sofindScopeForDataFolder(java.io.File)can resolve the caller ofDataStore.getOperator(File, Class)back to a scope (D-18) -- the same canonical-path keying 02-02's Task 2 used for the SQLite pool map, so two spellings of the same folder resolve to one entry.private voidRegister the@Scheduledmethods of every framework-owned object.private booleanregisterInstance(UltiToolsPlugin plugin) private voidregisterModuleEventHandlers(EventBus eventBus, UltiToolsPlugin plugin, Object bean) Scan a bean for @ModuleEventHandler methods and register them with the EventBus.private voidregisterModuleEventHandlersExternal(EventBus eventBus, String pluginName, Object bean) Scan a bean for @ModuleEventHandler methods and register them with EventBus for an external plugin.private static <T> voidregisterOwnType(SimpleContainer context, T instance) Register an object intocontextkeyed by its own concrete runtime class, in addition to whatever declared-type registration the caller has already done.private voidRegister a Bukkit listener for PlayerQuitEvent to clean up @PlayerCache maps.private voidRegister a Bukkit listener for PluginDisableEvent to auto-disconnect external plugins.private static voidreleaseCompleters(UltiToolsPlugin plugin, boolean nameStillInUse) Releasesplugin's tab-completion completers: those recorded against the instance, then -- unless another copy of the module shares its name -- those recorded by name only (#506).private static voidreleaseConfigEntities(UltiToolsPlugin plugin) private static voidreleaseEventHandlers(UltiToolsPlugin plugin, boolean nameStillInUse) Releasesplugin's EventBus handlers, by instance and then, unless another copy of the module shares its name, by name (#506).private static voidreleasePanelResponders(UltiToolsPlugin plugin, boolean nameStillInUse) Releasesplugin's panel responders, by instance and then, unless another copy of the module shares its name, by name (#506).private static voidRemoves the player-link handlerpluginset, if it is the owner (plan 22-21).private voidReleases every registration recorded against each copyincomingsupersedes -- panel responders, EventBus handlers, tab completers -- beforeincoming's container refresh andregisterSelf()(#562).voidreload()Reload all plugins.Reloads every loaded module, each isolated from the others' failures, then logs a summary and returns it (#509).(package private) static booleanrequiresNewerFramework(int moduleApiVersion, int frameworkApiVersion) Whether a module declaringmoduleApiVersionneeds a newer framework than one atframeworkApiVersion.private static voidresolveCooldownBindings(UltiToolsPlugin plugin, BaseCommandExecutor executor, Map<String, Integer> resolved, Map<String, Supplier<Long>> sources) Resolves every bound@CmdCDofexecutoronce, at load: shape, key, field, and range (an out-of-range value refuses the module).resolveEntityClass(String className, String moduleName, List<String> softDepend) LoadsclassNameand returns it wrapped in anOptionalwhen it carries@Table, forscanEntitiesInJar(java.io.File)'s per-entry step.private static JdbcTransactionManagerresolveJdbcTransactionManager(DataStore dataStore, DataScope scope, DataSource dataSource) Resolves theJdbcTransactionManagerwireTransactional(com.ultikits.ultitools.context.SimpleContainer, com.ultikits.ultitools.manager.DataScope, com.ultikits.ultitools.aop.AopProxyResolver)should bind the AOP interceptor to, for a JDBC-capabledataStore(02-09, T-02-TAM-11).private static FileresolveOwnJarFile(Class<?> aClass) Best-effort resolution of the jar fileaClasswas loaded from, via itsCodeSource.private voidincomingfailed to load: first releases every registration recorded againstincomingitself in the three registries -- what it registered during its container refresh andregisterSelf()must not stay behind, answering from its closed container -- then, when it was superseding loaded copies, gives them back the registrations released before its container refresh (#562).(package private) static StringrootCauseMessage(Throwable thrown) Walksthrown's cause chain for the deepestUltiToolsException, returning its message - orthrown.getMessage()if the chain holds none.private static voidrunReloadHalf(UltiToolsPlugin plugin, String what, ReloadReport report, Runnable half) private static voidrunUnregisterStep(UltiToolsPlugin plugin, String stepDescription, Runnable step) Runs oneunregister(UltiToolsPlugin)best-effort registry-cleanup step in isolation: a throw fromstepis logged vialogPluginUnregistrationFailure(String, Throwable), not rethrown, so it can neither skip nor mask any ofunregister's other steps (Codex review on #457, across four rounds on the same method: listener cleanup surviving command-cleanup failure, the context close surviving the unload hook throwing, mandatory cleanup surviving an early registry failure, and finally every registry step surviving every other one).scanEntitiesInJar(File pluginJar) Scans a plugin's own JAR for classes carrying@Table.scanExternalEntities(ExternalPluginAdapter adapter, Class<?>[] additionalEntities) Builds the entity set aDataScopefor an external plugin should carry (D-19): every@Tableclass found by scanning the plugin's own jar, plus whatever it declared viaUltiToolsAPI.connect(plugin, additionalEntities).scanPluginEntities(String pluginName, Class<? extends UltiToolsPlugin> pluginClass) Builds the entity set aDataScopeforpluginshould carry (D-19): every@Tableclass found by scanning the module's own jar, plus anything declared via@UltiToolsModule#additionalEntities()for entities that legitimately live elsewhere.private voidsetPluginStaticInstance(Class<? extends UltiToolsPlugin> pluginClass, UltiToolsPlugin plugin) Set the static instance field on a plugin class before @PostConstruct runs.private List<Class<? extends UltiToolsPlugin>> sortPluginsByDependencies(List<Class<? extends UltiToolsPlugin>> plugins) Sort plugins by their dependencies using Kahn's algorithm (topological sort).private static StringunenforceableCommandAnnotationMessage(Class<?> executorClass, Method method, Class<? extends Annotation> annotationType, Class<? extends CommandValidator> requiredValidatorType) Builds the refusal message forvalidateCommandExecutorContract(BaseCommandExecutor), naming the offending class and, whenmethodis non-null, the offending mapping method -- mirroringadditionalEntityRefusalMessage(String, Class, String)'s message-builder shape.private static StringuninvocableSuggestMethodMessage(Class<?> executorClass, Method mappingMethod, Method suggestMethod) Builds the refusal message forvalidateSuggestMethodSignatureForMethod(BaseCommandExecutor, Class, Method), naming the offending class, the mapping method and the offending suggest method's declaring class, name and parameter signature -- mirroringunknownSuggestKeyMessage(Class, Method, String)'s message-builder shape.private static StringunknownSuggestKeyMessage(Class<?> executorClass, Method method, String key) Builds the refusal message forvalidateSuggestKeysForMethod(Class, Method), naming the offending class, method and key -- mirroringunenforceableCommandAnnotationMessage(Class, Method, Class, Class)'s message-builder shape.voidunregister(UltiToolsPlugin plugin) Unloads a module: releases everything the framework registered for it, runs its unload hook throughUltiToolsPlugin.unregisterSelf(), closes its container and removes it from the loaded modules.private voidunregister(UltiToolsPlugin plugin, boolean nameStillInUse, boolean reportUnsaved) The body ofunregister(UltiToolsPlugin).voidunregisterExternal(ExternalPluginAdapter adapter) Unregister an external Bukkit plugin adapter from the framework.private voidunregisterExternalScope(File dataFolder) Removes a data folder's registered external plugin scope (02-14 Task 2), so a subsequentregisterExternal(ExternalPluginAdapter, Class[])for the same folder -- a legitimate reconnect -- is not refused byregisterExternalScope(java.io.File, com.ultikits.ultitools.manager.DataScope)'s displacement guard as if it were a different plugin still holding the folder.private voidUnloads the old version this load supersedes, through the fullunregister(UltiToolsPlugin)path (#506): its tasks are cancelled, its container closed, its registrations released and it is delisted.private voidunwindRefusedExternal(ExternalPluginAdapter adapter, SimpleContainer context, DataScope scope, List<Class<?>> ownershipAdded) Undoes whatregisterExternal(ExternalPluginAdapter, Class[])registered before a refusal after the folder scope was recorded (#537): closes the context (ifrefresh()got as far as building one), clears the adapter's context and data scope, removes the entity-ownership records this attempt inserted, and removes the folder scope -- only if it is still this attempt's own scope instance.private voidvalidateAdditionalEntity(String declaringPluginName, Class<?> declaringClass, File declaringOwnJarFile, Class<?> entityClass) Validates thatentityClass, declared viaadditionalEntities(D-19) bydeclaringPluginName, does not structurally belong to a DIFFERENT, already-known plugin before it is folded into that plugin'sDataScope(02-14 finding: this attribute previously accepted anyClass<?>the caller named, with zero validation -- letting any plugin claim ownership of any other module's real@Tableentity through the publicUltiToolsAPI.connect(plugin, additionalEntities...)surface, or the internal@UltiToolsModule#additionalEntities()equivalent).(package private) static voidThe per-executor half ofvalidateCommandExecutorContracts(SimpleContainer)-- package-private, not private, soPluginManagerCommandContractTest(same package) can drive it directly with a hand-built fixture, without reflection orsetAccessible(true).(package private) static voidvalidateCommandExecutorContracts(SimpleContainer pluginContext) Refuses to load a module whoseBaseCommandExecutorbean declares@CmdCDor@UsageLimit-- on a@CmdMappingmethod or on the executor class itself -- while its OWN validator chain (the exact chainonCommanddispatches through, not a freshly built default one) holds no instance of the corresponding validator type (SILENT-11 / D-01 half 2, D-04).(package private) static voidvalidateConfigBindings(UltiToolsPlugin plugin, SimpleContainer pluginContext) Load-time check of a module's config-bound@Scheduledand@CmdCDdeclarations (#531), run right aftervalidateCommandExecutorContracts(com.ultikits.ultitools.context.SimpleContainer)and before any Bukkit side effect, so a refusal is module-granular in exactly the same way.private static voidvalidateSuggestKeysForMethod(Class<?> executorClass, Method method) Validate constructor arguments for security.private static voidvalidateSuggestMethodSignatureForMethod(BaseCommandExecutor executor, Class<?> executorClass, Method method) T-05-fix Part 2's load-time half of method-name@CmdParam.suggest()resolution: asuggest()value that does NOT start with@names a method looked up EXACTLY asMethodInvocationCompleter.complete(TabCompletionContext)looks it up at completion time --MethodInvocationCompleter.getSuggestMethodsByName(Object, String)checks the executor's own class first, then any@CmdSuggest-referenced classes.private voidOne warning per module main class that two or more JARs in the modules folder declare, naming every one of them and the JAR the module's classes load from (UltiTools-Dev-Doc#96).(package private) static voidwireAop(SimpleContainer context, DataScope scope) Attaches AOP to a plugin container.private static voidwireJsonTransactional(SimpleContainer context, DataScope scope, AopProxyResolver resolver, JsonStore jsonStore) Builds and registers the@Transactionaladvisor for a plugin container backed by the JSON store, oncewireTransactional(com.ultikits.ultitools.context.SimpleContainer, com.ultikits.ultitools.manager.DataScope, com.ultikits.ultitools.aop.AopProxyResolver)has determineddataStorecannot supply a JDBCDataSourcebut is aJsonStore(02-05, D-03).private static voidwireTransactional(SimpleContainer context, DataScope scope, AopProxyResolver resolver) Builds and registers the@Transactionaladvisor for one plugin container.
-
Field Details
-
LEGACY_PLUGIN_LOADING_PROPERTY
The name of the JVM system property that opts back into the pre-6.3.0 degraded load order (D-10): every module in discovery order (file-name order, #476), with no dependency resolution at all. Modeled on Paper's own-Dpaper.useLegacyPluginLoading=trueprecedent -- a one-shot, consumed-at-bootstrap decision, which is why it is a system property rather than a reloadableconfig.ymlkey. The literal name is repeated (rather than referenced only through this constant) at every call site below, so the property is legible directly in the operator-facing message that names it, not only in code that reads it.- See Also:
-
RELOAD_SUMMARY_ALL_KEY
Framework i18n key: the/ul reloadsummary when every module reloaded (#509).- See Also:
-
RELOAD_SUMMARY_FAILED_KEY
Framework i18n key: the/ul reloadsummary naming the modules that failed (#509).- See Also:
-
RELOAD_SUMMARY_MIXED_PARTIAL_KEY
Framework i18n key: the line afterRELOAD_SUMMARY_FAILED_KEYwhen, besides the failed modules, some reloaded only partly; the partial modules' own lines follow it (#529; Codex review of #564, round 2).- See Also:
-
RELOAD_SUMMARY_PARTIAL_KEY
Framework i18n key: the/ul reloadsummary when no module failed but some reloaded only partly (#529).- See Also:
-
RELOAD_PARTIAL_LINE_KEY
Framework i18n key: one partially reloaded module and the parts that did not reload (#529). The same text/ul reload <name>replies; the literal is repeated in that command.- See Also:
-
MISSING_REQUIRED_PLUGIN_LOG_KEY
Framework i18n key for the one line logged when a module cannot load because a plugin itsplugin.ymllists underdepend:is not installed or not enabled (#554). Arguments: the module, the missing plugins joined by", ". Package-private so a test can assert both shipped catalogues translate it.- See Also:
-
pluginList
The loaded modules, in load order. Only this manager changes it: a module is listed byonPluginRegistered(com.ultikits.ultitools.abstracts.UltiToolsPlugin)and delisted byunregister(UltiToolsPlugin).A
CopyOnWriteArrayListbecause it is read from other threads (the asynchronous/upm list, the economy facade's module attribution) while the main thread loads and unloads modules. Every mutation replaces the backing array under the list's own lock, and bothgetPluginList()'s snapshot and every loop in this class read one whole array, so a reader never sees a half-applied change and a loop that unloads modules while it iterates cannot fail (#507). -
pluginClassList
-
discoveryRefusals
private int discoveryRefusalsModule JARs the last discovery tried and could not turn into a module class (18-41): refused while its declared main class was read -- nomain:, not anUltiToolsPlugin, a main class from another JAR, a linkage failure, a failed JAR validation. A second copy of a module whose classes load from another JAR is not one of them; the duplicate-copies warning reports it. -
duplicateCopiesSkipped
private int duplicateCopiesSkippedCopies skipped by discovery because their module's classes load from another JAR. -
moduleJarIndex
What the start-up scan read: per declared main class, every JAR in the modules folder that declares it, and the JAR its class was loaded from (#516)./upm uninstallreads it to find every file of a loaded module. -
moduleBeingRegistered
The module being registered on each thread, name and main class (#483); seegetModuleBeingRegistered(). AThreadLocalso it can never be read by, or leak into, another thread. -
supersedeHandovers
Incoming copy -> the actions that give the copies it supersedes back their registrations (#562). Filled byreleaseSupersededRegistrations(UltiToolsPlugin)before the incoming copy's container refresh, emptied bydiscardSupersededRegistrations(UltiToolsPlugin)when it activates or byrestoreSupersededRegistrations(UltiToolsPlugin)when it fails. Keyed by identity: two copies of one module may be equal to nothing but themselves. -
classLoader
-
taskManager
-
playerCacheManager
-
entityOwnership
Entity class -> owning plugin name, populated every time aDataScopeis minted (both internal modules and external plugins). Lets a refusal built byDataScope.refusalFor(Class)say "belongs to module X" instead of only "not registered to you" -- D-15 requires the two to be distinguishable. First registration wins; a genuine collision has never been observed across this project's 21@Tableclasses / 17 repositories (02-CONTEXT.md), so this is a defensive tie-break, not a load-bearing one. -
externalScopesByFolder
External plugin data folder (canonical path) -> theDataScopeminted for it, populated byregisterExternal(ExternalPluginAdapter, Class[]). LetsDataStore.getOperator(File, Class)resolve its caller back to a scope (D-18), for the default body a third-partyDataStorethat does not override that method inherits. -
connectedExternalScanPackages
Connected External Plugin API consumers, plugin name to scan package (#462): added whenregisterExternal(ExternalPluginAdapter, Class[])completes, removed byunregisterExternal(ExternalPluginAdapter). Read throughgetConnectedExternalScanPackages()by economy attribution, which otherwise could not name a caller that is not a module. -
FRAMEWORK_SCHEDULED_OWNER_TYPES
The framework-owned types whoseScheduledmethods this manager registers.This set is the wiring, not a description of it.
registerFrameworkScheduledOwners()registers exactly the instances whose types appear here and fails fast if the two disagree, andFrameworkScheduledWiringTestfails the build if a framework class carries@Scheduledwithout appearing here. Adding a type to this set and wiring its task are therefore the same act -- which is the point.The defect this prevents (#384):
TaskManager's two original entry points both iterate aSimpleContainer's beans, so an object the framework constructs directly was reached by neither.PlayerCacheManager.sweepExpiredEntries()carried a@Scheduled(period = 5 minutes)annotation, and its javadoc explained at length why a clock-driven sweep was chosen over a hand-rolledBukkitRunnable-- and it never ran once. Nothing reported an error, because nothing had looked.- Since:
- 6.3.0
-
FRAMEWORK_PACKAGE
The framework's own package, in both the dotted and the internal (slash) spelling.- See Also:
-
FRAMEWORK_PACKAGE_INTERNAL
- See Also:
-
INTERNAL_CLASS_NAME_PATTERN
A bare JVM internal (slash-separated) binary class name: one or more identifier segments joined by/, each starting with a letter,_or$. Deliberately rejects anything carrying a space, parenthesis or colon -- which is exactly the shape of the JVM-specific " (wrong name: ...)" detail some JVMs append to aNoClassDefFoundErrormessage for a class/file name mismatch (a jar-content problem, not "a type this class references is absent") and of any non-NoClassDefFoundErrorLinkageError's prose message (Codex review, PR #551 round 1, P2).
-
-
Constructor Details
-
PluginManager
public PluginManager()
-
-
Method Details
-
getPluginList
Returns the modules currently loaded, in load order, as an unmodifiable snapshot.The returned list is a copy taken at the moment of the call. It does not change when a module is loaded or unloaded afterwards, and every attempt to add, remove or clear throws
UnsupportedOperationException. Only this manager changes which modules are loaded:register(...)lists a module andunregister(UltiToolsPlugin)delists it.Since 6.3.0 (#507). Before, this returned the manager's live internal
ArrayList, so callers delisted unloaded modules by removing from it, and a reader on another thread could fail with aConcurrentModificationExceptionor see a trailingnullwhile the main thread unloaded a module. The signature and return type are unchanged.- Returns:
- an unmodifiable snapshot of the loaded modules
-
init
Initialize plugin manager. Please do not call this method manually.- Throws:
IOException- IO exception
-
loadDiscoveredModules
void loadDiscoveredModules()The part ofinit(ClassLoader)after discovery: orders the discovered module classes by their dependencies, registers each, and logs the load summary. Package-private so a test can drive it afterdiscoverModuleClasses(File)over a folder of its own.The summary's
Failedis every module JAR of this start that did not end up loaded (18-41): the JARs discovery refused, the module classes dependency resolution excluded (a cycle or a missing required plugin -- absent from the sorted list), and those whose registration failed. Before, it wassorted - success, which counted none of the first two, so a boot that refused a module printedFailed 0. A folder whose every JAR was refused still prints the summary;No UltiTools plugin found.is kept for a folder with no module JAR at all. -
discoverModuleClasses
Lists the modules folder once and reads each module JAR's declared main class, adding every class that loads topluginClassList. Package-private so a test can drive the scan over a folder of its own.- Parameters:
pluginFolder- the modules folder- Returns:
falsewhen the folder could not be listed
-
warnOnDuplicateModuleJars
private void warnOnDuplicateModuleJars()One warning per module main class that two or more JARs in the modules folder declare, naming every one of them and the JAR the module's classes load from (UltiTools-Dev-Doc#96).All module JARs share one class loader, so the copies are not loaded side by side and no version comparison runs between them: the class comes from the first JAR on the class path that carries it -- in file-name order (#476) -- and the other copies are refused. Without this line an operator who dropped a new version next to the old one would see the old one keep running with nothing saying why.
A main class none of whose copies loaded gets no line here: each copy has already been refused with its own line saying why.
-
isDuplicateCopy
Whether a JAR whose declared main class was loaded from another JAR is another copy of that module: the other JAR is in the same modules folder and its ownplugin.ymldeclares the samemain:. Anything else is a class borrowed from a JAR that is not this module, which stays refused with its own SEVERE line. Reads onlyplugin.yml.- Parameters:
pluginJar- the JAR being scannedsupplier- the JAR its declared main class was loaded frommainClassName- the main class it declares- Returns:
truewhen it is a duplicate copy
-
register
Register plugin.- Parameters:
pluginClass- Plugin class- Returns:
- Register result
-
registerClass
-
register
- Parameters:
plugin- UltiTools plugin instance- Returns:
- Register result
-
registerInstance
-
refusesForUninstalledRequiredPlugin
private static boolean refusesForUninstalledRequiredPlugin(PluginYmlReader.PluginYmlInfo pluginYml, String fallbackName) Refuses a module before anything of it runs when a plugin itsplugin.ymllists underdepend:is not installed on this server (#554): logs the one WARNING naming the module and those plugins and returnstrue; the caller returns without initializing, constructing, extracting or scanning anything of the module, so a class that belongs to the missing plugin is never loaded and never logs a trace.Only a plugin that is not installed at all counts here. A plugin that is installed but not enabled yet may still be enabled after UltiTools -- Bukkit enables in its own order, and only the plugins UltiTools itself lists under
depend:orsoftdepend:are guaranteed to come first -- so refusing it here would refuse a module that loads today. That case keeps the late check inlogPluginInitializationFailure(Class, String, Throwable).- Parameters:
pluginYml- the module's ownplugin.ymlfallbackName- the name to log when theplugin.ymlhas noname:- Returns:
truewhen the module was refused and the line logged
-
logMissingRequiredPlugins
Logs the oneMISSING_REQUIRED_PLUGIN_LOG_KEYline, without a trace (#554). -
logPluginInitializationFailure
static void logPluginInitializationFailure(Class<?> pluginClass, String moduleName, Throwable thrown) Logs a module's initialization failure (#554): when the failure is a class-not-found kind (NoClassDefFoundErrororClassNotFoundExceptionanywhere in the cause chain) and at least one plugin the module lists underdepend:is not installed or not enabled, one WARNING names the module and those plugins, without the raw trace -- the missing class belongs to the missing plugin and the trace says nothing more. In every other case, including a class-not-found failure while everydepend:plugin is present,logPluginInitializationFailure(String, Throwable)logs the existing message and trace unchanged.A plugin that is not installed at all is refused earlier, before construction, by
refusesForUninstalledRequiredPlugin(com.ultikits.ultitools.utils.PluginYmlReader.PluginYmlInfo, java.lang.String); this late check still serves a required plugin that is installed but not enabled, which the early check deliberately leaves alone.- Parameters:
pluginClass- the module's main class, whose own jar'splugin.ymlis readmoduleName- the module refusing to load, however the caller identifies itthrown- the throwable caught at the registration boundary
-
isClassNotFoundKind
Whetherthrown's cause chain holds aNoClassDefFoundErroror aClassNotFoundException. Bounded by identity-based cycle detection, likerootCauseMessage(Throwable). -
beginRegistration
private Map.Entry<String,Class<? extends UltiToolsPlugin>> beginRegistration(String name, Class<? extends UltiToolsPlugin> pluginClass) Records the module being registered on this thread (#483) and returns the record it replaces (a registration started from inside another module's registration), forendRegistration(java.util.Map.Entry<java.lang.String, java.lang.Class<? extends com.ultikits.ultitools.abstracts.UltiToolsPlugin>>)to restore. -
endRegistration
Restores the recordbeginRegistration(java.lang.String, java.lang.Class<? extends com.ultikits.ultitools.abstracts.UltiToolsPlugin>)replaced, or clears it; runs in afinally. -
getModuleBeingRegistered
The module being registered on the calling thread, as name and main class, ornull(#483). Set by bothregisterentry points before the module is constructed or assembled and cleared when the attempt ends, successful or not, so a module requesting the economy from its constructor, a@PostConstructmethod orregisterSelf()-- before it is ingetPluginList()-- can still be named. Per thread: another thread, even one the module starts during registration, never sees it. Not part of the module-facing API; public only because economy attribution lives in another package.- Returns:
- the module being registered on this thread, or
nullwhen none is - Since:
- 6.3.0
-
findRegistrationOwner
The module copy a registration filed undermoduleNameonly belongs to when it is made outside every module's load scope -- for example from a module's scheduled task after it loaded (#562 item 1): the copy listed as loaded under that name; when more than one copy is listed under it, the first listed, the one loaded earliest. A copy the framework is still loading is not listed yet; what it registers on the loading thread is attributed by its load scope instead, which the registries consult first. So while a newer copy replaces an older one, a registration made by name from any other thread belongs to the older copy and leaves with it. Not part of the module-facing API; public only because the registries live in other packages.- Parameters:
moduleName- the name the registration is filed under;nullmatches nothing- Returns:
- the listed copy, or
nullwhen no copy is listed undermoduleName, in which case the registration stays filed under the name only, as before 6.3.0 - Since:
- 6.3.0
-
logPluginInitializationFailure
Logs one refusal WARNING for a module that failed to initialize, surfacing the innermostUltiToolsException's message (module/file/field/value/constraint, per e.g.ConfigurationException.validationFailed(...)) instead of an outer wrapper's generic text (04-REVIEW.md WR-02). All three refusal-log sites in this class route through here so the message shape has exactly one place it is built.Package-private, not private:
PluginManagerTestlives in this same package and drives this method directly with a real multi-level chain, without reflection orsetAccessible(true). This is a test-seam choice, not an API widening - it is notpublic, so it never enters the published surface.- Parameters:
moduleName- the module refusing to load, however the caller identifies itthrown- the throwable caught at the registration boundary
-
logPluginUnregistrationFailure
Logs one WARNING for a module whoseunregister(UltiToolsPlugin)call threw, mirroringlogPluginInitializationFailure(String, Throwable)'s message shape so both refusal classes (load-time, unload-time) read the same way in the console. Package-private for the same reason as that method: a test-seam choice, not a widened API surface (WR-01, 16-REVIEW-lifecycle.md).- Parameters:
moduleName- the module whose unregistration failed, however the caller identifies itthrown- the throwable caught at theclose()loop boundary
-
rootCauseMessage
Walksthrown's cause chain for the deepestUltiToolsException, returning its message - orthrown.getMessage()if the chain holds none. Bounded via identity-based cycle detection (IdentityHashMap) so a self-referential or cyclic cause chain terminates instead of spinning.- Parameters:
thrown- the throwable whose cause chain is walked- Returns:
- the deepest
UltiToolsException's message, orthrown's own message
-
unregister
Unloads a module: releases everything the framework registered for it, runs its unload hook throughUltiToolsPlugin.unregisterSelf(), closes its container and removes it from the loaded modules.The last two always run, in that order, even when the unload hook throws; the hook's failure is then rethrown to the caller. Since 6.3.0 (#507) this method delists the module itself, by identity, so no caller has to. After the container is closed and before the module is delisted, the configuration layer releases the module's configuration entities through
ConfigManager.unregisterAll(UltiToolsPlugin)(#507); when the unload is part ofclose(), their never-saved changes are first named in one warning and dropped - nothing is written at stop, unload or replacement as of 6.3.0.The three registries a module can file registrations in by name -- tab-completion completers, EventBus handlers and panel responders -- are released by instance first: every registration recorded against this module instance goes, whatever name it was filed under. As of 6.3.0 that covers what the module registered while it loaded (its load scope, #506) and what was registered by its name after it loaded, while it was the copy listed under that name (#562 item 1, see
findRegistrationOwner(String)). Registrations filed under the module's name only -- made while no copy was listed under it -- are then released by name, unless another loaded copy of the module shares that name; they cannot be told apart from that copy's, so they stay until the last copy of the name is unloaded (#506).- Parameters:
plugin- UltiTools plugin instance
-
isNameInUseByAnotherLoadedCopy
Whether a loaded module other thanpluginitself carriesplugin's name.- Parameters:
plugin- the module about to be unloaded- Returns:
trueif releasing by name would also release another loaded copy's registrations
-
unregister
The body ofunregister(UltiToolsPlugin).- Parameters:
plugin- the module to unloadnameStillInUse- whether another copy of the module -- listed, or being activated as this copy's replacement -- shares its name, so that name-only registrations must not be released by name (#506)reportUnsaved- whether the module's never-saved configuration changes are named, once, before its entities are released - at shutdown and at a normal unload or uninstall; a superseded copy passesfalsebecause the replacement has already named them as dropped. Nothing is written either way (17-65; review round 1 R65-I5)
-
releaseCompleters
Releasesplugin's tab-completion completers: those recorded against the instance, then -- unless another copy of the module shares its name -- those recorded by name only (#506). -
releaseEventHandlers
Releasesplugin's EventBus handlers, by instance and then, unless another copy of the module shares its name, by name (#506). -
releasePanelResponders
Releasesplugin's panel responders, by instance and then, unless another copy of the module shares its name, by name (#506). -
releasePlayerLinkHandler
Removes the player-link handlerpluginset, if it is the owner (plan 22-21). Null-safe while the framework has no instance or no player-link API yet (an enable that failed before creating it). -
beginLoadScopes
Opens, on this thread, the registration scopes of the three registries a module files registrations in by name -- tab-completion completers, EventBus handlers, panel responders -- so that what the module registers while the framework loads it (its container refresh and itsregisterSelf()) is recorded against this instance (#506; Codex review of #564, round 3). Always paired withendLoadScopes()in afinally. -
endLoadScopes
private static void endLoadScopes()Closes the scopesbeginLoadScopes(UltiToolsPlugin)opened on this thread. -
runUnregisterStep
private static void runUnregisterStep(UltiToolsPlugin plugin, String stepDescription, Runnable step) Runs oneunregister(UltiToolsPlugin)best-effort registry-cleanup step in isolation: a throw fromstepis logged vialogPluginUnregistrationFailure(String, Throwable), not rethrown, so it can neither skip nor mask any ofunregister's other steps (Codex review on #457, across four rounds on the same method: listener cleanup surviving command-cleanup failure, the context close surviving the unload hook throwing, mandatory cleanup surviving an early registry failure, and finally every registry step surviving every other one).- Parameters:
plugin- the plugin being unregistered, for the log message and step contextstepDescription- a short, human-readable name forstep, folded into the WARNING log line if it throwsstep- the cleanup action to attempt
-
close
public void close()Unregister all plugins. -
registerFrameworkScheduledOwners
private void registerFrameworkScheduledOwners()Register the@Scheduledmethods of every framework-owned object.Called from
init(ClassLoader)oncetaskManagerand the owner instances exist. Cancellation is the mirror of this, inclose().- Throws:
IllegalStateException- if the instances offered do not matchFRAMEWORK_SCHEDULED_OWNER_TYPES. This is reachable only by editing one of the two without the other, which the guard test already fails the build for; it is a belt-and-braces check on an invariant, and cannot be triggered by user configuration. Failing loudly here is deliberate -- degrading quietly to "some of the tasks got registered" is the exact behaviour this whole change exists to remove.- Since:
- 6.3.0
-
frameworkScheduledOwners
The live instances corresponding toFRAMEWORK_SCHEDULED_OWNER_TYPES, in the same order.- Returns:
- the framework-owned objects to scan for
@Scheduledmethods - Since:
- 6.3.0
-
registerPlayerQuitListener
private void registerPlayerQuitListener()Register a Bukkit listener for PlayerQuitEvent to clean up @PlayerCache maps. -
registerPluginDisableListener
private void registerPluginDisableListener()Register a Bukkit listener for PluginDisableEvent to auto-disconnect external plugins.- Since:
- 6.2.2
-
applyReloadedConfigBindings
Applies a module's reloaded configuration to its config-bound@Scheduledtasks and@CmdCDcooldowns (#531). Called byUltiToolsPlugin.reloadSelf()after its configuration reload succeeded and before the module's ownonReload(); a refused configuration reload throws before this step, so the running timings stay.Bound
@Scheduledtasks are rescheduled byTaskManager.rescheduleBound(com.ultikits.ultitools.abstracts.UltiToolsPlugin)(see there for the phase-preserving rule). Each bound@CmdCDvalue is re-read from the config field it was resolved to at load -- never resolved again -- and merged into everyCooldownValidatorthat holds it. That cache changes only here after load, so a panel save, or a refused reload that left a new value in the config field, is never enforced until a reload succeeds. An out-of-range value keeps the running one and logs one WARNING. A cooldown already running keeps the end time it was stamped with.Failure isolation (gate-1 IN-01, IN-02). This step runs only on the main thread, where the task registry lives; called from any other thread it touches nothing and logs a WARNING, because a deferred copy could apply a value a later, refused reload wrote. Each half's own failure is logged against the module and swallowed, so one module's binding problem never aborts
/ul reloadfor the modules after it, and never skips this module'sonReload().Reported, not only logged (#595). Every value kept in place of a refused one, every half that failed, and a call refused off the main thread is also recorded as one partial reason in
report, so/ul reload,/ul reload <name>and a module's own reload command do not reply plain success while a timing was not updated.- Parameters:
plugin- the module whose configuration was just reloadedreport- the module's reload report, receiving one partial reason per value not applied- Since:
- 6.3.0
-
applyReloadedConfigBindings
applyReloadedConfigBindings(UltiToolsPlugin, ReloadReport)with a report nobody reads: the values not applied are only logged.- Parameters:
plugin- the module whose configuration was just reloaded- Since:
- 6.3.0
-
runReloadHalf
private static void runReloadHalf(UltiToolsPlugin plugin, String what, ReloadReport report, Runnable half) -
refreshCooldownBindings
-
refreshOne
private static void refreshOne(UltiToolsPlugin plugin, ReloadReport report, String bindingKey, Supplier<Long> source, Integer before, Map<String, Integer> next, Set<String> reported) Re-reads one bound cooldown intonext: the new value when it is valid, otherwise the running one with one WARNING per binding key -- also when the config's own reload failed. -
keepRunning
private static void keepRunning(UltiToolsPlugin plugin, ReloadReport report, String bindingKey, Integer before, Map<String, Integer> next, Set<String> reported, String why) -
describeBindingKey
Renderscom.example.EssentialsConfig#features.wild.cooldownasEssentialsConfig key 'features.wild.cooldown'. -
validateConfigBindings
Load-time check of a module's config-bound@Scheduledand@CmdCDdeclarations (#531), run right aftervalidateCommandExecutorContracts(com.ultikits.ultitools.context.SimpleContainer)and before any Bukkit side effect, so a refusal is module-granular in exactly the same way.Refuses the module when it uses any binding while declaring a
plugin.ymlapi-versionbelow 630; when a binding names a config class not registered exactly once for it, a key that matches no@ConfigEntrypath, a field that is not anint,long,IntegerorLong, a key without a config class or a config class without a key, or sets the annotation literal as well as the key; when a bound value is out of range (a period or delay below 1 second or above the ceiling, a cooldown below 0 or aboveInteger.MAX_VALUE, ornull); when the bound config's own load did not complete (for example, a failed write-back), so its values were never validated. Resolves every bound@CmdCDonce and, only when every executor resolved, stores its value and load-time source on the executor itself -- its exact owner -- throughConfigBoundCooldownState; noCooldownValidatorholds any of it.- Parameters:
plugin- the module being assembledpluginContext- its just-refreshed container- Throws:
PluginModuleException- for a binding that cannot workConfigurationException- for an out-of-range value
-
firstBinding
- Returns:
- the first config-bound declaration in the container, for messages, or
null
-
refuseConfigBindingsOutsideModules
Refuses config-bound@Scheduled/@CmdCDdeclarations in an external plugin's container, which has no module config registry to bind against (#531). Run before any Bukkit side effect.- Parameters:
context- an external plugin's just-refreshed container- Throws:
PluginModuleException- naming the first bound declaration found
-
baseCommandExecutors
-
cooldownDeclarations
Every@CmdCDthe executor's validator can meet, each paired with a display owner for messages: the class-level annotation, and for each@CmdMappingmethod the annotationCooldownValidatoritself resolves (most-derived-wins), in the same way.A list, not a map keyed by the owner: an owner string is for messages only, and keying by it let one declaration replace another whenever two owners rendered alike -- overloads sharing a name (Codex round 2 on #536), then overloads whose parameter types share a simple name (round 4). With no key there is nothing to collide; a binding key declared twice is resolved once by the callers.
-
parameterList
(Player, String)-- simple parameter type names, for a readable message owner. -
resolveCooldownBindings
private static void resolveCooldownBindings(UltiToolsPlugin plugin, BaseCommandExecutor executor, Map<String, Integer> resolved, Map<String, Supplier<Long>> sources) Resolves every bound@CmdCDofexecutoronce, at load: shape, key, field, and range (an out-of-range value refuses the module).- Parameters:
resolved- receives seconds keyed byCooldownValidator.bindingKey(CmdCD)sources- receives the load-time value source for each binding key
-
reload
public void reload()Reload all plugins. This operation only reload plugin configuration.Since 6.3.0 each module is reloaded in isolation (#509): a module whose reload throws is logged by its own
reloadSelf(), the remaining modules are still reloaded, and the closing summary names every module that failed instead of an unconditional "all reloaded". SeereloadAllAndReport(). -
reloadAllAndReport
Reloads every loaded module, each isolated from the others' failures, then logs a summary and returns it (#509)./ul reloadsends the returned lines to its sender. The summary also lists every module whose reload hook reported parts that did not reload, with those parts (#529).A module whose reload throws has already logged a failure line naming itself and its cause; it is counted as failed and the next module is reloaded. The isolation is
close()'s: everyExceptionorErrorone module throws is that module's failure, including aStackOverflowErrorfrom a recursive hook.- Returns:
- the summary lines, already localized, in the order they were logged
- Since:
- 6.3.0
-
loadPluginMainClass
private Class<? extends UltiToolsPlugin> loadPluginMainClass(ClassLoader classLoader, File pluginJar) Load module main class.Resolves the main class from the module jar's own
plugin.ymlmain:entry -- the same shape Bukkit/Paper's ownPluginDescriptionFileuses to find a plugin's main class -- and loads exactly that one declared class, rather than the pre-6.3.0-plan-17-30 approach of loading every class entry in the jar, in entry order, until one happened to be assignable toUltiToolsPlugin. That approach linked classes with nothing to do with main-class discovery: a class that exists only for an optional soft-dependency plugin (e.g. a PlaceholderAPI expansion) failed to link on every server where that optional plugin is absent, logging a misleading "skipped class...built against an older API" SEVERE line for a module that was never actually built against an older API. See the framework issue this fixes for the full real-machine reproduction.Every refusal path here logs a specific, actionable SEVERE naming the module and the exact reason (no
plugin.yml/nomain:, the declared class failed to load, the declared class is not a concreteUltiToolsPlugin, or -- because every module shares one classloader over the whole module directory -- the declared class resolved but actually belongs to a different, already-installed module's jar) and returnsnull-- never a silent skip, consistent with this milestone's core value that a declared surface must either work or say plainly that it does not.nullis not fatal to the caller:init(ClassLoader)simply omits the module frompluginClassList, so one refused jar never prevents any other jar in the same directory from loading.- Parameters:
classLoader- Class loaderpluginJar- Plugin jar file- Returns:
- Plugin main class, or
nullif the module is refused
-
frameworkLinkageFailure
Decides whether a failure to load a module's declared main class means the module was built against a different UltiTools-API than the one installed -- usually an older one, the case an operator meets after upgrading the framework but not the module (6.3.0 madeUltiToolsPlugin.unregisterSelf()andreloadSelf()final, and every released module overrides at least the first). The failure alone cannot say which side is older;frameworkApiMismatchRefusal(java.io.File, com.ultikits.ultitools.utils.PluginYmlReader.PluginYmlInfo, java.lang.String, java.lang.Throwable)decides that from the module's declaredapi-version.Two families qualify, and each only when the failure names a type in the framework package
com.ultikits.ultitools:IncompatibleClassChangeErrorand its subclasses (NoSuchMethodError,NoSuchFieldError,AbstractMethodError,IllegalAccessError,InstantiationError). The JVM throws these when a class was compiled against a different version of another class than the one present at run time -- a method made final (the "overrides final method" case), a member removed or narrowed, a method made abstract. They are the definition of a binary-incompatible API change; when the type they name is a framework type, the module was compiled against a different framework. A NoSuchMethodError naming a Bukkit or library member is the same defect against that other API, and blaming the framework would send the operator to replace the wrong thing.- A missing class:
NoClassDefFoundError, orClassNotFoundExceptionanywhere in the cause chain, whose missing class is a framework class (a class this release removed). A missing third-party class -- an absent optional plugin, an unshaded library -- is a different defect with a different fix, so it is not attributed to the framework's age. A JAR whose declared main class is itself absent names the module's own class, so it does not qualify either. ANoClassDefFoundErrorreading "Could not initialize class ..." reports a failed static initializer, not a missing class, so its own message never qualifies.
ExceptionInInitializerErrorwhose underlying cause is a qualifying failure -- a static initializer that died on a removed framework class -- does qualify, because that is the same version mismatch. One whose cause is anything else keeps the generic refusal, as doVerifyErrorandClassFormatError(a corrupt or hand-made class file) andUnsupportedClassVersionError(built for a NEWER Java, the opposite remedy).This is the cause the entity scan's per-module summary in
ModuleScanDiagnosticsreports, in the same words (ModuleScanDiagnostics.OLDER_API_CAUSEandModuleScanDiagnostics.COMPATIBILITY_POINTER). That summary covers any class skipped during the scan and therefore says "usually"; this method accepts only failures that name the framework, because here the module is refused outright and the line tells the operator what to replace.- Parameters:
failure- the exception caught while loading the declared main class- Returns:
- the link of the cause chain that names a framework type the module was compiled
against -- the one the refusal quotes -- or
nullwhen none does
-
frameworkApiMismatchRefusal
private static String frameworkApiMismatchRefusal(File pluginJar, PluginYmlReader.PluginYmlInfo ymlInfo, String mainClassName, Throwable failure) The one SEVERE line for a module refused byframeworkLinkageFailure(java.lang.Throwable): which JAR, what it declares itself to be (only the keys its plugin.yml declares), why it cannot load, what to do, and the JVM's own message for whoever investigates further.The direction of the advice comes from the same comparison
isUltiToolsVersionCompatible(com.ultikits.ultitools.abstracts.UltiToolsPlugin)applies after instantiation (requiresNewerFramework(int, int)): a module whose declaredapi-versionis above the installed framework's was built for a newer UltiTools-API, so the line says to upgrade the framework. In every other case -- anapi-versionequal to or below the framework's, an absent or non-integerapi-version, or a framework level that cannot be read -- no comparison says otherwise, and the line keeps the older-module wording, the cause in practice (an operator upgrading the framework but not the module). -
requiresNewerFramework
static boolean requiresNewerFramework(int moduleApiVersion, int frameworkApiVersion) Whether a module declaringmoduleApiVersionneeds a newer framework than one atframeworkApiVersion. The single definition of that comparison: both the post-load gateisUltiToolsVersionCompatible(com.ultikits.ultitools.abstracts.UltiToolsPlugin)and the load-failure wording use it. -
parseDeclaredApiVersion
A plugin.ymlapi-versionas an integer API level, ornullwhen absent or not an integer. -
installedFrameworkApiVersion
The installed framework's API level, ornullwhen it cannot be read (no env.yml, malformed version). -
scanEntitiesInJar
Scans a plugin's own JAR for classes carrying@Table. This pass visits every entry so a truncated result never makes a legitimate entity look unowned -- under D-15's fail-closed rule that would refuse a module that did nothing wrong. Its own 1,000-class cap (below) is reported, not enforced: crossing it produces one WARNING naming the jar and the count, and the scan continues to the end of the jar regardless.Independent of
loadPluginMainClass(java.lang.ClassLoader, java.io.File)(plan 17-30): that method no longer scans the jar at all -- it loads exactly the one classplugin.yml'smain:key names. This method still must load every class in the jar to see whether it carries@Table, so a class that exists only for an optional soft-dependency plugin (a PlaceholderAPI expansion, say) still fails to link here, on a server where that optional plugin is absent, and is still recorded as one "skipped class" the same way it always was -- plan 17-30 removes the main-class-discovery instance of that noise, not this one. A module whose only jar-wide scan is this one may still log a "skipped N class(es)...built against an older API" SEVERE line for a class that has nothing to do with age; narrowing that further needs a decision on whether an entity scan may also skip classes outside a declared scan root, which plan 17-30 does not make.- Parameters:
pluginJar- the module's own jar file- Returns:
- every
@Table-annotated class found, never null
-
classNameForEntityScan
Converts oneJarEntryfromscanEntitiesInJar(java.io.File)into a dotted class name, ornullwhen the entry is not a class file the entity scan cares about (aMETA-INFentry, or anything not ending in.class).- Parameters:
entry- the jar entry under inspection- Returns:
- the dotted class name, or null to skip the entry
-
resolveEntityClass
private static Optional<Class<?>> resolveEntityClass(String className, String moduleName, List<String> softDepend) LoadsclassNameand returns it wrapped in anOptionalwhen it carries@Table, forscanEntitiesInJar(java.io.File)'s per-entry step. Never throws: a class that cannot be loaded, or is loaded but is not an entity, both resolve toOptional.empty().- Parameters:
className- the dotted class name to loadmoduleName- the D-19 diagnostic identifier for the enclosingscanEntitiesInJar(java.io.File)'s jar, threaded through so this method's own catch block can accumulate into the correct modulesoftDepend- the scanning module's own declaredplugin.ymlsoftdepend:list (UltiEconomy#20); may be empty, nevernull- Returns:
- the loaded class if it is a
@Tableentity, otherwise empty
-
isAbsentSoftDependClasspathGap
UltiEconomy#20: decides whetherfailure, caught while loading a class during a module's entity scan, is fully explained by an optional integration the module itself declared -- a class whose declared type hierarchy references a plugin API type, where that plugin (a) is named in the module's ownplugin.ymlsoftdepend:list and (b) is not currently installed, or installed but not enabled, on this server.Bukkit's own
softdepend:contract is precisely "this plugin may legitimately be absent; the dependent must tolerate it" (seePluginDescriptionFile#getSoftDepend()); Spring Boot's@ConditionalOnClass/ClassUtils#isPresent(String, ClassLoader)treats an optional integration class's absence the same way -- a routing condition, not a defect. Neither precedent attributes an arbitrary missing-class failure to a specific optional dependency by guessing at a hardcoded package-name convention, and this method does not either -- that kind of fragile string-matching heuristic is exactly what GEN-07 removed fromSecurityPolicy. Instead this only ever downgrades a failure when ALL of:failureis specifically aNoClassDefFoundErrornaming a well-formed internal class name -- the scanned class itself WAS found (physically present in this module's own jar) and failed only because a type it references could not be found. Codex review (PR #551, round 1, P2):LinkageErroris a much broader family --VerifyError,ClassFormatError,UnsupportedClassVersionErrorand others all indicate corrupt or incompatible bytecode, not a missing optional type, and their messages are prose, not a class name --extractMissingClassName(java.lang.Throwable)would misparse the first word of that prose as a "missing class", almost certainly fail the availability check, and silently swallow a genuinely broken module into FINE. Narrowing toNoClassDefFoundError(and, insideextractMissingClassName(java.lang.Throwable), further rejecting a "wrong name" mismatch message) keeps every otherLinkageErrorsubtype at SEVERE. A bareClassNotFoundExceptionfor the scanned class's own name always means the module's own jar entry could not be loaded at all, never "an optional dependency is absent", and is never downgraded either;- the module declares at least one plugin under
softdepend:; - at least one of those declared plugins is not currently installed, or installed but not enabled; and
- the specific missing type
failurenames is not otherwise resolvable through any OTHER plugin that is currently installed and enabled on this server -- guarding against misclassifying a class that in fact belongs to something that IS present.
softdepend:at all, or whose every declaredsoftdepend:plugin is actually present and enabled, always stays SEVERE viaModuleScanDiagnostics-- this cannot silently swallow a module's own broken reference merely because it happens to run alongside some unrelated, genuinely-installed optional plugin.- Parameters:
failure- the exception caught while loading the scanned classsoftDepend- the scanning module's own declaredsoftdepend:list; may be empty, nevernull- Returns:
truewhen this failure should be logged at FINE and excluded from the module's SEVERE skipped-class summary
-
isPluginAbsentOrDisabled
- Parameters:
pluginName- a name from a module's declaredsoftdepend:list- Returns:
trueif that plugin is not currently installed, or installed but disabled
-
extractMissingClassName
Extraction of the internal (dot-qualified) name of the classfailurereports missing -- only for aNoClassDefFoundErrorwhose message is a well-formed bare internal class name; every other case (a differentLinkageErrorsubtype, a blank message, or a message shaped like the "wrong name" mismatch detail) returnsnullrather than guess, perINTERNAL_CLASS_NAME_PATTERN's own javadoc.- Parameters:
failure- the exception caught while loading a scanned class- Returns:
- the missing class's dotted name, or
nullif it cannot be determined
-
isClassAvailableFromAnyEnabledPlugin
WhethermissingClassNameis physically present in any currently installed AND enabled plugin's own jar -- i.e. whether the type aLinkageErrornamed as missing is, in fact, available somewhere on this server right now. Reads each candidate plugin's own jar entries directly viaresolveOwnJarFile(Class)rather than attempting to load the class through that plugin's own classloader, which would require assuming a specific cross-plugin classloader delegation topology this check has no need to depend on.- Parameters:
missingClassName- the dotted class name reported missing- Returns:
trueif some other enabled plugin's own jar carries that class
-
resolveOwnJarFile
Best-effort resolution of the jar fileaClasswas loaded from, via itsCodeSource. Never throws; returnsnullwhen the code source is unavailable or does not point at a real jar (e.g. exploded test classes) -- the same "best effort, class-name fallback" postureUltiToolsPlugin.resolveJarFileNameForError()already uses for the same lookup.- Parameters:
aClass- the class whose jar should be resolved- Returns:
- the jar file, or null if it cannot be resolved
-
scanPluginEntities
private Set<Class<?>> scanPluginEntities(String pluginName, Class<? extends UltiToolsPlugin> pluginClass) Builds the entity set aDataScopeforpluginshould carry (D-19): every@Tableclass found by scanning the module's own jar, plus anything declared via@UltiToolsModule#additionalEntities()for entities that legitimately live elsewhere. Never throws -- a plugin whose jar cannot be resolved (e.g. a directly-constructed test instance with no real jar) simply gets an entity set built from its declaredadditionalEntities()alone, which is empty by default.Takes the plugin's class rather than an instance -- everything this reads (the JAR's own
CodeSourceand the class-level@UltiToolsModuleannotation) is a class-level fact, andinitializePluginalready haspluginClassin hand before the instance exists.Every class named in
additionalEntities()is validated (02-14) before being folded in -- see the private provenance-check method below this one. A plugin can declare an entity that legitimately lives outside its own JAR (D-19's purpose), but not one that structurally belongs to a different, already-known module.- Parameters:
pluginName- the plugin's own name, for the refusal message and the ownership-conflict checkpluginClass- the plugin whose scope is being minted- Returns:
- the entity set for that plugin's scope
- Throws:
PluginModuleException- fail-closed, whenadditionalEntities()names an entity that does not live on this plugin's own classpath (02-14)
-
scanExternalEntities
private Set<Class<?>> scanExternalEntities(ExternalPluginAdapter adapter, Class<?>[] additionalEntities) Builds the entity set aDataScopefor an external plugin should carry (D-19): every@Tableclass found by scanning the plugin's own jar, plus whatever it declared viaUltiToolsAPI.connect(plugin, additionalEntities).Every class named in
additionalEntitiesis validated (02-14) before being folded in -- see the private provenance-check method belowscanPluginEntities. This is the public, reachable side of D-19:UltiToolsAPI.connect(plugin, additionalEntities...)lands here with zero prior trust established, so the validation matters most on this path.- Parameters:
adapter- the external plugin adapteradditionalEntities- entity classes declared viaconnect(...)- Returns:
- the entity set for that external plugin's scope
- Throws:
PluginModuleException- fail-closed, whenadditionalEntitiesnames an entity that does not live on this plugin's own classpath (02-14)
-
validateAdditionalEntity
private void validateAdditionalEntity(String declaringPluginName, Class<?> declaringClass, File declaringOwnJarFile, Class<?> entityClass) Validates thatentityClass, declared viaadditionalEntities(D-19) bydeclaringPluginName, does not structurally belong to a DIFFERENT, already-known plugin before it is folded into that plugin'sDataScope(02-14 finding: this attribute previously accepted anyClass<?>the caller named, with zero validation -- letting any plugin claim ownership of any other module's real@Tableentity through the publicUltiToolsAPI.connect(plugin, additionalEntities...)surface, or the internal@UltiToolsModule#additionalEntities()equivalent).The discriminator is "does this class belong to a DIFFERENT plugin", not "is this class mine" -- deliberately, since D-19 exists precisely so a module can declare an entity that lives outside its own JAR:
entityClassliving in the SAME physical jar asdeclaringOwnJarFile(a shared library shaded into the declaring plugin's own artifact, or a harmless redundant re-declaration of an entity the automatic scan already found) -- legitimate, accept.entityClassalready recorded inentityOwnershipunder a DIFFERENT plugin name -- refuse, naming the confirmed owner (matchesDataScope.refusalFor's message shape).entityClass's own jar is itself the own jar of a DIFFERENT, already-discoveredUltiToolsPluginmodule (pluginClassList, populated byinit()'s upfront jar scan BEFORE any individual plugin registers) or of a currently-loaded Bukkit plugin (Bukkit.getPluginManager().getPlugins(), populated by Bukkit's own plugin loading, independent of whether that plugin has calledUltiToolsAPI.connect(...)yet) -- structurally confirmed as belonging to a different plugin, refuse. This is what closes the first-registration-wins windowentityOwnership'sputIfAbsentsemantics alone cannot: it does not depend on which plugin happened to register first, only on which jar actually defines the class.- Anything else --
entityClasslives outside the declarer's own jar but is not structurally tied to any other known plugin, and no registry conflict exists -- a genuine shared-library / multi-module common artifact, D-19's stated purpose. Accept.
UltiToolsPluginmodule is loaded through ONE sharedURLClassLoadercovering the wholeplugins/folder (seeinit(ClassLoader)), so two different internal modules' classes always report the same classloader -- using that as a signal would make this check a no-op for the internal path, silently failing to close the exact vulnerability it exists to close. Jar identity (viajava.security.CodeSource) remains meaningful even when many jars share one classloader, which is why it is the signal used here instead.A residual gap, disclosed rather than silently accepted: an attacker who can obtain a byte-identical copy of another plugin's entity class (e.g. by depending on its published jar at build time) and shade that copy into their own jar would pass this check -- from the JVM's perspective it genuinely is a different, attacker-owned
Classobject, structurally indistinguishable from a legitimate shared-library entity. This method validates class PROVENANCE (which jar/module actually defined this exact class), not table name collision; a plugin that simply writes its own class with a matching@Tablename never reaches this method at all (noadditionalEntitiesdeclaration involved), and is a pre-existing, out-of-scope risk inherent to a single shared MySQL DataSource across all plugins -- not the vulnerability 02-SECURITY.md described or this plan closes.- Parameters:
declaringPluginName- the plugin declaringentityClassdeclaringClass- the declaring plugin's own main/adapter class, used to look it up inpluginClassListand excluded from the "belongs to another known plugin" checkdeclaringOwnJarFile- the declaring plugin's own resolved jar, ornullwhen unresolvable (e.g. a directly-constructed test instance)entityClass- the entity class named inadditionalEntities- Throws:
PluginModuleException- fail-closed (D-15), naming the declarer, the entity, and the actual owner where one is known
-
belongsToAnotherKnownPlugin
WhetherentityJarFileis itself the own jar of some plugin OTHER thandeclaringClass-- checked against every internal moduleinit()has already discovered (pluginClassList, regardless of load order) and every Bukkit plugin currently loaded (Bukkit.getPluginManager().getPlugins(), regardless of whether it has calledUltiToolsAPI.connect(...)yet). See the caller directly above this method for the full reasoning.- Parameters:
entityJarFile- the entity's resolved own jar, never nulldeclaringClass- the declaring plugin's own class, excluded from the comparison- Returns:
- true if a different, already-known plugin's own jar matches
-
additionalEntityRefusalMessage
private static String additionalEntityRefusalMessage(String declaringPluginName, Class<?> entityClass, String owner) Builds the refusal message for anadditionalEntitiesdeclaration that does not live on the declarer's own classpath -- the same "confirmed owner" vs. "no confirmed owner" distinctionDataScope.refusalFor(Class)makes (D-15), applied at declaration time instead of data-access time.- Parameters:
declaringPluginName- the plugin whose declaration is refusedentityClass- the entity class that was declaredowner- the confirmed owning plugin's name, ornullwhen none is known- Returns:
- the refusal message
-
registerEntityOwnership
Recordsscope's owned entities inentityOwnership, so a later ownership refusal (D-15) can name the actual owning module instead of only saying "not registered to you". Called every time aDataScopeis minted, for both internal modules and external plugins.- Parameters:
scope- the scope just minted
-
findOwningPlugin
Looks up which loaded plugin ownsentityClass, perentityOwnership.- Parameters:
entityClass- the entity class to look up- Returns:
- the owning plugin's name, or
nullif no loaded scope owns it
-
registerExternalScope
Records an external plugin'sDataScopeunder its data folder's canonical path, sofindScopeForDataFolder(java.io.File)can resolve the caller ofDataStore.getOperator(File, Class)back to a scope (D-18) -- the same canonical-path keying 02-02's Task 2 used for the SQLite pool map, so two spellings of the same folder resolve to one entry.Refuses (02-14 Task 2) rather than silently replacing when the canonical path already maps to a DIFFERENT plugin's scope --
externalScopesByFolderused a plain.put()before this, so wrapping another plugin (all public calls --new ExternalPluginAdapter(Bukkit.getPluginManager().getPlugin("Victim"))) and calling the publicregisterExternalwould displace the victim's legitimate scope here, poisoning the D-18 reverse lookupcheckOwnership(File, Class)depends on. Re-registration by the SAME plugin (matched by name) is idempotent, not refused -- a plugin reconnecting afterunregisterExternal(ExternalPluginAdapter)must not be permanently locked out.- Parameters:
dataFolder- the external plugin's own data folderscope- the scope just minted for it- Throws:
PluginModuleException- fail-closed, whendataFolderalready maps to a DIFFERENT plugin's registered scope (02-14)
-
findScopeForDataFolder
Resolves a data folder back to the registered external plugin scope that owns it (D-18).- Parameters:
dataFolder- the folder to resolve- Returns:
- the registered scope, or
nullif the folder matches no registered adapter
-
unregisterExternalScope
Removes a data folder's registered external plugin scope (02-14 Task 2), so a subsequentregisterExternal(ExternalPluginAdapter, Class[])for the same folder -- a legitimate reconnect -- is not refused byregisterExternalScope(java.io.File, com.ultikits.ultitools.manager.DataScope)'s displacement guard as if it were a different plugin still holding the folder.- Parameters:
dataFolder- the folder whose registration should be cleared
-
canonicalPath
-
passesCompatibilityGates
Compatibility gates that must run before the module's bean graph is built.These two gates originally ran after
initializePlugin, i.e. after the container'srefresh()-- the whole bean graph plus@PostConstruct-- had already completed. The consequence of that inversion landed exactly on the one scenario where it mattered: a module compiled against a newer API, precisely the kind of module whose beans reference nonexistent methods, would die insiderefresh()first and get reported by the generic catch block as a plain initialization failure -- so the friendly "UltiTools version too old" message never got the chance to be said. See issue #184.The gates only read metadata sourced from
plugin.yml(api-version/main/version), which is already in place once the instance is constructed and needs no container.- Parameters:
plugin- freshly constructed plugin instance, context not yet attached- Returns:
- true if the module may proceed to context construction
-
hasNewerVersionLoaded
Determines whether a newer version of the same module is already loaded. **Determination only -- never modifies an already-loaded module.**Unloading the old version this load supersedes is a separate concern, see
unregisterSupersededVersions(com.ultikits.ultitools.abstracts.UltiToolsPlugin). The two are split apart because the determination must happen ahead of bean construction, while the unload cannot -- unloading early would turn "the new version failed to initialize" into "the server is left with neither version". -
unregisterSupersededVersions
Unloads the old version this load supersedes, through the fullunregister(UltiToolsPlugin)path (#506): its tasks are cancelled, its container closed, its registrations released and it is delisted. When the incoming copy shares its name, it has already registered under it, so only the registrations recorded against the old instance are released and name-only ones stay with the incoming copy; a successor under a different name leaves the old name unused, and its name-only registrations are released too.Each old copy's unload is isolated (#528): a failure thrown by its unload hook is logged against that copy's name and version and does not abort the incoming registration. The isolation is
close()'s: everyExceptionorErrorcounts. Rethrowing anErrorhere would not abort anything cleanly --attemptPluginRegistration's own handler would take it, blame the incoming version and leave neither copy listed.May only be called after the new module's
registerSelf()returns true -- not "after the container is built". The two are one step apart: the container being built only means the bean graph could be constructed, the module is not yet active; and whenregisterSelf()returns false or throws, the caller closes the new container. If the old version had already been unloaded before that point, the module would be left with neither version -- while it was originally running fine.Registrations (#562): every registration recorded against an old copy -- panel responders, EventBus handlers, tab completers -- was already released before the new copy's container refresh, by
releaseSupersededRegistrations(UltiToolsPlugin), so the new copy could claim the types and keys the old copy held; had the new copy failed, they would have been given back to the old copy, which keeps running. The order is therefore: release the old copies' registrations, refresh the new copy's container and run itsregisterSelf(), then either unload the old copies here or, on failure, release what the new copy registered and restore the old copies' registrations. At no point do two copies hold the same type or key, and a copy never keeps a registration recorded against another copy: every release and restore matches the recorded instance, not the shared name. Which copy a registration is recorded against is decided when it is made: a registration made by name outside every load scope -- in the new copy's constructor, for example -- is recorded against the copy listed under that name at the time, which during a replacement is the old copy (seefindRegistrationOwner(String)). -
copiesSupersededBy
The loaded copiespluginsupersedes: same main class, older version. The same determinationunregisterSupersededVersions(UltiToolsPlugin)unloads by. -
releaseSupersededRegistrations
Releases every registration recorded against each copyincomingsupersedes -- panel responders, EventBus handlers, tab completers -- beforeincoming's container refresh andregisterSelf()(#562). Called only after the compatibility gates acceptedincoming. From here untilincomingactivates or fails, the superseded copies hold no registration, soincomingcan claim the panel message types and completer keys they held, and nothing it publishes while it loads reaches their code. Each registry hands back the action that restores what it released; they are kept untildiscardSupersededRegistrations(UltiToolsPlugin)orrestoreSupersededRegistrations(UltiToolsPlugin). Registrations filed under a module name only are not touched: they belong to whichever copy is listed under that name. -
discardSupersededRegistrations
incomingactivated: the registrations released from the copies it supersedes stay released, and those copies are unloaded next (#562). -
restoreSupersededRegistrations
incomingfailed to load: first releases every registration recorded againstincomingitself in the three registries -- what it registered during its container refresh andregisterSelf()must not stay behind, answering from its closed container -- then, when it was superseding loaded copies, gives them back the registrations released before its container refresh (#562). Those copies were never unloaded, so they keep running as before. Each step is isolated: a failure is logged and the next step still runs. Safe to call more than once and for a copy that superseded nothing. -
isUltiToolsVersionCompatible
-
attemptPluginRegistration
-
onPluginRegistered
-
registerModuleEventHandlers
Scan a bean for @ModuleEventHandler methods and register them with the EventBus. -
wireAop
Attaches AOP to a plugin container.Must be called before
SimpleContainer.refresh(): the resolver participates in bean instantiation, so beans created earlier are never proxied.@ExceptionCatchand@Transactionalare both wired in this release, on all three backends.DataStorenow exposes the plugin'sDataSourceviagetDataSource(DataScope), keyed by database file rather than by entity class, and this method builds oneTransactionManagerper plugin container from it and registers aTransactionInterceptoradvisor for that manager, for the two JDBC backends (SQLite, MySQL). Issues #195 and #196 are both addressed for the JDBC backends. The JSON backend is served by the per-plugin JSON transaction manager instead (02-05, D-03), obtained fromJsonStorewhengetDataSource(DataScope)throws. As of 6.3.0 it stages each transaction's writes apart from committed data, isolates them with table locks, and commits them atomically and durably through the plugin's journal, so the writes a@Transactionalmethod makes to several of the plugin's tables commit or roll back as one unit, with the propagation rules of the JDBC backends. One difference is documented: table locks belong to the thread, so a same-threadREQUIRES_NEWorNOT_SUPPORTEDbody does not wait for the locks of its suspended caller, and when it writes a key the caller has staged, the caller's later commit is the value that stays. Only aDataStorethat can supply neither aDataSourcenor its ownTransactionManagerstill declares@Transactionalunavailable.Scope limit 1 —
registerSingletonobjects are never proxied, but no longer silently unprotected. Only beans the container constructs itself (throughregisterBeanor component scanning) are ever offered to the resolver, becauseAopProxyResolver.resolve(Class)only runs on the constructor branch of bean creation. The plugin instance itself, an@ContextEntrybean (built by hand with reflection), config entities,@Configurationclasses, and the beans their@Beanmethods produce are all registered withregisterSingletoninstead, so none of them can ever be proxied: a ByteBuddy proxy is the bean itself in this framework's design, and it cannot be retrofitted onto an object the caller already constructed. Unlike the first three,@Configurationclasses and@Beanmethods are written by module authors rather than the framework. As of 6.3.0 they are no longer constructed beforewireAopruns:assemblePluginContainerwires AOP before the component scan (P-06). They are still never proxied, because they reach the container throughregisterSingleton.As of 6.3.0,
registerSingletonitself refuses to register an instance whose class carries@Transactionalor@ExceptionCatch— method-level or class-level — for exactly that reason: an annotation that can never take effect must fail the load rather than silently do nothing (D-15). An instance that is already a generated proxy is exempt, since it already honours its own annotations.registerSingletonalso now fully assembles (autowires, invokes@PostConstruct, runs theBeanPostProcessorchain) whatever it does accept, unconditionally and regardless ofSimpleContainer.refresh()state (D-14) — it is only proxying, not assembly, that remains structurally impossible here. See issue #308.Class-level scope. A class-level annotation is a default for the methods the annotated class declares, and for its subclasses. It does not apply to ancestors, so a bean does not extend its own annotation over everything it inherits - in particular not over the framework base classes it extends, where a swallowed exception would become a null that resurfaces as an unrelated failure far from its cause. Inherited methods must be locally redeclared to pick up a subclass's annotation. This is the rule Spring documents for a class-level
@Transactional. A method-level annotation is unaffected: it applies wherever the method is declared.Scope limit 2 - inside that scope, some methods are skipped silently. Two sets. First, methods this proxy cannot both override and reach through
super:private,static,final, package-private ones declared in another package, and the erased half of a generic override that a bridge method shadows. Second,equals(Object)/hashCode()/canEqual(Object), where swallowing an exception would replace it with a silent wrong answer - Lombok emits all three onto the annotated class itself, so class-level scope does not keep them out. Both skips are silent by deliberate choice and leave no diagnostic. A method-level annotation is skipped only when the proxy cannot reach the method at all, and never quietly: it is named in a startup warning, the way Spring ignores an annotation on a method it cannot advise. The one thing that still fails the load is a final class, which cannot be subclassed at all.Scope limit 3 - one kind of annotation is never seen. An annotation on an interface default method. The scan walks
getSuperclass()only, so@ExceptionCatchsilently does nothing there and@Transactionalis not even refused. Unchanged from 6.2.x, and not fixed here.- Parameters:
context- the plugin container, before refreshscope- the identity token minted for the caller, used to resolve that plugin'sDataSourcefor the@Transactionaladvisor
-
wireTransactional
private static void wireTransactional(SimpleContainer context, DataScope scope, AopProxyResolver resolver) Builds and registers the@Transactionaladvisor for one plugin container. For a backend whoseDataStoreexposes a JDBCDataSource(SQLite, MySQL), the advisor is bound to aDataSourceTransactionManager-- resolved viaresolveJdbcTransactionManager(com.ultikits.ultitools.interfaces.DataStore, com.ultikits.ultitools.manager.DataScope, javax.sql.DataSource), which for these two shipped backends is the SAME instance the store itself wires onto theDataOperators it hands out (02-09, T-02-TAM-11), not an independent second one. For the JSON backend, it is bound to the per-pluginJsonTransactionManagerinstead (02-05, D-03), obtained from the store itself, which stages writes per transaction, isolates them with table locks, and commits them atomically and durably through a journal. ADataStorethat can supply neither still declares the annotation unavailable, naming the configured backend.- Parameters:
context- the plugin container, before refreshscope- the identity token used to resolve the plugin'sDataSourceresolver- the resolverwireAopis assembling
-
resolveJdbcTransactionManager
private static JdbcTransactionManager resolveJdbcTransactionManager(DataStore dataStore, DataScope scope, DataSource dataSource) Resolves theJdbcTransactionManagerwireTransactional(com.ultikits.ultitools.context.SimpleContainer, com.ultikits.ultitools.manager.DataScope, com.ultikits.ultitools.aop.AopProxyResolver)should bind the AOP interceptor to, for a JDBC-capabledataStore(02-09, T-02-TAM-11).For the two backends this framework ships,
dataStoreitself owns a per-identity registry of these managers --SQLiteDataStore.transactionManagerFor(DataScope)/MysqlDataStore.transactionManagerFor(DataScope)-- and everygetOperatorcall on that same store resolves a manager through the identical registry before handing an operator to a module author. Calling that method here, instead of constructing a freshnew DataSourceTransactionManager(dataSource), is what makes the two paths converge on one shared instance: a@Transactionalmethod that callsDataOperator.insertAlljoins the same transaction the interceptor already opened, rather than opening an independent second one on its own connection (a defect worse than no transaction at all, because it looks correct).A third-party
DataStorethat supplies a JDBCDataSourcebut is neither of the two concrete types this framework ships has no such registry to share -- this method falls back to a manager scoped only to the AOP interceptor, exactlywireTransactional's pre-6.3.0 behavior. ADataOperatorthat store hands out separately (if any) would not join this manager's transactions; there is no seam on theDataStoreinterface (out of this plan's file scope) to close that gap generically.- Parameters:
dataStore- the storewireTransactional(com.ultikits.ultitools.context.SimpleContainer, com.ultikits.ultitools.manager.DataScope, com.ultikits.ultitools.aop.AopProxyResolver)resolved aDataSourcefromscope- the identity token used to resolve the managerdataSource- theDataSourcedataStore.getDataSource(scope)returned, used only for the third-party fallback- Returns:
- the
JdbcTransactionManagerto bind the interceptor to
-
wireJsonTransactional
private static void wireJsonTransactional(SimpleContainer context, DataScope scope, AopProxyResolver resolver, JsonStore jsonStore) Builds and registers the@Transactionaladvisor for a plugin container backed by the JSON store, oncewireTransactional(com.ultikits.ultitools.context.SimpleContainer, com.ultikits.ultitools.manager.DataScope, com.ultikits.ultitools.aop.AopProxyResolver)has determineddataStorecannot supply a JDBCDataSourcebut is aJsonStore(02-05, D-03).The advisor is bound to the store's
JsonTransactionManagerfor this plugin, the JSON transaction engine: it stages a transaction's writes per operator, locks each table the transaction writes against other writers until it ends, and, at a commit that changed something, forces one journal record to disk before it makes every staged write visible at once. The same manager serves every operator the store hands out for the plugin, so a@Transactionalmethod's writes to several of the plugin's tables form one transaction.- Parameters:
context- the plugin container, before refreshscope- the identity token used to resolve the managerresolver- the resolverwireAopis assemblingjsonStore- the JSON store to obtain the per-identity manager from
-
releaseConfigEntities
-
initializePlugin
private UltiToolsPlugin initializePlugin(ClassLoader classLoader, Class<? extends UltiToolsPlugin> pluginClass) Initialize module using its default (zero-argument) constructor. This is the live, undeprecated construction path --register(Class)calls it directly.- Parameters:
classLoader- Class loaderpluginClass- Plugin class- Returns:
- the initialized module, or
nullif a compatibility gate rejected it
-
finishInitializingPlugin
private UltiToolsPlugin finishInitializingPlugin(ClassLoader classLoader, Class<? extends UltiToolsPlugin> pluginClass, UltiToolsPlugin plugin) Run the construction-independent second half afterinitializePlugincompletes construction: the compatibility gate, then container assembly.- Parameters:
classLoader- Class loader to attach to the assembled containerpluginClass- Plugin classplugin- the already-constructed plugin instance- Returns:
- the initialized module, or
nullif a compatibility gate rejected it
-
assemblePluginContainer
private void assemblePluginContainer(SimpleContainer pluginContext, UltiToolsPlugin plugin, Class<? extends UltiToolsPlugin> pluginClass, ClassLoader loader) Assemble a plugin's container. Bothregister(UltiToolsPlugin)andinitializePlugin(java.lang.ClassLoader, java.lang.Class<? extends com.ultikits.ultitools.abstracts.UltiToolsPlugin>)route through this one method, so a capability added to either path is never silently missing from the other (WIRE-05, WIRE-06, #203/#326).First-assembly order (decision P-06):
- container setup:
setContext, display name, parent, shutdown hook, class loader; registerType(UltiToolsPlugin), which the component scan's conditional evaluation reads;- mint the
DataScopeand hand it to the plugin, thenwireAop(com.ultikits.ultitools.context.SimpleContainer, com.ultikits.ultitools.manager.DataScope); - open the load scopes and, inside them: the config entities made resolvable by type,
the component scan, the config entities assembled and registered by name, the
plugin's own singleton registration, the static instance field, the global
entity-ownership record,
refresh(), and@ContextEntrybean registration withautowireBean; then close the load scopes; - the command-executor contract and config-binding validations.
refresh()can each create beans --registerSingletonfully assembles its argument (D-14), and autowiring creates whatever it injects. So the data scope, AOP and the load scopes are in place before the first of them runs, and the config entities are resolvable by type before the scan, so a configuration class or@Beanproduct the scan creates can inject them. A config entity itself is assembled (autowired,@PostConstruct) only after the scan, once every definition of the module exists, so it can inject the module's own services, as it could in 6.2.5. A bean created beforewireAopcould never be proxied afterwards (C08); one created before the config entities or the data scope could not see them (C09); a registration made outside the load scopes, the@ContextEntrybean's included, would not be released with this module instance (C62). The static instance field and the global entity-ownership record keep their place after the scan and the plugin singleton and beforerefresh(), so the window in which a refused module leaves them behind is not widened.- Parameters:
pluginContext- a freshly created, not-yet-refreshed containerplugin- the plugin instancepluginClass- the plugin's classloader- the classloader to attach topluginContext-- deliberately a parameter rather than reading thePluginManager.classLoaderfield directly, soinitializePlugincan pass ITS OWNclassLoaderparameter (which may differ from the field, e.g. under test) exactly as it did before this method was extracted
- container setup:
-
registerByOwnType
Bindsinstanceto its own class incontext's type registry, without assembling it, sogetBean(Class)and by-type@Autowiredresolve it. -
validateCommandExecutorContracts
Refuses to load a module whoseBaseCommandExecutorbean declares@CmdCDor@UsageLimit-- on a@CmdMappingmethod or on the executor class itself -- while its OWN validator chain (the exact chainonCommanddispatches through, not a freshly built default one) holds no instance of the corresponding validator type (SILENT-11 / D-01 half 2, D-04). EveryCommandExecutorbean inpluginContextis checked; a bean that is not aBaseCommandExecutor(the legacyAbstractCommandExecutorgeneration -- removed in 6.3.0 -- never had aValidatorChainat all) is skipped.This is a STRUCTURAL check only -- it asks whether the required validator TYPE is present in the chain, never whether a given invocation would actually be blocked. No opt-out exists (D-04): a declared cooldown or usage limit that cannot be enforced is always a module-author bug, and Phase 3 D-08's module-granularity isolation -- this refusal alone fails the offending module, every other module in the same load pass still completes -- is the accepted escape hatch.
- Parameters:
pluginContext- the just-assembled, just-refresh()ed container to scan forCommandExecutorbeans- Throws:
PluginModuleException- fail-closed (D-01, D-04), naming the offending class and, when known, the offending mapping method
-
validateCommandExecutorContract
The per-executor half ofvalidateCommandExecutorContracts(SimpleContainer)-- package-private, not private, soPluginManagerCommandContractTest(same package) can drive it directly with a hand-built fixture, without reflection orsetAccessible(true). Mirrors the test-seam rationale already used forlogPluginInitializationFailure(String, Throwable): this is notpublic, so it never enters the published surface.@UsageLimit(LimitType.NONE)is exempt -- it declares no limit to enforce, so requiringUsageLockValidatorfor it would refuse a legitimate no-op declaration.- Parameters:
executor- the already-constructed executor instance- Throws:
PluginModuleException- fail-closed (D-01, D-04), naming the offending class and, when known, the offending mapping method
-
unenforceableCommandAnnotationMessage
private static String unenforceableCommandAnnotationMessage(Class<?> executorClass, Method method, Class<? extends Annotation> annotationType, Class<? extends CommandValidator> requiredValidatorType) Builds the refusal message forvalidateCommandExecutorContract(BaseCommandExecutor), naming the offending class and, whenmethodis non-null, the offending mapping method -- mirroringadditionalEntityRefusalMessage(String, Class, String)'s message-builder shape.- Parameters:
executorClass- the offending executor classmethod- the offending mapping method, ornullfor a class-level declarationannotationType- the declared annotation (@CmdCDor@UsageLimit)requiredValidatorType- the validator type the chain is missing- Returns:
- the refusal message
-
validateSuggestKeysForMethod
Validate constructor arguments for security. /** D-07's load-time half of@CmdParam.suggest(05-06 Task 2) -- formethod's@CmdParamparameters, asuggest()value beginning with@must name a key already registered inTabCompletionManagerat the moment this check runs, i.e. AFTER the module's ownpluginContext.refresh()(and therefore after any completer the module registers during its own@PostConstruct). Called from insidevalidateCommandExecutorContract(BaseCommandExecutor)'s existing@CmdMappingmethod walk -- not a second pass -- so the two checks' ordering stays in one place (D-01, D-04, D-07).A
suggest()value that does NOT start with@is out of scope here entirely -- including one naming a method that does not exist -- because that case keeps the published i18n hint-text fallback (CmdParam.java's javadoc), which D-07 leaves deliberately unchanged. No opt-out (D-04): an unknown@keyis always a module-author bug, and module-granularity isolation -- this refusal alone fails the offending module, every other module in the same load pass still completes -- is the accepted escape hatch.- Parameters:
executorClass- the executor classmethodbelongs to, for the refusal messagemethod- the@CmdMappingmethod whose parameters are checked- Throws:
PluginModuleException- fail-closed (D-07), naming the class, the method and the key
-
unknownSuggestKeyMessage
Builds the refusal message forvalidateSuggestKeysForMethod(Class, Method), naming the offending class, method and key -- mirroringunenforceableCommandAnnotationMessage(Class, Method, Class, Class)'s message-builder shape.- Parameters:
executorClass- the offending executor classmethod- the offending mapping methodkey- the unknown@key- Returns:
- the refusal message
-
validateSuggestMethodSignatureForMethod
private static void validateSuggestMethodSignatureForMethod(BaseCommandExecutor executor, Class<?> executorClass, Method method) T-05-fix Part 2's load-time half of method-name@CmdParam.suggest()resolution: asuggest()value that does NOT start with@names a method looked up EXACTLY asMethodInvocationCompleter.complete(TabCompletionContext)looks it up at completion time --MethodInvocationCompleter.getSuggestMethodsByName(Object, String)checks the executor's own class first, then any@CmdSuggest-referenced classes.A real-machine UAT run on Paper 1.21.11 caught
MethodInvocationCompleter .invokeSuggestMethodfalling into a finalelsebranch that invoked ANY unrecognized signature with ZERO arguments -- silently wrong, not a failure, so it surfaced only asIllegalArgumentExceptionthe first time a player pressed Tab. 16 of 24 real downstream@CmdParam(suggest=)call sites (every UltiWorlds suggest method, shaped(Player, String)) were broken this way. This check closes the gap at its source: if the resolved suggest method's signature is not one of the five shapesMethodInvocationCompleter.isInvocableSuggestSignature(Class[])accepts, the module is refused at load -- naming the class, the mapping method and the offending signature -- instead of loading cleanly and failing only on first invocation.A
suggest()value that resolves to NO method at all is out of scope here, exactly as it is forvalidateSuggestKeysForMethod(Class, Method)-- that keeps the published i18n hint-text fallback, deliberately unchanged (D-07).MethodInvocationCompleter.complete(TabCompletionContext)only ever invokes the FIRST methodgetSuggestMethodsByNamereturns when a name resolves to more than one overload, so only that first method's signature is validated here -- validating every overload would refuse a module for an overload that is never actually called.- Parameters:
executor- the already-constructed executor instance, needed for the SAME class hierarchy /@CmdSuggestlookupMethodInvocationCompleterperforms at completion timeexecutorClass- the executor classmethodbelongs to, for the refusal messagemethod- the@CmdMappingmethod whose parameters are checked- Throws:
PluginModuleException- fail-closed, naming the class, the method and the offending signature
-
uninvocableSuggestMethodMessage
private static String uninvocableSuggestMethodMessage(Class<?> executorClass, Method mappingMethod, Method suggestMethod) Builds the refusal message forvalidateSuggestMethodSignatureForMethod(BaseCommandExecutor, Class, Method), naming the offending class, the mapping method and the offending suggest method's declaring class, name and parameter signature -- mirroringunknownSuggestKeyMessage(Class, Method, String)'s message-builder shape.- Parameters:
executorClass- the offending executor classmappingMethod- the offending@CmdMappingmethodsuggestMethod- the resolved suggest method with the uninvocable signature- Returns:
- the refusal message
-
formatParameterTypes
FormatsparamTypesas a Java-source-like parameter list, e.g."(int)"or"(Player, String)", foruninvocableSuggestMethodMessage(Class, Method, Method).- Parameters:
paramTypes- the parameter types to format- Returns:
- the formatted parameter list, including the enclosing parentheses
-
registerBukkit
Register bukkit commands or listeners.Both registration entry points (
register(Class)andregister(UltiToolsPlugin)) now resolve commands and listeners as beans from the module's own container -- there is no longer a package-scanning branch here.manualRegister()and@ConditionalOnConfigare therefore honoured identically no matter which entry point loaded the module (WIRE-05 differences #6-#9, plan 04-08), and a command class extending the currentBaseCommandExecutorcan no longer reachCommandManager's legacy, castingregisterAll(UltiToolsPlugin, String)overload on either path (issue #272). The package-scanning overloads onCommandManagerandListenerManagerare retained -- both@Deprecated(forRemoval = true)-- for downstream callers only.- Parameters:
plugin- UltiTools module instance
-
setPluginStaticInstance
private void setPluginStaticInstance(Class<? extends UltiToolsPlugin> pluginClass, UltiToolsPlugin plugin) Set the static instance field on a plugin class before @PostConstruct runs. Most modules follow the pattern: private static XxxPlugin instance; This enables @PostConstruct methods to call Xxx.getInstance().getDataOperator() etc.- Parameters:
pluginClass- the plugin classplugin- the plugin instance
-
getPluginScanPackages
Get scan packages for a plugin class. Reads from @UltiToolsModule or @ComponentScan annotations, defaults to plugin class package.Widened from
privatetopublicby 16-06-PLAN.md (D-08):EconomyUtils' module-attribution helper needs the same real scan-package resolution this class already uses at registration time, rather than duplicating (and risking drifting from) this logic in another package. A new public method is additive, not a binary-compatibility break — japicmp reports only removed or changed members, never a widened one.- Parameters:
pluginClass- plugin class- Returns:
- scan packages
-
sortPluginsByDependencies
private List<Class<? extends UltiToolsPlugin>> sortPluginsByDependencies(List<Class<? extends UltiToolsPlugin>> plugins) Sort plugins by their dependencies using Kahn's algorithm (topological sort).A dependency cycle or a missing hard dependency no longer degrades every module to filesystem order (SILENT-08/D-10): only the refused modules are absent from the returned list; every other module still loads. A missing hard dependency refuses the declaring module and every module that transitively
dependson it (#676), never a module that only softDepends on, is named by a loadBefore of, or lists in plugin.yml loadAfter a refused module. Anything the sort cannot place - a cycle plus every module ordered after it - is refused as a cycle. When both occur in one start, the console names the missing dependencies, each cycle as a Loop path and every module not loaded. The pre-6.3.0 all-unsorted behaviour survives only as an explicit, cost-stating opt-in viaultitools.useLegacyPluginLoading.- Parameters:
plugins- list of plugin classes to sort- Returns:
- sorted list of plugin classes
-
logCycleDiagnosis
Logs the cycle detection line, then one Loop line and one author-hint line per cycle path. Shared by the circular-dependency handler and the mixed missing-plus-cycle branch of the missing-dependency handler so both print the same lines in the same order. Module names are concatenated, never used as a format string.- Parameters:
cyclePaths- each cycle as a list of plugin names whose first and last entries match
-
registerExternal
Register an external Bukkit plugin adapter with the framework. Creates an IoC container, scans for annotated components, and registers commands/listeners/tasks.- Parameters:
adapter- the external plugin adapter- Since:
- 6.2.2
-
registerExternal
Register an external Bukkit plugin adapter with the framework, declaring entity classes that legitimately live outside the plugin's own JAR (D-19), in addition to the entities found by scanning the plugin's own JAR for@Tableclasses.- Parameters:
adapter- the external plugin adapteradditionalEntities- entity classes owned by this plugin that live outside its own JAR- Since:
- 6.3.0
-
getConnectedExternalScanPackages
The connected External Plugin API consumers, plugin name to scan package (#462) -- a copy, safe to read from any thread. Not part of the module-facing API; public only because economy attribution lives in another package.- Returns:
- plugin name to scan package, for every external plugin registered and not yet unregistered
- Since:
- 6.3.0
-
registerOwnType
Register an object intocontextkeyed by its own concrete runtime class, in addition to whatever declared-type registration the caller has already done. Needed becauseSimpleContainer.registerType(Class, T)keys by exactClass: a constructor parameter declared as the connector's own concrete plugin class (rather than the generalJavaPlugintype) would otherwise never resolve (SILENT-16, #331).The unchecked cast is safe:
instance.getClass()is always assignable to itself, so capturing it asClass<T>for the sameinstanceof static typeTnever mismatches at runtime.- Type Parameters:
T- the object's static type- Parameters:
context- the container to register intoinstance- the object to register under its own concrete class
-
recordNewEntityOwnership
Recordsscope's owned entities inentityOwnershipwhere no owner is recorded yet, adding each entity it recorded toadded-- the records a refused registration may undo (#537, gate 1). Adds to the caller's list as it goes, so an exception midway still leaves the caller knowing what was recorded. -
unwindRefusedExternal
private void unwindRefusedExternal(ExternalPluginAdapter adapter, SimpleContainer context, DataScope scope, List<Class<?>> ownershipAdded) Undoes whatregisterExternal(ExternalPluginAdapter, Class[])registered before a refusal after the folder scope was recorded (#537): closes the context (ifrefresh()got as far as building one), clears the adapter's context and data scope, removes the entity-ownership records this attempt inserted, and removes the folder scope -- only if it is still this attempt's own scope instance. A record or scope this attempt found already in place (a live registration of the same plugin, for example) is left alone.- Parameters:
adapter- the refused adaptercontext- the container this attempt builtscope- the scope this attempt minted and registeredownershipAdded- the entities whose ownership record this attempt inserted
-
unregisterExternal
Unregister an external Bukkit plugin adapter from the framework. Tears down the IoC container, unregisters commands/listeners, and cleans up resources.- Parameters:
adapter- the external plugin adapter- Since:
- 6.2.2
-
registerModuleEventHandlersExternal
Scan a bean for @ModuleEventHandler methods and register them with EventBus for an external plugin.
-