Class PluginManager
-
Field Summary
FieldsModifier and TypeFieldDescriptionprivate ClassLoaderEntity 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[]).The framework-owned types whoseScheduledmethods this manager registers.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 filesystem/classpath order, with no dependency resolution at all.private PlayerCacheManagerprivate final List<Class<? extends UltiToolsPlugin>> private final List<UltiToolsPlugin> 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.voidApplies 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: type registration, component scan, the plugin's own singleton registration, config-entity beans, the static instance field,DataScopeminting plus entity ownership, AOP wiring,refresh(), and (afterrefresh())@ContextEntrybean registration withautowireBean.private booleanprivate static List<BaseCommandExecutor> baseCommandExecutors(SimpleContainer context) 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 static StringdescribeBindingKey(String bindingKey) Renderscom.example.EssentialsConfig#features.wild.cooldownasEssentialsConfig key 'features.wild.cooldown'.findOwningPlugin(Class<?> entityClass) Looks up which loaded plugin ownsentityClass, perentityOwnership.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.The live instances corresponding toFRAMEWORK_SCHEDULED_OWNER_TYPES, in the same order.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 booleanprivate static voidkeepRunning(UltiToolsPlugin plugin, String bindingKey, Integer before, Map<String, Integer> next, Set<String> reported, String why) private Class<? extends UltiToolsPlugin> loadPluginMainClass(ClassLoader classLoader, File pluginJar) Load module main class.(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 booleanCompatibility gates that must run before the module's bean graph is built.private static voidprivate static voidrefreshOne(UltiToolsPlugin plugin, 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).booleanregister(UltiToolsPlugin plugin) booleanregister(Class<? extends UltiToolsPlugin> pluginClass) Register plugin.private voidregisterBukkit(UltiToolsPlugin plugin) Register bukkit commands or listeners.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 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.voidreload()Reload all plugins.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) 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.(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, 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, independently ofloadPluginMainClass(java.lang.ClassLoader, java.io.File)'s scan (which returns as soon as it finds the module's main class and discards its ownscannedClassesset).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) 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.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.(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 filesystem/classpath order, 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:
-
pluginList
-
pluginClassList
-
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. -
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
-
-
Constructor Details
-
PluginManager
public PluginManager()
-
-
Method Details
-
init
Initialize plugin manager. Please do not call this method manually.- Throws:
IOException- IO exception
-
register
Register plugin.- Parameters:
pluginClass- Plugin class- Returns:
- Register result
-
register
- Parameters:
plugin- UltiTools plugin instance- Returns:
- Register result
-
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
- Parameters:
plugin- UltiTools plugin instance
-
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().- Parameters:
plugin- the module whose configuration was just reloaded- Since:
- 6.3.0
-
runReloadHalf
-
refreshCooldownBindings
-
refreshOne
private static void refreshOne(UltiToolsPlugin plugin, 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
-
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. -
loadPluginMainClass
private Class<? extends UltiToolsPlugin> loadPluginMainClass(ClassLoader classLoader, File pluginJar) Load module main class.- Parameters:
classLoader- Class loaderpluginJar- Plugin jar file- Returns:
- Plugin main class
-
scanEntitiesInJar
Scans a plugin's own JAR for classes carrying@Table, independently ofloadPluginMainClass(java.lang.ClassLoader, java.io.File)'s scan (which returns as soon as it finds the module's main class and discards its ownscannedClassesset). 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.loadPluginMainClass(java.lang.ClassLoader, java.io.File)'s 1,000-class cap is reported here, not enforced: crossing it produces one WARNING naming the jar and the count, and the scan continues to the end of the jar regardless.- 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
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 module- Returns:
- the loaded class if it is a
@Tableentity, otherwise empty
-
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.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. -
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 has its ownTransactionManagerinstead (02-05, D-03): a per-plugin snapshot manager that rolls back every JSON operator a@Transactionalmethod touched, obtained fromJsonStorewhengetDataSource(DataScope)throws. 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, and they are also constructed beforewireAopruns ininitializePlugin, an independent second reason they can never be proxied.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 a per-plugin snapshotJsonTransactionManagerinstead (02-05, D-03), obtained from the store itself. 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).- 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
-
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: type registration, component scan, the plugin's own singleton registration, config-entity beans, the static instance field,DataScopeminting plus entity ownership, AOP wiring,refresh(), and (afterrefresh())@ContextEntrybean registration withautowireBean. Bothregister(UltiToolsPlugin)andinitializePlugin(java.lang.ClassLoader, java.lang.Class<? extends com.ultikits.ultitools.abstracts.UltiToolsPlugin>)route through this one method now, so a capability added to either path is never silently missing from the other (WIRE-05, WIRE-06, #203/#326).- 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
-
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 affected module(s) - the cycle/missing declaration plus everything transitively depending on it - are absent from the returned list; every unrelated module still loads. The pre-6.3.0 all-unsorted behaviour survives only as an explicit, cost-stating opt-in via
ultitools.useLegacyPluginLoading.- Parameters:
plugins- list of plugin classes to sort- Returns:
- sorted list of plugin classes
-
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
-
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
-
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.
-