Class PluginDependencyResolver

java.lang.Object
com.ultikits.ultitools.manager.PluginDependencyResolver

@Internal public class PluginDependencyResolver extends Object
Plugin dependency resolver using Kahn's algorithm for topological sorting. Ensures plugins are loaded in the correct order based on their dependencies.

A failed resolve no longer refuses everything (D-10): resolve(List) throws PluginDependencyResolver.CircularDependencyException or PluginDependencyResolver.MissingDependencyException, and both carry a structured sortable prefix and a refused set alongside the message - disjoint, and together covering every input module - so a caller can load the unaffected modules instead of falling back to filesystem order.

Since:
6.2.0
Author:
wisdomme
  • Field Details

    • CIRCULAR_DEPENDENCY_MESSAGE_PREFIX

      private static final String CIRCULAR_DEPENDENCY_MESSAGE_PREFIX
      Shared start of the cycle sentence, used by the pure-cycle message and appended to a missing-dependency message when an independent cycle occurs in the same resolve.
      See Also:
    • logger

      private final Logger logger
  • Constructor Details

    • PluginDependencyResolver

      public PluginDependencyResolver(Logger logger)
      Constructs a new PluginDependencyResolver.
      Parameters:
      logger - the logger used to warn about a duplicate plugin.yml name: collision (#361 / IN-02) - a pick still has to be made, so this is the operator's only signal that one was
  • Method Details

    • resolve

      Resolves plugin load order using Kahn's algorithm for topological sorting.

      Ordering uses every edge kind (depends, present softDepends, loadBefore, plugin.yml loadAfter). A failed resolve partitions the input: the sortable prefix and the refused set are disjoint and together contain every input module. Refusal for a missing hard dependency follows depends edges only; anything the sort cannot place (a cycle and every module ordered after it) is refused as a cycle. When both occur, PluginDependencyResolver.MissingDependencyException is thrown and also carries the cycle paths.

      Parameters:
      pluginClasses - the list of plugin classes to sort
      Returns:
      sorted list of plugin classes
      Throws:
      PluginDependencyResolver.CircularDependencyException - if a dependency cycle is detected and no hard dependency is missing; the exception itself carries the sortable prefix, the refused set and the cycle paths
      PluginDependencyResolver.MissingDependencyException - if a required hard dependency is missing; the exception itself carries the sortable prefix, the refused set and the paths of any independent cycle
    • toClassList

      private List<Class<? extends UltiToolsPlugin>> toClassList(List<String> names, Map<String,PluginDependencyResolver.PluginNode> nodes)
      Converts a sorted list of plugin names back to their plugin classes.
    • buildAliasMap

      Builds the alias map every dependency-graph edge resolves through (D-12): each node's own simple class name maps to itself first (guaranteed unique, so it always takes priority), then each node's plugin.yml name: - when present and not already claimed by some other node's simple class name - also maps to that node's canonical (simple class) name. A dependency entry that does not appear in this map at all resolves to itself unchanged, exactly as it did before this map existed.
    • warnOnDuplicatePluginYmlNames

      private void warnOnDuplicatePluginYmlNames(Map<String,PluginDependencyResolver.PluginNode> nodes)
      Detects two or more nodes declaring the same plugin.yml name: and logs one WARNING per colliding name, naming every module that declared it (#361).

      This does not change which module wins the alias - that is still whichever comes first in nodes iteration order (discovery order), exactly as before. It only makes the operator aware that a pick happened, instead of the dependency graph silently depending on discovery order with nothing logged.

      IN-01 (gate-1 review, 16-REVIEW-residue.md): "discovery order" here is deterministic GIVEN a deterministic nodes iteration order. As of 6.3.0 (#476) this class's only production caller, PluginManager.init(ClassLoader), builds its input list from the modules folder in file-name order (ModuleFileTransactions#moduleJars) -- before that it was a raw File.listFiles() call, measured non-alphabetical and non-creation-order on ext4 -- so this WARNING names the same "winner" for the same folder on every file system.

      Gate-2 Codex finding, PR #478 round 1: the naive "first plugin.yml declarer wins" answer is wrong whenever the colliding name ALSO equals some other node's own simple class name - buildAliasMap(java.util.Map<java.lang.String, com.ultikits.ultitools.manager.PluginDependencyResolver.PluginNode>)'s FIRST loop claims every simple class name as its own alias before any plugin.yml name: is considered at all, so that OTHER node wins outright and none of the plugin.yml declarers do, regardless of their discovery order among themselves. This method now checks nodes.containsKey(ymlName) (keyed by simple class name) before falling back to "first declarer", so the reported winner always matches what buildAliasMap(java.util.Map<java.lang.String, com.ultikits.ultitools.manager.PluginDependencyResolver.PluginNode>) actually produces.

    • resolveAlias

      private String resolveAlias(String rawName, Map<String,String> aliasMap)
    • collectMissingHardDependencies

      private Map<String,List<String>> collectMissingHardDependencies(Map<String,PluginDependencyResolver.PluginNode> nodes, Set<String> availablePlugins, Map<String,String> aliasMap)
      Collects every node that declares at least one hard dependency not present among the available plugins, mapped to EVERY such dependency it names, not just the first (#361 / WR-05) - so a module missing three hard dependencies is told about all three in one refusal instead of one per restart. Each declared dependency is resolved through aliasMap before the availability check; the message itself still names the raw declared value.
    • buildMissingDependencyMessage

      private String buildMissingDependencyMessage(Map<String,List<String>> missingDependenciesByNode)
      Builds the refusal message naming every declaring module and every missing dependency it wanted (#361 / WR-05). A module with exactly one missing dependency keeps the exact singular wording this framework already used ("requires dependency 'X' which is not available"); a module with several gets the plural form joined into one clause ("requires dependencies 'X', 'Y' which are not available") so the single-element case never reads as a degenerate list.
    • expandForward

      private Set<String> expandForward(Set<String> seeds, Map<String,Set<String>> adjacencyList)
      Expands a seed set of node names forward along the given adjacency edges (dependency -> dependent), returning the seeds plus every node transitively reachable from them. Its only caller passes the hard-only adjacency from buildHardAdjacencyList(java.util.Map<java.lang.String, com.ultikits.ultitools.manager.PluginDependencyResolver.PluginNode>, java.util.Map<java.lang.String, java.lang.String>), so the refused set for a missing hard dependency follows depends edges alone (#676). A cycle does not use this method: Kahn's own partition already leaves out every node it cannot place.
    • restrictAdjacency

      private Map<String,Set<String>> restrictAdjacency(Map<String,Set<String>> adjacencyList, Set<String> keep)
      Restricts an adjacency list to a subset of nodes, dropping any edge touching a node outside the subset.
    • findCyclePaths

      private List<List<String>> findCyclePaths(Set<String> remaining, Map<String,Set<String>> adjacencyList)
      Finds every distinct cycle within the remaining sub-graph (the nodes Kahn's sort could not place) via a depth-first search restricted to that sub-graph, recording the back edge that closes each cycle. Each cycle is reported once, keyed by its member set, so revisiting the same loop from a second entry point does not duplicate it.
    • findCyclesFrom

      private void findCyclesFrom(String node, Set<String> remaining, Map<String,Set<String>> adjacencyList, Set<String> visited, Set<String> onStack, List<String> path, List<List<String>> cycles, Set<String> reportedKeys)
    • buildAdjacencyList

      private Map<String,Set<String>> buildAdjacencyList(Map<String,PluginDependencyResolver.PluginNode> nodes, Set<String> availablePlugins, Map<String,String> aliasMap)
      Builds the adjacency list for the dependency graph. An edge from A to B means B depends on A (so A must load before B).
    • buildHardAdjacencyList

      private Map<String,Set<String>> buildHardAdjacencyList(Map<String,PluginDependencyResolver.PluginNode> nodes, Map<String,String> aliasMap)
      Builds the hard-only adjacency list: an edge from A to B exactly when B declares depends on A (resolved through aliasMap) and A is present. This is the only graph a missing-dependency refusal propagates over (#676); softDepends, loadBefore and plugin.yml loadAfter only order modules that are present and never carry a missing-dependency refusal. (A cycle is refused by Kahn's partition over the full adjacency instead, which does follow every edge kind.)
    • calculateInDegrees

      private Map<String,Integer> calculateInDegrees(Set<String> nodeNames, Map<String,Set<String>> adjacencyList)
      Calculates in-degrees for a set of node names.
    • kahnSort

      private List<String> kahnSort(Set<String> nodes, Map<String,Set<String>> adjacencyList, Map<String,Integer> inDegree)
      Performs Kahn's algorithm for topological sorting.