package eu.stratosphere.util.dag;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import eu.stratosphere.util.dag.GraphLevelPartitioner.Level;
/**
* Utility class to pretty print arbitrary directed acyclic graphs. It needs a {@link Navigator} to traverse form the
* start nodes through the graph and optionally a {@link NodePrinter} to format the nodes.<br>
* <br>
* The nodes are printed in rows visualizing the dependencies. A dependency of node A is a node B iff there exists an
* edge from B to A. The entries in the last row have no incoming edges and thus dependencies. The entries on the next
* to last row only depend on nodes on the last row.<br>
* All nodes have a maximum width that can be adjusted ({@link #setWidth(int)}). <br>
* To visualize the edges, a {@link ConnectorProvider} provides printable strings between the nodes.
*
* @author Arvid Heise
* @param <Node>
* the class of the node
*/
public class GraphPrinter<Node> {
/**
* The default width of a column.
*/
public static final int DEFAULT_COLUMN_WIDTH = 20;
private NodePrinter<Node> nodePrinter = new StandardPrinter<Node>();
private int width = DEFAULT_COLUMN_WIDTH;
private String widthString = "%-" + this.width + "s";
private ConnectorProvider connectorProvider = new BoxConnectorProvider();
/**
* Returns the {@link ConnectorProvider} that provides printable strings for the edges between the nodes.
*
* @return the connector provider
*/
public ConnectorProvider getConnectorProvider() {
return this.connectorProvider;
}
/**
* Returns the {@link NodePrinter} responsible for formatting the nodes.
*
* @return the node printer
*/
public NodePrinter<Node> getNodePrinter() {
return this.nodePrinter;
}
/**
* Returns the maximum width of the nodes.
*
* @return the maximum width
*/
public int getWidth() {
return this.width;
}
/**
* Prints the directed acyclic graph to a string representation where each node is formatted by the specified
* {@link Formatter} format and all structural elements are formatted with the given width.
*
* @param appendable
* the target of the print operations
* @param navigator
* the navigator
* @param startNodes
* the start nodes
* @throws IOException
* if an I/O error occurred during the print operation
*/
public void print(final Appendable appendable, final Iterable<? extends Node> startNodes,
final ConnectionNavigator<Node> navigator)
throws IOException {
this.print(appendable, startNodes.iterator(), navigator);
}
/**
* Prints the directed acyclic graph to a string representation where each node is formatted by the specified
* {@link Formatter} format and all structural elements are formatted with the given width.
*
* @param appendable
* the target of the print operations
* @param navigator
* the navigator
* @param startNodes
* the start nodes
* @throws IOException
* if an I/O error occurred during the print operation
*/
public void print(final Appendable appendable, final Iterator<? extends Node> startNodes,
final ConnectionNavigator<Node> navigator)
throws IOException {
new PrintState(appendable, GraphLevelPartitioner.getLevels(startNodes, navigator)).printDAG();
}
/**
* Prints the directed acyclic graph to a string representation where each node is formatted by the specified
* {@link Formatter} format and all structural elements are formatted with the given width.
*
* @param appendable
* the target of the print operations
* @param navigator
* the navigator
* @param startNodes
* the start nodes
* @throws IOException
* if an I/O error occurred during the print operation
*/
public void print(final Appendable appendable, final Node[] startNodes, final ConnectionNavigator<Node> navigator)
throws IOException {
this.print(appendable, Arrays.asList(startNodes).iterator(), navigator);
}
/**
* Sets the {@link ConnectorProvider} providing printable strings for the edges between the nodes.
*
* @param connectorProvider
* the new connector provider
*/
public void setConnectorProvider(final ConnectorProvider connectorProvider) {
if (connectorProvider == null)
throw new NullPointerException("connectorProvider must not be null");
this.connectorProvider = connectorProvider;
}
/**
* Sets the {@link NodePrinter} responsible for formatting the nodes.
*
* @param nodePrinter
* the node printer
*/
public void setNodePrinter(final NodePrinter<Node> nodePrinter) {
if (nodePrinter == null)
throw new NullPointerException("nodePrinter must not be null");
this.nodePrinter = nodePrinter;
}
/**
* Sets the width of a column of nodes. The default width is
*
* @param width
*/
public void setWidth(final int width) {
this.width = width;
this.widthString = "%-" + width + "s";
}
/**
* Converts the directed acyclic graph to a string representation where each node is formatted by the specified
* {@link NodePrinter} and all structural elements are formatted with the given width.
*
* @param navigator
* the navigator
* @param startNodes
* the start nodes
* @return a string representation of the directed acyclic graph
*/
public String toString(final Iterable<? extends Node> startNodes, final ConnectionNavigator<Node> navigator) {
return this.toString(startNodes.iterator(), navigator);
}
/**
* Converts the directed acyclic graph to a string representation where each node is formatted by the specified
* {@link NodePrinter} and all structural elements are formatted with the given width.
*
* @param navigator
* the navigator
* @param startNodes
* the start nodes
* @return a string representation of the directed acyclic graph
*/
public String toString(final Iterator<? extends Node> startNodes, final ConnectionNavigator<Node> navigator) {
final StringBuilder builder = new StringBuilder();
try {
this.print(builder, startNodes, navigator);
} catch (final IOException e) {
// cannot happen since we use a StringBuilder
}
return builder.toString();
}
/**
* Converts the directed acyclic graph to a string representation where each node is formatted by the specified
* {@link NodePrinter} and all structural elements are formatted with the given width.
*
* @param navigator
* the navigator
* @param startNodes
* the start nodes
* @return a string representation of the directed acyclic graph
*/
public String toString(final Node[] startNodes, final ConnectionNavigator<Node> navigator) {
return this.toString(Arrays.asList(startNodes), navigator);
}
/**
* Formats node using a given format pattern and {@link String#format(String, Object...)}.
*
* @author Arvid Heise
* @param <Node>
* the class of the node
*/
public static class FormattedNodePrinter<Node> implements NodePrinter<Node> {
private final String format;
private FormattedNodePrinter(final String format) {
this.format = format;
}
@Override
public String toString(final Object node) {
return String.format(this.format, node.toString());
}
}
/**
* Placeholder to format connections between nodes above several levels. There are two types: spacers and
* connectors.
*
* @author Arvid Heise
*/
private static class Placeholder {
private final List<Object> targets = new ArrayList<Object>(1);
/**
* Initializes a spacer.
*/
public Placeholder() {
}
Placeholder(final Object target) {
this.targets.add(target);
}
public String toString(final ConnectorProvider connectorProvider) {
if (this.targets.isEmpty())
return "";
return connectorProvider.getConnectorString(ConnectorProvider.Route.TOP_DOWN);
// return this.targets.isEmpty() ? "" : "|";
// return this.targets.isEmpty() ? "" : String.valueOf(index) + targets.toString();
}
}
private class PrintState {
private final Appendable appender;
private final List<Level<Object>> levels;
private final IntList printDownline = new IntArrayList();
@SuppressWarnings({ "unchecked", "rawtypes" })
private PrintState(final Appendable builder, final List<Level<Node>> levels) {
this.appender = builder;
this.levels = (List) levels;
this.addPlaceholders(this.levels);
}
public Placeholder addPlaceholder(final Level<Object> level, int placeholderIndex, final Object to) {
for (int index = level.getLevelNodes().size(); index < placeholderIndex; index++) {
final Placeholder emptyPlaceholder = new Placeholder();
level.add(emptyPlaceholder);
}
for (; placeholderIndex < level.getLevelNodes().size(); placeholderIndex++) {
if (!(level.getLevelNodes().get(placeholderIndex) instanceof Placeholder))
break;
if (((Placeholder) level.getLevelNodes().get(placeholderIndex)).targets.isEmpty()) {
level.getLevelNodes().remove(placeholderIndex);
break;
}
}
final Placeholder placeholder = new Placeholder(to);
level.add(placeholderIndex, placeholder);
level.updateLink(placeholder, null, to);
// level.getLevelNodes().add(placeholderIndex, placeholder);
// this.outgoings.put(placeholder, new ArrayList<Object>(Arrays.asList(to)));
return placeholder;
}
private void addPlaceholders(final List<Level<Object>> levels) {
for (int levelIndex = levels.size() - 1; levelIndex >= 0; levelIndex--) {
final Level<Object> level = levels.get(levelIndex);
final List<Object> levelNodes = level.getLevelNodes();
int placeHolderIndex = 0;
for (int nodeIndex = 0; nodeIndex < levelNodes.size(); nodeIndex++) {
final Object node = levelNodes.get(nodeIndex);
final List<Object> inputs = level.getLinks(node);
for (int inputIndex = 0; inputIndex < inputs.size(); inputIndex++, placeHolderIndex++) {
final Object input = inputs.get(inputIndex);
if (levels.get(levelIndex - 1).getLevelNodes().indexOf(input) == -1) {
final Object placeholder = this.addPlaceholder(levels.get(levelIndex - 1),
placeHolderIndex,
input);
level.updateLink(node, input, placeholder);
}
}
}
}
}
private void append(final int index, final ConnectorProvider.Route connector,
final ConnectorProvider.Route padding)
throws IOException {
String connectorString = "";
if (index < this.printDownline.size() && this.printDownline.getInt(index) > 0) {
if (connector != null)
connectorString = GraphPrinter.this.connectorProvider.getConnectorString(connector,
ConnectorProvider.Route.TOP_DOWN);
else
connectorString = GraphPrinter.this.connectorProvider
.getConnectorString(ConnectorProvider.Route.TOP_DOWN);
} else if (connector != null)
connectorString = GraphPrinter.this.connectorProvider.getConnectorString(connector);
String paddedString = String.format(GraphPrinter.this.widthString, connectorString);
if (padding != null)
paddedString = paddedString.replaceAll(" ",
GraphPrinter.this.connectorProvider.getConnectorString(padding));
this.appender.append(paddedString);
}
private void increaseDownline(final int index, final int count) {
while (this.printDownline.size() < index + 1)
this.printDownline.add(0);
this.printDownline.set(index, this.printDownline.getInt(index) + count);
}
private void printConnection(final int sourceIndex, final int targetIndex) throws IOException {
final int startIndex = Math.min(sourceIndex, targetIndex);
for (int index = 0; index < startIndex; index++)
this.append(index, null, null);
if (sourceIndex != -1)
if (sourceIndex < targetIndex) {
this.append(startIndex, ConnectorProvider.Route.TOP_RIGHT,
ConnectorProvider.Route.LEFT_RIGHT);
for (int index = sourceIndex + 1; index < targetIndex; index++)
this.append(index, null, ConnectorProvider.Route.LEFT_RIGHT);
} else if (sourceIndex == targetIndex)
this.append(startIndex, ConnectorProvider.Route.TOP_DOWN, null);
else {
this.append(startIndex, ConnectorProvider.Route.RIGHT_DOWN,
ConnectorProvider.Route.RIGHT_LEFT);
for (int index = targetIndex + 1; index < sourceIndex; index++)
this.append(index, null, ConnectorProvider.Route.RIGHT_LEFT);
}
final int endIndex = Math.max(sourceIndex, targetIndex);
if (sourceIndex < targetIndex)
this.append(endIndex, ConnectorProvider.Route.LEFT_DOWN, null);
else if (sourceIndex > targetIndex)
this.append(endIndex, ConnectorProvider.Route.TOP_LEFT, null);
for (int index = endIndex + 1; index < this.printDownline.size(); index++)
this.append(index, null, null);
this.appender.append('\n');
}
private void printConnections(final int levelIndex, final Level<Object> level) throws IOException {
if (levelIndex > 0) {
boolean printedConnection = false;
for (int sourceIndex = 0; sourceIndex < level.getLevelNodes().size(); sourceIndex++) {
final Object node = level.getLevelNodes().get(sourceIndex);
final List<Object> inputs = level.getLinks(node);
this.increaseDownline(sourceIndex, -1);
for (int index = 0; index < inputs.size(); index++) {
final int targetIndex = this.levels.get(levelIndex - 1).getLevelNodes()
.indexOf(inputs.get(index));
this.printConnection(sourceIndex, targetIndex);
this.increaseDownline(targetIndex, 1);
printedConnection = true;
}
}
if (!printedConnection)
this.printConnection(-1, -1);
}
}
private void printDAG() throws IOException {
for (int levelIndex = this.levels.size() - 1; levelIndex >= 0; levelIndex--) {
final Level<Object> level = this.levels.get(levelIndex);
for (int sourceIndex = 0; sourceIndex < level.getLevelNodes().size(); sourceIndex++) {
final Object node = level.getLevelNodes().get(sourceIndex);
if (levelIndex == this.levels.size() - 1)
this.increaseDownline(sourceIndex, level.getLinks(node).size());
this.printNode(node);
}
this.appender.append('\n');
this.printConnections(levelIndex, level);
}
}
@SuppressWarnings("unchecked")
private void printNode(final Object node) throws IOException {
if (node instanceof Placeholder)
this.appender.append(String.format(GraphPrinter.this.widthString,
((Placeholder) node).toString(GraphPrinter.this.connectorProvider)));
else {
String nodeString = GraphPrinter.this.nodePrinter.toString((Node) node);
if (nodeString.length() > GraphPrinter.this.width)
nodeString = nodeString.substring(0, GraphPrinter.this.width);
this.appender.append(String.format(GraphPrinter.this.widthString, nodeString));
}
}
}
/**
* Default printer, which simply invokes {@link Object#toString()}
*
* @author Arvid Heise
* @param <Node>
* the class of the node
*/
public static class StandardPrinter<Node> implements NodePrinter<Node> {
@Override
public String toString(final Object node) {
return node.toString();
}
}
}