Class PluginDependencyResolver
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, so a caller can load the
unaffected modules instead of falling back to filesystem order.
- Since:
- 6.2.0
- Author:
- wisdomme
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic classException thrown when circular dependencies are detected.static classException thrown when required dependencies are missing.static classRepresents a dependency graph node containing plugin class and its dependencies. -
Field Summary
Fields -
Constructor Summary
ConstructorsConstructorDescriptionPluginDependencyResolver(Logger logger) Constructs a new PluginDependencyResolver. -
Method Summary
Modifier and TypeMethodDescriptionbuildAdjacencyList(Map<String, PluginDependencyResolver.PluginNode> nodes, Set<String> availablePlugins, Map<String, String> aliasMap) Builds the adjacency list for the dependency graph.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'splugin.ymlname:- when present and not already claimed by some other node's simple class name - also maps to that node's canonical (simple class) name.private StringbuildMissingDependencyMessage(Map<String, List<String>> missingDependenciesByNode) Builds the refusal message naming every declaring module and every missing dependency it wanted (#361 / WR-05).Calculates in-degrees for a set of node names.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.Expands a seed set of node names forward along the adjacency edges (dependency -> dependent), returning the seeds plus every node transitively reachable from them.Finds every distinct cycle within theremainingsub-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.private voidfindCyclesFrom(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) Performs Kahn's algorithm for topological sorting.List<Class<? extends UltiToolsPlugin>> resolve(List<Class<? extends UltiToolsPlugin>> pluginClasses) Resolves plugin load order using Kahn's algorithm for topological sorting.private StringresolveAlias(String rawName, Map<String, String> aliasMap) Restricts an adjacency list to a subset of nodes, dropping any edge touching a node outside the subset.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.private voidDetects two or more nodes declaring the sameplugin.ymlname:and logs one WARNING per colliding name, naming every module that declared it (#361).
-
Field Details
-
logger
-
-
Constructor Details
-
PluginDependencyResolver
Constructs a new PluginDependencyResolver.- Parameters:
logger- the logger used to warn about a duplicateplugin.ymlname: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
public List<Class<? extends UltiToolsPlugin>> resolve(List<Class<? extends UltiToolsPlugin>> pluginClasses) throws PluginDependencyResolver.CircularDependencyException, PluginDependencyResolver.MissingDependencyException Resolves plugin load order using Kahn's algorithm for topological sorting.- Parameters:
pluginClasses- the list of plugin classes to sort- Returns:
- sorted list of plugin classes
- Throws:
PluginDependencyResolver.CircularDependencyException- if a dependency cycle is detected; the exception itself carries the sortable prefix and the refused setPluginDependencyResolver.MissingDependencyException- if a required hard dependency is missing; the exception itself carries the sortable prefix and the refused set
-
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'splugin.ymlname:- 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
Detects two or more nodes declaring the sameplugin.ymlname: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
nodesiteration order (discovery order), exactly as before. It only makes the operator aware that a pick happened, instead of the dependency graph silently depending on filesystem directory-listing order with nothing logged.IN-01 (gate-1 review, 16-REVIEW-residue.md): "discovery order" here is deterministic GIVEN a deterministic
nodesiteration order, but this class's only production caller,PluginManager.init(ClassLoader), builds its input list from a rawFile.listFiles()call with no sort - measured non-alphabetical and non-creation-order on this environment's filesystem (see #476). This WARNING's named "winner" is therefore only as reproducible as that upstream order; not a regression this change introduces (resolution already picked "whichever came first" before this fix), and not addressed here deliberately - sorting the upstream list would itself be a load-order behaviour change on existing installs, requiring its own decision. Tracked as #476.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 anyplugin.ymlname:is considered at all, so that OTHER node wins outright and none of theplugin.ymldeclarers do, regardless of their discovery order among themselves. This method now checksnodes.containsKey(ymlName)(keyed by simple class name) before falling back to "first declarer", so the reported winner always matches whatbuildAliasMap(java.util.Map<java.lang.String, com.ultikits.ultitools.manager.PluginDependencyResolver.PluginNode>)actually produces. -
resolveAlias
-
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 throughaliasMapbefore the availability check; the message itself still names the raw declared value. -
buildMissingDependencyMessage
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
Expands a seed set of node names forward along the adjacency edges (dependency -> dependent), returning the seeds plus every node transitively reachable from them. Used both to compute the refused set for a missing hard dependency (this method) and, implicitly, by Kahn's own partition for a cycle (a node depending on a cycle member never reaches in-degree 0, so it is already excluded fromsortedNameswithout needing this call). -
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 theremainingsub-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
-
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). -
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.
-