You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@batchee.apache.org by rm...@apache.org on 2015/12/02 00:10:09 UTC

incubator-batchee git commit: extracting diagram mojo logic to be usable without maven

Repository: incubator-batchee
Updated Branches:
  refs/heads/master 99727da2f -> 2936aba3d


extracting diagram mojo logic to be usable without maven


Project: http://git-wip-us.apache.org/repos/asf/incubator-batchee/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-batchee/commit/2936aba3
Tree: http://git-wip-us.apache.org/repos/asf/incubator-batchee/tree/2936aba3
Diff: http://git-wip-us.apache.org/repos/asf/incubator-batchee/diff/2936aba3

Branch: refs/heads/master
Commit: 2936aba3dcafe86cb0bc2c6dd4e2a5e624551571
Parents: 99727da
Author: Romain Manni-Bucau <rm...@gmail.com>
Authored: Wed Dec 2 00:10:57 2015 +0100
Committer: Romain Manni-Bucau <rm...@gmail.com>
Committed: Wed Dec 2 00:10:57 2015 +0100

----------------------------------------------------------------------
 .../apache/batchee/tools/maven/DiagramMojo.java | 695 +----------------
 .../tools/maven/doc/DiagramGenerator.java       | 748 +++++++++++++++++++
 .../batchee/tools/maven/DiagramMojoTest.java    |   2 +-
 3 files changed, 758 insertions(+), 687 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/2936aba3/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/DiagramMojo.java
----------------------------------------------------------------------
diff --git a/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/DiagramMojo.java b/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/DiagramMojo.java
index 50aa945..4067733 100644
--- a/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/DiagramMojo.java
+++ b/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/DiagramMojo.java
@@ -17,83 +17,14 @@
 package org.apache.batchee.tools.maven;
 
 
-import edu.uci.ics.jung.algorithms.layout.AbstractLayout;
-import edu.uci.ics.jung.algorithms.layout.CircleLayout;
-import edu.uci.ics.jung.algorithms.layout.FRLayout;
-import edu.uci.ics.jung.algorithms.layout.KKLayout;
-import edu.uci.ics.jung.algorithms.layout.Layout;
-import edu.uci.ics.jung.algorithms.layout.SpringLayout;
-import edu.uci.ics.jung.algorithms.shortestpath.DijkstraDistance;
-import edu.uci.ics.jung.algorithms.shortestpath.Distance;
-import edu.uci.ics.jung.algorithms.shortestpath.UnweightedShortestPath;
-import edu.uci.ics.jung.graph.DirectedSparseGraph;
-import edu.uci.ics.jung.graph.Graph;
-import edu.uci.ics.jung.graph.util.Context;
-import edu.uci.ics.jung.graph.util.Pair;
-import edu.uci.ics.jung.visualization.Layer;
-import edu.uci.ics.jung.visualization.RenderContext;
-import edu.uci.ics.jung.visualization.VisualizationViewer;
-import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
-import edu.uci.ics.jung.visualization.decorators.EdgeShape;
-import edu.uci.ics.jung.visualization.decorators.ToStringLabeller;
-import edu.uci.ics.jung.visualization.renderers.BasicEdgeLabelRenderer;
-import edu.uci.ics.jung.visualization.renderers.Renderer;
-import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
-import org.apache.batchee.container.jsl.ExecutionElement;
-import org.apache.batchee.container.jsl.JobModelResolver;
-import org.apache.batchee.container.jsl.TransitionElement;
-import org.apache.batchee.container.navigator.JobNavigator;
-import org.apache.batchee.jaxb.End;
-import org.apache.batchee.jaxb.Fail;
-import org.apache.batchee.jaxb.Flow;
-import org.apache.batchee.jaxb.JSLJob;
-import org.apache.batchee.jaxb.Next;
-import org.apache.batchee.jaxb.Split;
-import org.apache.batchee.jaxb.Step;
-import org.apache.batchee.jaxb.Stop;
-import org.apache.commons.collections15.Transformer;
-import org.apache.commons.collections15.functors.ConstantTransformer;
+import org.apache.batchee.tools.maven.doc.DiagramGenerator;
 import org.apache.maven.plugin.AbstractMojo;
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.MojoFailureException;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
-import org.codehaus.plexus.util.IOUtil;
 
-import javax.imageio.ImageIO;
-import javax.swing.JFrame;
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.GridLayout;
-import java.awt.Paint;
-import java.awt.Point;
-import java.awt.Rectangle;
-import java.awt.RenderingHints;
-import java.awt.Shape;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Ellipse2D;
-import java.awt.geom.Point2D;
-import java.awt.image.AffineTransformOp;
-import java.awt.image.BufferedImage;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
 
 @Mojo(name = "diagram")
 public class DiagramMojo extends AbstractMojo {
@@ -133,624 +64,16 @@ public class DiagramMojo extends AbstractMojo {
 
     @Override
     public void execute() throws MojoExecutionException, MojoFailureException {
-        final String content = slurp(validInput());
-
-        final JSLJob job = new JobModelResolver().resolveModel(content);
-
-        final List<ExecutionElement> executionElements = job.getExecutionElements();
-        if (executionElements == null) {
-            getLog().warn("No step found, no diagram will be generated.");
-            return;
-        }
-
-        final Diagram diagram = new Diagram(job.getId());
-        visitBatch(job, diagram);
-
-        draw(diagram);
-    }
-
-    private void visitBatch(final JSLJob job, final Diagram diagram) throws MojoExecutionException {
-        final Map<String, Node> nodes = new HashMap<String, Node>();
-
-        String first = null;
-        try {
-            first = new JobNavigator(job).getFirstExecutionElement(null).getId();
-        } catch (final Exception e) {
-            // no-op
-        }
-
-        // create nodes
-        final List<ExecutionElement> executionElements = job.getExecutionElements();
-        final Collection<ExecutionElement> allElements = new HashSet<ExecutionElement>();
-        initNodes(diagram, nodes, allElements, executionElements);
-
-        // create edges
-        for (final ExecutionElement element : allElements) {
-            final String id = element.getId();
-            final Node source = nodes.get(id);
-            if (id.equals(first)) {
-                source.root();
-            }
-
-            if (Step.class.isInstance(element)) {
-                final String next = Step.class.cast(element).getNextFromAttribute();
-                if (next != null) {
-                    final Node target = addNodeIfMissing(diagram, nodes, next, Node.Type.STEP);
-                    diagram.addEdge(new Edge("next"), source, target);
-                }
-            }
-
-            for (final TransitionElement transitionElement : element.getTransitionElements()) {
-                if (Stop.class.isInstance(transitionElement)) {
-                    final Stop stop = Stop.class.cast(transitionElement);
-
-                    final String restart = stop.getRestart();
-                    if (restart != null) {
-                        final Node target = addNodeIfMissing(diagram, nodes, restart, Node.Type.STEP);
-                        diagram.addEdge(new Edge("stop(" + stop.getOn() + ")"), source, target);
-                    }
-
-                    final String exitStatus = stop.getRestart();
-                    if (exitStatus != null) {
-                        final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK);
-                        diagram.addEdge(new Edge("stop(" + stop.getOn() + ")"), source, target);
-                    }
-                } else if (Fail.class.isInstance(transitionElement)) {
-                    final Fail fail = Fail.class.cast(transitionElement);
-                    final String exitStatus = fail.getExitStatus();
-                    final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK);
-                    diagram.addEdge(new Edge("fail(" + fail.getOn() + ")"), source, target);
-                } else if (End.class.isInstance(transitionElement)) {
-                    final End end = End.class.cast(transitionElement);
-                    final String exitStatus = end.getExitStatus();
-                    final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK);
-                    diagram.addEdge(new Edge("end(" + end.getOn() + ")"), source, target);
-                } else if (Next.class.isInstance(transitionElement)) {
-                    final Next end = Next.class.cast(transitionElement);
-                    final String to = end.getTo();
-                    final Node target = addNodeIfMissing(diagram, nodes, to, Node.Type.STEP);
-                    diagram.addEdge(new Edge("next(" + end.getOn() + ")"), source, target);
-                } else {
-                    getLog().warn("Unknown next element: " + transitionElement);
-                }
-            }
-        }
-    }
-
-    private void initNodes(final Diagram diagram, final Map<String, Node> nodes,
-                           final Collection<ExecutionElement> allElements, final Collection<ExecutionElement> executionElements) {
-        for (final ExecutionElement element : executionElements) {
-            final String id = element.getId();
-            allElements.add(element);
-
-            addNodeIfMissing(diagram, nodes, id, Node.Type.STEP);
-
-            if (Split.class.isInstance(element)) {
-                final Split split = Split.class.cast(element);
-                final List<Flow> flows = split.getFlows();
-                for (final Flow flow : flows) {
-                    initNodes(diagram, nodes, allElements, flow.getExecutionElements());
-                }
-            } else if (Flow.class.isInstance(element)) {
-                initNodes(diagram, nodes, allElements, Flow.class.cast(element).getExecutionElements());
-            } // else if step or decision -> ok
-        }
-    }
-
-    private static Node addNodeIfMissing(final Diagram diagram, final Map<String, Node> nodes, final String id, final Node.Type type) {
-        Node node = nodes.get(id);
-        if (node == null) {
-            node = new Node(id, type);
-            nodes.put(id, node);
-            diagram.addVertex(node);
-        }
-        return node;
-    }
-
-    private void draw(final Diagram diagram) throws MojoExecutionException {
-        final Layout<Node, Edge> diagramLayout = newLayout(diagram);
-
-        final Dimension outputSize = new Dimension(width, height);
-        final VisualizationViewer<Node, Edge> viewer = new GraphViewer(diagramLayout, rotateEdges);
-
-        if (LevelLayout.class.isInstance(diagramLayout)) {
-            LevelLayout.class.cast(diagramLayout).vertexShapeTransformer = viewer.getRenderContext().getVertexShapeTransformer();
-        }
-
-        diagramLayout.setSize(outputSize);
-        diagramLayout.reset();
-        viewer.setPreferredSize(diagramLayout.getSize());
-        viewer.setSize(diagramLayout.getSize());
-
-        // saving it too
-        if (!output.exists() && !output.mkdirs()) {
-            throw new MojoExecutionException("Can't create '" + output.getPath() + "'");
-        }
-        saveView(diagramLayout.getSize(), outputSize, diagram.getName(), viewer);
-
-        // viewing the window if necessary
-        if (view) {
-            final JFrame window = createWindow(viewer, diagram.getName());
-            final CountDownLatch latch = new CountDownLatch(1);
-            window.setVisible(true);
-            window.addWindowListener(new WindowAdapter() {
-                @Override
-                public void windowClosed(WindowEvent e) {
-                    super.windowClosed(e);
-                    latch.countDown();
-                }
-            });
-            try {
-                latch.await();
-            } catch (final InterruptedException e) {
-                getLog().error("can't await window close event", e);
-            }
-        }
-    }
-
-    private Layout<Node, Edge> newLayout(final Diagram diagram) {
-        final Layout<Node, Edge> diagramLayout;
-        if (layout != null && layout.startsWith("spring")) {
-            diagramLayout = new SpringLayout<Node, Edge>(diagram, new ConstantTransformer(Integer.parseInt(config("spring", "100"))));
-        } else if (layout != null && layout.startsWith("kk")) {
-            Distance<Node> distance = new DijkstraDistance<Node, Edge>(diagram);
-            if (layout.endsWith("unweight")) {
-                distance = new UnweightedShortestPath<Node, Edge>(diagram);
+        new DiagramGenerator(path, failIfMissing, view, width, height, adjust, output, format, outputFileName, rotateEdges, layout) {
+            @Override
+            protected void warn(final String s) {
+                getLog().warn(s);
             }
-            diagramLayout = new KKLayout<Node, Edge>(diagram, distance);
-        } else if (layout != null && layout.equalsIgnoreCase("circle")) {
-            diagramLayout = new CircleLayout<Node, Edge>(diagram);
-        } else if (layout != null && layout.equalsIgnoreCase("fr")) {
-            diagramLayout = new FRLayout<Node, Edge>(diagram);
-        } else {
-            final LevelLayout levelLayout = new LevelLayout(diagram);
-            levelLayout.adjust = adjust;
-
-            diagramLayout = levelLayout;
-        }
-        return diagramLayout;
-    }
-
-    private String config(final String name, final String defaultValue) {
-        final String cst = layout.substring(name.length());
-        String len = defaultValue;
-        if (!cst.isEmpty()) {
-            len = cst;
-        }
-        return len;
-    }
-
-    private JFrame createWindow(final VisualizationViewer<Node, Edge> viewer, final String name) {
-        viewer.setBackground(Color.WHITE);
-
-        final DefaultModalGraphMouse<Node, Edge> gm = new DefaultModalGraphMouse<Node, Edge>();
-        gm.setMode(DefaultModalGraphMouse.Mode.PICKING);
-        viewer.setGraphMouse(gm);
-
-        final JFrame frame = new JFrame(name + " viewer");
-        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
-        frame.setLayout(new GridLayout());
-        frame.getContentPane().add(viewer);
-        frame.pack();
-
-        return frame;
-    }
-
-    private void saveView(final Dimension currentSize, final Dimension desiredSize, final String name, final VisualizationViewer<Node, Edge> viewer) throws MojoExecutionException {
-        BufferedImage bi = new BufferedImage(currentSize.width, currentSize.height, BufferedImage.TYPE_INT_ARGB);
-
-        final Graphics2D g = bi.createGraphics();
-        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
-
-        final boolean db = viewer.isDoubleBuffered();
-        viewer.setDoubleBuffered(false);
-        viewer.paint(g);
-        viewer.setDoubleBuffered(db);
-        if (!currentSize.equals(desiredSize)) {
-            final double xFactor = desiredSize.width * 1. / currentSize.width;
-            final double yFactor = desiredSize.height * 1. / currentSize.height;
-            final double factor = Math.min(xFactor, yFactor);
-            getLog().info("optimal size is (" + currentSize.width + ", " + currentSize.height + ")");
-            getLog().info("scaling with a factor of " + factor);
-
-            final AffineTransform tx = new AffineTransform();
-            tx.scale(factor, factor);
-            final AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
-            BufferedImage biNew = new BufferedImage((int) (bi.getWidth() * factor), (int) (bi.getHeight() * factor), bi.getType());
-            bi = op.filter(bi, biNew);
-        }
-        g.dispose();
-
-        OutputStream os = null;
-        try {
-            final File file = new File(output, (outputFileName != null ? outputFileName : name) + "." + format);
-            os = new FileOutputStream(file);
-            if (!ImageIO.write(bi, format, os)) {
-                throw new MojoExecutionException("can't save picture " + name + "." + format);
-            }
-            getLog().info("Saved " + file.getAbsolutePath());
-        } catch (final IOException e) {
-            throw new MojoExecutionException("can't save the diagram", e);
-        } finally {
-            if (os != null) {
-                try {
-                    os.flush();
-                    os.close();
-                } catch (final IOException e) {
-                    // no-op
-                }
-            }
-        }
-    }
-
-
-    private File validInput() throws MojoExecutionException {
-        final File file = new File(path);
-        if (!file.exists()) {
-            final String msg = "Can't find '" + path + "'";
-            if (failIfMissing) {
-                throw new MojoExecutionException(msg);
-            }
-            getLog().error(msg);
-        }
-        return file;
-    }
-
-    private String slurp(final File file) throws MojoExecutionException {
-        final String content;
-        FileInputStream fis = null;
-        try {
-            fis = new FileInputStream(file);
-            content = IOUtil.toString(fis);
-        } catch (final Exception e) {
-            throw new MojoExecutionException(e.getMessage(), e);
-        } finally {
-            IOUtil.close(fis);
-        }
-        return content;
-    }
-
-    private static class Diagram extends DirectedSparseGraph<Node, Edge> {
-        private final String name;
-
-        private Diagram(final String name) {
-            this.name = name;
-        }
-
-        public String getName() {
-            return name;
-        }
-    }
-
-    private static class Node {
-        public static enum Type {
-            STEP, SINK
-        }
-
-        private final String text;
-        private final Type type;
-        private boolean root = false;
-
-        private Node(final String text, final Type type) {
-            this.text = text;
-            this.type = type;
-        }
-
-        public void root() {
-            root = true;
-        }
-    }
-
-    private static class Edge {
-        private final String text;
-
-        private Edge(final String text) {
-            this.text = text;
-        }
-    }
-
-    private static class GraphViewer extends VisualizationViewer<Node, Edge> {
-        private final boolean rotateEdges;
-
-        public GraphViewer(final Layout<Node, Edge> nodeEdgeLayout, final boolean rotateEdges) {
-            super(nodeEdgeLayout);
-            this.rotateEdges = rotateEdges;
-            init();
-        }
-
-        private void init() {
-            setOpaque(true);
-            setBackground(new Color(255, 255, 255, 0));
-
-            final RenderContext<Node, Edge> context = getRenderContext();
 
-            context.setVertexFillPaintTransformer(new VertexFillPaintTransformer());
-            context.setVertexShapeTransformer(new VertexShapeTransformer(getFontMetrics(getFont())));
-            context.setVertexLabelTransformer(new VertexLabelTransformer());
-            getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR);
-
-            context.setEdgeLabelTransformer(new EdgeLabelTransformer());
-            context.setEdgeShapeTransformer(new EdgeShape.Line<Node, Edge>());
-            context.setEdgeLabelClosenessTransformer(new EdgeLabelClosenessTransformer());
-            context.getEdgeLabelRenderer().setRotateEdgeLabels(rotateEdges);
-            getRenderer().setEdgeLabelRenderer(new EdgeLabelRenderer());
-        }
-    }
-
-    private static class VertexShapeTransformer implements Transformer<Node, Shape> {
-        private static final int X_MARGIN = 4;
-        private static final int Y_MARGIN = 2;
-
-        private FontMetrics metrics;
-
-        public VertexShapeTransformer(final FontMetrics f) {
-            metrics = f;
-        }
-
-        @Override
-        public Shape transform(final Node i) {
-            final int w = metrics.stringWidth(i.text) + X_MARGIN;
-            final int h = metrics.getHeight() + Y_MARGIN;
-
-            // centering
-            final AffineTransform transform = AffineTransform.getTranslateInstance(-w / 2.0, -h / 2.0);
-            switch (i.type) {
-                case SINK:
-                    return transform.createTransformedShape(new Ellipse2D.Double(0, 0, w, h));
-                default:
-                    return transform.createTransformedShape(new Rectangle(0, 0, w, h));
-            }
-        }
-    }
-
-    private static class VertexFillPaintTransformer implements Transformer<Node, Paint> {
-        @Override
-        public Paint transform(final Node node) {
-            if (node.root) {
-                return Color.GREEN;
-            }
-
-            switch (node.type) {
-                case SINK:
-                    return Color.RED;
-                default:
-                    return Color.WHITE;
-            }
-        }
-    }
-
-    private static class EdgeLabelTransformer implements Transformer<Edge, String> {
-        @Override
-        public String transform(Edge i) {
-            return i.text;
-        }
-    }
-
-    private static class VertexLabelTransformer extends ToStringLabeller<Node> {
-        public String transform(final Node node) {
-            return node.text;
-        }
-    }
-
-    private static class EdgeLabelClosenessTransformer implements Transformer<Context<Graph<Node, Edge>, Edge>, Number> {
-        @Override
-        public Number transform(final Context<Graph<Node, Edge>, Edge> context) {
-            return 0.5;
-        }
-    }
-
-    private static class EdgeLabelRenderer extends BasicEdgeLabelRenderer<Node, Edge> {
-        public void labelEdge(final RenderContext<Node, Edge> rc, final Layout<Node, Edge> layout, final Edge e, final String label) {
-            if (label == null || label.length() == 0) {
-                return;
-            }
-
-            final Graph<Node, Edge> graph = layout.getGraph();
-            // don't draw edge if either incident vertex is not drawn
-            final Pair<Node> endpoints = graph.getEndpoints(e);
-            final Node v1 = endpoints.getFirst();
-            final Node v2 = endpoints.getSecond();
-            if (!rc.getEdgeIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Edge>getInstance(graph, e))) {
-                return;
-            }
-
-            if (!rc.getVertexIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Node>getInstance(graph, v1)) ||
-                    !rc.getVertexIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Node>getInstance(graph, v2))) {
-                return;
-            }
-
-            final Point2D p1 = rc.getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v1));
-            final Point2D p2 = rc.getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v2));
-
-            final GraphicsDecorator g = rc.getGraphicsContext();
-            final Component component = prepareRenderer(rc, rc.getEdgeLabelRenderer(), label, rc.getPickedEdgeState().isPicked(e), e);
-            final Dimension d = component.getPreferredSize();
-
-            final AffineTransform old = g.getTransform();
-            final AffineTransform xform = new AffineTransform(old);
-            final FontMetrics fm = g.getFontMetrics();
-            int w = fm.stringWidth(e.text);
-            double p = Math.max(0, p1.getX() + p2.getX() - w);
-            xform.translate(Math.min(layout.getSize().width - w, p / 2), (p1.getY() + p2.getY() - fm.getHeight()) / 2);
-            g.setTransform(xform);
-            g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true);
-
-            g.setTransform(old);
-        }
-    }
-
-    private static class LevelLayout extends AbstractLayout<Node, Edge> {
-        private static final int X_MARGIN = 4;
-
-        private Transformer<Node, Shape> vertexShapeTransformer = null;
-        private boolean adjust;
-
-        public LevelLayout(final Diagram nodeEdgeGraph) {
-            super(nodeEdgeGraph);
-        }
-
-        @Override
-        public void initialize() {
-            final Map<Node, Integer> level = levels();
-            final List<List<Node>> nodes = sortNodeByLevel(level);
-            final int ySpace = maxHeight(nodes);
-            final int nLevels = nodes.size();
-            final int yLevel = Math.max(0, getSize().height - nLevels * ySpace) / Math.max(1, nLevels - 1);
-
-            int y = ySpace / 2;
-            int maxWidth = getSize().width;
-            for (final List<Node> currentNodes : nodes) {
-                if (currentNodes.size() == 1) { // only 1 => centering manually
-                    setLocation(currentNodes.iterator().next(), new Point(getSize().width / 2, y));
-                } else {
-                    int x = 0;
-                    final int xLevel = Math.max(0, getSize().width - width(currentNodes) - X_MARGIN) / (currentNodes.size() - 1);
-                    Collections.sort(currentNodes, new NodeComparator((Diagram) graph, locations));
-
-                    for (Node node : currentNodes) {
-                        Rectangle b = getBound(node, vertexShapeTransformer);
-                        int step = b.getBounds().width / 2;
-                        x += step;
-                        setLocation(node, new Point(x, y));
-                        x += xLevel + step;
-                    }
-
-                    maxWidth = Math.max(maxWidth, x - xLevel);
-                }
-                y += yLevel + ySpace;
-            }
-
-            if (adjust) {
-                adjust = false;
-                setSize(new Dimension(maxWidth, y + ySpace));
-                initialize();
-                adjust = true;
-            }
-        }
-
-        @Override
-        public void reset() {
-            initialize();
-        }
-
-        private int width(List<Node> nodes) {
-            int sum = 0;
-            for (Node node : nodes) {
-                sum += getBound(node, vertexShapeTransformer).width;
-            }
-            return sum;
-        }
-
-        private int maxHeight(final List<List<Node>> nodes) {
-            int max = 0;
-            for (final List<Node> list : nodes) {
-                for (final Node n : list) {
-                    max = Math.max(max, getBound(n, vertexShapeTransformer).height);
-                }
-            }
-            return max;
-        }
-
-        private Rectangle getBound(final Node n, final Transformer<Node, Shape> vst) {
-            if (vst == null) {
-                return new Rectangle(0, 0);
-            }
-            return vst.transform(n).getBounds();
-        }
-
-        private List<List<Node>> sortNodeByLevel(final Map<Node, Integer> level) {
-            final int levels = max(level);
-
-            final List<List<Node>> sorted = new ArrayList<List<Node>>();
-            for (int i = 0; i < levels; i++) {
-                sorted.add(new ArrayList<Node>());
-            }
-
-            for (final Map.Entry<Node, Integer> entry : level.entrySet()) {
-                sorted.get(entry.getValue()).add(entry.getKey());
-            }
-            return sorted;
-        }
-
-        private int max(final Map<Node, Integer> level) {
-            int i = 0;
-            for (Map.Entry<Node, Integer> l : level.entrySet()) {
-                if (l.getValue() >= i) {
-                    i = l.getValue() + 1;
-                }
-            }
-            return i;
-        }
-
-        private Map<Node, Integer> levels() {
-            final Map<Node, Integer> out = new HashMap<Node, Integer>();
-            for (final Node node : graph.getVertices()) { // init
-                out.put(node, 0);
-            }
-
-            final Map<Node, Collection<Node>> successors = new HashMap<Node, Collection<Node>>();
-            final Map<Node, Collection<Node>> predecessors = new HashMap<Node, Collection<Node>>();
-            for (final Node node : graph.getVertices()) {
-                successors.put(node, graph.getSuccessors(node));
-                predecessors.put(node, graph.getPredecessors(node));
-            }
-
-            boolean done;
-            do {
-                done = true;
-                for (final Node node : graph.getVertices()) {
-                    int nodeLevel = out.get(node);
-                    for (final Node successor : successors.get(node)) {
-                        if (out.get(successor) <= nodeLevel
-                                && successor != node
-                                && !predecessors.get(node).contains(successor)) {
-                            done = false;
-                            out.put(successor, nodeLevel + 1);
-                        }
-                    }
-                }
-            } while (!done);
-
-            final int min = Collections.min(out.values());
-            for (final Map.Entry<Node, Integer> entry : out.entrySet()) {
-                out.put(entry.getKey(), entry.getValue() - min);
-            }
-
-            return out;
-        }
-    }
-
-    private static class NodeComparator implements Comparator<Node> { // sort by predecessor location
-        private final Diagram graph;
-        private final Map<Node, Point2D> locations;
-
-        public NodeComparator(final Diagram diagram, final Map<Node, Point2D> points) {
-            graph = diagram;
-            locations = points;
-        }
-
-        @Override
-        public int compare(Node o1, Node o2) {
-            final Collection<Node> p1 = graph.getPredecessors(o1);
-            final Collection<Node> p2 = graph.getPredecessors(o2);
-
-            // mean value is used but almost always there is only one predecessor
-            final int m1 = mean(p1);
-            final int m2 = mean(p2);
-            return m1 - m2;
-        }
-
-        private int mean(final Collection<Node> p) {
-            if (p.size() == 0) {
-                return 0;
-            }
-            int mean = 0;
-            for (final Node n : p) {
-                mean += locations.get(n).getX();
+            @Override
+            protected void info(String s) {
+                getLog().info(s);
             }
-            return mean / p.size();
-        }
+        }.execute();
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/2936aba3/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/doc/DiagramGenerator.java
----------------------------------------------------------------------
diff --git a/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/doc/DiagramGenerator.java b/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/doc/DiagramGenerator.java
new file mode 100644
index 0000000..839b2b3
--- /dev/null
+++ b/tools/maven-plugin/src/main/java/org/apache/batchee/tools/maven/doc/DiagramGenerator.java
@@ -0,0 +1,748 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.batchee.tools.maven.doc;
+
+import edu.uci.ics.jung.algorithms.layout.AbstractLayout;
+import edu.uci.ics.jung.algorithms.layout.CircleLayout;
+import edu.uci.ics.jung.algorithms.layout.FRLayout;
+import edu.uci.ics.jung.algorithms.layout.KKLayout;
+import edu.uci.ics.jung.algorithms.layout.Layout;
+import edu.uci.ics.jung.algorithms.layout.SpringLayout;
+import edu.uci.ics.jung.algorithms.shortestpath.DijkstraDistance;
+import edu.uci.ics.jung.algorithms.shortestpath.Distance;
+import edu.uci.ics.jung.algorithms.shortestpath.UnweightedShortestPath;
+import edu.uci.ics.jung.graph.DirectedSparseGraph;
+import edu.uci.ics.jung.graph.Graph;
+import edu.uci.ics.jung.graph.util.Context;
+import edu.uci.ics.jung.graph.util.Pair;
+import edu.uci.ics.jung.visualization.Layer;
+import edu.uci.ics.jung.visualization.RenderContext;
+import edu.uci.ics.jung.visualization.VisualizationViewer;
+import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
+import edu.uci.ics.jung.visualization.decorators.EdgeShape;
+import edu.uci.ics.jung.visualization.decorators.ToStringLabeller;
+import edu.uci.ics.jung.visualization.renderers.BasicEdgeLabelRenderer;
+import edu.uci.ics.jung.visualization.renderers.Renderer;
+import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
+import org.apache.batchee.container.jsl.ExecutionElement;
+import org.apache.batchee.container.jsl.JobModelResolver;
+import org.apache.batchee.container.jsl.TransitionElement;
+import org.apache.batchee.container.navigator.JobNavigator;
+import org.apache.batchee.jaxb.End;
+import org.apache.batchee.jaxb.Fail;
+import org.apache.batchee.jaxb.Flow;
+import org.apache.batchee.jaxb.JSLJob;
+import org.apache.batchee.jaxb.Next;
+import org.apache.batchee.jaxb.Split;
+import org.apache.batchee.jaxb.Step;
+import org.apache.batchee.jaxb.Stop;
+import org.apache.commons.collections15.Transformer;
+import org.apache.commons.collections15.functors.ConstantTransformer;
+import org.codehaus.plexus.util.IOUtil;
+
+import javax.imageio.ImageIO;
+import javax.swing.JFrame;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.GridLayout;
+import java.awt.Paint;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Point2D;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+public abstract class DiagramGenerator {
+    protected final String path;
+    protected final boolean failIfMissing;
+    protected final boolean view;
+    protected final int width;
+    protected final int height;
+    protected final boolean adjust;
+    protected final File output;
+    protected final String format;
+    protected final String outputFileName;
+    protected final boolean rotateEdges;
+    protected final String layout;
+
+//CHECKSTYLE:OFF
+    public DiagramGenerator(final String path, final boolean failIfMissing,
+                            final boolean view, final int width, final int height,
+                            final boolean adjust, final File output, final String format,
+                            final String outputFileName, final boolean rotateEdges, final String layout) {
+//CHECKSTYLE:ON
+        this.path = path;
+        this.failIfMissing = failIfMissing;
+        this.view = view;
+        this.width = width;
+        this.height = height;
+        this.adjust = adjust;
+        this.output = output;
+        this.format = format;
+        this.outputFileName = outputFileName;
+        this.rotateEdges = rotateEdges;
+        this.layout = layout;
+    }
+
+    public void execute() {
+        final String content = slurp(validInput());
+
+        final JSLJob job = new JobModelResolver().resolveModel(content);
+
+        final List<ExecutionElement> executionElements = job.getExecutionElements();
+        if (executionElements == null) {
+            warn("No step found, no diagram will be generated.");
+            return;
+        }
+
+        final Diagram diagram = new Diagram(job.getId());
+        visitBatch(job, diagram);
+
+        draw(diagram);
+    }
+
+    private void visitBatch(final JSLJob job, final Diagram diagram) {
+        final Map<String, Node> nodes = new HashMap<String, Node>();
+
+        String first = null;
+        try {
+            first = new JobNavigator(job).getFirstExecutionElement(null).getId();
+        } catch (final Exception e) {
+            // no-op
+        }
+
+        // create nodes
+        final List<ExecutionElement> executionElements = job.getExecutionElements();
+        final Collection<ExecutionElement> allElements = new HashSet<ExecutionElement>();
+        initNodes(diagram, nodes, allElements, executionElements);
+
+        // create edges
+        for (final ExecutionElement element : allElements) {
+            final String id = element.getId();
+            final Node source = nodes.get(id);
+            if (id.equals(first)) {
+                source.root();
+            }
+
+            if (Step.class.isInstance(element)) {
+                final String next = Step.class.cast(element).getNextFromAttribute();
+                if (next != null) {
+                    final Node target = addNodeIfMissing(diagram, nodes, next, Node.Type.STEP);
+                    diagram.addEdge(new Edge("next"), source, target);
+                }
+            }
+
+            for (final TransitionElement transitionElement : element.getTransitionElements()) {
+                if (Stop.class.isInstance(transitionElement)) {
+                    final Stop stop = Stop.class.cast(transitionElement);
+
+                    final String restart = stop.getRestart();
+                    if (restart != null) {
+                        final Node target = addNodeIfMissing(diagram, nodes, restart, Node.Type.STEP);
+                        diagram.addEdge(new Edge("stop(" + stop.getOn() + ")"), source, target);
+                    }
+
+                    final String exitStatus = stop.getRestart();
+                    if (exitStatus != null) {
+                        final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK);
+                        diagram.addEdge(new Edge("stop(" + stop.getOn() + ")"), source, target);
+                    }
+                } else if (Fail.class.isInstance(transitionElement)) {
+                    final Fail fail = Fail.class.cast(transitionElement);
+                    final String exitStatus = fail.getExitStatus();
+                    final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK);
+                    diagram.addEdge(new Edge("fail(" + fail.getOn() + ")"), source, target);
+                } else if (End.class.isInstance(transitionElement)) {
+                    final End end = End.class.cast(transitionElement);
+                    final String exitStatus = end.getExitStatus();
+                    final Node target = addNodeIfMissing(diagram, nodes, exitStatus, Node.Type.SINK);
+                    diagram.addEdge(new Edge("end(" + end.getOn() + ")"), source, target);
+                } else if (Next.class.isInstance(transitionElement)) {
+                    final Next end = Next.class.cast(transitionElement);
+                    final String to = end.getTo();
+                    final Node target = addNodeIfMissing(diagram, nodes, to, Node.Type.STEP);
+                    diagram.addEdge(new Edge("next(" + end.getOn() + ")"), source, target);
+                } else {
+                    warn("Unknown next element: " + transitionElement);
+                }
+            }
+        }
+    }
+
+    protected abstract void warn(String s);
+    protected abstract void info(String s);
+
+    private void initNodes(final Diagram diagram, final Map<String, Node> nodes,
+                           final Collection<ExecutionElement> allElements, final Collection<ExecutionElement> executionElements) {
+        for (final ExecutionElement element : executionElements) {
+            final String id = element.getId();
+            allElements.add(element);
+
+            addNodeIfMissing(diagram, nodes, id, Node.Type.STEP);
+
+            if (Split.class.isInstance(element)) {
+                final Split split = Split.class.cast(element);
+                final List<Flow> flows = split.getFlows();
+                for (final Flow flow : flows) {
+                    initNodes(diagram, nodes, allElements, flow.getExecutionElements());
+                }
+            } else if (Flow.class.isInstance(element)) {
+                initNodes(diagram, nodes, allElements, Flow.class.cast(element).getExecutionElements());
+            } // else if step or decision -> ok
+        }
+    }
+
+    private static Node addNodeIfMissing(final Diagram diagram, final Map<String, Node> nodes, final String id, final Node.Type type) {
+        Node node = nodes.get(id);
+        if (node == null) {
+            node = new Node(id, type);
+            nodes.put(id, node);
+            diagram.addVertex(node);
+        }
+        return node;
+    }
+
+    private void draw(final Diagram diagram) {
+        final Layout<Node, Edge> diagramLayout = newLayout(diagram);
+
+        final Dimension outputSize = new Dimension(width, height);
+        final VisualizationViewer<Node, Edge> viewer = new GraphViewer(diagramLayout, rotateEdges);
+
+        if (LevelLayout.class.isInstance(diagramLayout)) {
+            LevelLayout.class.cast(diagramLayout).vertexShapeTransformer = viewer.getRenderContext().getVertexShapeTransformer();
+        }
+
+        diagramLayout.setSize(outputSize);
+        diagramLayout.reset();
+        viewer.setPreferredSize(diagramLayout.getSize());
+        viewer.setSize(diagramLayout.getSize());
+
+        // saving it too
+        if (!output.exists() && !output.mkdirs()) {
+            throw new IllegalStateException("Can't create '" + output.getPath() + "'");
+        }
+        saveView(diagramLayout.getSize(), outputSize, diagram.getName(), viewer);
+
+        // viewing the window if necessary
+        if (view) {
+            final JFrame window = createWindow(viewer, diagram.getName());
+            final CountDownLatch latch = new CountDownLatch(1);
+            window.setVisible(true);
+            window.addWindowListener(new WindowAdapter() {
+                @Override
+                public void windowClosed(WindowEvent e) {
+                    super.windowClosed(e);
+                    latch.countDown();
+                }
+            });
+            try {
+                latch.await();
+            } catch (final InterruptedException e) {
+                warn("can't await window close event: " + e.getMessage());
+            }
+        }
+    }
+
+    private Layout<Node, Edge> newLayout(final Diagram diagram) {
+        final Layout<Node, Edge> diagramLayout;
+        if (layout != null && layout.startsWith("spring")) {
+            diagramLayout = new SpringLayout<Node, Edge>(diagram, new ConstantTransformer(Integer.parseInt(config("spring", "100"))));
+        } else if (layout != null && layout.startsWith("kk")) {
+            Distance<Node> distance = new DijkstraDistance<Node, Edge>(diagram);
+            if (layout.endsWith("unweight")) {
+                distance = new UnweightedShortestPath<Node, Edge>(diagram);
+            }
+            diagramLayout = new KKLayout<Node, Edge>(diagram, distance);
+        } else if (layout != null && layout.equalsIgnoreCase("circle")) {
+            diagramLayout = new CircleLayout<Node, Edge>(diagram);
+        } else if (layout != null && layout.equalsIgnoreCase("fr")) {
+            diagramLayout = new FRLayout<Node, Edge>(diagram);
+        } else {
+            final LevelLayout levelLayout = new LevelLayout(diagram);
+            levelLayout.adjust = adjust;
+
+            diagramLayout = levelLayout;
+        }
+        return diagramLayout;
+    }
+
+    private String config(final String name, final String defaultValue) {
+        final String cst = layout.substring(name.length());
+        String len = defaultValue;
+        if (!cst.isEmpty()) {
+            len = cst;
+        }
+        return len;
+    }
+
+    private JFrame createWindow(final VisualizationViewer<Node, Edge> viewer, final String name) {
+        viewer.setBackground(Color.WHITE);
+
+        final DefaultModalGraphMouse<Node, Edge> gm = new DefaultModalGraphMouse<Node, Edge>();
+        gm.setMode(DefaultModalGraphMouse.Mode.PICKING);
+        viewer.setGraphMouse(gm);
+
+        final JFrame frame = new JFrame(name + " viewer");
+        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        frame.setLayout(new GridLayout());
+        frame.getContentPane().add(viewer);
+        frame.pack();
+
+        return frame;
+    }
+
+    private void saveView(final Dimension currentSize, final Dimension desiredSize, final String name, final VisualizationViewer<Node, Edge> viewer) {
+        BufferedImage bi = new BufferedImage(currentSize.width, currentSize.height, BufferedImage.TYPE_INT_ARGB);
+
+        final Graphics2D g = bi.createGraphics();
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+
+        final boolean db = viewer.isDoubleBuffered();
+        viewer.setDoubleBuffered(false);
+        viewer.paint(g);
+        viewer.setDoubleBuffered(db);
+        if (!currentSize.equals(desiredSize)) {
+            final double xFactor = desiredSize.width * 1. / currentSize.width;
+            final double yFactor = desiredSize.height * 1. / currentSize.height;
+            final double factor = Math.min(xFactor, yFactor);
+            info("optimal size is (" + currentSize.width + ", " + currentSize.height + ")");
+            info("scaling with a factor of " + factor);
+
+            final AffineTransform tx = new AffineTransform();
+            tx.scale(factor, factor);
+            final AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
+            BufferedImage biNew = new BufferedImage((int) (bi.getWidth() * factor), (int) (bi.getHeight() * factor), bi.getType());
+            bi = op.filter(bi, biNew);
+        }
+        g.dispose();
+
+        OutputStream os = null;
+        try {
+            final File file = new File(output, (outputFileName != null ? outputFileName : name) + "." + format);
+            os = new FileOutputStream(file);
+            if (!ImageIO.write(bi, format, os)) {
+                throw new IllegalStateException("can't save picture " + name + "." + format);
+            }
+            info("Saved " + file.getAbsolutePath());
+        } catch (final IOException e) {
+            throw new IllegalStateException("can't save the diagram", e);
+        } finally {
+            if (os != null) {
+                try {
+                    os.flush();
+                    os.close();
+                } catch (final IOException e) {
+                    // no-op
+                }
+            }
+        }
+    }
+
+
+    private File validInput() {
+        final File file = new File(path);
+        if (!file.exists()) {
+            final String msg = "Can't find '" + path + "'";
+            if (failIfMissing) {
+                throw new IllegalStateException(msg);
+            }
+            warn(msg);
+        }
+        return file;
+    }
+
+    private String slurp(final File file) {
+        final String content;
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(file);
+            content = IOUtil.toString(fis);
+        } catch (final Exception e) {
+            throw new IllegalStateException(e.getMessage(), e);
+        } finally {
+            IOUtil.close(fis);
+        }
+        return content;
+    }
+
+    private static class Diagram extends DirectedSparseGraph<Node, Edge> {
+        private final String name;
+
+        private Diagram(final String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    private static class Node {
+        public static enum Type {
+            STEP, SINK
+        }
+
+        private final String text;
+        private final Type type;
+        private boolean root = false;
+
+        private Node(final String text, final Type type) {
+            this.text = text;
+            this.type = type;
+        }
+
+        public void root() {
+            root = true;
+        }
+    }
+
+    private static class Edge {
+        private final String text;
+
+        private Edge(final String text) {
+            this.text = text;
+        }
+    }
+
+    private static class GraphViewer extends VisualizationViewer<Node, Edge> {
+        private final boolean rotateEdges;
+
+        public GraphViewer(final Layout<Node, Edge> nodeEdgeLayout, final boolean rotateEdges) {
+            super(nodeEdgeLayout);
+            this.rotateEdges = rotateEdges;
+            init();
+        }
+
+        private void init() {
+            setOpaque(true);
+            setBackground(new Color(255, 255, 255, 0));
+
+            final RenderContext<Node, Edge> context = getRenderContext();
+
+            context.setVertexFillPaintTransformer(new VertexFillPaintTransformer());
+            context.setVertexShapeTransformer(new VertexShapeTransformer(getFontMetrics(getFont())));
+            context.setVertexLabelTransformer(new VertexLabelTransformer());
+            getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR);
+
+            context.setEdgeLabelTransformer(new EdgeLabelTransformer());
+            context.setEdgeShapeTransformer(new EdgeShape.Line<Node, Edge>());
+            context.setEdgeLabelClosenessTransformer(new EdgeLabelClosenessTransformer());
+            context.getEdgeLabelRenderer().setRotateEdgeLabels(rotateEdges);
+            getRenderer().setEdgeLabelRenderer(new EdgeLabelRenderer());
+        }
+    }
+
+    private static class VertexShapeTransformer implements Transformer<Node, Shape> {
+        private static final int X_MARGIN = 4;
+        private static final int Y_MARGIN = 2;
+
+        private FontMetrics metrics;
+
+        public VertexShapeTransformer(final FontMetrics f) {
+            metrics = f;
+        }
+
+        @Override
+        public Shape transform(final Node i) {
+            final int w = metrics.stringWidth(i.text) + X_MARGIN;
+            final int h = metrics.getHeight() + Y_MARGIN;
+
+            // centering
+            final AffineTransform transform = AffineTransform.getTranslateInstance(-w / 2.0, -h / 2.0);
+            switch (i.type) {
+                case SINK:
+                    return transform.createTransformedShape(new Ellipse2D.Double(0, 0, w, h));
+                default:
+                    return transform.createTransformedShape(new Rectangle(0, 0, w, h));
+            }
+        }
+    }
+
+    private static class VertexFillPaintTransformer implements Transformer<Node, Paint> {
+        @Override
+        public Paint transform(final Node node) {
+            if (node.root) {
+                return Color.GREEN;
+            }
+
+            switch (node.type) {
+                case SINK:
+                    return Color.RED;
+                default:
+                    return Color.WHITE;
+            }
+        }
+    }
+
+    private static class EdgeLabelTransformer implements Transformer<Edge, String> {
+        @Override
+        public String transform(Edge i) {
+            return i.text;
+        }
+    }
+
+    private static class VertexLabelTransformer extends ToStringLabeller<Node> {
+        public String transform(final Node node) {
+            return node.text;
+        }
+    }
+
+    private static class EdgeLabelClosenessTransformer implements Transformer<Context<Graph<Node, Edge>, Edge>, Number> {
+        @Override
+        public Number transform(final Context<Graph<Node, Edge>, Edge> context) {
+            return 0.5;
+        }
+    }
+
+    private static class EdgeLabelRenderer extends BasicEdgeLabelRenderer<Node, Edge> {
+        public void labelEdge(final RenderContext<Node, Edge> rc, final Layout<Node, Edge> layout, final Edge e, final String label) {
+            if (label == null || label.length() == 0) {
+                return;
+            }
+
+            final Graph<Node, Edge> graph = layout.getGraph();
+            // don't draw edge if either incident vertex is not drawn
+            final Pair<Node> endpoints = graph.getEndpoints(e);
+            final Node v1 = endpoints.getFirst();
+            final Node v2 = endpoints.getSecond();
+            if (!rc.getEdgeIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Edge>getInstance(graph, e))) {
+                return;
+            }
+
+            if (!rc.getVertexIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Node>getInstance(graph, v1)) ||
+                !rc.getVertexIncludePredicate().evaluate(Context.<Graph<Node, Edge>, Node>getInstance(graph, v2))) {
+                return;
+            }
+
+            final Point2D p1 = rc.getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v1));
+            final Point2D p2 = rc.getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v2));
+
+            final GraphicsDecorator g = rc.getGraphicsContext();
+            final Component component = prepareRenderer(rc, rc.getEdgeLabelRenderer(), label, rc.getPickedEdgeState().isPicked(e), e);
+            final Dimension d = component.getPreferredSize();
+
+            final AffineTransform old = g.getTransform();
+            final AffineTransform xform = new AffineTransform(old);
+            final FontMetrics fm = g.getFontMetrics();
+            int w = fm.stringWidth(e.text);
+            double p = Math.max(0, p1.getX() + p2.getX() - w);
+            xform.translate(Math.min(layout.getSize().width - w, p / 2), (p1.getY() + p2.getY() - fm.getHeight()) / 2);
+            g.setTransform(xform);
+            g.draw(component, rc.getRendererPane(), 0, 0, d.width, d.height, true);
+
+            g.setTransform(old);
+        }
+    }
+
+    private static class LevelLayout extends AbstractLayout<Node, Edge> {
+        private static final int X_MARGIN = 4;
+
+        private Transformer<Node, Shape> vertexShapeTransformer = null;
+        private boolean adjust;
+
+        public LevelLayout(final Diagram nodeEdgeGraph) {
+            super(nodeEdgeGraph);
+        }
+
+        @Override
+        public void initialize() {
+            final Map<Node, Integer> level = levels();
+            final List<List<Node>> nodes = sortNodeByLevel(level);
+            final int ySpace = maxHeight(nodes);
+            final int nLevels = nodes.size();
+            final int yLevel = Math.max(0, getSize().height - nLevels * ySpace) / Math.max(1, nLevels - 1);
+
+            int y = ySpace / 2;
+            int maxWidth = getSize().width;
+            for (final List<Node> currentNodes : nodes) {
+                if (currentNodes.size() == 1) { // only 1 => centering manually
+                    setLocation(currentNodes.iterator().next(), new Point(getSize().width / 2, y));
+                } else {
+                    int x = 0;
+                    final int xLevel = Math.max(0, getSize().width - width(currentNodes) - X_MARGIN) / (currentNodes.size() - 1);
+                    Collections.sort(currentNodes, new NodeComparator((Diagram) graph, locations));
+
+                    for (Node node : currentNodes) {
+                        Rectangle b = getBound(node, vertexShapeTransformer);
+                        int step = b.getBounds().width / 2;
+                        x += step;
+                        setLocation(node, new Point(x, y));
+                        x += xLevel + step;
+                    }
+
+                    maxWidth = Math.max(maxWidth, x - xLevel);
+                }
+                y += yLevel + ySpace;
+            }
+
+            if (adjust) {
+                adjust = false;
+                setSize(new Dimension(maxWidth, y + ySpace));
+                initialize();
+                adjust = true;
+            }
+        }
+
+        @Override
+        public void reset() {
+            initialize();
+        }
+
+        private int width(List<Node> nodes) {
+            int sum = 0;
+            for (Node node : nodes) {
+                sum += getBound(node, vertexShapeTransformer).width;
+            }
+            return sum;
+        }
+
+        private int maxHeight(final List<List<Node>> nodes) {
+            int max = 0;
+            for (final List<Node> list : nodes) {
+                for (final Node n : list) {
+                    max = Math.max(max, getBound(n, vertexShapeTransformer).height);
+                }
+            }
+            return max;
+        }
+
+        private Rectangle getBound(final Node n, final Transformer<Node, Shape> vst) {
+            if (vst == null) {
+                return new Rectangle(0, 0);
+            }
+            return vst.transform(n).getBounds();
+        }
+
+        private List<List<Node>> sortNodeByLevel(final Map<Node, Integer> level) {
+            final int levels = max(level);
+
+            final List<List<Node>> sorted = new ArrayList<List<Node>>();
+            for (int i = 0; i < levels; i++) {
+                sorted.add(new ArrayList<Node>());
+            }
+
+            for (final Map.Entry<Node, Integer> entry : level.entrySet()) {
+                sorted.get(entry.getValue()).add(entry.getKey());
+            }
+            return sorted;
+        }
+
+        private int max(final Map<Node, Integer> level) {
+            int i = 0;
+            for (Map.Entry<Node, Integer> l : level.entrySet()) {
+                if (l.getValue() >= i) {
+                    i = l.getValue() + 1;
+                }
+            }
+            return i;
+        }
+
+        private Map<Node, Integer> levels() {
+            final Map<Node, Integer> out = new HashMap<Node, Integer>();
+            for (final Node node : graph.getVertices()) { // init
+                out.put(node, 0);
+            }
+
+            final Map<Node, Collection<Node>> successors = new HashMap<Node, Collection<Node>>();
+            final Map<Node, Collection<Node>> predecessors = new HashMap<Node, Collection<Node>>();
+            for (final Node node : graph.getVertices()) {
+                successors.put(node, graph.getSuccessors(node));
+                predecessors.put(node, graph.getPredecessors(node));
+            }
+
+            boolean done;
+            do {
+                done = true;
+                for (final Node node : graph.getVertices()) {
+                    int nodeLevel = out.get(node);
+                    for (final Node successor : successors.get(node)) {
+                        if (out.get(successor) <= nodeLevel
+                            && successor != node
+                            && !predecessors.get(node).contains(successor)) {
+                            done = false;
+                            out.put(successor, nodeLevel + 1);
+                        }
+                    }
+                }
+            } while (!done);
+
+            final int min = Collections.min(out.values());
+            for (final Map.Entry<Node, Integer> entry : out.entrySet()) {
+                out.put(entry.getKey(), entry.getValue() - min);
+            }
+
+            return out;
+        }
+    }
+
+    private static class NodeComparator implements Comparator<Node> { // sort by predecessor location
+        private final Diagram graph;
+        private final Map<Node, Point2D> locations;
+
+        public NodeComparator(final Diagram diagram, final Map<Node, Point2D> points) {
+            graph = diagram;
+            locations = points;
+        }
+
+        @Override
+        public int compare(Node o1, Node o2) {
+            final Collection<Node> p1 = graph.getPredecessors(o1);
+            final Collection<Node> p2 = graph.getPredecessors(o2);
+
+            // mean value is used but almost always there is only one predecessor
+            final int m1 = mean(p1);
+            final int m2 = mean(p2);
+            return m1 - m2;
+        }
+
+        private int mean(final Collection<Node> p) {
+            if (p.size() == 0) {
+                return 0;
+            }
+            int mean = 0;
+            for (final Node n : p) {
+                mean += locations.get(n).getX();
+            }
+            return mean / p.size();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/2936aba3/tools/maven-plugin/src/test/java/org/apache/batchee/tools/maven/DiagramMojoTest.java
----------------------------------------------------------------------
diff --git a/tools/maven-plugin/src/test/java/org/apache/batchee/tools/maven/DiagramMojoTest.java b/tools/maven-plugin/src/test/java/org/apache/batchee/tools/maven/DiagramMojoTest.java
index e60f4fb..72fd5c4 100644
--- a/tools/maven-plugin/src/test/java/org/apache/batchee/tools/maven/DiagramMojoTest.java
+++ b/tools/maven-plugin/src/test/java/org/apache/batchee/tools/maven/DiagramMojoTest.java
@@ -45,7 +45,7 @@ public class DiagramMojoTest {
         assertTrue(target.exists());
     }
 
-    @Test(expectedExceptions = MojoExecutionException.class)
+    @Test(expectedExceptions = Exception.class)
     public void fail() throws MojoFailureException, MojoExecutionException {
         final DiagramMojo mojo = new DiagramMojo();
         mojo.path = "missing-batch.xml";