Class ConditionalRegistrationEvaluator

java.lang.Object
com.ultikits.ultitools.context.ConditionalRegistrationEvaluator

@Internal public final class ConditionalRegistrationEvaluator extends Object
The single shared @ConditionalOnConfig registration decision (D-17).

ComponentScanner.shouldRegister(Class) is the only caller of this evaluator in src/main -- recording the decision here therefore catches every path the annotation is honoured on, without needing a second copy of the decision anywhere else.

Reload drift reporting (issue #392, D-01). @ConditionalOnConfig is evaluated exactly once, during component scanning at plugin startup. ul reload re-reads the config file but this framework deliberately does not rebuild the container or re-register/unregister the component on reload -- see D-01 in the issue for the rejected "unregister and re-register the module" alternative and why it was rejected (it would discard the whole module's in-memory state, turning a config reload into a de-facto restart while players are online). Instead, shouldRegister(Class, SimpleContainer) records every decision it makes, and reportDrift(UltiToolsPlugin) -- called from UltiToolsPlugin.reloadSelf() -- re-evaluates each recorded class and logs a Level.WARNING for any class whose answer has changed, naming the class, the config file, the key, the new direction, and that a restart is required. This turns a previously silent no-op into a visible one without changing what reload actually does.

Where the record lives, and why. The record is a private static map keyed by the owning UltiToolsPlugin instance (identity, not equality -- a plugin has no value-based equals()), because: (a) the decision is inherently per-plugin, since the same class annotated on two different plugins would resolve against two different resource folders; (b) it is released by an explicit clear(UltiToolsPlugin) call from PluginManager.unregister, the same place this codebase already releases the other module-scoped registries that would otherwise pin the module's ClassLoader after unload (TabCompletionManager, EventBus, PanelResponderRegistry) -- the record holds Class<?> objects, so leaving it behind would pin the loader exactly the same way; (c) it is safe for the framework's own core context, which has no UltiToolsPlugin bean at all, because evaluation exits down the existing fail-open branch before anything is recorded, and a class carrying no @ConditionalOnConfig returns true before that. Rejected alternative: storing the record as a field on UltiToolsPlugin would make garbage collection automatic (no clear() call needed), but it would widen a public abstract class's API surface for an ApiStatus.Internal bookkeeping concern that has nothing to do with what a module author subclasses UltiToolsPlugin for.

Since:
6.3.0
  • Field Details

  • Constructor Details

    • ConditionalRegistrationEvaluator

      private ConditionalRegistrationEvaluator()
  • Method Details

    • shouldRegister

      public static boolean shouldRegister(Class<?> clazz, SimpleContainer container)
      Check if a class should be registered based on @ConditionalOnConfig.

      The two "the decision cannot be determined" branches stay fail-open -- a component that should have been enabled but was not is harder to diagnose than the reverse -- but each now emits a Level.WARNING record naming the evaluated class and the reason, rather than silently registering by default (D-20). The config-file-missing branch is unaffected: it returns condition.negate(), i.e. missing means disabled, which already matches the annotation's own javadoc.

      The decision made here -- including a false (skipped) decision, which never appears in any container -- is recorded against the resolved plugin so a later reportDrift(UltiToolsPlugin) call can detect drift (issue #392, D-01).

      Parameters:
      clazz - the class to check
      container - the container used to resolve the owning UltiToolsPlugin
      Returns:
      true if the class should be registered
    • evaluate

      private static boolean evaluate(UltiToolsPlugin plugin, ConditionalOnConfig condition)
      The decision logic extracted verbatim from the pre-6.3.0 shouldRegister body -- no existing decision semantics changed by this extraction. Kept separate from shouldRegister(Class, SimpleContainer) so reportDrift(UltiToolsPlugin) can re-run exactly the same logic against the current file contents.
      Parameters:
      plugin - the resolved, non-null owning plugin
      condition - the class's @ConditionalOnConfig annotation
      Returns:
      true if the condition currently evaluates to "register"
    • record

      private static void record(UltiToolsPlugin plugin, Class<?> clazz, boolean decision)
    • reportDrift

      public static List<String> reportDrift(UltiToolsPlugin plugin)
      Re-evaluate every @ConditionalOnConfig decision recorded for plugin and log (and return) one Level.WARNING message for each class whose answer has changed since it was recorded (issue #392, D-01).

      Per D-01, this method only reports -- it never registers, unregisters, or rebuilds anything. Re-evaluation runs outside RECORD_LOCK (it does disk I/O via YamlConfiguration.loadConfiguration(File), which must never run while holding a lock other reload/scan threads might contend on); the plugin's recorded map is snapshotted inside the lock first. Each class is re-evaluated in its own try/catch so one unreadable config file cannot abort the reload or the remaining classes.

      Parameters:
      plugin - the plugin being reloaded
      Returns:
      an unmodifiable list of the drift messages emitted, in recording order; empty if there was no drift, if plugin is null, or if nothing is recorded for plugin
    • isActuallyRegistered

      private static boolean isActuallyRegistered(SimpleContainer context, Class<?> clazz)
      Whether clazz is actually present in context right now -- a real, constructed singleton, not merely a registered BeanDefinition (D-03).

      Deliberately does not use the container's membership-predicate method: that method ORs a bean definition in with an actual singleton, and using it here would repeat the exact untrue claim this method exists to avoid. The composition used instead -- SimpleContainer.getBeanNamesForType(Class) (does not instantiate) followed by SimpleContainer.getSingleton(String, boolean) (Level 1 cache read only, no early reference) -- distinguishes a registered definition from a constructed instance.

      Parameters:
      context - the plugin's container, or null if none is wired yet
      clazz - the class to check
      Returns:
      true if a constructed singleton assignable to clazz exists
    • driftMessage

      private static String driftMessage(Class<?> clazz, ConditionalOnConfig condition, boolean currentDecision, boolean actuallyPresent)
    • adviceFor

      private static String adviceFor(boolean actuallyPresent, boolean currentDecision)
      D-04: the action advice follows the observed (actuallyPresent, currentDecision) pair instead of a single fixed template -- the old unconditional "a restart is required" sentence sent an operator to restart the server in order to remove a component that was never actually there.

      Four branches, three from D-04's own decision and a fourth extension it does not enumerate:

      • present, now disabled -> a restart is required to remove it
      • absent, now disabled -> no restart needed; if the operator expected it to be present, the cause is at startup, so the startup log is where to look
      • absent, now enabled -> a restart is required to create it
      • present, now enabled (D-04 does not name this pair; reachable when a bean is registered through a non-scanner path such as registerSingleton/@Bean after the scan-time decision was recorded) -> applying D-04's own principle, the observed state already matches this decision, so no restart is needed
      Parameters:
      actuallyPresent - whether a constructed singleton is observed in the container now
      currentDecision - whether the condition currently evaluates to "register"
      Returns:
      the advice sentence, ending the overall drift message
    • clear

      public static void clear(UltiToolsPlugin plugin)
      Release every decision recorded for plugin.

      Called from PluginManager.unregister when a module unloads -- the record holds Class<?> references, and would otherwise pin the module's ClassLoader after unload, exactly like the TabCompletionManager / EventBus / PanelResponderRegistry releases it sits alongside there.

      Parameters:
      plugin - the plugin whose recorded decisions should be released; null is a no-op, as is a plugin with nothing recorded