The Architecture of Open Source Applications

Violet

Cay Horstmann

In 2002, I wrote an undergraduate textbook on object-oriented design and patterns [Hor05]. As with so many books, this one was motivated by frustration with the canonical curriculum. Frequently, computer science students learn how to design a single class in their first programming course, and then have no further training in object-oriented design until their senior level software engineering course. In that course, students rush through a couple of weeks of UML and design patterns, which gives no more than an illusion of knowledge. My book supports a semester-long course for students with a background in Java programming and basic data structures (typically from a Java-based CS1/CS2 sequence). The book covers object-oriented design principles and design patterns in the context of familiar situations. For example, the Decorator design pattern is introduced with a Swing JScrollPane, in the hope that this example is more memorable than the canonical Java streams example.

[A Violet Object Diagram]

Figure 22.1: A Violet Object Diagram

I needed a light subset of UML for the book: class diagrams, sequence diagrams, and a variant of object diagrams that shows Java object references (Figure 22.1). I also wanted students to draw their own diagrams. However, commercial offerings such as Rational Rose were not only expensive but also cumbersome to learn and use [Shu05], and the open source alternatives available at the time were too limited or buggy to be useful1, in which diagrams are specified by textual declarations rather than the more common point-and-click interface.}. In particular, sequence diagrams in ArgoUML were seriously broken.

I decided to try my hand at implementing the simplest editor that is (a) useful to students and (b) an example of an extensible framework that students can understand and modify. Thus, Violet was born.

22.1. Introducing Violet

Violet is a lightweight UML editor, intended for students, teachers, and authors who need to produce simple UML diagrams quickly. It is very easy to learn and use. It draws class, sequence, state, object and use-case diagrams. (Other diagram types have since been contributed.) It is open-source and cross-platform software. In its core, Violet uses a simple but flexible graph framework that takes full advantage of the Java 2D graphics API.

The Violet user interface is purposefully simple. You don't have to go through a tedious sequence of dialogs to enter attributes and methods. Instead, you just type them into a text field. With a few mouse clicks, you can quickly create attractive and useful diagrams.

Violet does not try to be an industrial-strength UML program. Here are some features that Violet does not have:

  • Violet does not generate source code from UML diagrams or UML diagrams from source code.
  • Violet does not carry out any semantic checking of models; you can use Violet to draw contradictory diagrams.
  • Violet does not generate files that can be imported into other UML tools, nor can it read model files from other tools.
  • Violet does not attempt to lay out diagrams automatically, except for a simple "snap to grid" facility.

(Attempting to address some of these limitations makes good student projects.)

When Violet developed a cult following of designers who wanted something more than a cocktail napkin but less than an industrial-strength UML tool, I published the code on SourceForge under the GNU General Public License. Starting in 2005, Alexandre de Pellegrin joined the project by providing an Eclipse plugin and a prettier user interface. He has since made numerous architectural changes and is now the primary maintainer of the project.

In this article, I discuss some of the original architectural choices in Violet as well as its evolution. A part of the article is focused on graph editing, but other parts—such as the use of JavaBeans properties and persistence, Java WebStart and plugin architecture—should be of general interest.

22.2. The Graph Framework

Violet is based on a general graph editing framework that can render and edit nodes and edges of arbitrary shapes. The Violet UML editor has nodes for classes, objects, activation bars (in sequence diagrams), and so on, and edges for the various edge shapes in UML diagrams. Another instance of the graph framework might display entity-relationship diagrams or railroad diagrams.

[A Simple Instance of the Editor Framework]

Figure 22.2: A Simple Instance of the Editor Framework

In order to illustrate the framework, let us consider an editor for very simple graphs, with black and white circular nodes and straight edges (Figure 22.2). The SimpleGraph class specifies prototype objects for the node and edge types, illustrating the prototype pattern:

public class SimpleGraph extends AbstractGraph
{
  public Node[] getNodePrototypes()
  {
    return new Node[]
    {
      new CircleNode(Color.BLACK),
      new CircleNode(Color.WHITE)
    };
  }
  public Edge[] getEdgePrototypes()
  {
    return new Edge[]
    {
      new LineEdge()
    };
  }
}

Prototype objects are used to draw the node and edge buttons at the top of Figure 22.2. They are cloned whenever the user adds a new node or edge instance to the graph. Node and Edge are interfaces with the following key methods:

  • Both interfaces have a getShape method that returns a Java2D Shape object of the node or edge shape.
  • The Edge interface has methods that yield the nodes at the start and end of the edge.
  • The getConnectionPoint method in the Node interface type computes an optimal attachment point on the boundary of a node (see Figure 22.3).
  • The getConnectionPoints method of the Edge interface yields the two end points of the edge. This method is needed to draw the "grabbers" that mark the currently selected edge.
  • A node can have children that move together with the parent. A number of methods are provided for enumerating and managing children.
[Finding a Connection Point on the Boundary of the Node Shape]

Figure 22.3: Finding a Connection Point on the Boundary of the Node Shape

Convenience classes AbstractNode and AbstractEdge implement a number of these methods, and classes RectangularNode and SegmentedLineEdge provide complete implementations of rectangular nodes with a title string and edges that are made up of line segments.

In the case of our simple graph editor, we would need to supply subclasses CircleNode and LineEdge that provide a draw method, a contains method, and the getConnectionPoint method that describes the shape of the node boundary. The code is given below, and Figure 22.4 shows a class diagram of these classes (drawn, of course, with Violet).

public class CircleNode extends AbstractNode
{
  public CircleNode(Color aColor)
  {
    size = DEFAULT_SIZE;
    x = 0;
    y = 0;
    color = aColor;
  }

  public void draw(Graphics2D g2)
  {
    Ellipse2D circle = new Ellipse2D.Double(x, y, size, size);
    Color oldColor = g2.getColor();
    g2.setColor(color);
    g2.fill(circle);
    g2.setColor(oldColor);
    g2.draw(circle);
  }

  public boolean contains(Point2D p)
  {
    Ellipse2D circle = new Ellipse2D.Double(x, y, size, size);
    return circle.contains(p);
  }

  public Point2D getConnectionPoint(Point2D other)
  {
    double centerX = x + size / 2;
    double centerY = y + size / 2;
    double dx = other.getX() - centerX;
    double dy = other.getY() - centerY;
    double distance = Math.sqrt(dx * dx + dy * dy);
    if (distance == 0) return other;
    else return new Point2D.Double(
      centerX + dx * (size / 2) / distance,
      centerY + dy * (size / 2) / distance);
  }

  private double x, y, size, color;
  private static final int DEFAULT_SIZE = 20;
}

public class LineEdge extends AbstractEdge
{
  public void draw(Graphics2D g2)
  { g2.draw(getConnectionPoints()); }

  public boolean contains(Point2D aPoint)
  {
    final double MAX_DIST = 2;
    return getConnectionPoints().ptSegDist(aPoint) < MAX_DIST;
  }
}
[Class Diagram for a Simple Graph]

Figure 22.4: Class Diagram for a Simple Graph

In summary, Violet provides a simple framework for producing graph editors. To obtain an editor instance, define node and edge classes and provide methods in a graph class that yield prototype node and edge objects.

Of course, there are other graph frameworks available, such as JGraph [Ald02] and JUNG2. However, those frameworks are considerably more complex, and they provide frameworks for drawing graphs, not for applications that draw graphs.

22.3. Use of JavaBeans Properties

In the golden days of client-side Java, the JavaBeans specification was developed in order to provide portable mechanisms for editing GUI components in visual GUI builder environments. The vision was that a third-party GUI component could be dropped into any GUI builder, where its properties could be configured in the same way as the standard buttons, text components, and so on.

Java does not have native properties. Instead, JavaBeans properties can be discovered as pairs of getter and setter methods, or specified with companion BeanInfo classes. Moreover, property editors can be specified for visually editing property values. The JDK even contains a few basic property editors, for example for the type java.awt.Color.

The Violet framework makes full use of the JavaBeans specification. For example, the CircleNode class can expose a color property simply by providing two methods:

public void setColor(Color newValue)
public Color getColor()

No further work is necessary. The graph editor can now edit node colors of circle nodes (Figure 22.5).

[Editing Circle Colors with the default JavaBeans Color Editor]

Figure 22.5: Editing Circle Colors with the default JavaBeans Color Editor

22.4. Long-Term Persistence

Just like any editor program, Violet must save the user's creations in a file and reload them later. I had a look at the XMI specification3 which was designed as a common interchange format for UML models. I found it cumbersome, confusing, and hard to consume. I don't think I was the only one—XMI had a reputation for poor interoperability even with the simplest models [PGL+05].

I considered simply using Java serialization, but it is difficult to read old versions of a serialized object whose implementation has changed over time. This problem was also anticipated by the JavaBeans architects, who developed a standard XML format for long-term persistence4. A Java object—in the case of Violet, the UML diagram—is serialized as a sequence of statements for constructing and modifying it. Here is an example:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.0" class="java.beans.XMLDecoder">
 <object class="com.horstmann.violet.ClassDiagramGraph">
  <void method="addNode">
   <object id="ClassNode0" class="com.horstmann.violet.ClassNode">
    <void property="name">…</void>
   </object>
   <object class="java.awt.geom.Point2D$Double">
    <double>200.0</double>
    <double>60.0</double>
   </object>
  </void>
  <void method="addNode">
   <object id="ClassNode1" class="com.horstmann.violet.ClassNode">
    <void property="name">…</void>
   </object>
   <object class="java.awt.geom.Point2D$Double">
    <double>200.0</double>
    <double>210.0</double>
   </object>
  </void>
  <void method="connect">
   <object class="com.horstmann.violet.ClassRelationshipEdge">
    <void property="endArrowHead">
     <object class="com.horstmann.violet.ArrowHead" field="TRIANGLE"/>
    </void>
   </object>
   <object idref="ClassNode0"/>
   <object idref="ClassNode1"/>
  </void>
 </object>
</java>

When the XMLDecoder class reads this file, it executes these statements (package names are omitted for simplicity).

ClassDiagramGraph obj1 = new ClassDiagramGraph();
ClassNode ClassNode0 = new ClassNode();
ClassNode0.setName(…);
obj1.addNode(ClassNode0, new Point2D.Double(200, 60));
ClassNode ClassNode1 = new ClassNode();
ClassNode1.setName(…);
obj1.addNode(ClassNode1, new Point2D.Double(200, 60));
ClassRelationShipEdge obj2 = new ClassRelationShipEdge();
obj2.setEndArrowHead(ArrowHead.TRIANGLE);
obj1.connect(obj2, ClassNode0, ClassNode1);

As long as the semantics of the constructors, properties, and methods has not changed, a newer version of the program can read a file that has been produced by an older version.

Producing such files is quite straightforward. The encoder automatically enumerates the properties of each object and writes setter statements for those property values that differ from the default. Most basic datatypes are handled by the Java platform; however, I had to supply special handlers for Point2D, Line2D, and Rectangle2D. Most importantly, the encoder must know that a graph can be serialized as a sequence of addNode and connect method calls:

encoder.setPersistenceDelegate(Graph.class, new DefaultPersistenceDelegate()
{
  protected void initialize(Class<?> type, Object oldInstance,
    Object newInstance, Encoder out)
  {
    super.initialize(type, oldInstance, newInstance, out);
    AbstractGraph g = (AbstractGraph) oldInstance;
    for (Node n : g.getNodes())
      out.writeStatement(new Statement(oldInstance, "addNode", new Object[]
      {
        n,
        n.getLocation()
      }));
    for (Edge e : g.getEdges())
      out.writeStatement(new Statement(oldInstance, "connect", new Object[]
      {
        e, e.getStart(), e.getEnd()
      }));
   }
 });

Once the encoder has been configured, saving a graph is as simple as:

encoder.writeObject(graph);

Since the decoder simply executes statements, it requires no configuration. Graphs are simply read with:

Graph graph = (Graph) decoder.readObject();

This approach has worked exceedingly well over numerous versions of Violet, with one exception. A recent refactoring changed some package names and thereby broke backwards compatibility. One option would have been to keep the classes in the original packages, even though they no longer matched the new package structure. Instead, the maintainer provided an XML transformer for rewriting the package names when reading a legacy file.

22.5. Java WebStart

Java WebStart is a technology for launching an application from a web browser. The deployer posts a JNLP file that triggers a helper application in the browser which downloads and runs the Java program. The application can be digitally signed, in which case the user must accept the certificate, or it can be unsigned, in which case the program runs in a sandbox that is slightly more permissive than the applet sandbox.

I do not think that end users can or should be trusted to judge the validity of a digital certificate and its security implications. One of the strengths of the Java platform is its security, and I feel it is important to play to that strength.

The Java WebStart sandbox is sufficiently powerful to enable users to carry out useful work, including loading and saving files and printing. These operations are handled securely and conveniently from the user perspective. The user is alerted that the application wants to access the local filesystem and then chooses the file to be read or written. The application merely receives a stream object, without having an opportunity to peek at the filesystem during the file selection process.

It is annoying that the developer must write custom code to interact with a FileOpenService and a FileSaveService when the application is running under WebStart, and it is even more annoying that there is no WebStart API call to find out whether the application was launched by WebStart.

Similarly, saving user preferences must be implemented in two ways: using the Java preferences API when the application runs normally, or using the WebStart preferences service when the application is under WebStart. Printing, on the other hand, is entirely transparent to the application programmer.

Violet provides simple abstraction layers over these services to simplify the lot of the application programmer. For example, here is how to open a file:

FileService service = FileService.getInstance(initialDirectory);
  // detects whether we run under WebStart
FileService.Open open = fileService.open(defaultDirectory, defaultName,
  extensionFilter);
InputStream in = open.getInputStream();
String title = open.getName();

The FileService.Open interface is implemented by two classes: a wrapper over JFileChooser or the JNLP FileOpenService.

No such convenience is a part of the JNLP API itself, but that API has received little love over its lifetime and has been widely ignored. Most projects simply use a self-signed certificate for their WebStart application, which gives users no security. This is a shame—open source developers should embrace the JNLP sandbox as a risk-free way to try out a project.

22.6. Java 2D

Violet makes intensive use of the Java2D library, one of the lesser known gems in the Java API. Every node and edge has a method getShape that yields a java.awt.Shape, the common interface of all Java2D shapes. This interface is implemented by rectangles, circles, paths, and their unions, intersections, and differences. The GeneralPath class is useful for making shapes that are composed of arbitrary line and quadratic/cubic curve segments, such as straight and curved arrows.

To appreciate the flexibility of the Java2D API, consider the following code for drawing a shadow in the AbstractNode.draw method:

Shape shape = getShape();
if (shape == null) return;
g2.translate(SHADOW_GAP, SHADOW_GAP);
g2.setColor(SHADOW_COLOR);
g2.fill(shape);
g2.translate(-SHADOW_GAP, -SHADOW_GAP);
g2.setColor(BACKGROUND_COLOR);
g2.fill(shape);

A few lines of code produce a shadow for any shape, even shapes that a developer may add at a later point.

Of course, Violet saves bitmap images in any format that the javax.imageio package supports; that is, GIF, PNG, JPEG, and so on. When my publisher asked me for vector images, I noted another advantage of the Java 2D library. When you print to a PostScript printer, the Java2D operations are translated into PostScript vector drawing operations. If you print to a file, the result can be consumed by a program such as ps2eps and then imported into Adobe Illustrator or Inkscape. Here is the code, where comp is the Swing component whose paintComponent method paints the graph:

DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
String mimeType = "application/postscript";
StreamPrintServiceFactory[] factories;
StreamPrintServiceFactory.lookupStreamPrintServiceFactories(flavor, mimeType);
FileOutputStream out = new FileOutputStream(fileName);
PrintService service = factories[0].getPrintService(out);
SimpleDoc doc = new SimpleDoc(new Printable() {
  public int print(Graphics g, PageFormat pf, int page) {
      if (page >= 1) return Printable.NO_SUCH_PAGE;
      else {
        double sf1 = pf.getImageableWidth() / (comp.getWidth() + 1);
        double sf2 = pf.getImageableHeight() / (comp.getHeight() + 1);
        double s = Math.min(sf1, sf2);
        Graphics2D g2 = (Graphics2D) g;
        g2.translate((pf.getWidth() - pf.getImageableWidth()) / 2,
            (pf.getHeight() - pf.getImageableHeight()) / 2);
        g2.scale(s, s);

        comp.paint(g);
        return Printable.PAGE_EXISTS;
      }
  }
}, flavor, null);
DocPrintJob job = service.createPrintJob();
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
job.print(doc, attributes);

At the beginning, I was concerned that there might be a performance penalty when using general shapes, but that has proven not to be the case. Clipping works well enough that only those shape operations that are required for updating the current viewport are actually executed.

22.7. No Swing Application Framework

Most GUI frameworks have some notion of an application that manages a set of documents that deals with menus, toolbars, status bars, etc. However, this was never a part of the Java API. JSR 2965 was supposed to supply a basic framework for Swing applications, but it is currently inactive. Thus, a Swing application author has two choices: reinvent a good number of wheels or base itself on a third party framework. At the time that Violet was written, the primary choices for an application framework were the Eclipse and NetBeans platform, both of which seemed too heavyweight at the time. (Nowadays, there are more choices, among them JSR 296 forks such as GUTS6.) Thus, Violet had to reinvent mechanisms for handling menus and internal frames.

In Violet, you specify menu items in property files, like this:

file.save.text=Save
file.save.mnemonic=S
file.save.accelerator=ctrl S
file.save.icon=/icons/16x16/save.png

A utility method creates the menu item from the prefix (here file.save). The suffixes .text, .mnemonic, and so on, are what nowadays would be called "convention over configuration". Using resource files for describing these settings is obviously far superior to setting up menus with API calls because it allows for easy localization. I reused that mechanism in another open source project, the GridWorld environment for high school computer science education7.

An application such as Violet allows users to open multiple "documents", each containing a graph. When Violet was first written, the multiple document interface (MDI) was still commonly used. With MDI, the main frame has a menu bar, and each view of a document is displayed in an internal frame with a title but no menu bar. Each internal frame is contained in the main frame and can be resized or minimized by the user. There are operations for cascading and tiling windows.

Many developers disliked MDI, and so this style user interface has gone out of fashion. For a while, a single document interface (SDI), in which an application displays multiple top level frames, was considered superior, presumably because those frames can be manipulated with the standard window management tools of the host operating system. When it became clear that having lots of top level windows isn't so great after all, tabbed interfaces started to appear, in which multiple documents are again contained in a single frame, but now all displayed at full size and selectable with tabs. This does not allow users to compare two documents side by side, but seems to have won out.

The original version of Violet used an MDI interface. The Java API has a internal frames feature, but I had to add support for tiling and cascading. Alexandre switched to a tabbed interface, which is somewhat better-supported by the Java API. It would be desirable to have an application framework where the document display policy was transparent to the developer and perhaps selectable by the user.

Alexandre also added support for sidebars, a status bar, a welcome panel, and a splash screen. All this should ideally be a part of a Swing application framework.

22.8. Undo/Redo

Implementing multiple undo/redo seems like a daunting task, but the Swing undo package ([Top00], Chapter 9) gives good architectural guidance. An UndoManager manages a stack of UndoableEdit objects. Each of them has an undo method that undoes the effect of the edit operation, and a redo method that undoes the undo (that is, carries out the original edit operation). A CompoundEdit is a sequence of UndoableEdit operations that should be undone or redone in their entirety. You are encouraged to define small, atomic edit operations (such as adding or removing a single edge or node in the case of a graph) that are grouped into compound edits as necessary.

A challenge is to define a small set of atomic operations, each of which can be undone easily. In Violet, they are:

  • adding or removing a node or edge
  • attaching or detaching a node's child
  • moving a node
  • changing a property of a node or edge

Each of these operations has an obvious undo. For example the undo of adding a node is the node's removal. The undo of moving a node is to move it by the opposite vector.

[An Undo Operation Must Undo Structural Changes in the Model]

Figure 22.6: An Undo Operation Must Undo Structural Changes in the Model

Note that these atomic operations are not the same as the actions in the user interface or the methods of the Graph interface that the user interface actions invoke. For example, consider the sequence diagram in Figure 22.6, and suppose the user drags the mouse from the activation bar to the lifeline on the right. When the mouse button is released, the method:

public boolean addEdgeAtPoints(Edge e, Point2D p1, Point2D p2)

is invoked. That method adds an edge, but it may also carry out other operations, as specified by the participating Edge and Node subclasses. In this case, an activation bar will be added to the lifeline on the right. Undoing the operation needs to remove that activation bar as well. Thus, the model (in our case, the graph) needs to record the structural changes that need to be undone. It is not enough to collect controller operations.

As envisioned by the Swing undo package, the graph, node, and edge classes should send UndoableEditEvent notifications to an UndoManager whenever a structural edit occurs. Violet has a more general design where the graph itself manages listeners for the following interface:

public interface GraphModificationListener
{
  void nodeAdded(Graph g, Node n);
  void nodeRemoved(Graph g, Node n);
  void nodeMoved(Graph g, Node n, double dx, double dy);
  void childAttached(Graph g, int index, Node p, Node c);
  void childDetached(Graph g, int index, Node p, Node c);
  void edgeAdded(Graph g, Edge e);
  void edgeRemoved(Graph g, Edge e);
  void propertyChangedOnNodeOrEdge(Graph g, PropertyChangeEvent event);
}

The framework installs a listener into each graph that is a bridge to the undo manager. For supporting undo, adding generic listener support to the model is overdesigned—the graph operations could directly interact with the undo manager. However, I also wanted to support an experimental collaborative editing feature.

If you want to support undo/redo in your application, think carefully about the atomic operations in your model (and not your user interface). In the model, fire events when a structural change happens, and allow the Swing undo manager to collect and group these events.

22.9. Plugin Architecture

For a programmer familiar with 2D graphics, it is not difficult to add a new diagram type to Violet. For example, the activity diagrams were contributed by a third party. When I needed to create railroad diagrams and ER diagrams, I found it faster to write Violet extensions instead of fussing with Visio or Dia. (Each diagram type took a day to implement.)

These implementations do not require knowledge of the full Violet framework. Only the graph, node, and edge interfaces and convenience implementations are needed. In order to make it easier for contributors to decouple themselves from the evolution of the framework, I designed a simple plugin architecture.

Of course, many programs have a plugin architecture, many quite elaborate. When someone suggested that Violet should support OSGi, I shuddered and instead implemented the simplest thing that works.

Contributors simply produce a JAR file with their graph, node, and edge implementations and drop it into a plugins directory. When Violet starts, it loads those plugins, using the Java ServiceLoader class. That class was designed to load services such as JDBC drivers. A ServiceLoader loads JAR files that promise to provide a class implementing a given interface (in our case, the Graph interface.)

Each JAR file must have a subdirectory META-INF/services containing a file whose name is the fully qualified classname of the interface (such as com.horstmann.violet.Graph), and that contains the names of all implementing classes, one per line. The ServiceLoader constructs a class loader for the plugin directory, and loads all plugins:

ServiceLoader<Graph> graphLoader = ServiceLoader.load(Graph.class, classLoader);
for (Graph g : graphLoader) // ServiceLoader<Graph> implements Iterable<Graph>
  registerGraph(g);

This is a simple but useful facility of standard Java that you might find valuable for your own projects.

22.10. Conclusion

Like so many open source projects, Violet was born of an unmet need—to draw simple UML diagrams with a minimum of fuss. Violet was made possible by the amazing breadth of the Java SE platform, and it draws from a diverse set of technologies that are a part of that platform. In this article, I described how Violet makes use of Java Beans, Long-Term Persistence, Java Web Start, Java 2D, Swing Undo/Redo, and the service loader facility. These technologies are not always as well understood as the basics of Java and Swing, but they can greatly simplify the architecture of a desktop application. They allowed me, as the initial sole developer, to produce a successful application in a few months of part-time work. Relying on these standard mechanisms also made it easier for others to improve on Violet and to extract pieces of it into their own projects.

Footnotes

  1. At the time, I was not aware of Diomidis Spinellis' admirable UMLGraph program [Spi03]
  2. http://jung.sourceforge.net
  3. http://www.omg.org/technology/documents/formal/xmi.htm
  4. http://jcp.org/en/jsr/detail?id=57
  5. http://jcp.org/en/jsr/detail?id=296
  6. http://kenai.com/projects/guts
  7. http://horstmann.com/gridworld