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 from the pre-6.3.0 shouldRegister body; since #612 the value is read with the framework's document reader (readBoolean(java.io.File, com.ultikits.ultitools.annotations.ConditionalOnConfig)). 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"
    • readBoolean

      private static boolean readBoolean(File configFile, ConditionalOnConfig condition)
      Reads condition.path() from configFile with the framework's own document reader and key semantics, the same the module's config entity binds the file with (#612): the path is a nested key path, a flat dotted key such as features.chat: false, or a mix (ConfigDocument.readings(String)). Bukkit's YamlConfiguration, which this used before, splits every key at its dots and lets a later section replace an earlier flat key, so it could read a different value from the entity and change its answer after the entity's missing-key insert. Only a boolean value counts, as with Bukkit's getBoolean: anything else, an absent path or a file that cannot be read or parsed reads false.
      Throws:
      ConfigurationException - when the file holds the path in more than one form (refused like the entity's load; the message names the file and the path, never a value)
    • 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 ConfigDocument.load(java.nio.file.Path), 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