diff --git a/ccm-core/src/main/java/com/arsdigita/util/Graph.java b/ccm-core/src/main/java/com/arsdigita/util/Graph.java new file mode 100755 index 000000000..24e0492de --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/Graph.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library 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 2.1 of + * the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.List; + +/** + * The graph class allows you to build graphs of objects. + * + * @author Archit Shah (ashah@mit.edu) + * @author Vadim Nasardinov (vadimn@redhat.com) + * @version $Date$ + * @since 2003-01-22 + **/ +public interface Graph { + /** + * Creates a copy of this graph. + **/ + Graph copy(); + + /** + * Sets the graph's label. + **/ + void setLabel(String label); + + /** + * Returns the graph's label. + **/ + String getLabel(); + + /** + * Adds a node to the graph. + **/ + void addNode(Object node); + + /** + * Returns true if the graph has this node. + **/ + boolean hasNode(Object node); + + + /** + * Returns true if the graph has this edge. + * + * @pre hasNode(edge.getTail()) && hasNode(edge.getHead()) + **/ + boolean hasEdge(Graph.Edge edge); + + /** + * Returns the count of nodes in this graph. + **/ + int nodeCount(); + + /** + * Adds an edge to the graph. + **/ + void addEdge(Graph.Edge edge); + + /** + * A convenient shortcut for addEdge(new Graph.Edge(tail, head, + * label)). + * + * @see #addEdge(Graph.Edge) + **/ + void addEdge(Object tail, Object head, Object label); + + /** + * Returns a list of nodes that this graph has. (Todo: this should probably + * return a Set.) + **/ + List getNodes(); + + /** + * Removes specified node and all edges incident to it. + * + * @returns true if this Graph contains the specified node + **/ + boolean removeNode(Object node); + + /** + * A convenient shortcut for removeEdge(new Graph.Edge(tail, head, + * label)). + * + * @see #removeEdge(Graph.Edge) + **/ + boolean removeEdge(Object tail, Object head, Object label); + + /** + * Removes specified edge. + * + * @returns true if this Graph has the specified edge + **/ + boolean removeEdge(Graph.Edge edge); + + /** + * Removes all nodes and edges. + **/ + void removeAll(); + + /** + * Returns a list of outgoing edges leaving this node. + **/ + List getOutgoingEdges(Object node); + + /** + * Returns the number of outgoing edges this node has. A convenient shortcut + * for getOutgoingEdges(node).size(). + * + * @see #getOutgoingEdges(Object) + **/ + int outgoingEdgeCount(Object node); + + /** + * @see #outgoingEdgeCount(Object) + **/ + int incomingEdgeCount(Object node); + + /** + * @see #getOutgoingEdges(Object) + **/ + List getIncomingEdges(Object node); + + /** + * An edge is an ordered pair of nodes with a label attached to it. The + * first node of the pair is called the tail and the second the + * head. + * + *

Implementing classes are expected to supply a constructor of the form + * Graph.Edge(Object tail, Object head, Object label).

+ **/ + interface Edge { + + /** + * Returns the tail node of the edge. + * + * @see #getHead() + **/ + Object getTail(); + + /** + * Returns the head node of the edge. + * + * @see #getTail() + **/ + Object getHead(); + + /** + * Returns the label associated with this edge. The label can be + * anything, depending on your particular graph. For example, if your + * nodes represent cities and edges represent freeways, then the label + * can be an Float representing the the distance (the + * length of the route). + **/ + Object getLabel(); + + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/GraphEdge.java b/ccm-core/src/main/java/com/arsdigita/util/GraphEdge.java new file mode 100755 index 000000000..f969436f4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/GraphEdge.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library 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 2.1 of + * the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +/** + * Graph edge. + * + * @author Archit Shah (ashah@mit.edut) + * @author Vadim Nasardinov (vadimn@redhat.com) + * @since 2003-01-22 + * @version $Revision$ $Date$ + **/ +public final class GraphEdge implements Graph.Edge { + private Object m_tail; + private Object m_head; + private Object m_label; + + /** + * @pre tail != null + * @pre head != null + **/ + public GraphEdge(Object tail, Object head, Object label) { + Assert.exists(tail, Object.class); + Assert.exists(head, Object.class); + m_tail = tail; + m_head = head; + m_label = label; + } + + /** + * @set return != null + **/ + public Object getTail() { + return m_tail; + } + + /** + * @set return != null + **/ + public Object getHead() { + return m_head; + } + + public Object getLabel() { + return m_label; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(m_tail).append(" -> ").append(m_head); + buf.append("[").append(m_label).append("]"); + return buf.toString(); + } + + public boolean equals(Object obj) { + if ( obj == null ) return false; + + if (obj instanceof Graph.Edge) { + Graph.Edge that = (Graph.Edge) obj; + Object thatLabel = that.getLabel(); + + boolean equalLabels = + (m_label == null && thatLabel == null ) || + (m_label != null && m_label.equals(thatLabel)) || + (thatLabel != null && thatLabel.equals(m_label)); + + return + m_tail.equals(that.getTail()) && + m_head.equals(that.getHead()) && + equalLabels; + } else { + return false; + } + } + + public int hashCode() { + return m_tail.hashCode() + m_head.hashCode() + + (m_label == null ? 0 : m_label.hashCode()); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/GraphFormatter.java b/ccm-core/src/main/java/com/arsdigita/util/GraphFormatter.java new file mode 100755 index 000000000..e499fa193 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/GraphFormatter.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library 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 2.1 of + * the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +/** + * Implementations of this interface can be used for pretty-printing graphs. + * + * @author Vadim Nasardinov (vadimn@redhat.com) + * @version $Date$ + * @since 2003-01-23 + **/ +public interface GraphFormatter { + + /** + * Returns graph attributes. + * + *

For example, if you choose to pretty-print your graph in the DOT + * language, then the graph attributes section may look like so:

+ * + *
+     *  digraph mygraph {
+     *     // the following two lines are graph attributes
+     *     node[shape=box,fontsize=8,fontname=verdana,height=0.2,width=0.2,style=filled];
+     *     ranksep=0.05;
+     *
+     *     // the following lines are nodes and edges
+     *     A -> B -> C -> D;
+     *     B -> E -> F;
+     *     C -> G;
+     *     D -> I;
+     *     D -> J -> H;
+     *  }
+
+     **/
+    String graphAttributes(Graph graph);
+
+    /**
+     * Returns a textual representation of the node, preferably a short one that
+     * can be used in the following plain-text representation of the tree.
+     *
+     * 
+     * digraph tree {
+     *     A -> B -> C -> D;
+     *     B -> E -> F;
+     *     C -> G;
+     *     D -> I;
+     *     D -> J -> H;
+     * }
+     * 
+ * + *

Example implementation:

+ * + *
+     *  public String formatNode(Object node) {
+     *      return node == null ? null : ((ObjectType) node).getName();
+     *  }
+     * 
+ **/ + String nodeName(Object node); + + /** + * Returns [bracketed] node attributes. + * + *
+     *  digraph g {
+     *      C[label="The C Language"];
+     *      J;
+     *      C -> J;
+     *  }
+     * 
+ **/ + String nodeAttributes(Object node); + + /** + * Returns a short textual label describing the edge. + **/ + String edge(Object edge); +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/GraphSet.java b/ccm-core/src/main/java/com/arsdigita/util/GraphSet.java new file mode 100755 index 000000000..3d4b514a8 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/GraphSet.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library 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 2.1 of + * the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.ArrayList; +import java.util.Collections; +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 com.arsdigita.util.Assert; + +/** + * A Set-based implementation of the {@link com.arsdigita.util.Graph} interface. + * Once you've added a node to this graph, you must not mutate the node in a way + * that affects its equals(Object) and hashCode() + * methods. + * + *

This class permits the null node.

+ * + *

This implementation is not synchronized..

+ * + * @author Archit Shah (ashah@mit.edut) + * @author Vadim Nasardinov (vadimn@redhat.com) + * @since 2003-01-22 + * @version $Date$ + **/ +public class GraphSet implements Graph { + private final static String LINE_SEP = System.getProperty("line.separator"); + + private Set m_nodes = new HashSet(); + private Map m_outgoingEdges = new HashMap(); + private Map m_incomingEdges = new HashMap(); + private String m_label = "directed_graph"; + + public Graph copy() { + Graph newGraph = new GraphSet(); + for (Iterator nodes = getNodes().iterator(); nodes.hasNext(); ) { + Object node = nodes.next(); + newGraph.addNode(node); + for (Iterator edges=getOutgoingEdges(node).iterator(); edges.hasNext(); ) { + Graph.Edge edge = (Graph.Edge) edges.next(); + newGraph.addEdge(edge.getTail(), edge.getHead(), edge.getLabel()); + } + } + newGraph.setLabel(getLabel()); + return newGraph; + } + + public void setLabel(String label) { + Assert.isTrue(label !=null, "label is not null"); + m_label = label; + } + + public String getLabel() { + return m_label; + } + + public void addNode(Object name) { + m_nodes.add(name); + } + + public boolean hasNode(Object nodeName) { + return m_nodes.contains(nodeName); + } + + /** + * @pre hasNode(edge.getTail()) && hasNode(edge.getHead()) + **/ + public boolean hasEdge(Graph.Edge edge) { + return outgoingEdges(edge.getTail()).contains(edge); + } + + public int nodeCount() { + return m_nodes.size(); + } + + public void addEdge(Graph.Edge edge) { + m_nodes.add(edge.getTail()); + m_nodes.add(edge.getHead()); + outgoingEdges(edge.getTail()).add(edge); + incomingEdges(edge.getHead()).add(edge); + } + + public void addEdge(Object tail, Object head, Object label) { + addEdge(new GraphEdge(tail, head, label)); + } + + public List getNodes() { + return new ArrayList(m_nodes); + } + + public boolean removeNode(Object nodeName) { + boolean hasNode = m_nodes.remove(nodeName); + if (hasNode) { + Set out = (Set) m_outgoingEdges.remove(nodeName); + if (out != null) { + for (Iterator it = out.iterator(); it.hasNext(); ) { + Graph.Edge e = (Graph.Edge) it.next(); + incomingEdges(e.getHead()).remove(e); + } + } + Set in = (Set) m_incomingEdges.remove(nodeName); + if (in != null) { + for (Iterator it = in.iterator(); it.hasNext(); ) { + Graph.Edge e = (Graph.Edge) it.next(); + outgoingEdges(e.getTail()).remove(e); + } + } + } + return hasNode; + } + + public boolean removeEdge(Object tail, Object head, Object label) { + return removeEdge(new GraphEdge(tail, head, label)); + } + + public boolean removeEdge(Graph.Edge edge) { + boolean hasEdge = outgoingEdges(edge.getTail()).remove(edge); + if (hasEdge) { incomingEdges(edge.getHead()).remove(edge); } + return hasEdge; + } + + public void removeAll() { + m_nodes.clear(); + m_outgoingEdges.clear(); + m_incomingEdges.clear(); + } + + private Set outgoingEdges(Object nodeName) { + Set edges = (Set) m_outgoingEdges.get(nodeName); + if (edges == null) { + edges = new HashSet(4); + m_outgoingEdges.put(nodeName, edges); + } + + return edges; + } + + private static String objToString(Object obj) { + return obj == null ? "null" : obj.toString(); + } + + public List getOutgoingEdges(Object node) { + Assert.isTrue(hasNode(node), objToString(node)); + return new ArrayList(outgoingEdges(node)); + } + + public int outgoingEdgeCount(Object node) { + Assert.isTrue(hasNode(node), objToString(node)); + return outgoingEdges(node).size(); + } + + public int incomingEdgeCount(Object node) { + Assert.isTrue(hasNode(node), objToString(node)); + return incomingEdges(node).size(); + } + + private Set incomingEdges(Object nodeName) { + Set edges = (Set) m_incomingEdges.get(nodeName); + if ( edges == null ) { + edges = new HashSet(); + m_incomingEdges.put(nodeName, edges); + } + return edges; + } + + public List getIncomingEdges(Object node) { + Assert.isTrue(hasNode(node), objToString(node)); + return new ArrayList(incomingEdges(node)); + } + + /** + * Returns a printable representation of the graph that has the following + * form. + * + *
+     * digraph foo {
+     *     Boston -> New_York [label="214 miles"];
+     *     Boston -> Chicago [label="983 miles"];
+     *     New_York -> Chicago [label="787 miles"];
+     *     Boston -> Westford [label="35 miles"];
+     *     Raleigh -> Westford [label="722 miles"];
+     * }
+     * 
+ * + *

Note that to get a neat printable representation, each node and edge + * label must have a short printable representation.

+ **/ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("graph ").append(getLabel()).append(" {"); + sb.append(LINE_SEP); + List sortedNodes = new ArrayList(m_nodes); + Collections.sort(sortedNodes); + for (Iterator nodes=sortedNodes.iterator(); nodes.hasNext(); ) { + Object node = nodes.next(); + for (Iterator edges = getOutgoingEdges(node).iterator(); edges.hasNext(); ) { + Graph.Edge edge = (Graph.Edge) edges.next(); + sb.append(" "); + sb.append(objToString(node)).append(" -> "); + sb.append(objToString(edge.getHead())); + sb.append("[label=\"").append(edge.getLabel()); + sb.append("\"];"); + sb.append(LINE_SEP); + } + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/Graphs.java b/ccm-core/src/main/java/com/arsdigita/util/Graphs.java new file mode 100755 index 000000000..e92130e66 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/Graphs.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library 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 2.1 of + * the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Stack; + +/** + * A collection of methods that operate on {@link com.arsdigita.util.Graph + * graphs}. + * + * @author Archit Shah (ashah@mit.edu) + * @author Vadim Nasardinov (vadimn@redhat.com) + * @version $Date$ + * @since 2003-01-22 + **/ +public class Graphs { + + /** + * A simple implementation of the {@link Graphs.EdgeSelector} interface that + * selects outgoing edges. + **/ + public final static EdgeSelector FORWARD_SELECTOR = new EdgeSelector() { + public boolean test(Graph.Edge edge, boolean forward) { + return forward; + } + }; + + private static final String INDENT = " "; + + private Graphs() {} + + /** + * Finds a path in graph from begin to end + * + * @see #findPath(Graph, Object, Graphs.EdgeSelector, Graphs.NodeSelector) + * @returns list of edges representing the found path + * @throws NullPointerException if any of the three arguments is null. + **/ + public static final List findPath(Graph graph, Object begin, Object end) { + NodeSelector terminator = new EqualityTerminator(end); + return findPath(graph, begin, FORWARD_SELECTOR, terminator); + } + + /** + * Performs a traversal of graph looking for path that starts + * with startNode and terminates with a node that satisfies the + * test specified by terminator and consists only of those + * edges that satisfy the test specified by selector. + * + * @return a path from start node to a node that satisfies the condition + * specified by terminator, such that all edges in the path + * satisfy the condition specified by selector. An empty list + * is returned, if no such path can be found. + **/ + public static List findPath(Graph graph, Object startNode, + EdgeSelector selector, + NodeSelector terminator) { + + Stack path = new Stack(); + findPathRecur + (graph, startNode, selector, terminator, new HashSet(), path); + return path; + } + + + private static final boolean findPathRecur + (Graph graph, Object start, + EdgeSelector selector, NodeSelector terminator, + Set searched, Stack path) { + + Iterator it = graph.getOutgoingEdges(start).iterator(); + searched.add(start); + + while (it.hasNext()) { + Graph.Edge edge = (Graph.Edge) it.next(); + if ( !selector.test(edge, true) ) { continue; } + + path.push(edge); + final Object node = edge.getHead(); + + if (terminator.test(node)) { + return true; + } + + if (!searched.contains(node)) { + boolean found = findPathRecur + (graph, node, selector, terminator, searched, path); + + if ( found ) { return true; } + } + path.pop(); + } + return false; + } + + /** + * @param edgePath a list of edges such as the one returned by + * {@link #findPath(Graph, Object, Object)}. + * @return the same path represented as a list of nodes rather than edges. + * + * @throws IllegalStateException if edgePath is not a valid + * path. For example, (a->b, c->d, d->e) is invalid, because + * the edge between b and c is missing. + * @throws NullPointerException if edgePath is null + **/ + public static final List edgePathToNodePath(List edgePath) { + List path = new ArrayList(); + Graph.Edge lastEdge = null; + for (Iterator edges = edgePath.iterator(); edges.hasNext(); ) { + Graph.Edge edge = (Graph.Edge) edges.next(); + if ( lastEdge != null ) { + if ( !lastEdge.getHead().equals(edge.getTail()) ) { + throw new IllegalArgumentException + ("non-contiguous path segment:\n" + lastEdge + "\n" + + edge); + } + } + path.add(edge.getTail()); + lastEdge = edge; + } + if ( lastEdge != null ) { + path.add(lastEdge.getHead()); + } + return path; + } + + private static String objToString(Object obj) { + return obj == null ? "null" : obj.toString(); + } + + /** + * @return nodes reachable from start, including the + * start node itself. + * + * @pre graph.hasNode(start) + **/ + public static Graph nodesReachableFrom(Graph graph, Object start) { + Assert.isTrue(graph.hasNode(start)); + Graph result = new GraphSet(); + result.addNode(start); + Set processedTails = new HashSet(); + nodesReachableRecurse(graph, start, processedTails, result); + return result; + } + + private static void nodesReachableRecurse(Graph gg, Object currentNode, + Set processedTails, + Graph accumulator) { + + processedTails.add(currentNode); + + for (Iterator edges=gg.getOutgoingEdges(currentNode).iterator(); edges.hasNext(); ) { + Graph.Edge edge = (Graph.Edge) edges.next(); + if ( processedTails.contains(edge.getHead()) ) { + continue; + } + accumulator.addEdge(edge); + nodesReachableRecurse + (gg, edge.getHead(), processedTails, accumulator); + } + } + + /** + * Returns a list of nodes in gg that have no outgoing edges. + **/ + public static List getSinkNodes(Graph gg) { + List result = new ArrayList(); + for (Iterator nodes = gg.getNodes().iterator(); nodes.hasNext(); ) { + Object node = nodes.next(); + if ( gg.getOutgoingEdges(node).size() == 0 ) { + result.add(node); + } + } + return result; + } + + /** + * Pretty-prints the tree in a format patterned off of the DOT + * language. + * + * @pre tree != null + * @pre fmtr != null + * @pre writer != null + **/ + public static void printTree(Tree tree, GraphFormatter fmtr, + PrintWriter writer) { + + Assert.exists(tree, "tree"); + Assert.exists(fmtr, "formatter"); + Assert.exists(writer, "writer"); + + writer.println("digraph " + tree.getLabel() + " {"); + printTreeRecurse(tree, fmtr, writer); + writer.println("}"); + } + + private static void printTreeRecurse(Tree tree, GraphFormatter fmtr, + PrintWriter writer) { + + String root = fmtr.nodeName(tree.getRoot()); + for (Iterator ii=tree.getSubtrees().iterator(); ii.hasNext(); ) { + Tree.EdgeTreePair pair = (Tree.EdgeTreePair) ii.next(); + String edge = fmtr.edge(pair.getEdge()); + String child = fmtr.nodeName(pair.getTree().getRoot()); + writer.print(INDENT + root + " -> " + child); + if ( edge != null ) { + writer.print("[label=\"" + edge + "\"]"); + } + writer.println(";"); + printTreeRecurse(pair.getTree(), fmtr, writer); + } + } + + /** + * Pretty-prints the graph. + * + * @see #printTree(Tree, GraphFormatter, PrintWriter) + * @pre graph != null + * @pre fmtr != null + * @pre writer != null + **/ + public static void printGraph(Graph graph, GraphFormatter fmtr, + PrintWriter writer) { + + Assert.exists(graph, "tree"); + Assert.exists(fmtr, "formatter"); + Assert.exists(writer, "writer"); + + writer.println("digraph " + graph.getLabel() + " {"); + String graphAttrs = fmtr.graphAttributes(graph); + if ( graphAttrs != null ) { + writer.println(graphAttrs); + } + for (Iterator nodes=graph.getNodes().iterator(); nodes.hasNext(); ) { + Object node = nodes.next(); + int nodeCount = graph.outgoingEdgeCount(node) + + graph.incomingEdgeCount(node); + + String nodeName = fmtr.nodeName(node); + String nodeAttrs = fmtr.nodeAttributes(node); + + if ( nodeCount==0 || nodeAttrs != null ) { + writer.print(INDENT + nodeName); + + if ( nodeAttrs == null ) { + writer.println(";"); + } else { + writer.println(nodeAttrs + ";"); + } + } + + if (graph.outgoingEdgeCount(node) == 0) { + // we'll print this node when we print the outgoing edges of + // some other node + continue; + } + Iterator edges = graph.getOutgoingEdges(node).iterator(); + while (edges.hasNext()) { + Graph.Edge edge = (Graph.Edge) edges.next(); + StringBuffer sb = new StringBuffer(); + sb.append(INDENT).append(fmtr.nodeName(edge.getTail())); + sb.append(" -> ").append(fmtr.nodeName(edge.getHead())); + if ( edge.getLabel() != null ) { + sb.append("[label=\""); + sb.append(fmtr.edge(edge.getLabel())); + sb.append("\"]"); + } + sb.append(";"); + writer.println(sb.toString()); + } + } + writer.println("}"); + } + + /** + * @see #findPath(Graph, Object, Graphs.EdgeSelector, Graphs.NodeSelector) + **/ + public interface EdgeSelector { + /** + * @param edge the edge to test + * @param forward the value of false indicates that an edge + * is to be traversed backwards. Useful, when you want to find a path + * starting with the destination and working your way back to the + * source. The value true indicates that edge is being + * considered for traversal in the natural, forward direction. + **/ + boolean test(Graph.Edge edge, boolean forward); + } + + /** + * @see #findPath(Graph, Object, Graphs.EdgeSelector, Graphs.NodeSelector) + **/ + public interface NodeSelector { + boolean test(Object node); + } + + public final static class EqualityTerminator implements NodeSelector { + private Object m_node; + + /** + * @throws NullPointerException if node is null + **/ + public EqualityTerminator(Object node) { + if (node==null) { throw new NullPointerException("node"); } + + m_node = node; + } + + public boolean test(Object node) { + return m_node.equals(node); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/util/Tree.java b/ccm-core/src/main/java/com/arsdigita/util/Tree.java new file mode 100755 index 000000000..cfdc21e0a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/util/Tree.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library 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 2.1 of + * the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.util; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * This class a represents the tree abstraction. This + * implementation takes a recursive definition where a tree is a root node + * connected to other (sub)trees. + * + *

This implementation allows the same node to be used in more than position + * in the tree. For example, you can do something like this:

+ * + *
+ *  Tree aa = new Tree("a");
+ *  Tree bb = aa.addChild("b");
+ *  bb.addChild("a");
+ *  aa.addChild("c");
+ * 
+ * + *

This can be visualized as follows:

+ * + *
+ *    a
+ *   / \
+ *  /   \
+ * b     c
+ *  \
+ *   \
+ *    a
+ * 
+ * + *

The only ways to add node to the tree is through the {@link #Tree(Object) + * constructor} and the {@link #addChild(Object)} {@link #addChild(Object, + * Object)} methods.

+ * + * @author Vadim Nasardinov (vadimn@redhat.com) + * @version $Date$ + * @since 2003-01-23 + **/ +public class Tree { + private Tree m_parent; + private final Object m_root; + private final List m_children; + private String m_label; + + /** + * @param root the root node of the tree + * @pre root != null + **/ + public Tree(Object root) { + this(null, root); + } + + private Tree(Tree parent, Object root) { + m_parent = parent; + m_root = root; + m_children = new ArrayList(); + } + + public void setLabel(String label) { + m_label = label; + } + + public String getLabel() { + return m_label; + } + + /** + * Returns the root of this tree. + **/ + public Object getRoot() { + return m_root; + } + + /** + * Adds a child element to the root of this tree. Returns the subtree rooted + * at the newly created node. + **/ + public Tree addChild(Object child, Object edge) { + Tree tree = new Tree(this, child); + m_children.add(new EdgeTreePair(edge, tree)); + return tree; + } + + /** + * A shortcut for addChild(child, null). + * + * @see #addChild(Object, Object) + **/ + public Tree addChild(Object child) { + return addChild(child, null); + } + + /** + * Adds subtree to the root node of this tree. + * + *

Note: This doesn't check for + * cycles. If you do something like,

+ * + *
+     * tree.addSubtree(tree);
+     * 
+ * + *

you're on your own. I'll add a check for cycles like this when I have + * the time. (This probably means never.)

+ * + * @pre subtree != null && subtree.getParent() == 0 + **/ + public void addSubtree(Tree subtree, Object edge) { + Assert.exists(subtree, "subtree"); + Assert.isTrue(subtree.getParent() == null, "parent must be null"); + subtree.m_parent = this; + m_children.add(new EdgeTreePair(edge, subtree)); + } + + /** + * A shortcut for addSubtree(subtree,null). + * + * @see #addSubtree(Tree, Object) + **/ + public void addSubtree(Tree subtree) { + addSubtree(subtree, null); + } + + /** + * Returns the tree rooted at the parent node of the root of this tree or + * null, if the root of this tree has no parent node. + * + **/ + public Tree getParent() { + return m_parent; + } + + /** + * Returns the list of {@link Tree.EdgeTreePair edge-tree pairs} parented to + * the root node of this tree in the order in which they were initially + * added. Manipulating the returned list does not affect this tree. For + * example, if you remove an element from the list, no changes are made to + * this tree. + **/ + public List getSubtrees() { + return new ArrayList(m_children); + } + + /** + * Returns a copy of this tree. The returned copy does not have a parent + * tree. In other words, the returned tree is no longer a part of a bigger + * tree, even if this tree was. + * + @ return.getParent() == null + **/ + public Tree copy() { + Tree result = new Tree(getRoot()); + copyRecurse(this, result); + return result; + } + + private static void copyRecurse(Tree from, Tree to) { + Iterator children = from.getSubtrees().iterator(); + while ( children.hasNext() ) { + EdgeTreePair pair = (EdgeTreePair) children.next(); + Tree result = to.addChild(pair.getTree().getRoot(), pair.getEdge()); + copyRecurse(pair.getTree(), result); + } + } + + public int nodeCount() { + return nodeCountRecurse(this); + } + + private static int nodeCountRecurse(Tree tree) { + int result = 1; + for ( Iterator ii=tree.getSubtrees().iterator(); ii.hasNext(); ) { + EdgeTreePair pair = (EdgeTreePair) ii.next(); + result += nodeCountRecurse(pair.getTree()); + } + return result; + } + + /** + * Returns the maximum depth of the tree. + **/ + public int depth() { + return depthRecurse(this); + } + + private static int depthRecurse(Tree tree) { + if ( tree.getSubtrees().size() == 0 ) { + return 1; + } + int maxDepth = 0; + for (Iterator ii=tree.getSubtrees().iterator(); ii.hasNext(); ) { + EdgeTreePair pair = (EdgeTreePair) ii.next(); + int depth = depthRecurse(pair.getTree()); + if ( depth > maxDepth ) { + maxDepth = depth; + } + } + return maxDepth + 1; + } + + /** + * Returns the list of trees such that each of the returned trees is rooted + * at the ancestor nodes of this tree or an empty list, if the root of this + * tree has no ancestors. The closer ancestors appear first in the list. + **/ + public List getAncestors() { + List result = new ArrayList(); + if ( getParent() == null ) { + return result; + } + ancestorsRecurse(getParent(), result); + return result; + } + + private static void ancestorsRecurse(Tree node, List accumulator) { + accumulator.add(node); + if ( node.getParent() != null ) { + ancestorsRecurse(node.getParent(), accumulator); + } + } + + /** + * Takes a list of trees and returns a new list where each tree from the + * passed in list is replaced with its root node. + * + * @pre trees != null + **/ + public static List treesToNodes(List trees) { + Assert.exists(trees, "trees"); + List result = new ArrayList(); + for (Iterator ii=trees.iterator(); ii.hasNext(); ) { + result.add(((Tree) ii.next()).getRoot()); + } + return result; + } + + /** + * Nodes in a tree are connected with edges. An edge can be object that + * characterizes the relationship between the parent and the child nodes. + * For example, if you use the tree to represent the composition structure + * of XSLT stylesheets, then the edge object may be a string with one of two + * possible values: "xsl:import" and "xsl:include", allowing you to + * distinguish the method by which the parent stylesheet incorporates the + * child. + * + *

The edge-tree pair class represents an order pair where the first + * element is the edge, and the second is the subtree rooted at the head + * node of the edge.

+ **/ + public static class EdgeTreePair { + private Object m_edge; + private Tree m_tree; + + private EdgeTreePair(Object edge, Tree tree) { + m_edge = edge; + m_tree = tree; + } + + public Object getEdge() { + return m_edge; + } + + /** + * Returns the subtree rooted at the head node of the edge. + **/ + public Tree getTree() { + return m_tree; + } + } +}