package ecologylab.bigsemantics.tools; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import ecologylab.appframework.PropertiesAndDirectories; import ecologylab.appframework.SingletonApplicationEnvironment; import ecologylab.bigsemantics.collecting.SemanticsSessionScope; import ecologylab.bigsemantics.cyberneko.CybernekoWrapper; import ecologylab.bigsemantics.generated.library.RepositoryMetadataTypesScope; import ecologylab.bigsemantics.metametadata.MetaMetadata; import ecologylab.serialization.SIMPLTranslationException; import ecologylab.serialization.SimplTypesScope; import ecologylab.serialization.formatenums.StringFormat; /** * This class generates SVG graphs using dot (graphviz), to visualize the type hierarchy in the * meta-metadata repository. * * @author quyin */ public class MmdTypeHierarchyVisualizer extends SingletonApplicationEnvironment { public static final int MAGIC_EXIT_VALUE = -6174; protected static class Node { public String name = ""; public Node parent = null; public String parentLinkInfo = null; public Set<Node> children = new HashSet<Node>(); } SemanticsSessionScope sessionScope; Map<String, Node> allNodes; Node rootOfRoots; Set<String> rootNames; public MmdTypeHierarchyVisualizer() throws SIMPLTranslationException { super(MmdTypeHierarchyVisualizer.class.getSimpleName()); check(isDotRunnable(), "Cannot run graphviz/dot: is it installed and in your PATH?"); SimplTypesScope metadataTScope = RepositoryMetadataTypesScope.get(); sessionScope = new SemanticsSessionScope(metadataTScope, CybernekoWrapper.class); allNodes = new HashMap<String, Node>(); for (MetaMetadata mmd : sessionScope.getMetaMetadataRepository().getMetaMetadataCollection()) { String name = mmd.getName(); if (name != null && !name.isEmpty()) { Node node = null; if (allNodes.containsKey(name)) { node = allNodes.get(name); } else { node = new Node(); node.name = mmd.getName(); allNodes.put(name, node); } String extendedMmdName = mmd.getExtendsAttribute(); String typeMmdName = mmd.getType(); String parentNodeName = null; if (extendedMmdName != null && !extendedMmdName.isEmpty()) { // extends node.parentLinkInfo = "extends"; parentNodeName = extendedMmdName; } else if (typeMmdName != null && !typeMmdName.isEmpty()) { // type node.parentLinkInfo = "type"; parentNodeName = typeMmdName; } else { if (rootOfRoots == null) rootOfRoots = node; else error("Cannot find extends or type attribute: " + node.name); } if (parentNodeName != null) { if (allNodes.containsKey(parentNodeName)) { node.parent = allNodes.get(parentNodeName); } else { node.parent = new Node(); node.parent.name = parentNodeName; allNodes.put(parentNodeName, node.parent); } node.parent.children.add(node); } } else { error("Meta-metadata with empty name: " + SimplTypesScope.serialize(mmd, StringFormat.XML)); } } } protected boolean isDotRunnable() { StringBuilder err = new StringBuilder(); boolean testDot = runProgram(null, null, err, "dot", "-V") == 0 && err.toString().startsWith("dot - graphviz version"); err = new StringBuilder(); boolean testUnflatten = runProgram(null, null, err, "unflatten", "-?") == 0 && err.toString().startsWith("Usage:"); return testDot && testUnflatten; } public void visualize(String outSvgPathPrefix, boolean showTypeMmds, String... roots) throws IOException { List<Node> orderedNodes = new ArrayList<Node>(); sortNodesPreOrder(orderedNodes); rootNames = new HashSet<String>(); for (String rootName : roots) rootNames.add(rootName); if (rootNames.isEmpty()) rootNames.add(rootOfRoots.name); for (Node root : orderedNodes) { if (rootNames.contains(root.name)) { String dotScriptSrc = generateDotScript(root, showTypeMmds); System.out.println("\n========Dot script begins(root name: " + root.name + ")========\n"); System.out.println(dotScriptSrc); System.out.println("\n========Dot script ends========\n"); generateSvg(dotScriptSrc, outSvgPathPrefix + "-" + root.name + ".svg"); } } } protected void sortNodesPreOrder(List<Node> result) { sortNodesPreOrderHelper(result, rootOfRoots); } protected void sortNodesPreOrderHelper(List<Node> result, Node current) { result.add(current); for (Node child : current.children) sortNodesPreOrderHelper(result, child); } protected String generateDotScript(Node root, boolean showTypeMmds) throws IOException { StringBuilder result = new StringBuilder(); appendDotScriptHead(result); generateDotScriptHelper(root, result, showTypeMmds); appendDotScriptTail(result); return result.toString(); } protected void appendDotScriptHead(Appendable appendable) throws IOException { appendable.append("// Generated by DotScriptGenerator\n"); appendable.append("\n"); appendable.append("digraph mmd_type_hierarchy {\n"); appendable.append(" graph [ rankdir=\"BT\" ]\n"); appendable.append(" node [ shape=\"box\", style=\"rounded\" ]\n"); appendable.append(" edge [ penwidth=2 ]\n"); appendable.append(" // mmd type inheritance relations below:\n"); } protected void generateDotScriptHelper(Node currentNode, Appendable result, boolean showTypeMmds) throws IOException { for (Node child : currentNode.children) { boolean isARoot = rootNames.contains(child.name); if (isARoot) { // special style for subgraph roots result.append(String.format(" %s [ style=\"rounded,filled\" ]\n", child.name)); } if ("extends".equals(child.parentLinkInfo)) { // extends result.append(String.format(" %s -> %s\n", child.name, currentNode.name)); } else if ("type".equals(child.parentLinkInfo) && showTypeMmds) { // type // special style for type mmd result.append(String.format(" %s [ color=\"gray\", style=\"rounded,dashed\" ]\n", child.name)); result.append(String.format(" %s -> %s [ style=\"dashed\", color=\"gray\"]\n", child.name, currentNode.name)); } if (!isARoot) generateDotScriptHelper(child, result, showTypeMmds); } } protected void appendDotScriptTail(Appendable appendable) throws IOException { appendable.append("}\n"); appendable.append("\n"); appendable.append("// End of generated Dot script\n"); appendable.append("\n"); } protected boolean generateSvg(String dotScriptSrc, String outSvgPath) { StringBuilder out = new StringBuilder(); int exitCode = runProgram(dotScriptSrc, out, null, "unflatten", "-f", "-l6"); if (exitCode == 0) { String unflattenSrc = out.toString(); StringBuilder err = new StringBuilder(); exitCode = runProgram(unflattenSrc, out, err, "dot", "-Tsvg", "-o" + outSvgPath); if (exitCode == 0) { return true; } else { error("Exit code: " + exitCode); error("Stdout output: " + out.toString()); error("Stderr output: " + err.toString()); } } return false; } protected void readAll(Reader reader, Appendable out) throws IOException { char[] buffer = new char[1024]; while (true) { int n = reader.read(buffer, 0, 1024); if (n < 0) break; if (out != null) { String s = String.valueOf(buffer, 0, n); out.append(s); } } } protected int runProgram(CharSequence stdIn, Appendable stdOut, Appendable stdErr, String... cmd) { ProcessBuilder procBuilder = new ProcessBuilder(cmd); Process proc = null; int exitValue = MAGIC_EXIT_VALUE; try { proc = procBuilder.start(); if (stdIn != null) { OutputStreamWriter writer = new OutputStreamWriter(proc.getOutputStream()); writer.append(stdIn); writer.close(); } InputStreamReader outReader = new InputStreamReader(proc.getInputStream()); readAll(outReader, stdOut); outReader.close(); InputStreamReader errReader = new InputStreamReader(proc.getErrorStream()); readAll(errReader, stdErr); errReader.close(); exitValue = proc.waitFor(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } finally { if (proc != null) proc.destroy(); } return exitValue; } public static void showHelpAndExit() { String progName = MmdTypeHierarchyVisualizer.class.getSimpleName(); System.err.format("Usage: %s <OPTIONS> [<output-SVG-path-prefix>]\n", progName); System.err.format("Opts:\n"); System.err.format(" -h Show this help message.\n"); System.err.format(" -s Show only types that define new schemas.\n"); System.err.format(" -r Comma separated list of root types. For each root type, an individual SVG file will be generated.\n"); System.err.format("Args:\n"); System.err.format(" <output-SVG-path-prefix>\n"); System.err.format(" Path prefix for output SVG files. Default to the user's document " + "folder, starting with mmd_types.\n"); System.exit(-1); } public static void main(String[] args) throws SIMPLTranslationException, IOException { boolean showTypeMmds = true; String outSvgPathPrefix = PropertiesAndDirectories.userDocumentDir() + File.separator + "mmd_types"; String[] roots = { "metadata", "document", "search", "compound_document", "creative_work" }; for (int i = 0; i < args.length; ++i) { if ("-h".equals(args[i])) { showHelpAndExit(); } else if ("-s".equals(args[i])) { showTypeMmds = false; } else if ("-r".equals(args[i])) { i++; roots = args[i].split(","); } else { outSvgPathPrefix = args[i]; } } MmdTypeHierarchyVisualizer v = new MmdTypeHierarchyVisualizer(); v.visualize(outSvgPathPrefix, showTypeMmds, roots); } }