All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.anarres.graphviz.builder.GraphVizGraph Maven / Gradle / Ivy

There is a newer version: 1.0.12
Show newest version
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.anarres.graphviz.builder;

import com.google.common.base.CharMatcher;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.io.ByteSink;
import com.google.common.io.Files;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author shevek
 */
public class GraphVizGraph {

    private static final Logger LOG = LoggerFactory.getLogger(GraphVizGraph.class);
    private static final long serialVersionUID = 1L;
    private int counter = 0;
    @CheckForNull
    private GraphVizLabel label;
    private final Predicate scopes;
    private transient final Set comments = new HashSet();
    private transient final Map graphOptions = new HashMap();
    private transient final Map nodes = new HashMap();
    private transient final Map edges = new HashMap();
    private transient final Map clusters = new HashMap();

    public GraphVizGraph(@Nonnull Predicate scopes) {
        this.scopes = scopes;
        graphOptions.put("compound", "true");
    }

    public GraphVizGraph() {
        this(Predicates.alwaysTrue());
    }

    protected boolean isScopeVisible(@Nonnull GraphVizScope scope) {
        return scopes.apply(scope);
    }

    @CheckForNull
    public GraphVizLabel getLabel() {
        return label;
    }

    @Nonnull
    public GraphVizLabel label() {
        if (label == null)
            label = new GraphVizLabel();
        return label;
    }

    @Nonnull
    @SuppressWarnings("unchecked")
    public GraphVizGraph label(@Nonnull CharSequence csq) {
        label().set(csq);
        return this;
    }

    @Nonnull
    public Collection getComments() {
        return comments;
    }

    @Nonnull
    public GraphVizGraph comment(String text) {
        comments.add(text);
        return this;
    }

    @Nonnull
    public Map getGraphOptions() {
        return graphOptions;
    }

    @CheckForNull
    public String getGraphOption(@Nonnull String key) {
        return graphOptions.get(key);
    }

    @CheckForNull
    public String getGraphOption(GraphVizGraphOption key) {
        return getGraphOption(key.name());
    }

    public void setGraphOption(@Nonnull String key, @CheckForNull String value) {
        if (value == null)
            graphOptions.remove(key);
        else
            graphOptions.put(key, value);
    }

    public void setGraphOption(@Nonnull GraphVizGraphOption key, @CheckForNull String value) {
        setGraphOption(key.name(), value);
    }

    private void clear(@Nonnull GraphVizScope scope, @Nonnull Iterable data) {
        Iterator it = data.iterator();
        while (it.hasNext()) {
            GraphVizObject.Key key = it.next();
            if (Objects.equal(key.getScope(), scope))
                it.remove();
        }
    }

    public void clear(@Nonnull GraphVizScope scope) {
        clear(scope, clusters.keySet());
        for (GraphVizCluster cluster : clusters.values()) {
            clear(scope, cluster.getClusterKeys());
            clear(scope, cluster.getNodeKeys());
        }

        clear(scope, nodes.keySet());

        Iterator it = edges.keySet().iterator();
        while (it.hasNext()) {
            GraphVizEdge.Key key = it.next();
            if (Objects.equal(key.getSourceKey().getScope(), scope))
                it.remove();
            else if (Objects.equal(key.getTargetKey().getScope(), scope))
                it.remove();
        }
    }

    @Nonnull
    public Collection getNodes() {
        return nodes.values();
    }

    @Nonnull
    public GraphVizNode node(@Nonnull GraphVizScope scope, @Nonnull Object object) {
        GraphVizNode.Key key = new GraphVizNode.Key(scope, object);
        if (!isScopeVisible(scope))
            return new GraphVizNode(this, key, -1);
        GraphVizNode node = nodes.get(key);
        if (node == null) {
            int id = counter++;
            // LOG.info("n" + id + " -> " + object, new Exception());
            node = new GraphVizNode(this, key, id);
            nodes.put(key, node);
        }
        return node;
    }

    public boolean containsNode(@Nonnull GraphVizScope scope, @Nonnull Object object) {
        GraphVizNode.Key key = new GraphVizNode.Key(scope, object);
        return nodes.containsKey(key);
    }

    @Nonnull
    public Collection getEdges() {
        return edges.values();
    }

    /**
     * Ensures that this graph contains a given edge.
     *
     * @param source The source node of the edge.
     * @param target The target node of the edge.
     * @param edgeId An optional identifier for this edge, allowing for multigraphs.
     * @return Either an existing, or a newly created edge.
     */
    @Nonnull
    public GraphVizEdge edge(@Nonnull GraphVizObject source, @Nonnull GraphVizObject target, @CheckForNull Object edgeId) {
        GraphVizEdge.Key key = new GraphVizEdge.Key(source.getKey(), target.getKey(), edgeId);
        if (!isScopeVisible(source.getScope()))
            return new GraphVizEdge(this, key, "", "");
        if (!isScopeVisible(target.getScope()))
            return new GraphVizEdge(this, key, "", "");
        GraphVizEdge edge = edges.get(key);
        if (edge == null) {
            edge = new GraphVizEdge(this, key, source.getId(), target.getId());
            edges.put(key, edge);
            if (source instanceof GraphVizCluster)
                edge.logicalTail(source.getId());
            if (target instanceof GraphVizCluster)
                edge.logicalHead(target.getId());
        }
        return edge;
    }

    @Nonnull
    public GraphVizEdge edge(@Nonnull GraphVizObject source, @Nonnull GraphVizObject target) {
        return edge(source, target, null);
    }

    @Nonnull
    public GraphVizEdge edge(@Nonnull GraphVizScope scope, @Nonnull Object source, @Nonnull Object target, Object edgeId) {
        return edge(node(scope, source), node(scope, target), edgeId);
    }

    @Nonnull
    public GraphVizEdge edge(@Nonnull GraphVizScope scope, @Nonnull Object source, @Nonnull Object target) {
        return edge(scope, source, target, null);
    }

    public boolean containsEdge(@Nonnull GraphVizObject source, @Nonnull GraphVizObject target, @CheckForNull Object edgeId) {
        GraphVizEdge.Key key = new GraphVizEdge.Key(source.getKey(), target.getKey(), edgeId);
        return edges.containsKey(key);
    }

    public boolean containsEdge(@Nonnull GraphVizObject source, @Nonnull GraphVizObject target) {
        return containsEdge(source, target, null);
    }

    public boolean containsEdge(@Nonnull GraphVizScope scope, @Nonnull Object source, @Nonnull Object target, @CheckForNull Object edgeId) {
        GraphVizObject.Key k_source = new GraphVizObject.Key(scope, source);
        GraphVizObject.Key k_target = new GraphVizObject.Key(scope, target);
        GraphVizEdge.Key key = new GraphVizEdge.Key(k_source, k_target, edgeId);
        return edges.containsKey(key);
    }

    public boolean containsEdge(@Nonnull GraphVizScope scope, @Nonnull Object source, @Nonnull Object target) {
        return containsEdge(scope, source, target, null);
    }

    @Nonnull
    private GraphVizCluster cluster(@CheckForNull GraphVizCluster parent, @Nonnull GraphVizScope scope, @Nonnull Object object) {
        GraphVizCluster.Key key = new GraphVizCluster.Key(scope, object);
        if (!isScopeVisible(scope))
            return new GraphVizCluster(this, key, null, -1);
        GraphVizCluster cluster = clusters.get(key);
        if (cluster == null) {
            cluster = new GraphVizCluster(this, key, parent, counter++);
            clusters.put(key, cluster);
        }
        return cluster;
    }

    @Nonnull
    public GraphVizCluster cluster(@Nonnull GraphVizScope scope, @Nonnull Object object) {
        return cluster(null, scope, object);
    }

    @Nonnull
    public GraphVizCluster subcluster(@Nonnull GraphVizCluster parent, @Nonnull Object object) {
        return cluster(parent, parent.getScope(), object);
    }

    private static boolean append(@Nonnull Appendable writer, @Nonnull String key, @CheckForNull Object value, boolean quote, boolean first) throws IOException {
        if (value == null)
            return first;
        if (!first)
            writer.append(',');
        writer.append(key).append('=');
        if (quote)
            writer.append('"');
        writer.append(String.valueOf(value));
        if (quote)
            writer.append('"');
        return false;
    }

    private static void writeIndent(@Nonnull Writer writer, @Nonnegative int depth) throws IOException {
        for (int i = 0; i < depth; i++)
            writer.write('\t');
    }

    private final CharMatcher NEWLINE = CharMatcher.is('\n');

    private void writeCommentsTo(@Nonnull Writer writer, @Nonnull Iterable comments, int indent) throws IOException {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < indent; i++)
            buf.append("\t");
        buf.append("// ");
        String prefix = buf.toString();

        // This is what is called "premature optimization".
        for (String comment : comments) {
            writer.write(prefix);
            if (NEWLINE.matchesAnyOf(comment))
                comment = NEWLINE.replaceFrom(comment, "\n" + prefix);
            writer.write(comment);
            writer.write('\n');
        }
    }

    private void writeClustersTo(
            @Nonnull Writer writer,
            @Nonnull Map> clusterMap,
            @CheckForNull GraphVizCluster parent,
            @Nonnegative int depth) throws IOException {
        Iterable _clusters = clusterMap.remove(parent);
        if (_clusters == null)
            return;
        for (GraphVizCluster cluster : _clusters) {
            writeCommentsTo(writer, cluster.getComments(), depth + 1);
            writeIndent(writer, depth);
            writer.append("\tsubgraph ").append(cluster.getId()).append(" {\n");
            writeIndent(writer, depth);
            writer.append("\t\t// scope=").append(String.valueOf(System.identityHashCode(cluster.getKey().getScope()))).append("\n");
            writeIndent(writer, depth);
            writer.append("\t\t").append(cluster.getId()).append(" [label=\"\",shape=point,style=invis];\n");
            if (cluster.getLabel() != null) {
                writeIndent(writer, depth);
                writer.append("\t\t").append("label=\"").append(String.valueOf(cluster.getLabel())).append("\";\n");
            }
            for (Map.Entry e : cluster.getAttributes().entrySet())
                writer.append("\t\t").append(e.getKey()).append("=\"").append(String.valueOf(e.getValue())).append("\";\n");
            for (GraphVizNode.Key key : cluster.getNodeKeys()) {
                GraphVizNode node = node(key.getScope(), key.getObject());
                writeIndent(writer, depth);
                writer.append("\t\t").append(node.getId()).append(";\n");
            }
            writeClustersTo(writer, clusterMap, cluster, depth + 1);
            writeIndent(writer, depth);
            writer.append("\t}\n");
        }
    }

    /** This map allows null keys, and the null key should be present. */
    @Nonnull
    private Map> newClusterMap() {
        Map> clusterMap = new HashMap>();
        for (GraphVizCluster child : clusters.values()) {
            @CheckForNull
            GraphVizCluster parent = child.getParent();
            List children = clusterMap.get(parent);
            if (children == null) {
                children = new ArrayList();
                clusterMap.put(parent, children);
            }
            children.add(child);
        }
        return clusterMap;
    }

    public void writeTo(@Nonnull Writer out) throws IOException {
        Writer writer;
        if (out instanceof BufferedWriter)
            writer = out;
        else if (out instanceof StringWriter)
            writer = out;
        else
            writer = new BufferedWriter(out);
        writeCommentsTo(writer, comments, 0);
        writer.write("digraph G {\n");
        // writer.write("\tcompound=true;\n");
        // writer.write("\tranksep=1.5;\n");
        writer.write("\tnode [shape=box];\n");
        for (Map.Entry e : graphOptions.entrySet()) {
            writer.write("\t");
            writer.write(e.getKey());
            writer.write("=");
            writer.write(e.getValue());
            writer.write(";\n");
        }
        if (getLabel() != null) {
            writer.append("\t");
            append(writer, "label", getLabel(), true, true);
            writer.append(";\n");
        }

        for (GraphVizNode node : nodes.values()) {
            writeCommentsTo(writer, node.getComments(), 1);
            writer.append("\t").append(node.getId()).append(" [");
            boolean first = true;
            // first = append(writer, "color", node.getColor(), true, first);
            // first = append(writer, "style", node.getStyle(), true, first);
            // first = append(writer, "shape", node.getShape(), true, first);
            String text = node.toLabelString();
            if (text != null) {
                // GraphVizLabel label = node.getLabel();
                // I don't like this much, it's too heuristic.
                // However, I think escaping of < is required by default, so this should actually be safe.
                boolean html = text.startsWith("<");
                if (html)
                    first = append(writer, "label", "<" + text + ">", false, first);
                else
                    first = append(writer, "label", text, true, first);
            }
            for (Map.Entry e : node.getAttributes().entrySet())
                first = append(writer, e.getKey(), e.getValue(), true, first);
            writer.append("];\n");
        }

        for (Map.Entry ee : edges.entrySet()) {
            // GraphVizEdge.Key key = ee.getKey();
            GraphVizEdge edge = ee.getValue();

            writeCommentsTo(writer, edge.getComments(), 1);
            writer.append("\t").append(edge.getSourceId());
            writer.append(" -> ").append(edge.getTargetId());
            writer.append(" [");
            boolean first = true;
            // first = append(writer, "color", edge.getColor(), true, first);
            // first = append(writer, "style", edge.getStyle(), true, first);
            first = append(writer, "arrowhead", edge.getHeadShape(), true, first);
            first = append(writer, "arrowtail", edge.getTailShape(), true, first);
            String labelKey = getGraphOption(GraphVizGraphOption.forcelabels) == null ? "label" : "xlabel";
            first = append(writer, labelKey, edge.getLabel(), true, first);
            first = append(writer, "headlabel", edge.getHeadLabel(), true, first);
            first = append(writer, "taillabel", edge.getTailLabel(), true, first);
            first = append(writer, "lhead", edge.getLogicalHead(), false, first);
            first = append(writer, "ltail", edge.getLogicalTail(), false, first);
            for (Map.Entry e : edge.getAttributes().entrySet())
                first = append(writer, e.getKey(), e.getValue(), true, first);
            writer.append("];\n");
        }

        /*
         for (GraphVizCluster cluster : clusters.values()) {
         writer.append("\tsubgraph ").append(cluster.getId()).append(" {\n");
         if (cluster.getLabel() != null)
         writer.append("\t\t").append("label=\"").append(String.valueOf(cluster.getLabel())).append("\";\n");
         for (GraphVizNode.Key key : cluster.getNodeKeys()) {
         GraphVizNode node = node(key.getScope(), key.getObject());
         writer.append("\t\t").append(node.getId()).append(";\n");
         }
         writer.append("\t}\n");
         }
         */
        Map> clusterMap = newClusterMap();
        writeClustersTo(writer, clusterMap, null, 0);
        writer.write("}\n");
        writer.flush();
    }

    public void writeTo(@Nonnull OutputStream out) throws IOException {
        writeTo(new OutputStreamWriter(out, Charsets.UTF_8));
    }

    public void writeTo(@Nonnull File file) throws IOException {
        ByteSink sink = Files.asByteSink(file);
        OutputStream out = sink.openBufferedStream();
        try {
            writeTo(out);
        } finally {
            out.close();
        }
    }

    @Nonnull
    public String writeToString() throws IOException {
        StringWriter writer = new StringWriter();
        writeTo(writer);
        return writer.toString();
    }

    @Override
    public String toString() {
        return "GraphVizGraph(" + nodes.size() + " nodes, " + edges.size() + " edges)";
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy