| ///usr/bin/env jbang "$0" "$@" ; exit $? | |
| //JAVA 17+ | |
| //DEPS guru.nidi:graphviz-java:0.18.1 | |
| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import guru.nidi.graphviz.attribute.*; | |
| import guru.nidi.graphviz.engine.Engine; | |
| import guru.nidi.graphviz.engine.Format; | |
| import guru.nidi.graphviz.engine.Graphviz; | |
| import guru.nidi.graphviz.model.*; | |
| import guru.nidi.graphviz.parse.Parser; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.nio.file.Files; | |
| import java.nio.file.Paths; | |
| import java.util.*; | |
| import java.util.regex.Pattern; | |
| import static guru.nidi.graphviz.model.Factory.*; | |
| public class ReactorGraph { | |
| private static final LinkedHashMap<String, Pattern> CLUSTER_PATTERNS = new LinkedHashMap<>(); | |
| static { | |
| CLUSTER_PATTERNS.put("JLine", Pattern.compile("^org\\.jline:.*")); | |
| CLUSTER_PATTERNS.put("Maven 4 API", Pattern.compile("^org\\.apache\\.maven:maven-api-(?!impl).*")); | |
| CLUSTER_PATTERNS.put("Maven Resolver 2", Pattern.compile("^org\\.apache\\.maven\\.resolver:.*")); | |
| CLUSTER_PATTERNS.put("Maven 4 Implementation", Pattern.compile("^org\\.apache\\.maven:maven-(support|impl|di|core|cli|xml|jline|logging|executor|testing):.*")); | |
| CLUSTER_PATTERNS.put("Maven 3 Compatibility", Pattern.compile("^org\\.apache\\.maven:maven-(artifact|builder-support|compat|embedder|model|model-builder|plugin-api|repository-metadata|resolver-provider|settings|settings-builder|toolchain-builder|toolchain-model):.*")); | |
| CLUSTER_PATTERNS.put("Sisu", Pattern.compile("(^org\\.eclipse\\.sisu:.*)|(.*:guice:.*)|(.*:javax.inject:.*)|(.*:javax.annotation-api:.*)|(.*:aopalliance:.*)")); | |
| CLUSTER_PATTERNS.put("Plexus", Pattern.compile("^org\\.codehaus\\.plexus:.*")); | |
| CLUSTER_PATTERNS.put("XML Parsing", Pattern.compile("(.*:woodstox-core:.*)|(.*:stax2-api:.*)")); | |
| CLUSTER_PATTERNS.put("Wagon", Pattern.compile("^org\\.apache\\.maven\\.wagon:.*")); | |
| CLUSTER_PATTERNS.put("SLF4j", Pattern.compile("^org\\.slf4j:.*")); | |
| CLUSTER_PATTERNS.put("Commons", Pattern.compile("^commons-cli:.*")); | |
| CLUSTER_PATTERNS.put("Testing", Pattern.compile("^.*:(mockito-core|junit-jupiter-api):.*")); | |
| } | |
| private static final Pattern HIDDEN_NODES = Pattern.compile(".*:(maven-docgen|roaster-api|roaster-jdt|velocity-engine-core|commons-lang3|asm|logback-classic|slf4j-simple|domtrip-core|domtrip-maven):.*"); | |
| public static void main(String[] args) { | |
| try { | |
| // Parse DOT file generated by org.fusesource.mvnplugins:maven-graph-plugin:reactor | |
| MutableGraph originalGraph = new Parser().read(new File("target/graph/reactor-graph.dot")); | |
| // Create final graph | |
| MutableGraph clusteredGraph = mutGraph("G").setDirected(true); | |
| clusteredGraph.graphAttrs().add(GraphAttr.COMPOUND); | |
| clusteredGraph.graphAttrs().add(Label.of("Reactor Graph")); | |
| clusteredGraph.nodeAttrs().add("fontname", "Arial"); | |
| // Create clusters | |
| Map<String, MutableGraph> clusters = new HashMap<>(); | |
| for (String clusterName : CLUSTER_PATTERNS.keySet()) { | |
| String key = "cluster_" + clusterName.replaceAll("\\s+", ""); | |
| MutableGraph cluster = mutGraph(key).setDirected(true); | |
| cluster.graphAttrs().add(Label.of(clusterName)); | |
| clusters.put(clusterName, cluster); | |
| clusteredGraph.add(cluster); | |
| } | |
| // Map to store new nodes by node name | |
| Map<String, MutableNode> nodeMap = new HashMap<>(); | |
| Map<String, String> nodeToCluster = new HashMap<>(); | |
| Map<String, String> newNames = new HashMap<>(); | |
| // First pass: Create nodes and organize them into clusters | |
| for (MutableNode originalNode : originalGraph.nodes()) { | |
| String oldNodeName = originalNode.name().toString(); | |
| if (HIDDEN_NODES.matcher(oldNodeName).matches()) { | |
| continue; | |
| } | |
| String nodeName = oldNodeName; | |
| if (originalNode.get("label") instanceof Label l) { | |
| nodeName = l.value(); | |
| // fix "guice\njar:classes" label caused by classifier | |
| int i = nodeName.indexOf('\\'); | |
| if (i > 0) { | |
| nodeName = nodeName.substring(0, i); | |
| } | |
| } | |
| MutableNode newNode = mutNode(nodeName); | |
| nodeMap.put(nodeName, newNode); | |
| newNames.put(oldNodeName, nodeName); | |
| boolean added = false; | |
| for (Map.Entry<String, Pattern> entry : CLUSTER_PATTERNS.entrySet()) { | |
| if (entry.getValue().matcher(oldNodeName).matches()) { | |
| clusters.get(entry.getKey()).add(newNode); | |
| nodeToCluster.put(nodeName, entry.getKey()); | |
| added = true; | |
| break; | |
| } | |
| } | |
| if (!added) { | |
| clusteredGraph.add(newNode); | |
| } | |
| } | |
| // Second pass: Add links to the clustered graph | |
| Map<String, MutableNode> substitutes = new HashMap<>(); | |
| Set<Pair> existingLinks = new HashSet<>(); | |
| for (MutableNode node : originalGraph.nodes()) { | |
| for (Link link : node.links()) { | |
| String sourceName = newNames.get(link.from().name().toString()); | |
| String targetName = newNames.get(link.to().name().toString()); | |
| String sourceCluster = nodeToCluster.get(sourceName); | |
| String targetCluster = nodeToCluster.get(targetName); | |
| MutableNode sourceNode = nodeMap.get(sourceName); | |
| MutableNode targetNode = nodeMap.get(targetName); | |
| if (sourceNode != null && targetNode != null ) { | |
| if (!Objects.equals(sourceCluster, targetCluster)) { | |
| // Inter-cluster link | |
| if (sourceCluster != null) { | |
| sourceName = "cluster_" + sourceCluster.replaceAll("\\s+", ""); | |
| } | |
| if (targetCluster != null) { | |
| targetName = "cluster_" + targetCluster.replaceAll("\\s+", ""); | |
| } | |
| sourceNode = substitutes.computeIfAbsent(sourceName, n -> createNode(n, clusteredGraph)); | |
| targetNode = substitutes.computeIfAbsent(targetName, n -> createNode(n, clusteredGraph)); | |
| } | |
| if (existingLinks.add(new Pair(sourceName, targetName))) { | |
| sourceNode.addLink(targetNode); | |
| } | |
| } | |
| } | |
| } | |
| // Write intermediary graph to DOT file | |
| String dotContent = Graphviz.fromGraph(clusteredGraph).render(Format.DOT).toString(); | |
| Files.write(Paths.get("target/graph/intermediary_graph.dot"), dotContent.getBytes()); | |
| System.out.println("Intermediary graph written to intermediary_graph.dot"); | |
| // Render graph to SVG | |
| Graphviz.fromGraph(clusteredGraph) | |
| .engine(Engine.FDP) | |
| .render(Format.SVG).toFile(new File("target/graph/intermediary_graph.svg")); | |
| System.out.println("Final graph rendered to intermediary_graph.svg"); | |
| // Generate and render the high-level graph | |
| MutableGraph highLevelGraph = generateHighLevelGraph(clusteredGraph, clusters, nodeToCluster, nodeMap); | |
| // Write high-level graph to DOT file | |
| String highLevelDotContent = Graphviz.fromGraph(highLevelGraph).render(Format.DOT).toString(); | |
| Files.write(Paths.get("target/graph/high_level_graph.dot"), highLevelDotContent.getBytes()); | |
| System.out.println("High-level graph written to high_level_graph.dot"); | |
| // Render high-level graph to SVG | |
| Graphviz.fromGraph(highLevelGraph) | |
| .engine(Engine.DOT) | |
| .render(Format.SVG).toFile(new File("target/site/images/maven-deps.svg")); | |
| System.out.println("High-level graph rendered to maven-deps.svg"); | |
| } catch (IOException e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| private static MutableGraph generateHighLevelGraph(MutableGraph clusteredGraph, Map<String, MutableGraph> clusters, | |
| Map<String, String> nodeToCluster, Map<String, MutableNode> nodeMap) { | |
| MutableGraph highLevelGraph = mutGraph("HighLevelGraph").setDirected(true); | |
| highLevelGraph.graphAttrs().add(GraphAttr.COMPOUND); | |
| highLevelGraph.graphAttrs().add(Label.of("High-Level Reactor Graph")); | |
| highLevelGraph.nodeAttrs().add("fontname", "Arial"); | |
| Map<String, MutableNode> highLevelNodes = new HashMap<>(); | |
| // Create nodes for each cluster | |
| for (Map.Entry<String, MutableGraph> entry : clusters.entrySet()) { | |
| String key = entry.getKey(); | |
| String clusterName = key.replaceAll("\\s+", ""); | |
| MutableGraph cluster = entry.getValue(); | |
| String headerColor = clusterName.startsWith("Maven") ? "black" : "#808080"; // #808080 is a middle gray | |
| String prefix; | |
| switch (clusterName) { | |
| case "Maven4API": prefix = "../api/"; break; | |
| case "Maven4Implementation": prefix = "../impl/"; break; | |
| case "Maven3Compatibility": prefix = "../compat/"; break; | |
| case "MavenResolver2": prefix = "https://maven.apache.org/resolver/"; break; | |
| default: prefix = null; | |
| } | |
| StringBuilder labelBuilder = new StringBuilder(); | |
| labelBuilder.append("<table border='0' cellborder='0' cellspacing='0'>") | |
| .append("<th border='1'><td cellpadding='5' bgcolor='") | |
| .append(headerColor) | |
| .append("'><font color='white'><b> ") | |
| .append(key) | |
| .append(" </b></font></td></th>"); | |
| cluster.nodes().stream().map(MutableNode::name).map(Label::toString).sorted() | |
| .forEach(nodeName -> { | |
| labelBuilder.append("<tr><td align='left'"); | |
| if (prefix != null) { | |
| labelBuilder.append(" href='").append(prefix).append(nodeName) | |
| .append("' title='").append(nodeName).append("' target='_parent'"); | |
| } | |
| labelBuilder.append(">").append(nodeName).append("</td></tr>"); | |
| }); | |
| labelBuilder.append("</table>"); | |
| MutableNode clusterNode = mutNode(clusterName).add(Label.html(labelBuilder.toString())) | |
| .add("shape", "rectangle"); | |
| highLevelNodes.put(clusterName, clusterNode); | |
| highLevelGraph.add(clusterNode); | |
| } | |
| // Add individual nodes for unclustered nodes | |
| for (MutableNode node : clusteredGraph.nodes()) { | |
| String nodeName = node.name().toString(); | |
| if (!nodeToCluster.containsKey(nodeName) && !nodeName.startsWith("cluster_")) { | |
| throw new IllegalStateException("All nodes should be in a cluster: " + node.name()); | |
| } | |
| } | |
| // Add edges | |
| Set<Pair> existingLinks = new HashSet<>(); | |
| for (MutableNode node : clusteredGraph.nodes()) { | |
| String sourceName = node.name().toString().replace("cluster_", ""); | |
| String sourceCluster = nodeToCluster.getOrDefault(sourceName, sourceName); | |
| for (Link link : node.links()) { | |
| String targetName = link.to().name().toString().replace("cluster_", ""); | |
| String targetCluster = nodeToCluster.getOrDefault(targetName, targetName); | |
| Pair linkPair = new Pair(sourceCluster, targetCluster); | |
| if (existingLinks.add(linkPair)) { | |
| MutableNode sourceNode = highLevelNodes.get(sourceCluster); | |
| MutableNode targetNode = highLevelNodes.get(targetCluster); | |
| if (sourceNode != null && targetNode != null && sourceNode != targetNode) { | |
| sourceNode.addLink(targetNode); | |
| } | |
| } | |
| } | |
| } | |
| // force top group | |
| MutableGraph topGroup = mutGraph("top") | |
| .setDirected(true) | |
| .graphAttrs().add(Rank.inSubgraph(Rank.RankType.SAME)) | |
| .add( | |
| mutNode("MavenResolver2"), | |
| mutNode("Maven4API"), | |
| mutNode("Maven4Implementation") | |
| ); | |
| highLevelGraph.add(topGroup); | |
| return highLevelGraph; | |
| } | |
| private static MutableNode createNode(String n, MutableGraph clusteredGraph) { | |
| MutableNode t = mutNode(n); | |
| clusteredGraph.add(t); | |
| return t; | |
| } | |
| record Pair(String from, String to) {}; | |
| } |