View Javadoc

1   /*
2    * Copyright (c) 2003-2008, by Henrik Arro and Contributors
3    *
4    * This file is part of JSeq, a tool to automatically create
5    * sequence diagrams by tracing program execution.
6    *
7    * See <http://jseq.sourceforge.net> for more information.
8    *
9    * JSeq is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of
12   * the License, or (at your option) any later version.
13   *
14   * JSeq is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17   * GNU Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public License
20   * along with JSeq. If not, see <http://www.gnu.org/licenses/>.
21   */
22  
23  package th.co.edge.jseq.svg;
24  
25  import javax.xml.parsers.DocumentBuilder;
26  import javax.xml.parsers.DocumentBuilderFactory;
27  import javax.xml.parsers.ParserConfigurationException;
28  
29  import org.w3c.dom.DOMImplementation;
30  import org.w3c.dom.Document;
31  import org.w3c.dom.DocumentType;
32  import org.w3c.dom.Element;
33  
34  import th.co.edge.jseq.Activation;
35  import th.co.edge.jseq.ActivationList;
36  import th.co.edge.jseq.Diagram;
37  import th.co.edge.jseq.MockObject;
38  import th.co.edge.jseq.MockObjectMap;
39  import th.co.edge.jseq.TextDiagram;
40  import th.co.edge.jseq.util.XMLUtil;
41  
42  /**
43   * A <code>SVGGenerator</code> is used to create a sequence diagram in <a
44   * href="http://www.w3.org/Graphics/SVG/">SVG</a> format.
45   */
46  public class SVGGenerator {
47      private static final String SVG_1_1_PUBLIC_ID = "-//W3C//DTD SVG 1.1//EN";
48      private static final String SVG_1_1_SYSTEM_ID =
49              "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd";
50      private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
51  
52      private static final int COLUMN_WIDTH = 100;
53      private static final int ROW_HEIGHT = 30;
54      private static final int LIFE_LINE_LEFT_MARGIN = 150;
55      private static final int LIFE_LINE_TOP_MARGIN = 75;
56      private static final int LIFE_LINE_EXTRA_HEIGHT = 100;
57      private static final int NUM_ROWS_BETWEEN_DIAGRAMS = 5;
58      private static final int EXTRA_DIAGRAM_WIDTH = 200;
59      private static final int EXTRA_DIAGRAM_HEIGHT = 50;
60      private static final int HEADER_LEFT_MARGIN = 25;
61      private static final int HEADER_TOP_MARGIN = 50;
62      private static final int HEADER_VERTICAL_SHIFT = 20;
63      private static final int INDENT_FIRST_ARROW = 55;
64      private static final int ARROW_HEAD_WIDTH = 5;
65      private static final int ARROW_HEAD_HEIGHT = 5;
66      private static final int ARROW_VERTICAL_MARGIN = 100;
67      private static final int SELF_ARROW_WIDTH = 45;
68      private static final int SELF_ARROW_HEIGHT = 30;
69      private static final int ACTIVATION_BOX_WIDTH = 10;
70      private static final int ACTIVATION_BOX_TOP_MARGIN = 15;
71      private static final int ACTIVATION_BOX_BOTTOM_MARGIN = 10;
72      private static final int METHOD_NAME_BOTTOM_MARGIN = 5;
73  
74      private DocumentBuilder builder;
75  
76      private MockObjectMap objectMap;
77      private int startRow;
78      private int row;
79      private int maxX;
80      private int maxY;
81      private Element groupHeaders;
82      private Element groupCalls;
83      private Element groupActivationBoxes;
84      private Element groupLifeLines;
85  
86      /**
87       * Creates a new <code>SVGGenerator</code>.
88       *
89       * @throws ParserConfigurationException
90       *             if there is an error in the XML parser configuration (should
91       *             normally not occur)
92       */
93      public SVGGenerator() throws ParserConfigurationException {
94          DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
95          factory.setNamespaceAware(true);
96          this.builder = factory.newDocumentBuilder();
97      }
98  
99      /**
100      * Returns a new SVG sequence diagram based on the given
101      * <code>ActivationList</code>, representing the root activations.
102      *
103      * @param activationList
104      *            the root activations to use to generate the diagram
105      *
106      * @return an SVG <code>TextDiagram</code> that can be written to file as
107      *         XML
108      */
109     public Diagram generate(ActivationList activationList) {
110         Document doc = createSVGDocument(activationList);
111         return new TextDiagram(XMLUtil.toString(doc));
112     }
113 
114     /**
115      * Returns a new XML <code>Document</code> representing an
116      * <code>ActivationList</code> as an SVG document.
117      *
118      * @param activationList
119      *            the root activations to use to generate this diagram
120      *
121      * @return an XML <code>Document</code> representing
122      *         <code>activationList</code> as an SVG document
123      */
124     public Document createSVGDocument(ActivationList activationList) {
125         DOMImplementation impl = builder.getDOMImplementation();
126         DocumentType docType =
127                 impl.createDocumentType("svg", SVG_1_1_PUBLIC_ID,
128                         SVG_1_1_SYSTEM_ID);
129         Document doc = impl.createDocument(SVG_NAMESPACE, "svg", docType);
130         Element root = doc.getDocumentElement();
131         root.setAttribute("xmlns", SVG_NAMESPACE);
132         this.startRow = 0;
133         for (Activation activation : activationList) {
134             fillSVGDocument(doc, activation);
135             this.startRow += row + NUM_ROWS_BETWEEN_DIAGRAMS;
136         }
137         root.setAttributeNS(null, "width", Integer.toString(maxX +
138                 EXTRA_DIAGRAM_WIDTH));
139         root.setAttributeNS(null, "height", Integer.toString(maxY +
140                 EXTRA_DIAGRAM_HEIGHT));
141         return doc;
142     }
143 
144     private void fillSVGDocument(Document doc, Activation activation) {
145         this.objectMap = MockObjectMap.addAll(activation);
146         this.row = 0;
147         createGroups(doc);
148         addHeaders(doc);
149         addCalls(doc, activation);
150         addLifelines(doc);
151     }
152 
153     private void createGroups(Document doc) {
154         groupHeaders = doc.createElementNS(SVG_NAMESPACE, "g");
155         groupHeaders.setAttributeNS(null, "id", "Headers");
156         groupCalls = doc.createElementNS(SVG_NAMESPACE, "g");
157         groupCalls.setAttributeNS(null, "id", "Calls");
158         groupLifeLines = doc.createElementNS(SVG_NAMESPACE, "g");
159         groupLifeLines.setAttributeNS(null, "id", "Lifelines");
160         groupActivationBoxes = doc.createElementNS(SVG_NAMESPACE, "g");
161         groupActivationBoxes.setAttributeNS(null, "id", "ActivationBoxes");
162         Element root = doc.getDocumentElement();
163         root.appendChild(groupHeaders);
164         root.appendChild(groupLifeLines);
165         root.appendChild(groupActivationBoxes);
166         root.appendChild(groupCalls);
167     }
168 
169     private void addHeaders(Document doc) {
170         int x = HEADER_LEFT_MARGIN;
171         int y = HEADER_TOP_MARGIN + startRow * ROW_HEIGHT;
172         int column = 0;
173         for (MockObject object : objectMap.listView()) {
174             String name = object.getName();
175             if (name.lastIndexOf(".") >= 0) {
176                 name = name.substring(name.lastIndexOf(".") + 1);
177             }
178             x += COLUMN_WIDTH;
179             Element text = doc.createElementNS(SVG_NAMESPACE, "text");
180             text.setAttributeNS(null, "x", Integer.toString(x));
181             text.setAttributeNS(null, "y", Integer.toString(getHeaderY(y,
182                     column++)));
183             text.appendChild(doc.createTextNode(XMLUtil.makeXMLSafe(name)));
184             groupHeaders.appendChild(text);
185         }
186         if (x > maxX) {
187             maxX = x;
188         }
189     }
190 
191     private int getHeaderY(int y, int column) {
192         int headerY;
193         if (column % 2 == 0) {
194             headerY = y;
195         } else {
196             headerY = y - HEADER_VERTICAL_SHIFT;
197         }
198         return headerY;
199     }
200 
201     private void addCalls(Document doc, Activation activation) {
202         int firstRow = row;
203         addCall(doc, activation);
204         for (Activation nestedActivation : activation.getNestedActivations()) {
205             addCalls(doc, nestedActivation);
206         }
207         int lastRow = row;
208         addActivationBox(doc, activation, firstRow, lastRow);
209     }
210 
211     private void addCall(Document doc, Activation activation) {
212         MockObject sender = null;
213         if (activation.getParent() != null) {
214             sender = objectMap.get(activation.getParent().getClassName());
215         }
216         MockObject receiver = objectMap.get(activation.getClassName());
217         String methodName = activation.getMethod().name();
218         if (activation.getNumRepetitions() > 1) {
219             methodName =
220                     "*[" + activation.getNumRepetitions() + "] " + methodName;
221         }
222         if (sender == receiver) {
223             addSelfArrow(doc, sender, methodName);
224         } else {
225             addArrow(doc, sender, receiver, methodName);
226         }
227     }
228 
229     private void addSelfArrow(Document doc, MockObject sender, String methodName) {
230         int x1 =
231                 LIFE_LINE_LEFT_MARGIN + ACTIVATION_BOX_WIDTH / 2 +
232                         sender.getColumn() * COLUMN_WIDTH;
233         int y1 = ARROW_VERTICAL_MARGIN + (row + startRow) * ROW_HEIGHT;
234         int x2 = x1 + SELF_ARROW_WIDTH;
235         int y2 = y1;
236         int x3 = x2;
237         int y3 = y2 + SELF_ARROW_HEIGHT;
238         int x4 = x1;
239         int y4 = y3;
240         String points =
241                 x1 + "," + y1 + "," + x2 + "," + y2 + "," + x3 + "," + y3 +
242                         "," + x4 + "," + y4;
243         Element line = doc.createElementNS(SVG_NAMESPACE, "polyline");
244         line.setAttributeNS(null, "fill", "none");
245         line.setAttributeNS(null, "stroke", "black");
246         line.setAttributeNS(null, "points", points);
247         groupCalls.appendChild(line);
248 
249         addMethodName(doc, methodName, x1, y1, x2, y2);
250         addArrowHead(doc, x3, y3, x4, y4);
251 
252         row += 2;
253     }
254 
255     private void addArrow(Document doc, MockObject sender, MockObject receiver,
256             String methodName) {
257         int x1 =
258                 (sender == null ? INDENT_FIRST_ARROW : LIFE_LINE_LEFT_MARGIN +
259                         ACTIVATION_BOX_WIDTH / 2 + sender.getColumn() *
260                         COLUMN_WIDTH);
261         int y1 = ARROW_VERTICAL_MARGIN + (row + startRow) * ROW_HEIGHT;
262         int x2 =
263                 LIFE_LINE_LEFT_MARGIN - ACTIVATION_BOX_WIDTH / 2 +
264                         receiver.getColumn() * COLUMN_WIDTH;
265         int y2 = y1;
266         if (x1 > x2) {
267             x1 -= ACTIVATION_BOX_WIDTH;
268             x2 += ACTIVATION_BOX_WIDTH;
269         }
270         Element line = doc.createElementNS(SVG_NAMESPACE, "line");
271         line.setAttributeNS(null, "stroke", "black");
272         line.setAttributeNS(null, "x1", Integer.toString(x1));
273         line.setAttributeNS(null, "y1", Integer.toString(y1));
274         line.setAttributeNS(null, "x2", Integer.toString(x2));
275         line.setAttributeNS(null, "y2", Integer.toString(y2));
276         groupCalls.appendChild(line);
277 
278         addMethodName(doc, methodName, x1, y1, x2, y2);
279         addArrowHead(doc, x1, y1, x2, y2);
280 
281         row++;
282     }
283 
284     private void addMethodName(Document doc, String methodName, int x1, int y1,
285             int x2, int y2) {
286         Element text = doc.createElementNS(SVG_NAMESPACE, "text");
287         int x;
288         if (x1 < x2) {
289             x = x1 + ACTIVATION_BOX_WIDTH;
290         } else {
291             x = x1 - COLUMN_WIDTH + ACTIVATION_BOX_WIDTH * 2;
292         }
293         int y = y1 - METHOD_NAME_BOTTOM_MARGIN;
294         text.setAttributeNS(null, "x", Integer.toString(x));
295         text.setAttributeNS(null, "y", Integer.toString(y));
296         text.appendChild(doc.createTextNode(XMLUtil.makeXMLSafe(methodName)));
297         groupCalls.appendChild(text);
298     }
299 
300     private void addArrowHead(Document doc, int x1, int y1, int x2, int y2) {
301         Element line = doc.createElementNS(SVG_NAMESPACE, "polyline");
302         line.setAttributeNS(null, "fill", "none");
303         line.setAttributeNS(null, "stroke", "black");
304         String points;
305         if (x1 < x2) {
306             points =
307                     (x2 - ARROW_HEAD_WIDTH) + "," + (y2 - ARROW_HEAD_HEIGHT) +
308                             "," + x2 + "," + y2 + "," +
309                             (x2 - ARROW_HEAD_WIDTH) + "," +
310                             (y2 + ARROW_HEAD_HEIGHT);
311         } else {
312             points =
313                     (x2 + ARROW_HEAD_WIDTH) + "," + (y2 - ARROW_HEAD_HEIGHT) +
314                             "," + x2 + "," + y2 + "," +
315                             (x2 + ARROW_HEAD_WIDTH) + "," +
316                             (y2 + ARROW_HEAD_HEIGHT);
317         }
318         line.setAttributeNS(null, "points", points);
319         groupCalls.appendChild(line);
320     }
321 
322     private void addActivationBox(Document doc, Activation activation,
323             int firstRow, int lastRow) {
324         MockObject sender = null;
325         if (activation.getParent() != null) {
326             sender = objectMap.get(activation.getParent().getClassName());
327         }
328         MockObject receiver = objectMap.get(activation.getClassName());
329         if (sender != receiver) {
330             int x =
331                     LIFE_LINE_LEFT_MARGIN - ACTIVATION_BOX_WIDTH / 2 +
332                             receiver.getColumn() * COLUMN_WIDTH;
333             int y =
334                     LIFE_LINE_TOP_MARGIN + ACTIVATION_BOX_TOP_MARGIN +
335                             (startRow + firstRow) * ROW_HEIGHT;
336             int width = ACTIVATION_BOX_WIDTH;
337             int height =
338                     (lastRow - firstRow) * ROW_HEIGHT -
339                             ACTIVATION_BOX_BOTTOM_MARGIN;
340             Element rect = doc.createElementNS(SVG_NAMESPACE, "rect");
341             rect.setAttributeNS(null, "fill", "white");
342             rect.setAttributeNS(null, "stroke", "gray");
343             rect.setAttributeNS(null, "x", Integer.toString(x));
344             rect.setAttributeNS(null, "y", Integer.toString(y));
345             rect.setAttributeNS(null, "width", Integer.toString(width));
346             rect.setAttributeNS(null, "height", Integer.toString(height));
347             groupActivationBoxes.appendChild(rect);
348         }
349     }
350 
351     private void addLifelines(Document doc) {
352         for (int col = 0; col < objectMap.listView().size(); col++) {
353             int x1 = LIFE_LINE_LEFT_MARGIN + col * COLUMN_WIDTH;
354             int y1 = LIFE_LINE_TOP_MARGIN + startRow * ROW_HEIGHT;
355             int x2 = x1;
356             int y2 = LIFE_LINE_EXTRA_HEIGHT + (row + startRow) * ROW_HEIGHT;
357             Element line = doc.createElementNS(SVG_NAMESPACE, "line");
358             line.setAttributeNS(null, "stroke", "gray");
359             line.setAttributeNS(null, "stroke-dasharray", "10,5");
360             line.setAttributeNS(null, "x1", Integer.toString(x1));
361             line.setAttributeNS(null, "y1", Integer.toString(y1));
362             line.setAttributeNS(null, "x2", Integer.toString(x2));
363             line.setAttributeNS(null, "y2", Integer.toString(y2));
364             groupLifeLines.appendChild(line);
365             maxY = y2;
366         }
367     }
368 }