package net.jangaroo.dependencies; import com.google.common.base.Charsets; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multiset; import com.google.common.io.Files; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; public class CyclicRequiredDependencies { public static final String PATH_PREFIX = "/target/classes/META-INF/resources/joo/classes/"; public static final String PATH_SUFFIX = ".js"; public static void main(String[] args) throws IOException { if (args.length != 2) { System.out.println("Usage: java -jar ... <BASEDIR> <OUTFILE>"); } else { String baseDir = args[0]; List<File> files = new ArrayList<>(); fillInCompiledClassFiles(new File(baseDir), files); Multimap<String, String> requires = HashMultimap.create(); Set<String> staticallyInitialized = new HashSet<>(); for (File file : files) { analyzeFile(file, requires, staticallyInitialized); } Set<String> classesInCycles = getNodesInMarkedCycles(requires, staticallyInitialized); File outFile = new File(args[1]); try (PrintWriter writer = new PrintWriter(new FileWriter(outFile))) { writeGraph(writer, requires, classesInCycles, staticallyInitialized); } } } private static void fillInCompiledClassFiles(File baseDir, List<File> files) { for (File file : baseDir.listFiles()) { if (file.isDirectory()) { fillInCompiledClassFiles(file, files); } else { String path = file.getPath(); if (path.contains(PATH_PREFIX) && path.endsWith(PATH_SUFFIX)) { files.add(file); } } } } private static void analyzeFile(File file, Multimap<String, String> requires, Set<String> staticallyInitialized) throws IOException { String path = file.getPath(); String className = path .substring(path.indexOf(PATH_PREFIX) + PATH_PREFIX.length(), path.length() - PATH_SUFFIX.length()) .replace('/', '.'); List<String> lines = Files.readLines(file, Charsets.UTF_8); int lastBrace = -1; for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); if (line.indexOf('}') != -1) { lastBrace = i; } if (line.contains("HAS_STATIC_CODE")) { staticallyInitialized.add(className); } } if (lastBrace == -1) { return; } String descriptorLine = lines.get(lastBrace); int startPos = descriptorLine.lastIndexOf('['); int endPos = descriptorLine.lastIndexOf(']'); String dependenciesString = descriptorLine.substring(startPos + 1, endPos); String[] dependencies = dependenciesString.split("[\",]+"); for (String dependency : dependencies) { if (!dependency.isEmpty() && !dependency.equals(className)) { requires.put(className, dependency); } } } private static Set<String> getNodesInMarkedCycles(Multimap<String, String> edges, Set<String> markedNodes) { Multiset<String> classNames = edges.keys(); Set<String> result = new HashSet<>(); for (String className : classNames) { if (isContainedInMarkedCycle(className, edges, markedNodes)) { result.add(className); } } return result; } private static boolean isContainedInMarkedCycle(String node, Multimap<String, String> edges, Set<String> markedNodes) { // Compute all reachable marked nodes. Set<String> reachable = new HashSet<>(); Deque<String> todo = new LinkedList<>(); todo.addAll(edges.get(node)); while (!todo.isEmpty()) { String current = todo.removeLast(); if (reachable.add(current)) { todo.addAll(edges.get(current)); } } reachable.retainAll(markedNodes); // Compute all reachable marked nodes and nodes reachable from those nodes. todo.clear(); todo.addAll(reachable); reachable.clear(); while (!todo.isEmpty()) { String current = todo.removeLast(); if (reachable.add(current)) { todo.addAll(edges.get(current)); } } return reachable.contains(node); } private static void writeGraph(PrintWriter writer, Multimap<String, String> requires, Set<String> classesInCycles, Set<String> staticallyInitialized) { writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); writer.println("<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\" xmlns:java=\"http://www.yworks.com/xml/yfiles-common/1.0/java\" xmlns:sys=\"http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0\" xmlns:x=\"http://www.yworks.com/xml/yfiles-common/markup/2.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:y=\"http://www.yworks.com/xml/graphml\" xmlns:yed=\"http://www.yworks.com/xml/yed/3\" xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd\">"); writer.println("<key for=\"node\" id=\"d6\" yfiles.type=\"nodegraphics\"/>"); for (String nodeId : classesInCycles) { writer.println(" <node id=\"" + nodeId + "\">"); writer.println(" <data key=\"d6\">"); writer.println(" <y:ShapeNode>"); if (staticallyInitialized.contains(nodeId)) { writer.println(" <y:Fill color=\"#FFA0A0\" transparent=\"false\"/>"); } else { writer.println(" <y:Fill color=\"#A0FFA0\" transparent=\"false\"/>"); } writer.println(" <y:Geometry height=\"20.0\" width=\"" + 8 * nodeId.length() + "\" x=\"765.099609375\" y=\"-442.9166666666667\"/>\n"); writer.println(" <y:NodeLabel>" + nodeId + "</y:NodeLabel>"); writer.println(" </y:ShapeNode>"); writer.println(" </data>"); writer.println(" </node>"); } for (Map.Entry<String, String> entry : requires.entries()) { if (classesInCycles.contains(entry.getKey()) && classesInCycles.contains(entry.getValue())) { writer.println(" <edge source=\"" + entry.getKey() + "\" target=\"" + entry.getValue() + "\"/>"); } } writer.println("</graphml>"); } }