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 - 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
-
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.buildAliasMap(Map<String, PluginDependencyResolver.PluginNode> nodes, Map<String, String> displayNames) Derives aliases from the complete graph without losing nodes.Keep existing diagnostic names, qualifying only colliding simple names.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 declaresdependson A (resolved throughaliasMap) and A is present.private StringbuildMissingDependencyMessage(Map<String, List<String>> missingDependenciesByNode, Map<String, String> displayNames) 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 given 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) kahnSort(Set<String> nodes, Map<String, Set<String>> adjacencyList, Map<String, Integer> inDegree, Comparator<String> loadOrder) 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 voidwarnOnAmbiguousReferencedSimpleNames(Map<String, PluginDependencyResolver.PluginNode> nodes, Map<String, String> aliasMap) Logs one WARNING per main-class simple name that two or more modules share and that some module actually names independs,softDepends,loadBeforeor a plugin.ymlloadAfter(#841).private voidwarnOnDuplicatePluginYmlNames(Map<String, PluginDependencyResolver.PluginNode> nodes, Map<String, String> aliasMap, Map<String, String> displayNames) Detects two or more nodes declaring the sameplugin.ymlname:and logs one WARNING per colliding name, naming every module that declared it (#361).
-
Field Details
-
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
-
-
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.Ordering uses every edge kind (
depends, presentsoftDepends,loadBefore,plugin.ymlloadAfter). 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 followsdependsedges only; anything the sort cannot place (a cycle and every module ordered after it) is refused as a cycle. When both occur,PluginDependencyResolver.MissingDependencyExceptionis 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 pathsPluginDependencyResolver.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
private Map<String,String> buildAliasMap(Map<String, PluginDependencyResolver.PluginNode> nodes, Map<String, String> displayNames) Derives aliases from the complete graph without losing nodes. Simple names keep their historical last-discovered target and take priority over plugin.yml names. LinkedHashMap replacement retains the first insertion position, matching the old graph's alias order. Previously established plugin.yml aliases are filled first; recovered nodes only add names that were unavailable before. This index chooses names, never which classes can load. -
warnOnAmbiguousReferencedSimpleNames
private void warnOnAmbiguousReferencedSimpleNames(Map<String, PluginDependencyResolver.PluginNode> nodes, Map<String, String> aliasMap) Logs one WARNING per main-class simple name that two or more modules share and that some module actually names independs,softDepends,loadBeforeor a plugin.ymlloadAfter(#841). Such a name resolves to the last-discovered module (historical behaviour, unchanged); the warning lists every fully qualified class and the winner, aswarnOnDuplicatePluginYmlNames(java.util.Map<java.lang.String, com.ultikits.ultitools.manager.PluginDependencyResolver.PluginNode>, java.util.Map<java.lang.String, java.lang.String>, java.util.Map<java.lang.String, java.lang.String>)does for plugin.yml names. A shared simple name that nothing references is not reported: it decides nothing. -
buildDisplayNames
Keep existing diagnostic names, qualifying only colliding simple names. -
displaySet
-
displayCycles
-
warnOnDuplicatePluginYmlNames
private void warnOnDuplicatePluginYmlNames(Map<String, PluginDependencyResolver.PluginNode> nodes, Map<String, String> aliasMap, Map<String, String> displayNames) 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 refuse either module or change the alias selection. The existing winners retain priority, and recovered nodes only supply previously unavailable aliases. The warning makes the operator aware that a pick happened instead of silently choosing a target. Ordinary, unique-simple-name inputs still use the first declarer in discovery order.
IN-01 (gate-1 review, 16-REVIEW-residue.md): "discovery order" here is deterministic GIVEN a deterministic
nodesiteration 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 rawFile.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.The actual winner is read from the completed alias index, not inferred from graph keys: simple-name aliases take priority (#478), and recovered same-simple-name nodes must not steal an established plugin.yml alias (#752). Diagnostics and graph edges therefore use the same authoritative alias selection.
-
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
private String buildMissingDependencyMessage(Map<String, List<String>> missingDependenciesByNode, Map<String, String> displayNames) 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 given adjacency edges (dependency -> dependent), returning the seeds plus every node transitively reachable from them. Its only caller passes the hard-only adjacency frombuildHardAdjacencyList(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 followsdependsedges 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 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). -
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 declaresdependson A (resolved throughaliasMap) and A is present. This is the only graph a missing-dependency refusal propagates over (#676);softDepends,loadBeforeandplugin.ymlloadAfteronly 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, Comparator<String> loadOrder) Performs Kahn's algorithm for topological sorting.
-