Java tutorial
/* * Copyright (C) 2013 denkbares GmbH * * This is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. * * This software is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this software; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF * site: http://www.fsf.org. */ package de.knowwe.visualization.dot; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; import com.denkbares.collections.MultiMap; import com.denkbares.strings.Strings; import com.denkbares.utils.Log; import de.knowwe.visualization.ConceptNode; import de.knowwe.visualization.Config; import de.knowwe.visualization.Edge; import de.knowwe.visualization.GraphDataBuilder.NODE_TYPE; import de.knowwe.visualization.SubGraphData; import de.knowwe.visualization.util.FileUtils; import de.knowwe.visualization.util.SAXBuilderSingleton; import de.knowwe.visualization.util.Utils; /** * @author Jochen Reutelshfer * @created 29.04.2013 */ public class DOTRenderer { // appearance of outer node private static final String outerLabel = "[ shape=\"none\" fontsize=\"0\" fontcolor=\"white\" ];\n"; private static final int TIMEOUT = 50000; private static final Set<String> cleanedCacheDirectories = new HashSet<>(); public static String getFilePath(Config config) { return getDirectoryPath(config) + config.getCacheFileID(); } private static String getDirectoryPath(Config config) { String path = config.getCacheDirectoryPath() + FileUtils.FILE_SEPARATOR + FileUtils.KNOWWEEXTENSION_FOLDER + FileUtils.FILE_SEPARATOR + FileUtils.TMP_FOLDER + FileUtils.FILE_SEPARATOR; handleCleanup(path); return path; } /** * Cleans up all cache directories once on startup. */ private static void handleCleanup(String path) { if (cleanedCacheDirectories.add(path)) { File dir = new File(path); if (dir.exists() && dir.isDirectory()) { //noinspection ConstantConditions for (File file1 : dir.listFiles()) { //noinspection ResultOfMethodCallIgnored file1.delete(); } } } } private static String buildLabel(RenderingStyle style) { StringBuilder result = new StringBuilder(); result.append(" shape=\"").append(style.getShape()).append("\" "); if (!Strings.isBlank(style.getStyle())) { result.append(" style=\"").append(style.getStyle()).append("\" "); } if (!Strings.isBlank(style.getFillcolor())) { result.append(" fillcolor=\"").append(style.getFillcolor()).append("\" "); } return result.toString(); } private static String buildRelation(String arrowtail, String color) { return " arrowtail=\"" + arrowtail + "\" " + " color=\"" + color + "\" "; } /** * Given the label of the inner relation, the method returns the String of the appearance of the relation. * * @created 06.09.2012 */ private static String innerRelation(String label, String color, boolean isBiDirectional) { // Basic Relation Attributes String arrowtail = "normal"; String dir = "[ "; if (color == null) { // black is default color = "black"; } if (isBiDirectional) { dir += "dir=\"both\" "; } return dir + "label = \"" + label + "\"" + buildRelation(arrowtail, color) + " ];\n"; } /** * The sources from the maps are being written into the String-dotSource. * * @created 18.08.2012 */ static String createDotSources(SubGraphData data, Config config) { // we clean up the graph before rendering (e. g. undesired cycles etc) // if (config.getShowInverse() == Config.ShowInverse.FALSE_DOT_BASED || config.getShowInverse() == Config.ShowInverse.FALSE) { // simplifyGraph(data); // } // we clean up the graph before rendering (e. g. undesired cycles etc) simplifyGraph(data, config); String dotSource = "digraph {\n"; // printing title of graph top left of visualization if (config.getTitle() != null) { dotSource += "graph [label = \"" + config.getTitle() + " (" + new Date() + ")\", labelloc = \"t\", labeljust = \"left\", fontsize = 10];\n"; } //only useful for neato, ignored for dot dotSource += "sep=\"+25,25\";\n"; dotSource += "splines = true;\n"; if (Strings.isBlank(config.getOverlap())) { dotSource += "overlap=false;\n"; } else { dotSource += "overlap=" + config.getOverlap() + ";\n"; } // using rankSame constraints for custom layouting String rankSameValue = config.getRankSame(); if (!Strings.isBlank(rankSameValue) && rankSameValue.contains(",")) { StringBuilder sb = new StringBuilder(); String[] values = rankSameValue.split(","); for (String value : values) { sb.append("\"").append(value.trim()).append("\" "); } //{rank=same; q4 q3} dotSource += "{rank=same; " + sb + "}\n"; } // set graph size and rankDir dotSource += DOTRenderer.setSizeAndRankDir(config.getRankDir(), config.getWidth(), config.getHeight(), config.getSize(), data.getConceptDeclarations().size()); dotSource += generateGraphSource(data, config); dotSource += "}"; return dotSource; } @SuppressWarnings("Duplicates") private static Set<Edge> getDoubleEdges(Set<Edge> edges) { Set<Edge> doubleEdges = new HashSet<>(); ConceptNode o, s; Edge e1, e2; //look up every Edge in my SubGraphData for (int i = 0; i < edges.size() - 1; i++) { //for (Edge e1 : myEdges) { //memorize Object & Subject e1 = (Edge) edges.toArray()[i]; o = e1.getObject(); s = e1.getSubject(); //look up every OTHER Edge for (int j = i + 1; j < edges.size(); j++) { //for (Edge e2 : myEdges) { // If subject == object (both directions) AND predicates are the same { e2 = (Edge) edges.toArray()[j]; if (o.equals(e2.getSubject()) && s.equals(e2.getObject()) && e1.getPredicate().equals(e2.getPredicate())) { //mark first edge as bidirectional and add second Edge to redundant Set e1.setBidirectionalEdge(true); doubleEdges.add(e2); // data.removeEdge(e2); } } } } return doubleEdges; } private static Set<Edge> getRecursiveEdges(Set<Edge> edges) { Set<Edge> recursiveEdges = new HashSet<>(); ConceptNode s, o; for (Edge e : edges) { s = e.getSubject(); o = e.getObject(); if (s.equals(o)) { recursiveEdges.add(e); } } return recursiveEdges; } private static void simplifyGraph(SubGraphData data, Config config) { Set<Edge> redundantEdges; if (!config.isShowRedundant()) { //mark all the superProperties (sets superProperty attributes) data = defineSuperProperties(data, data.getSubPropertiesMap()); //receive all double edges redundantEdges = getDoubleEdges(data.getAllEdges()); //receive and add all recursive edges redundantEdges.addAll(getRecursiveEdges(data.getAllEdges())); //receive and add all edges which are SuperProperties redundantEdges.addAll(getRedundantSuperPropertyEdges(data.getAllEdges())); // loop through redundant Set to remove all redundant Edges redundantEdges.forEach(data::removeEdge); } if (!config.isShowInverse()) { // change predicates of inverse Edges and receive now redundant Edges redundantEdges = defineInverseProperties(data, data.getInversePropertiesMap()); // loop through redundant Set to remove all redundant Edges, once again redundantEdges.forEach(data::removeEdge); } } private static Set<Edge> defineInverseProperties(SubGraphData data, MultiMap<String, String> inversePropertiesMap) { Set<Edge> edges = data.getAllEdges(); String relationURI, inverseURI, p1, p2; Set<Edge> redundantEdges = new HashSet<>(); Set<String> inverseProperties; ConceptNode o1, o2, s1, s2; // Check every Edge for inverseProperties for (Edge e1 : edges) { relationURI = e1.getRelationURI(); s1 = e1.getSubject(); o1 = e1.getObject(); p1 = e1.getPredicate(); inverseProperties = inversePropertiesMap.getValues(relationURI); if (inversePropertiesMap.getValues(relationURI).size() != 1) { continue; } inverseURI = (String) inversePropertiesMap.getValues(relationURI).toArray()[0]; // Check if this there's a inverseProperty to this Edge's relationURI // and it's not an reflexive inverse // and isn't already redundant if (inverseProperties.size() == 1 && !inverseURI.equals(relationURI) && !redundantEdges.contains(e1)) { for (Edge e2 : edges) { s2 = e2.getSubject(); o2 = e2.getObject(); // Check if this edge is the inverse we are looking for if (e2.getRelationURI().equals(inverseURI) && s1.equals(o2) && o1.equals(s2)) { // if so: change Predicate of first edge, change to bidirectional and add second (inverse) edge to redundant edges p2 = e2.getPredicate(); e1.setPredicate(p1 + " | " + p2); e1.setBidirectionalEdge(true); redundantEdges.add(e2); } } } } return redundantEdges; } private static SubGraphData defineSuperProperties(SubGraphData data, MultiMap<String, String> subPropertiesMap) { Set<Edge> edges = data.getAllEdges(); String relationURI, subURI; ConceptNode o1, o2, s1, s2; Set<String> subProperties; // Check every Edge for SubProperties for (Edge e1 : edges) { relationURI = e1.getRelationURI(); s1 = e1.getSubject(); o1 = e1.getObject(); // Check if there is SubProperties to this Edge's relationURI if (!subPropertiesMap.getValues(relationURI).isEmpty()) { subProperties = subPropertiesMap.getValues(relationURI); // Check edges once again for SubProperties, skip if it's the same edge for (Edge e2 : edges) { // skip if same edge if (!e1.equals(e2)) { s2 = e2.getSubject(); o2 = e2.getObject(); subURI = e2.getRelationURI(); // check for same Source, Destination (swapped aswell!) and for being SubProperty // mark e1 as SuperProperty if true, continue outer loop (break inner loop) if ((s1.equals(s2) && o1.equals(o2)) || (s1.equals(o2) && o1.equals(s2)) && subProperties.contains(subURI)) { e1.setSuperProperty(true); } } } } } return data; } private static Set<Edge> getRedundantSuperPropertyEdges(Set<Edge> edges) { Set<Edge> redundantSuperPropertyEdges = new HashSet<>(); // Checks if isSuperProperty and adds if true for (Edge e : edges) { if (e.isSuperProperty()) { redundantSuperPropertyEdges.add(e); } } return redundantSuperPropertyEdges; } private static String generateGraphSource(SubGraphData data, Config config) { Collection<ConceptNode> dotSourceLabel = data.getConceptDeclarations(); StringBuilder dotSource = new StringBuilder(); // iterate over the labels and add them to the dotSource Map<ConceptNode, Set<Edge>> clusters = data.getClusters(); for (ConceptNode node : dotSourceLabel) { appendNodeDefinitionLineToSource(data, config, dotSource, node); // if the literal mode is 'TABLE', the literals are contained within the node // if the literal mode is 'NODES' we need to define literal nodes from the clusters if (Config.LiteralMode.NODES == config.getLiteralMode()) { Set<Edge> clusterEdges = clusters.get(node); if (clusterEdges != null) { for (Edge clusterEdge : clusterEdges) { ConceptNode object = clusterEdge.getObject(); if (object != null) { appendNodeDefinitionLineToSource(data, config, dotSource, object); } } } } } /* iterate over the relations and add them to the dotSource */ // add level zero "default cluster" for (Edge key : clusters.get(ConceptNode.DEFAULT_CLUSTER_NODE)) { appendEdgeSource(config, dotSource, key); } // add literals contained in the clusters if the literal mode is 'NODES' if (Config.LiteralMode.NODES == config.getLiteralMode()) { for (ConceptNode node : dotSourceLabel) { if ((node.getType() != NODE_TYPE.LITERAL)) { Set<Edge> edges = clusters.get(node); if (edges != null) { for (Edge key : edges) { appendEdgeSource(config, dotSource, key); } } } } } return dotSource.toString(); } private static void appendNodeDefinitionLineToSource(SubGraphData data, Config config, StringBuilder dotSource, ConceptNode node) { RenderingStyle style = node.getStyle(); // root is rendered highlighted if (node.isRoot()) { style.addStyle("bold"); style.setFontstyle(RenderingStyle.Fontstyle.UNDERLINING); } String label; if (node.isOuter()) { label = DOTRenderer.outerLabel; } else { String nodeLabel = clearLabel(node.getConceptLabel()); if ((node.getType() != NODE_TYPE.LITERAL) && Config.LiteralMode.TABLE == config.getLiteralMode()) { // we render the node as a table, showing the selected literals nodeLabel = createHTMLTable(node, data, style.getFontstyle(), config); } else { nodeLabel = createNodeLabel(nodeLabel, style.getFontstyle()); } label = DOTRenderer.createDotConceptLabel(style, node.getConceptUrl(), nodeLabel, true); } dotSource.append("\"").append(node.getName()).append("\"").append(label); } private static String createHTMLTable(ConceptNode node, SubGraphData data, RenderingStyle.Fontstyle fontStyle, Config config) { final Map<ConceptNode, Set<Edge>> clusters = data.getClusters(); final Set<Edge> edges = clusters.get(node); String label = node.getName(); if (!"false".equalsIgnoreCase(config.getShowLabels())) { label = node.getConceptLabel(); } String nodeLabel = createNodeLabel(escapeDot(label), fontStyle); if (edges == null || edges.isEmpty()) { return nodeLabel; } else { // if necessary, remove now unnecessary starttag for html again if (nodeLabel.startsWith("<")) { nodeLabel = nodeLabel.substring(1, nodeLabel.length() - 1); } // create table with cluster edges StringBuilder buffy = new StringBuilder(); buffy.append("<"); // start html label buffy.append("<TABLE BORDER=\"0\">"); // header row with actual node clazz and name buffy.append("<TR>"); buffy.append("<TD BORDER=\"2\">"); if (!Strings.isBlank(node.getClazz())) { buffy.append("<I>"); buffy.append(node.getClazz()); buffy.append("</I>"); } buffy.append("</TD>"); String conceptName = nodeLabel.replace("\\n", "<BR ALIGN=\"CENTER\"/>"); buffy.append("<TD BORDER=\"2\">"); buffy.append("<B>"); buffy.append(Strings.unquote(conceptName)); buffy.append("</B>"); buffy.append("</TD>"); buffy.append("</TR>"); for (Edge edge : edges) { buffy.append("<TR>"); buffy.append("<TD BORDER=\"1\">"); buffy.append(escapeDot(edge.getPredicate())); buffy.append("</TD>"); buffy.append("<TD BORDER=\"1\">"); buffy.append(escapeDot(edge.getObject().getConceptLabel())); buffy.append("</TD>"); buffy.append("</TR>"); } buffy.append("</TABLE>"); buffy.append(">"); // start html label return buffy.toString(); } } private static String escapeDot(String text) { text = text.replace("&", "&"); text = text.replace("<", "<"); text = text.replace(">", ">"); return text; } private static String createNodeLabel(String name, RenderingStyle.Fontstyle f) { // prepareLabel adds linebreaks where necessary and possible // however that is not required as we generate only svg //String nodeLabel = Utils.prepareLabel(name); String nodeLabel = Utils.clean(name, Utils.LINE_BREAK); if (f == RenderingStyle.Fontstyle.NORMAL) { nodeLabel = "\"" + nodeLabel + "\""; } else { nodeLabel = styleLabel(nodeLabel, f); } return nodeLabel; } private static String styleLabel(String targetLabel, RenderingStyle.Fontstyle f) { String label = targetLabel.replace("\\n", "<BR ALIGN=\"CENTER\"/>"); switch (f) { case BOLD: return "<<B>" + label + "</B>>"; case ITALIC: return "<<I>" + label + "</I>>"; case UNDERLINING: return "<<U>" + label + "</U>>"; default: return targetLabel; } } private static void appendEdgeSource(Config config, StringBuilder dotSource, Edge key) { // private static void appendEdgeSource(Config config, StringBuilder dotSource, Edge key, boolean isBiDirectional) { String label = DOTRenderer.innerRelation(key.getPredicate(), config.getRelationColors().get(key.getPredicate()), key.isBidirectionalEdge()); if (key.isOuter()) { boolean arrowHead = key.getSubject().isOuter(); label = DOTRenderer.getOuterEdgeLabel(key.getPredicate(), arrowHead); } dotSource.append("\"").append(key.getSubject().getName()).append("\"").append(" -> ").append("\"") .append(key.getObject().getName()).append("\" ").append(label); } private static String clearLabel(String label) { String xsdStringAnnotation = "^^http://www.w3.org/2001/XMLSchema#string"; if (label.endsWith(xsdStringAnnotation)) { label = label.substring(0, label.length() - xsdStringAnnotation.length()); } return label; } private static String getOuterEdgeLabel(String relation, boolean showArrowHead) { // Relation Attributes String arrowhead = "arrowhead=\"none\" "; String arrowtail = ""; if (showArrowHead) { arrowhead = ""; arrowtail = "arrowtail = \"normal\" "; } String color = "#8b8989"; String style = "dashed"; return "[ label=\"" + relation + "\" fontcolor=\"#8b8989\" " + arrowhead + arrowtail + " color=\"" + color + "\" style=\"" + style + "\" ];\n"; } private static String insertPrefixed(String dotSource, Config config) { String added = config.getDotAddLine(); if (added != null) dotSource += added; return dotSource; } /** * @created 30.10.2012 */ private static String setSizeAndRankDir(Config.RankDir rankDirSetting, String width, String height, String graphSize, int numberOfConcepts) { String rankDir = rankDirSetting.name(); String size = ""; String ratio = ""; if (width != null || height != null) { width = getSizeProperties(width); height = getSizeProperties(height); if (height != null && width != null) { // ratio = " ratio=\"" + Double.valueOf(height)/Double.valueOf(width) + "\" "; ratio = "ratio=\"fill\" "; size = width + "," + height + "!"; } else if (height != null) { size = "10000000," + height + "!"; } else { size = width + ",10000000"; } } else if (graphSize != null) { if (graphSize.matches("\\d+px")) { graphSize = graphSize.substring(0, graphSize.length() - 2); } if (graphSize.matches("\\d+")) { size = String.valueOf(Double.valueOf(graphSize) * 0.010415597); } else { size = calculateAutomaticGraphSize(numberOfConcepts); } } else { if (numberOfConcepts == 1 || numberOfConcepts == 2) { size = calculateAutomaticGraphSize(numberOfConcepts); } } String source = "graph [ "; source += "rankdir=\"" + rankDir + "\" "; if (!Strings.isBlank(size)) { source += "size=\"" + size + "\" "; } if (!Strings.isBlank(ratio)) { source += ratio; } source += "]\n"; return source; } private static String getSizeProperties(String property) { if (property != null) { if (property.matches("\\d+px")) { property = property.substring(0, property.length() - 2); } if (property.matches("\\d+")) { property = String.valueOf(Double.valueOf(property) * 0.010415597); } } return property; } private static String calculateAutomaticGraphSize(int numberOfConcepts) { if (numberOfConcepts == 1) return "1"; if (numberOfConcepts == 2) return "3"; return null; } /** * The dot, svg and png files are created and written and returned (for easy cleanup). * * @created 20.08.2012 */ static File[] createAndWriteFiles(Config config, String dotSource) { File dotFile = createFile("dot", getFilePath(config)); File svgFile = createFile("svg", getFilePath(config)); // File pngFile = createFile("png", filePath); boolean dotFileDeleted = dotFile.delete(); boolean svgFileDeleted = svgFile.delete(); if (!svgFileDeleted) { Log.warning("Could not delete file." + svgFile.getName()); } // pngFile.delete(); FileUtils.writeFile(dotFile, dotSource); // create svg try { convertDot(svgFile, dotFile, getSVGCommand(config, dotFile, svgFile)); // convertDot(png, dot, createPngCommand(dotApp, dot, png)); augmentSVG(svgFile); } catch (IOException e) { Log.warning("Exception while generating visualization", e); } return new File[] { dotFile, svgFile }; } private static String[] getSVGCommand(Config config, File dot, File svg) { String dotApp = config.getDotApp(); String layout = config.getLayout(); if (layout != null) { return new String[] { dotApp, dot.getAbsolutePath(), "-Tsvg", "-o", svg.getAbsolutePath(), "-K" + layout.toLowerCase() }; } else { return new String[] { dotApp, dot.getAbsolutePath(), "-Tsvg", "-o", svg.getAbsolutePath() }; } } private static String createDotConceptLabel(RenderingStyle style, String targetURL, String targetLabel, boolean prepareLabel) { String newLineLabelValue; String url = ""; if (targetURL != null) { // url = "URL=\"" + Strings.encodeHtml(targetURL) + "\" "; } if (prepareLabel) { // prevents HTML rendering ! } newLineLabelValue = "[ " + url + DOTRenderer.buildLabel(style) + "label=" + targetLabel + " ];\n"; return newLineLabelValue; } /** * Returns true, if a valid dot installation can be found. */ static boolean checkDotInstallation(Config config) { String dotApp = config.getDotApp(); try { ProcessBuilder builder = new ProcessBuilder(dotApp, "-V"); Process process = builder.start(); process.waitFor(1, TimeUnit.SECONDS); int exitValue = process.exitValue(); if (exitValue != 0) { return false; } } catch (IOException | InterruptedException e) { return false; } return true; } /** * Adds the target-tag to every URL in the svg-file * * @created 01.08.2012 */ private static void augmentSVG(File svg) throws IOException { Log.finest("Starting to augment SVG: " + svg.getAbsolutePath()); try { // check if svg file is closed, otherwise wait timeout second long start = System.currentTimeMillis(); while (!Utils.isFileClosed(svg)) { if ((System.currentTimeMillis() - start) > TIMEOUT) { Log.warning("Exceded timeout while waiting for SVG file to be closed."); return; } } Document doc = SAXBuilderSingleton.getInstance().build(svg); Element root = doc.getRootElement(); if (root == null) return; findAndAugmentElements(root); XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat()); xmlOutputter.output(doc, new FileWriter(svg)); Log.finest("Finished augmenting SVG: " + svg.getAbsolutePath()); } catch (JDOMException e) { Log.warning("Exception while augmenting SVG " + svg.getAbsolutePath(), e); } } /** * Iterates through all the children of root to find all a-tag elements. * * @created 21.12.2013 */ private static void findAndAugmentElements(Element root) { List<?> children = root.getChildren(); Iterator<?> iter = children.iterator(); //noinspection WhileLoopReplaceableByForEach while (iter.hasNext()) { Element childElement = (Element) iter.next(); if (childElement.getName().equals("a")) { addTargetAttribute(childElement); } else { findAndAugmentElements(childElement); } } } /** * Adds the target-attribute to the element. * * @created 21.12.2013 */ private static void addTargetAttribute(Element element) { Attribute target = new Attribute("target", "_top"); element.setAttribute(target); } private static void convertDot(File file, File dot, String... command) throws IOException { FileUtils.checkWriteable(file); FileUtils.checkReadable(dot); try { ProcessBuilder processBuilder = new ProcessBuilder(command); Process process = processBuilder.start(); process.waitFor(); int exitValue = process.exitValue(); if (exitValue != 0) { FileUtils.printStream(process.getErrorStream()); throw new IOException( "Command could not successfully be executed: " + Strings.concat(" ", command)); } } catch (InterruptedException e) { //Thread was interrupted by GraphReRenderer //Log.warning(e.getMessage(), e); } } private static File createFile(String type, String path) { String filename = path + "." + type; return new File(filename); } }