Make class diagrams in DIA automatically

There is usually the case that you need to draw a class diagram after having written the code. If you have only a few classes then it might be easier just to draw it by hand in DIA or a similar application. When you have a lot of classes, there are other options like using an IDE with reverse-engineering capabilities like NetBeans or using an excellent standalone utility that serves this purpose: UMLGraph. Using an IDE is an excellent choice if you can make it export your class diagram into a vector format, like PostScript. Using UMLGraph is an excellent option if you can afford the layout provided by Graphviz and its inability to do custom layout of its output.

For that reason, I’ve developed a small Python utility that reads specifications of class inheritance and outputs a DIA file, so that you can do the layout on your own (which is not always the best solution, but it works). The input specification should look like the following snippet:

Child1 -> Parent
Child2 -> Parent
Parent -> GrandParent
Alone
Sample class diagram

Sample class diagram

You can easily create such a specification by find, grep and sed on your source code files. For example, the following command creates the specification for a PHP project:

find -name "*.php" | xargs grep -h "^\s*class" | sed -e "s/.*class \(\w*\) \
extends \(\w*\).*/\1 -> \2/" -e "s/.*class \(\w*\).*/\1/" | sort -k3
#!/usr/bin/python

"""
Converts a specification of class inheritance into a DIA file. It reads from
standard input and writes to standard output. Input should look like:
Child1 -> Parent
Child2 -> Parent
Parent -> GrandParent
Alone

Example:
./make_dia_class_diagram.py < specification > class_diagram.dia
"""

import sys

file_start = '''<?xml version="1.0" encoding="UTF-8"?>
<dia:diagram xmlns:dia="http://www.lysator.liu.se/~alla/dia/">
  <dia:layer name="Background" visible="true" active="true">'''

file_stop = '''  </dia:layer>
</dia:diagram>'''

class_template = '''<dia:object type="UML - Class" version="0" id="O%(id)d">
  <dia:attribute name="name">
    <dia:string>#%(name)s#</dia:string>
  </dia:attribute>
  <dia:attribute name="visible_attributes">
    <dia:boolean val="false"/>
  </dia:attribute>
  <dia:attribute name="visible_operations">
    <dia:boolean val="false"/>
  </dia:attribute>
  <dia:attribute name="elem_corner">
    <dia:point val="%(left)d,%(top)d"/>
  </dia:attribute>
</dia:object>'''

relation_template = '''<dia:object type="UML - Generalization" version="1" id="O%(id)d">
  <dia:connections>
    <dia:connection handle="0" to="O%(parent)d" connection="6"/>
    <dia:connection handle="1" to="O%(child)d" connection="1"/>
  </dia:connections>
</dia:object>'''

def add_if_not_found(dict, key, value):
  """
  Add a key-value pair into dict if key does not already exists. Returns True
  if dict was modified, otherwise returns False.
  """

  if key not in dict:
    dict[key] = value
    return True
  return False

def get_depth(dict, field, key):
  """
  Traverses dict on field until it finds False. Starts from key. Returns the
  number of hops.
  """
  depth = 0
  while dict[key][field] != False:
    key = dict[key][field]
    depth += 1
  return depth

def make_class(parent, order):
  """Makes and returns a class object."""
  return {'parent':parent,'order':order}

def read_input():
  """
  Reads input of class inheritance definitions. Returns a dictionary of
  classes.
  """
  id = 0
  classes = {}
  lines = sys.stdin.readlines()
  for line in lines:
    line = line.strip()
    if len(line) == 0:
      continue
    relation = [x.strip() for x in line.split('->')]
    added = add_if_not_found(classes, relation[-1], make_class(False,id))
    if added:
      id += 1
    if len(relation) == 2:
      classes[relation[0]] = make_class(relation[1],id)
      id += 1
  return classes

def write_output(classes):
  """Writes a DIA file to the output."""
  map = {}
  id = 0
  print file_start
  for name in classes:
    params = {
      'id':id,
      'name':name,
      'left':5*classes[name]['order'],
      'top':5*get_depth(classes, 'parent', name)
    }
    print class_template % params
    map[name] = id
    id += 1
  for child in classes:
    if classes[child]['parent'] == False:
      continue
    params = {'id':id,'child':map[child],'parent':map[classes[child]['parent']]}
    print relation_template % params
    id += 1
  print file_stop

def main():
  write_output(read_input())

if __name__ == '__main__':
  main()

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: