Class BaseCommandExecutor
- All Implemented Interfaces:
org.bukkit.command.CommandExecutor,org.bukkit.command.TabCompleter,org.bukkit.command.TabExecutor
- Direct Known Subclasses:
CloudLoginCommand,PluginInstallCommands,UltiToolsCommands
The default validator chain applies four validators in order: sender type (driven by the
class-level @CmdTarget, overridable per method), permission and operator requirement
(driven by @CmdExecutor), cooldown (applied only after a successful invocation), and
usage locking (acquired and released around the call in a try-finally). Execution flow in
onCommand(org.bukkit.command.CommandSender, org.bukkit.command.Command, java.lang.String, java.lang.String[]): help short-circuit, immutable CommandContext construction, scored
method matching via matchMethod(java.lang.String[]), parameter parsing, the validator chain, an
argument-count check, then invocation. Subclasses MUST implement handleHelp(org.bukkit.command.CommandSender).
Validator release at unload (case 48, @since 6.3.0). CooldownValidator and
UsageLockValidator register themselves with PlayerCacheManager at their first
dispatch. When this executor is torn down (module unload), the framework validators of the
chain it dispatches through -- and its two field validators -- are released (unregistered, and
a lock validator's locks cleared) unless another loaded executor that has dispatched still uses
them; the last such executor's unload releases them, in whatever order the executors unload. A
sharer that had not dispatched yet holds no state through them and simply registers them again
on its first dispatch.
- Since:
- 6.2.0
- Version:
- 2.0.0
- Author:
- wisdomme
-
Field Summary
FieldsModifier and TypeFieldDescriptionResolved seconds of this executor's config-bound@CmdCDannotations, keyed byCooldownValidator.bindingKey, and their load-time value sources (#531).private final CooldownValidatorprivate static final List<WeakReference<BaseCommandExecutor>> Executors that have dispatched at least once and have not been torn down (case 48): the sharers an unloading executor must not release a validator from under.private static final Objectprivate booleanWhether this executor is inDISPATCHED_EXECUTORS; written under its lock.private final UsageLockValidatorprivate static final Objectprivate final TypeParserRegistryprivate final ValidatorChain -
Constructor Summary
ConstructorsConstructorDescriptionCreates a new command executor with default validators.BaseCommandExecutor(ValidatorChain validatorChain) Creates a new command executor with a custom validator chain. -
Method Summary
Modifier and TypeMethodDescriptionvoidaddValidator(CommandValidator validator) Adds a custom validator to the chain.private voidarmTimeoutWatcher(CommandContext context, AsyncCommand asyncCommand, AtomicBoolean reported) Arms the delayed watcher that reports a timeout message to the sender if the command body has not already completed by then.protected Object[]buildMethodParams(CommandContext context, Method method) Builds the method parameters from the command context.private intcalculateMatchScore(String[] formatArgs, String[] actualArgs) Calculates a match score for format vs actual args.private static voidcompleteAll(CommandContext context, List<CommandValidator> ranValidators, boolean commandSucceeded) Calls every validator'sonCompletein chain order.protected ValidatorChainCreates the default validator chain with standard validators.private static StringRenders a throwable for the user in a form that is never"null".private voiddispatchAsyncCommand(CommandContext context, AsyncCommand asyncCommand, org.bukkit.scheduler.BukkitRunnable runnable, AtomicBoolean reported, List<CommandValidator> ranValidators) Sends the optional "processing" message, dispatchesrunnableasynchronously exactly once, and arms the timeout watcher if configured.private static voiddispatchScheduled(CommandContext context, List<CommandValidator> ranValidators, Runnable schedule) Hands a body to the scheduler; if the scheduler refuses it (the plugin is disabling, for example), the body never runs and so never reaches itsonCompletehooks, so the validators that passed are told of the refusal instead (#568) before the exception leaves.protected voidexecuteCommand(CommandContext context, Method method, Object[] params, ValidatorChain.ChainValidationResult validationResult) Executes the command method.private StringExtract module name from the concrete command executor class.private StringextractParameterName(String formatArg) protected StringGets the help command string.Gets all registered command mappings.private booleanhandleGatedHelp(CommandContext context) Runs the sender-type and permission validators, then shows help if they pass.protected abstract voidhandleHelp(org.bukkit.command.CommandSender sender) Handles the help command.private voidinvokeCommandBody(CommandContext context, Method method, Object[] params, List<CommandValidator> ranValidators, TriggerContext triggerCtx, AtomicBoolean reported) Runs the matched command method plus its surrounding bookkeeping (audit-user context, post-action validator hooks, error reporting, the WIRE-12 completion flag) -- the body of theBukkitRunnableexecuteCommand(com.ultikits.ultitools.abstracts.command.CommandContext, java.lang.reflect.Method, java.lang.Object[], com.ultikits.ultitools.abstracts.command.validation.ValidatorChain.ChainValidationResult)schedules.private booleanisParameter(String arg) private static booleanisSenderType(Class<?> paramType) private static booleanisUsedByAnotherExecutor(CommandValidator validator, List<BaseCommandExecutor> others) Whether one ofothersstill dispatches throughvalidator(the same instance).private voidRecords this executor inDISPATCHED_EXECUTORSat its first dispatch, so an unloading executor sharing a validator with it leaves that validator registered (case 48).private static List<BaseCommandExecutor> Removesleavingand every cleared reference fromDISPATCHED_EXECUTORSand returns the executors that remain.protected MethodmatchMethod(String[] args) Matches arguments to a registered method.private static voidnotifyRefused(CommandContext context, ValidatorChain.ChainValidationResult validationResult) Tells each validator that passed for this dispatch that it was refused before the mapped method ran, in chain order, so a validator that acquired something invalidate--UsageLockValidator's lock -- releases it (#568).private static voidnotifyRefusedAndRethrow(CommandContext context, ValidatorChain.ChainValidationResult validationResult, RuntimeException cause) notifyRefused(com.ultikits.ultitools.abstracts.command.CommandContext, com.ultikits.ultitools.abstracts.command.validation.ValidatorChain.ChainValidationResult)for a dispatch leaving bycause: every hook still runs, and a hook that throws is attached tocauseas suppressed rather than replacing it.booleanonCommand(org.bukkit.command.CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) onTabComplete(org.bukkit.command.CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) parseParameters(String[] args, String format) Parses command arguments into named parameters.protected ObjectparseParameterValue(String[] values, Class<?> type) Parses a parameter value to the target type.voidremoveValidator(CommandValidator validator) Removes a validator from the chain.private voidreportCommandExecutionError(CommandContext context, Method method, Exception e, TriggerContext triggerCtx) Sends the user-facing failure message, logs the exception, and best-effort reports it toErrorReportCollectorfor a command method that threw.private ObjectresolveCmdParam(CommandContext context, Parameter parameter, Class<?> paramType) private ObjectresolveParameter(CommandContext context, Parameter parameter) private ObjectresolveSender(CommandContext context, Class<?> paramType) private voidScans methods for command mappings.Generates tab completion suggestions.private voidReleases the self-registering validators this executor dispatched through fromPlayerCacheManagertracking and bulk-clears everyUsageLockValidator's locks when this executor is torn down (module unload).protected booleanvalidateParameterCount(String[] args, String format, org.bukkit.command.CommandSender sender, org.bukkit.command.Command command) Validates the parameter count.
-
Field Details
-
mappings
-
validatorChain
-
configBoundCooldownSeconds
Resolved seconds of this executor's config-bound@CmdCDannotations, keyed byCooldownValidator.bindingKey, and their load-time value sources (#531). Kept on the executor -- the exact owner of a binding -- so the state lives and dies with it and nothing shared (such as aCooldownValidatorseveral executors use) has to release it on any exit path. Replaced as whole immutable maps; written on the main thread, read on dispatch threads. Reached only throughConfigBoundCooldownState. -
configBoundCooldownSources
-
cooldownValidator
-
lockValidator
-
parserRegistry
-
DISPATCHED_EXECUTORS
Executors that have dispatched at least once and have not been torn down (case 48): the sharers an unloading executor must not release a validator from under. Held weakly so the registry never keeps a module's executor -- or its class loader -- reachable; an entry joins at the executor's first dispatch and leaves at its teardown, which also prunes cleared references. Guarded byDISPATCHED_EXECUTORS_LOCK. -
DISPATCHED_EXECUTORS_LOCK
-
dispatchedExecutorMember
private volatile boolean dispatchedExecutorMemberWhether this executor is inDISPATCHED_EXECUTORS; written under its lock. -
PARSE_FAILED_SENTINEL
-
-
Constructor Details
-
BaseCommandExecutor
public BaseCommandExecutor()Creates a new command executor with default validators.The chain's framework validators are released at this executor's unload unless another loaded executor that has dispatched still uses them (one built over
getValidatorChain(), for example); a sharer that had not dispatched simply registers them again on its first dispatch (case 48). -
BaseCommandExecutor
Creates a new command executor with a custom validator chain.This is the other half of the construction surface
createDefaultValidatorChain()warns about: a chain supplied directly here is subject to the exact same load-time contract. If any@CmdMappingmethod on this class -- or the class itself -- declares@CmdCDwhilevalidatorChainholds noCooldownValidator, or declares@UsageLimit(any scope butNONE) while it holds noUsageLockValidator, the module is refused at load, naming this class and the offending mapping method (SILENT-11 / D-01, D-04; enforced byPluginManager.validateCommandExecutorContract). There is no opt-out: Phase 3 D-08's module-granularity isolation is the accepted escape hatch -- only this module fails to load, every other module still starts.At this executor's unload, the
CooldownValidator/UsageLockValidatorinstances invalidatorChainare released -- unregistered fromPlayerCacheManager, a lock validator's locks cleared -- unless another loaded executor that has dispatched still uses them, as when several executors or modules share one chain. A sharer that had not dispatched simply registers them again on its first dispatch (case 48).- Parameters:
validatorChain- the custom validator chain
-
-
Method Details
-
createDefaultValidatorChain
Creates the default validator chain with standard validators. Override this method to customize the validation pipeline.An override that drops
CooldownValidatororUsageLockValidatorfrom the chain while a mapping on this class -- or the class itself -- declares@CmdCDor@UsageLimit(any scope butNONE) respectively IS refused at load, naming the offending class and, when known, the offending mapping method (SILENT-11 / D-01, D-04; enforced byPluginManager.validateCommandExecutorContract, hooked at the end of container assembly, before any command reaches Bukkit). There is no opt-out: no system property, config key, or annotation attribute disables this check. Phase 3 D-08's module-granularity isolation is the accepted escape hatch instead -- only the offending module fails to load, every other module still starts.- Returns:
- the validator chain
-
unregisterValidatorsFromPlayerCache
private void unregisterValidatorsFromPlayerCache()Releases the self-registering validators this executor dispatched through fromPlayerCacheManagertracking and bulk-clears everyUsageLockValidator's locks when this executor is torn down (module unload).The validators released are those of the chain this executor dispatches through -- a chain passed to
BaseCommandExecutor(ValidatorChain)or returned by an overriddencreateDefaultValidatorChain()-- together withcooldownValidator/lockValidator, taken as one identity set (case 48). Before, only the two fields were released; a custom chain that did not contain them left its own validators, which register themselves withPlayerCacheManagerat their first dispatch, strongly held there after the module unloaded -- one more per module update, and a module's own validator subclass pinned its old class loader. OnlyCooldownValidatorandUsageLockValidator(and their subclasses) register themselves, so only they are released.A candidate that another loaded executor still uses is left untouched: still registered, its locks held, its cooldowns enforced and its quit cleanup running. "Still uses" means an executor that has dispatched at least once, has not been torn down, is still reachable and whose current chain contains the candidate by identity (
DISPATCHED_EXECUTORS). The last such executor's unload releases it. Release goes through each validator'sreleasePlayerCacheRegistration(), which also re-arms its lazy registration, so a sharer that had not dispatched yet -- or a later executor over the same chain -- registers it again on its next dispatch.Reached through the framework's EXISTING
@PreDestroycontainer lifecycle --PluginManager.unregister(plugin)already callsplugin.getContext().close(), which invokes every singleton bean's@PreDestroymethods -- rather than a second, purpose-built unload hook (GEN-08, D-03). These validators are registered withPlayerCacheManagerSEPARATELY from this executor bean itself (their sweepable state lives on them, not on this class), so the pre-existing per-beanplayerCacheManager.unregisterBean(bean)loop already inPluginManager.unregister(...)-- which only ever sees the beans a plugin's own container constructed -- never reaches them on its own; this hook is what does.UsageLockValidator.clearAllLocks()has no per-player analogue and, unlikeUsageLockValidator.clearPlayerLocks(java.util.UUID)/CooldownValidator.clearCooldowns(java.util.UUID), no quit-path call site either (GEN-08) -- module unload is the correct point to bulk-release whatever locks a released lock validator still held at teardown.- Since:
- 6.3.0
-
joinDispatchedExecutors
private void joinDispatchedExecutors()Records this executor inDISPATCHED_EXECUTORSat its first dispatch, so an unloading executor sharing a validator with it leaves that validator registered (case 48). -
leaveDispatchedExecutors
Removesleavingand every cleared reference fromDISPATCHED_EXECUTORSand returns the executors that remain. The lock is released before any caller reads another executor's chain. -
isUsedByAnotherExecutor
private static boolean isUsedByAnotherExecutor(CommandValidator validator, List<BaseCommandExecutor> others) Whether one ofothersstill dispatches throughvalidator(the same instance). -
scanCommandMappings
private void scanCommandMappings()Scans methods for command mappings. -
handleHelp
protected abstract void handleHelp(org.bukkit.command.CommandSender sender) Handles the help command. Override to provide custom help.- Parameters:
sender- the command sender
-
getHelpCommand
Gets the help command string.- Returns:
- the help command
-
onCommand
public boolean onCommand(org.bukkit.command.CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) - Specified by:
onCommandin interfaceorg.bukkit.command.CommandExecutor
-
notifyRefused
private static void notifyRefused(CommandContext context, ValidatorChain.ChainValidationResult validationResult) Tells each validator that passed for this dispatch that it was refused before the mapped method ran, in chain order, so a validator that acquired something invalidate--UsageLockValidator's lock -- releases it (#568). Driven from the chain's own passed-validator list, likeonComplete, never from a named field. -
notifyRefusedAndRethrow
private static void notifyRefusedAndRethrow(CommandContext context, ValidatorChain.ChainValidationResult validationResult, RuntimeException cause) notifyRefused(com.ultikits.ultitools.abstracts.command.CommandContext, com.ultikits.ultitools.abstracts.command.validation.ValidatorChain.ChainValidationResult)for a dispatch leaving bycause: every hook still runs, and a hook that throws is attached tocauseas suppressed rather than replacing it. -
executeCommand
protected void executeCommand(CommandContext context, Method method, Object[] params, ValidatorChain.ChainValidationResult validationResult) Executes the command method. Supports both synchronous and asynchronous execution via @AsyncCommand or @RunAsync.A synchronous body runs at once, inside the dispatch, when this is called on the server's primary thread -- which is where Bukkit dispatches every command a player, the console, a command block or the panel sends -- and is handed to
runTaskonly when it is not (as of 6.3.0, #541; before, every synchronous body was deferred one tick). An@AsyncCommand/@RunAsyncbody always runs asynchronously.- Parameters:
context- the command contextmethod- the method to executeparams- the method parametersvalidationResult- the chain result produced byonCommand's validation step for THIS invocation; itsValidatorChain.ChainValidationResult.getPassedValidators()is the sole source that drives each validator'sCommandValidator.onComplete(CommandContext, boolean)call -- see D-01. Never recomputed here: re-validating would run every validator twice and re-trigger their gates (e.g. a second lock acquisition attempt).
-
dispatchScheduled
private static void dispatchScheduled(CommandContext context, List<CommandValidator> ranValidators, Runnable schedule) Hands a body to the scheduler; if the scheduler refuses it (the plugin is disabling, for example), the body never runs and so never reaches itsonCompletehooks, so the validators that passed are told of the refusal instead (#568) before the exception leaves. -
invokeCommandBody
private void invokeCommandBody(CommandContext context, Method method, Object[] params, List<CommandValidator> ranValidators, TriggerContext triggerCtx, AtomicBoolean reported) Runs the matched command method plus its surrounding bookkeeping (audit-user context, post-action validator hooks, error reporting, the WIRE-12 completion flag) -- the body of theBukkitRunnableexecuteCommand(com.ultikits.ultitools.abstracts.command.CommandContext, java.lang.reflect.Method, java.lang.Object[], com.ultikits.ultitools.abstracts.command.validation.ValidatorChain.ChainValidationResult)schedules.Split out of
executeCommandpurely to bring the enclosing method's NPathComplexity back under threshold; the control flow below, and therefore the runtime behaviour, is unchanged from before the extraction (refactor only, per D-01/D-02/T-02-REP design notes that used to live on the anonymousrun()this replaces).- Since:
- 6.3.0
-
completeAll
private static void completeAll(CommandContext context, List<CommandValidator> ranValidators, boolean commandSucceeded) Calls every validator'sonCompletein chain order. Each runs even when an earlier one throws, so one validator's failing hook cannot keep a laterUsageLockValidatorfrom releasing its lock (#568); the first exception is rethrown afterwards, later ones suppressed. -
handleGatedHelp
Runs the sender-type and permission validators, then shows help if they pass.Before 6.3.0 the help short-circuit sat at the very top of
onCommand(org.bukkit.command.CommandSender, org.bukkit.command.Command, java.lang.String, java.lang.String[]), ahead of theCommandContextbeing built, so it bypassed the entire validator chain (#381). Two consequences were measured on a real server: a console received the full help of a@CmdTarget(PLAYER)command -- including, in one module, an admin section gated only onhasPermission, which a console always satisfies -- and modules had begun working around the absent guarantee in incompatible ways. One testedsender instanceof Playerand silently printed nothing for a console; another printed everything. Neither is wrong against a contract that promised nothing.Cooldown and usage-limit are deliberately not applied here. Help is not the guarded action: withholding an explanation because the command itself is on cooldown would be perverse, and
UsageLockValidatoracquires a lock that only the invocation path releases. This is a choice, stated so it cannot later be mistaken for the same oversight in a different place.Applicability is consulted, same as the normal dispatch path (#413). Before invoking a sender-type or permission validator here,
ValidatorChain.isApplicable(com.ultikits.ultitools.abstracts.command.validation.CommandValidator, com.ultikits.ultitools.abstracts.command.CommandContext)is called -- the exact same static methodValidatorChain.validate(com.ultikits.ultitools.abstracts.command.CommandContext)/#validateAllcall internally, not a second, independent copy of the one-line condition it wraps (WR-02). A validator that reports it does not apply to this context is skipped, not invoked and happened to pass. This does not weaken enforcement: a validator that DOES apply and denies still denies help, exactly as before.- Parameters:
context- the command context, already built- Returns:
truealways -- Bukkit's contract for a handled command- Since:
- 6.3.0
-
reportCommandExecutionError
private void reportCommandExecutionError(CommandContext context, Method method, Exception e, TriggerContext triggerCtx) Sends the user-facing failure message, logs the exception, and best-effort reports it toErrorReportCollectorfor a command method that threw. Split out ofinvokeCommandBody(com.ultikits.ultitools.abstracts.command.CommandContext, java.lang.reflect.Method, java.lang.Object[], java.util.List<com.ultikits.ultitools.abstracts.command.validation.CommandValidator>, com.ultikits.ultitools.manager.TriggerContext, java.util.concurrent.atomic.AtomicBoolean)for the same NPathComplexity reason as that extraction.- Since:
- 6.3.0
-
describe
Renders a throwable for the user in a form that is never"null".Every command body is invoked reflectively, so anything it throws arrives wrapped in an
InvocationTargetException, whose owngetMessage()isnullby construction -- the detail belongs to the cause. Reporting the wrapper's message therefore appended a literal"null"to the error prefix for every command failure, sending the user to look for a null-pointer bug that was not there and hiding the real one in the server log (#385).The cause's own message can also be null --
NullPointerExceptionthrown by a bare dereference is the common case -- so the class's simple name is used as the fallback. It is a poor message, but"NullPointerException"tells the reader more than"null"does, and the full stack trace is one line below in the log either way.- Parameters:
cause- the unwrapped throwable- Returns:
- its message, or its simple class name when the message is null or blank
- Since:
- 6.3.0
-
dispatchAsyncCommand
private void dispatchAsyncCommand(CommandContext context, AsyncCommand asyncCommand, org.bukkit.scheduler.BukkitRunnable runnable, AtomicBoolean reported, List<CommandValidator> ranValidators) Sends the optional "processing" message, dispatchesrunnableasynchronously exactly once, and arms the timeout watcher if configured. Split out ofexecuteCommand(com.ultikits.ultitools.abstracts.command.CommandContext, java.lang.reflect.Method, java.lang.Object[], com.ultikits.ultitools.abstracts.command.validation.ValidatorChain.ChainValidationResult)for the same NPathComplexity reason asinvokeCommandBody(com.ultikits.ultitools.abstracts.command.CommandContext, java.lang.reflect.Method, java.lang.Object[], java.util.List<com.ultikits.ultitools.abstracts.command.validation.CommandValidator>, com.ultikits.ultitools.manager.TriggerContext, java.util.concurrent.atomic.AtomicBoolean)'s extraction.- Since:
- 6.3.0
-
armTimeoutWatcher
private void armTimeoutWatcher(CommandContext context, AsyncCommand asyncCommand, AtomicBoolean reported) Arms the delayed watcher that reports a timeout message to the sender if the command body has not already completed by then. Split out ofdispatchAsyncCommand(com.ultikits.ultitools.abstracts.command.CommandContext, com.ultikits.ultitools.annotations.command.AsyncCommand, org.bukkit.scheduler.BukkitRunnable, java.util.concurrent.atomic.AtomicBoolean, java.util.List<com.ultikits.ultitools.abstracts.command.validation.CommandValidator>)for the same NPathComplexity reason as its sibling extractions.- Since:
- 6.3.0
-
matchMethod
Matches arguments to a registered method.- Parameters:
args- the command arguments- Returns:
- the matched method, or null if not found
-
calculateMatchScore
Calculates a match score for format vs actual args. Weights: an exact literal match scores 10, a<param>placeholder scores 1, and an exact-length match earns a +5 bonus. A format string is written against these three numbers.- Parameters:
formatArgs- the format argumentsactualArgs- the actual arguments- Returns:
- the match score (-1 if no match)
-
isParameter
-
parseParameters
Parses command arguments into named parameters.- Parameters:
args- the command argumentsformat- the command format- Returns:
- map of parameter names to values
-
extractParameterName
-
validateParameterCount
protected boolean validateParameterCount(String[] args, String format, org.bukkit.command.CommandSender sender, org.bukkit.command.Command command) Validates the parameter count. -
buildMethodParams
Builds the method parameters from the command context.- Parameters:
context- the command contextmethod- the method to invoke- Returns:
- the method parameters, or null if parsing fails
-
resolveParameter
-
isSenderType
-
resolveSender
-
resolveCmdParam
-
parseParameterValue
Parses a parameter value to the target type.- Parameters:
values- the string valuestype- the target type- Returns:
- the parsed value
- Throws:
TypeParseException
-
onTabComplete
@Nullable public List<String> onTabComplete(org.bukkit.command.CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) - Specified by:
onTabCompletein interfaceorg.bukkit.command.TabCompleter
-
suggest
protected List<String> suggest(org.bukkit.entity.Player player, org.bukkit.command.Command command, String[] args) Generates tab completion suggestions. Override this method to provide custom suggestions.- Parameters:
player- the player requesting completioncommand- the commandargs- the current arguments- Returns:
- list of suggestions
-
getMappings
Gets all registered command mappings.- Returns:
- unmodifiable map of format to method
-
addValidator
Adds a custom validator to the chain.- Parameters:
validator- the validator to add
-
removeValidator
Removes a validator from the chain.- Parameters:
validator- the validator to remove
-
extractModuleName
Extract module name from the concrete command executor class. Uses the simple class name as a reasonable approximation.
-