package net.sourceforge.pmd.eclipse.ui.views.dataflow; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.dfa.DataFlowNode; import net.sourceforge.pmd.lang.dfa.VariableAccess; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; /** * Builds a Dataflow Graph * * @author SebastianRaffel ( 08.06.2005 ) */ public class DataflowGraph extends Composite { private List<NodeCanvas> nodes; private List<PathCanvas> paths; protected int nodeRadius = 12; protected int lineLength = 25; protected int rowHeight = 2*nodeRadius+lineLength; protected Color bgColor; protected Color nodeColor; protected Color textColor; protected boolean marked; protected Color markColor; protected Color markColor2; protected Color markColor3; /** * Inner class for creating Nodes, * each Node is a Label and has its own PaintListener * * @author SebastianRaffel ( 08.06.2005 ) */ private class NodeCanvas extends Canvas implements PaintListener { private DataFlowNode node; private int radius; private Color bgColor; private Color nodeColor; private Color textColor; protected boolean marked; protected Color markColor; /** * Constructor * * @param parent, the parent Composite * @param inode, the DataFlowNode * @param coordinates, where to put the Label * @param nodeRadius, radius of the Node */ public NodeCanvas(Composite parent, DataFlowNode inode, Point coordinates, int nodeRadius) { super(parent, SWT.NONE); node = inode; radius = nodeRadius; Display display = parent.getDisplay(); // Default Colors bgColor = display.getSystemColor(SWT.COLOR_WHITE); //new Color(null,255,255,255); nodeColor = display.getSystemColor(SWT.COLOR_GRAY); //new Color(null,128,128,128); textColor = display.getSystemColor(SWT.COLOR_WHITE); //new Color(null,255,255,255); markColor = display.getSystemColor(SWT.COLOR_RED); //new Color(null,192,0,0); // set location and size of the Label setLocation(coordinates); setSize((2*radius)+1, (2*radius)+1); // +1 to avoid cropping on right & bottom setBackground(bgColor); // we have our own Paint Listener addPaintListener(this); } // /** // * Set the Color for the Background, Node and Text in the Node // * // * @param backGround // * @param node // * @param text // */ // public void setColors(Color backGround, Color node, Color text) { // if (backGround != null) { // bgColor = backGround; // setBackground(bgColor); // } // nodeColor = node; // textColor = text; // } /** * Returns the Text-Line of the Node, * different Nodes can have the same Line * * @return line */ public int getLine() { return node.getLine(); } /** * Gets the Dataflow-Index of the Node * * @return index */ public int getIndex() { return node.getIndex(); } /** * Checks, if this Node contains a variable of the given name * * @param varName * @return true, if the Name of the Variable is found, * false otherwise */ public boolean containsVariable(String varName) { List<VariableAccess> vars = node.getVariableAccess(); if (vars == null) return false; for (VariableAccess va : vars) { if (va.getVariableName().equalsIgnoreCase(varName)) return true; } return false; } /** * @return the DataFlowNode */ public DataFlowNode getINode() { return node; } /** * Checks, if this Node has been marked * during painting a Path * * @return true if the Node has been marked, * false otherwise */ public boolean isMarked() { return marked; } /* @see org.eclipse.swt.events.PaintListener#paintControl(org.eclipse.swt.events.PaintEvent) */ public void paintControl(PaintEvent e) { // when we want to mark the Node, // we change the Drawing-Background e.gc.setBackground(marked ? markColor : nodeColor); // an area is filled with this BG-Color e.gc.fillArc(0, 0, 2*radius, 2*radius, 0, 360); // now outline it e.gc.drawArc(0, 0, 2*radius, 2*radius, 0, 360); // draw the Node's Index String indexString = String.valueOf(node.getIndex()); int xPos = radius-2-4*(indexString.length()/2); int yPos = radius/2; e.gc.setForeground(textColor); e.gc.drawString(indexString, xPos-1, yPos-1); } /** * Sets a Node as being marked * and paints is in a different - marking - Color * * @param isMarked * @param color */ public void mark(boolean isMarked, Color color) { marked = isMarked; markColor = color; redraw(); } } /** * Inner class for creating Paths, * we don't create our own labels here, * because SWT doesn't support transparency, * so we can't overlay paths * * @author SebastianRaffel ( 08.06.2005 ) */ private class PathCanvas implements PaintListener { private int index1; private int index2; private int radius; private Rectangle bounds; private Color lineColor; private int arrowWidth; private int arrowHeight; protected boolean marked; protected Color markColor; /** * Constructor, * Indexes can be twisted (first index > second Index) * as well as y-Positions * * @param parent, the parent composite * @param nodeIndex1, the first Index * @param nodeIndex2, the second Index * @param x, the x-Position * @param y1, the upper y-Position * @param y2, the lower y Position * @param nodeRadius, the radius of the Node */ public PathCanvas(Composite parent, int nodeIndex1, int nodeIndex2, int x, int y1, int y2, int nodeRadius) { index1 = nodeIndex1; index2 = nodeIndex2; radius = nodeRadius; arrowWidth = 4; arrowHeight = 7; Display disp = parent.getDisplay(); lineColor = disp.getSystemColor(SWT.COLOR_BLACK); markColor = new Color(null,192,0,0); // set the bounds to put the Path into bounds = calculateBounds(x, y1, y2); parent.addPaintListener(this); } /** * @return the first Index */ public int getIndex1() { return index1; } /** * @return the second Index */ public int getIndex2() { return index2; } // /** // * Sets the Paths Color // * // * @param color // */ // public void setColor(Color color) { // lineColor = color; // } /** * calculate the bounds, where to put the Path, * this bounds are absolute to the parent Element * (e.g. we don't put it on 50,75 and draw from 0,0, * but we draw from 50,75) * * @param x * @param y1 * @param y2 * @return a Rectangle of coordinates */ private Rectangle calculateBounds(int x, int y1, int y2) { int newX1 = 0; int newX2 = 0; int newY1 = 0; int newY2 = 0; // calculate by comparing the Nodes Indexes if (index1 < index2) { if (index2-index1 == 1) { // 1 -> 2 newX1 = x+radius-arrowWidth; newX2 = x+radius+arrowWidth; newY1 = y1+2*radius; newY2 = y2; } else if (index2-index1 > 1) { // 1 --\ // | // 2 <-/ newY1 = y1+radius-1; newY2 = y2+radius; int n = (index2-index1) * 3 + 10; n += Math.random()*5; newX1 = x-n; newX2 = x; } } else { if (index1-index2 == 1) { // 1 <- 2 newX1 = x+radius-arrowWidth; newX2 = x+radius+arrowWidth; newY1 = y2+2*radius; newY2 = y1; } else if (index1-index2 > 1) { // 1 <-\ // | // 2 --/ newY1 = y2+radius-1; newY2 = y1+radius; int n = (index1-index2) * 3 + 10; n += Math.random()*5; newX1 = x+2*radius; newX2 = x+2*radius+n; } } return new Rectangle(newX1, newY1, newX2, newY2); } /* @see org.eclipse.swt.events.PaintListener#paintControl(org.eclipse.swt.events.PaintEvent) */ public void paintControl(PaintEvent e) { // change Back- and Foreground // because the line in drawn with the FG-Color, // and the triangle is filled with the BG-Color if (marked) { e.gc.setForeground(markColor); e.gc.setBackground(markColor); } else { e.gc.setForeground(lineColor); e.gc.setBackground(lineColor); } int width = bounds.width-bounds.x; if (index1 < index2) { if (index2-index1 == 1) { // 1 -> 2 e.gc.drawLine( bounds.x+width/2, bounds.y, bounds.x+width/2, bounds.height); e.gc.fillPolygon(new int[] { bounds.x, bounds.height-arrowHeight, bounds.width, bounds.height-arrowHeight, bounds.x+width/2, bounds.height }); } else if (index2-index1 > 1) { // 1 --\ // | // 2 <-/ e.gc.drawPolyline(new int[] { bounds.x+width, bounds.y, bounds.x, bounds.y+width, bounds.x, bounds.height-width, bounds.width, bounds.height }); e.gc.fillPolygon(new int[] { bounds.width, bounds.height, bounds.width-arrowHeight-arrowWidth/2+1, bounds.height-arrowWidth, bounds.width-arrowWidth, bounds.height-arrowHeight-arrowWidth/2 }); } } else { if (index1-index2 == 1) { // 1 <- 2 e.gc.drawLine( bounds.x+width/2, bounds.y, bounds.x+width/2, bounds.height); e.gc.fillPolygon(new int[] { bounds.x+arrowWidth, bounds.y, bounds.x, bounds.y+arrowHeight, bounds.x+2*arrowWidth, bounds.y+arrowHeight }); } else if (index1-index2 > 1) { // 1 <-\ // | // 2 --/ e.gc.drawPolyline(new int[] { bounds.x, bounds.y, bounds.width, bounds.y+width, bounds.x+width, bounds.height-width, bounds.x, bounds.height }); e.gc.fillPolygon(new int[] { bounds.x, bounds.y, bounds.x+arrowWidth, bounds.y+arrowHeight+arrowWidth/2, bounds.x+arrowHeight+arrowWidth/2, bounds.y+arrowWidth }); } } } /** * Sets a Path a marked and color it * * @param isMarked * @param color */ public void mark(boolean isMarked, Color color) { marked = isMarked; markColor = color; } } /** * Constructor * * @param parent * @param node * @param radius * @param length */ public DataflowGraph(Composite parent, Node node, int radius, int length, int height) { super(parent, SWT.NONE); if (node == null) return; nodeRadius = radius; lineLength = length; rowHeight = height; nodes = new ArrayList<NodeCanvas>(); paths = new ArrayList<PathCanvas>(); Display display = parent.getDisplay(); // Default Colors bgColor = display.getSystemColor(SWT.COLOR_WHITE); //new Color(null,255,255,255); nodeColor = display.getSystemColor(SWT.COLOR_GRAY); //new Color(null,192,192,192); textColor = display.getSystemColor(SWT.COLOR_BLACK); //new Color(null,0,0,0); markColor = new Color(null,192,0,0); markColor2 = new Color(null,128,0,128); markColor3 = new Color(null,0,0,96); setSize(parent.getSize()); setBackground(bgColor); createDataflowGraph(node); } @Override public void addMouseListener(final MouseListener listener) { if (nodes != null) { Iterator<NodeCanvas> nodeIterator = nodes.iterator(); for (int i=0; nodeIterator.hasNext(); i++) { final int row = i; NodeCanvas node = nodeIterator.next(); node.addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent e) { e.y += row * DataflowGraphViewer.ROW_HEIGHT; listener.mouseDown(e); } }); } } super.addMouseListener(listener); } /** * Set the Graph Node-Radius, and Length of the * (direct) Lines from one Node to another * * @param radius * @param length */ public void setGraphData(int radius, int length){ nodeRadius = radius; lineLength = length; redraw(); } /** * Builds the DataflowGraph out of the given SimpleNode * * @param node */ private void createDataflowGraph(Node node) { List<DataFlowNode> flow = node.getDataFlowNode().getFlow(); // the Data-Flow gives us all the Nodes // every Node has children, for which we can build Paths for (int i=0; i<flow.size(); i++) { DataFlowNode inode = flow.get(i); // create a new Node and add it to the List Point location = new Point( (getSize().x-2*nodeRadius)/2, i*rowHeight + lineLength/2); NodeCanvas nod = new NodeCanvas(this, inode, location, nodeRadius); nodes.add(nod); // get the Nodes children and build Paths between them List<DataFlowNode> children = inode.getChildren(); for (DataFlowNode dfNode : children) { // create a new Path and add it to the List int x = (getSize().x-2*nodeRadius)/2; int y1 = inode.getIndex()*rowHeight + lineLength/2; int y2 = dfNode.getIndex()*rowHeight + lineLength/2; PathCanvas path = new PathCanvas(this, inode.getIndex(), dfNode.getIndex(), x, y1, y2, nodeRadius); paths.add(path); } } } /** * Returns the graphical Node of the given Index * * @param index * @return the NodeCanvas */ private NodeCanvas getNode(int index) { if (nodes == null) return null; for (NodeCanvas node : nodes) { if (node.getIndex() == index) return node; } return null; } /** * Returns the Path between the given Indexes * * @param index1 * @param index2 * @return the Path */ private PathCanvas getPath(int index1, int index2) { if (paths == null) return null; for (PathCanvas path : paths) { if (path.getIndex1() == index1 && path.getIndex2() == index2) { return path; } } return null; } /** * Checks, if a Path in the Graph has been marked * * @return true, if a path is coloured, false otherwise */ public boolean isMarked() { return marked; } /** * Un-marks a path, sets all colours to normal */ public void demark() { for (NodeCanvas node : nodes) { node.mark(false, null); } for (PathCanvas path : paths) { path.mark(false, null); } redraw(); marked = false; } /** * Marks one single node. * @param index index of the node */ public void markNode(int index) { NodeCanvas node = getNode(index); node.mark(true, markColor3); } /** * Marks a Path from the given first line to the second line * <br>Given are two Lines in the Text and a Variable, * where an anomaly has occurred, the method colors the nodes, * that lie at the lines and calculates and also colors * _one possible_ Path between them. * * @param line1 * @param line2 * @param varName */ public void markPath(int line1, int line2, String varName) { if (nodes == null || paths == null) return; // twist the Lines if needed if (line1 > line2) { int temp = line1; line1 = line2; line2 = temp; } // an Anomaly can have multiple starting points // but - so we say here - only one ending point List<DataFlowNode> startNodes = new ArrayList<DataFlowNode>(); DataFlowNode endNode = null; for (NodeCanvas node : nodes) { // first we clear all nodes not needed if (!node.containsVariable(varName)) { node.mark(false, null); continue; } if (node.getLine() == line1) { // if a Node is set at the given first Line we color it and add it as starting Node node.mark(true, markColor); startNodes.add(node.getINode()); } else if (node.getLine() == line2) { // ... else, if we found a Node at the ending Line, we mark it and set it as ending Node node.mark(true, markColor); endNode = node.getINode(); } else { node.mark(false, null); } } for (PathCanvas deMarkedPath : paths) { // we then clear all Paths deMarkedPath.mark(false, null); } // ... to mark some of them again List<PathCanvas> pathsToMark = new ArrayList<PathCanvas>(); for (DataFlowNode start : startNodes) { // from every starting Node we search for a Path to the ending node List<PathCanvas> pathList = findPath(start, endNode, new ArrayList<DataFlowNode>()); if (pathList == null) continue; // we get a List of PathCanvas, that build up the searched Path for (PathCanvas currentPath : pathList) { // if some PathCanvas are already found and // set to mark, we don't want to mark them again if (!pathsToMark.contains(currentPath)) pathsToMark.add(currentPath); } } // now we have a clear List of Paths that we can color for (int m=0; m<pathsToMark.size(); m++) { PathCanvas markedPath = pathsToMark.get(m); markedPath.mark(true, markColor); // the PathList contains Paths from the beginning to end, // like (1,2),(2,3),...,(12,13); we search for the Nodes // that are "visited" and mark them in another color, // so we can see them as "stopovers" (like 2,3,...,12) if (m<pathsToMark.size()-1) { NodeCanvas markedNode = getNode(markedPath.getIndex2()); if (!markedNode.isMarked()) { markedNode.mark(true, markColor2); } } } redraw(); marked = true; } /** * Recursively finds a Path from the starting Node to the ending Node, * the visited-List contains Paths, that already have been visited, so * the Function does not produce Loops, the List should be a * new ArrayList() when calling this Function * * @param start * @param end * @param visited * @return an List of PathCanvas, that build up the path from * Start-Node to End-Node or null, if there no such path could be found */ protected List<PathCanvas> findPath(DataFlowNode start, DataFlowNode end, List<DataFlowNode> visited) { // this is the break-Condition for the Recursion // if the Node's direct children contain the ending Node // we return the Path from the current Node to the End if (start.getChildren().contains(end)) { List<PathCanvas> found = new ArrayList<PathCanvas>(); PathCanvas path = getPath(start.getIndex(), end.getIndex()); if (path != null) { found.add(path); return found; } } else { // this is the Search for (DataFlowNode node : start.getChildren()) { // here we avoid Loops by checking the visited nodes if (visited.contains(node)) continue; // ... and adding the current Node visited.add(node); // the Recursion: find the Path from // the current Node's children to the End List<PathCanvas> isFound = findPath(node, end, visited); if (isFound == null) continue; else { // if a Path (from child to end) is found // we can add the Path from this Node to the child PathCanvas path2 = isFound.get(0); PathCanvas path1 = getPath(start.getIndex(), path2.getIndex1()); if (path1 != null) { isFound.add(0, path1); return isFound; } } } } return null; } }