Class BaseCommandExecutor

java.lang.Object
com.ultikits.ultitools.abstracts.command.BaseCommandExecutor
All Implemented Interfaces:
org.bukkit.command.CommandExecutor, org.bukkit.command.TabCompleter, org.bukkit.command.TabExecutor
Direct Known Subclasses:
CloudLoginCommand, PluginInstallCommands, UltiToolsCommands

public abstract class BaseCommandExecutor extends Object implements org.bukkit.command.TabExecutor
Base command executor with improved architecture using Chain of Responsibility pattern. This class provides a cleaner, more extensible command handling system.

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 Details

    • mappings

      private final com.google.common.collect.BiMap<String,Method> mappings
    • validatorChain

      private final ValidatorChain validatorChain
    • configBoundCooldownSeconds

      volatile Map<String,Integer> configBoundCooldownSeconds
      Resolved seconds of this executor's config-bound @CmdCD annotations, keyed by CooldownValidator.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 a CooldownValidator several 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 through ConfigBoundCooldownState.
    • configBoundCooldownSources

      volatile Map<String,Supplier<Long>> configBoundCooldownSources
    • cooldownValidator

      private final CooldownValidator cooldownValidator
    • lockValidator

      private final UsageLockValidator lockValidator
    • parserRegistry

      private final TypeParserRegistry parserRegistry
    • DISPATCHED_EXECUTORS

      private static final List<WeakReference<BaseCommandExecutor>> 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 by DISPATCHED_EXECUTORS_LOCK.
    • DISPATCHED_EXECUTORS_LOCK

      private static final Object DISPATCHED_EXECUTORS_LOCK
    • dispatchedExecutorMember

      private volatile boolean dispatchedExecutorMember
      Whether this executor is in DISPATCHED_EXECUTORS; written under its lock.
    • PARSE_FAILED_SENTINEL

      private static final Object 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

      public BaseCommandExecutor(ValidatorChain validatorChain)
      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 @CmdMapping method on this class -- or the class itself -- declares @CmdCD while validatorChain holds no CooldownValidator, or declares @UsageLimit (any scope but NONE) while it holds no UsageLockValidator, the module is refused at load, naming this class and the offending mapping method (SILENT-11 / D-01, D-04; enforced by PluginManager.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/UsageLockValidator instances in validatorChain are released -- unregistered from PlayerCacheManager, 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

      protected ValidatorChain createDefaultValidatorChain()
      Creates the default validator chain with standard validators. Override this method to customize the validation pipeline.

      An override that drops CooldownValidator or UsageLockValidator from the chain while a mapping on this class -- or the class itself -- declares @CmdCD or @UsageLimit (any scope but NONE) respectively IS refused at load, naming the offending class and, when known, the offending mapping method (SILENT-11 / D-01, D-04; enforced by PluginManager.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 from PlayerCacheManager tracking and bulk-clears every UsageLockValidator'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 overridden createDefaultValidatorChain() -- together with cooldownValidator/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 with PlayerCacheManager at 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. Only CooldownValidator and UsageLockValidator (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's releasePlayerCacheRegistration(), 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 @PreDestroy container lifecycle -- PluginManager.unregister(plugin) already calls plugin.getContext().close(), which invokes every singleton bean's @PreDestroy methods -- rather than a second, purpose-built unload hook (GEN-08, D-03). These validators are registered with PlayerCacheManager SEPARATELY from this executor bean itself (their sweepable state lives on them, not on this class), so the pre-existing per-bean playerCacheManager.unregisterBean(bean) loop already in PluginManager.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, unlike UsageLockValidator.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 in DISPATCHED_EXECUTORS at its first dispatch, so an unloading executor sharing a validator with it leaves that validator registered (case 48).
    • leaveDispatchedExecutors

      private static List<BaseCommandExecutor> leaveDispatchedExecutors(BaseCommandExecutor leaving)
      Removes leaving and every cleared reference from DISPATCHED_EXECUTORS and 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 of others still dispatches through validator (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

      protected String 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:
      onCommand in interface org.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 in validate -- UsageLockValidator's lock -- releases it (#568). Driven from the chain's own passed-validator list, like onComplete, 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 by cause: every hook still runs, and a hook that throws is attached to cause as 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 runTask only when it is not (as of 6.3.0, #541; before, every synchronous body was deferred one tick). An @AsyncCommand/@RunAsync body always runs asynchronously.

      Parameters:
      context - the command context
      method - the method to execute
      params - the method parameters
      validationResult - the chain result produced by onCommand's validation step for THIS invocation; its ValidatorChain.ChainValidationResult.getPassedValidators() is the sole source that drives each validator's CommandValidator.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 its onComplete hooks, 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 the BukkitRunnable executeCommand(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 executeCommand purely 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 anonymous run() this replaces).

      Since:
      6.3.0
    • completeAll

      private static void completeAll(CommandContext context, List<CommandValidator> ranValidators, boolean commandSucceeded)
      Calls every validator's onComplete in chain order. Each runs even when an earlier one throws, so one validator's failing hook cannot keep a later UsageLockValidator from releasing its lock (#568); the first exception is rethrown afterwards, later ones suppressed.
    • handleGatedHelp

      private boolean handleGatedHelp(CommandContext context)
      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 the CommandContext being 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 on hasPermission, which a console always satisfies -- and modules had begun working around the absent guarantee in incompatible ways. One tested sender instanceof Player and 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 UsageLockValidator acquires 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 method ValidatorChain.validate(com.ultikits.ultitools.abstracts.command.CommandContext)/#validateAll call 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:
      true always -- Bukkit's contract for a handled command
      Since:
      6.3.0
    • reportCommandExecutionError

      private void reportCommandExecutionError(CommandContext context, Method method, Exception e, TriggerContext triggerCtx)
      Since:
      6.3.0
    • describe

      private static String describe(Throwable cause)
      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 own getMessage() is null by 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 -- NullPointerException thrown 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)
      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 of dispatchAsyncCommand(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

      @Nullable protected Method matchMethod(String[] args)
      Matches arguments to a registered method.
      Parameters:
      args - the command arguments
      Returns:
      the matched method, or null if not found
    • calculateMatchScore

      private int calculateMatchScore(String[] formatArgs, String[] actualArgs)
      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 arguments
      actualArgs - the actual arguments
      Returns:
      the match score (-1 if no match)
    • isParameter

      private boolean isParameter(String arg)
    • parseParameters

      protected Map<String,String[]> parseParameters(String[] args, String format)
      Parses command arguments into named parameters.
      Parameters:
      args - the command arguments
      format - the command format
      Returns:
      map of parameter names to values
    • extractParameterName

      private String extractParameterName(String formatArg)
    • validateParameterCount

      protected boolean validateParameterCount(String[] args, String format, org.bukkit.command.CommandSender sender, org.bukkit.command.Command command)
      Validates the parameter count.
    • buildMethodParams

      @Nullable protected Object[] buildMethodParams(CommandContext context, Method method)
      Builds the method parameters from the command context.
      Parameters:
      context - the command context
      method - the method to invoke
      Returns:
      the method parameters, or null if parsing fails
    • resolveParameter

      private Object resolveParameter(CommandContext context, Parameter parameter)
    • isSenderType

      private static boolean isSenderType(Class<?> paramType)
    • resolveSender

      private Object resolveSender(CommandContext context, Class<?> paramType)
    • resolveCmdParam

      private Object resolveCmdParam(CommandContext context, Parameter parameter, Class<?> paramType)
    • parseParameterValue

      protected Object parseParameterValue(String[] values, Class<?> type) throws TypeParseException
      Parses a parameter value to the target type.
      Parameters:
      values - the string values
      type - 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:
      onTabComplete in interface org.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 completion
      command - the command
      args - the current arguments
      Returns:
      list of suggestions
    • getMappings

      public Map<String,Method> getMappings()
      Gets all registered command mappings.
      Returns:
      unmodifiable map of format to method
    • addValidator

      public void addValidator(CommandValidator validator)
      Adds a custom validator to the chain.
      Parameters:
      validator - the validator to add
    • removeValidator

      public void removeValidator(CommandValidator validator)
      Removes a validator from the chain.
      Parameters:
      validator - the validator to remove
    • extractModuleName

      private String extractModuleName()
      Extract module name from the concrete command executor class. Uses the simple class name as a reasonable approximation.