Monday, 14 January 2013

Using Quick Sequence Diagram Editor (sdedit) in your Delphi applications

My first article of the year is about Quick Sequence Diagram Editor (sdedit), a tool for creating UML sequence diagrams from textual descriptions of objects and messages that follow a really easy syntax. The ones who follow my blog will realise that I love logging details for my applications and with this component you will be able to create a nice sequence diagram from any of your real business scenarios. I have been dealing lately with loads of different multi-threaded applications and it was when doing some of my homework for Uni when I found this marvellous gem.
I have written a small and quite straight-forward wrapper in Delphi which will help you to generate the ".sdx" file and run the converter calling the library via java command. All the source code and examples can be found in my personal repository on Google code: thundax-delphi-personal-repository where I keep all my tests and things I want to try out.
With few easy callings, you can instantiate the TUML class and use "start" and "call" methods to generate things like this:

More complex example:


It is really useful when doing reports or to display any of your business scenarios in a nice way. One of my tasks was to create a small application which would send messages across different instances in a supercomputer and this is the best way I found to really present what was going on in there. You can generate huge diagrams and sdedit will cope with it.

Here is the piece of code for the wrapper:
type
  TUML = class(TObject)
  private
    FFormat: string;
    FOutFile: TLog;
    FUmlFileName: string;
  public
    property Format: string read FFormat write FFormat;
    constructor Create(umlFileName: string);
    class function Start(umlFileName: string): TUML;
    procedure Define(name : String; kind : String); overload;
    procedure Define(); overload;
    procedure Call(caller: string; callee: string; method: string; params: string); overload;
    procedure Call(caller: string; callee: string; method: string; params: string; result: string); overload;
    procedure Convert();
  end;

const
  DefaultFormat = 'jpg';
  DefaultLocationLibrary = '..\..\thirdpartylibs\sdedit\sdedit.jar';

implementation

uses
  ShellAPI, Windows;

{ TUML }

procedure TUML.Call(caller, callee, method, params: string);
begin
  FOutFile.print(caller + ':' + callee + '.' + method + '(' + params + ')');
end;

procedure TUML.Call(caller, callee, method, params, result: string);
begin
  FOutFile.print(caller + ':' + result + '=' + callee + '.' + method + '(' + params + ')');
end;

procedure TUML.Convert();
var
  params: string;
  FileINFormat: string;
  FileOutFormat: string;
begin
  FileINFormat := FUmlFileName + '.sdx';
  FileOutFormat := FUmlFileName + '.' + FFormat;
  params := '-jar ' + DefaultLocationLibrary + ' -o ' + FileOutFormat + ' -t ' + FFormat + ' -r landscape ' + FileINFormat;
  ShellExecute(0, nil, 'java.exe', PChar(params), nil, SW_HIDE);
end;

constructor TUML.Create(umlFileName: string);
begin
  FOutFile := TLog.Start(umlFileName + '.sdx');
  FFormat := DefaultFormat;
  FUmlFileName := umlFileName;
end;

procedure TUML.Define;
begin
  FOutFile.print('');
end;

procedure TUML.Define(name, kind: String);
begin
  FOutFile.print(name + ':' + kind);
end;

class function TUML.Start(umlFileName: string): TUML;
begin
  result := Create(umlFileName);
end;
And this is a more realistic approach, where we can have for example 3 different threads which will perform different operations at a different times. All of them are asynchronous and they use the TUML class to show how long every thread took to finish its processing.

Example:
procedure TestTUML.TestUMLDiagramAsynchronous;
var
  lock: TCriticalSection;
  thread1, thread2, thread3: TThread;
  sw : TStopWatch;
begin
  if fileExists(FUmlFileName + '.sdx') then
    DeleteFile(PChar(FUmlFileName + '.sdx'));

  if fileExists(FUmlFileName + '.jpg') then
    DeleteFile(PChar(FUmlFileName + '.jpg'));

  lock := TCriticalSection.Create;
  FUML.Define('Application', 'TApplication');
  FUML.Define('MainThread', 'TThread');
  FUML.Define('thread1', 'TThread');
  FUML.Define('thread2', 'TThread');
  FUML.Define('thread3', 'TThread');
  FUML.Define();

  FUML.Call('Application','MainThread','Start','');

  thread1 := TThread.CreateAnonymousThread( procedure
  var
    b : Boolean;
    i : integer;
    j : Integer;
  begin
    thread1.NameThreadForDebugging('thread1a');
    b := True;
    i := 0;
    lock.Acquire;
    sw := TStopWatch.StartNew;
    j := 10 + Random(300);
    while b do
    begin
      inc(i);
      b:= (i < j);
      sleep(10);
    end;
    FUML.Call('MainThread','thread1','CreateAnonymousThread',IntToStr(j), 'DONE(' + IntToStr(sw.ElapsedMilliseconds) + 'ms)');
    lock.Release;
  end);
  thread1.FreeOnTerminate := false;
  thread1.Start;

  thread2 := TThread.CreateAnonymousThread( procedure
  var
    b : Boolean;
    i : integer;
    j : Integer;
  begin
    thread2.NameThreadForDebugging('thread2a');
    b := True;
    i := 0;
    lock.Acquire;
    sw := TStopWatch.StartNew;
    j := 10 + Random(300);
    while b do
    begin
      inc(i);
      b:= (i < j);
      sleep(10);
    end;
    FUML.Call('MainThread','thread2','CreateAnonymousThread',IntToStr(j), 'DONE(' + IntToStr(sw.ElapsedMilliseconds) + 'ms)');
    lock.Release;
  end);
  thread2.FreeOnTerminate := false;
  thread2.Start;

  thread3 := TThread.CreateAnonymousThread( procedure
  var
    b : Boolean;
    i : integer;
    j : Integer;
  begin
    thread3.NameThreadForDebugging('thread3a');
    b := True;
    i := 0;
    lock.Acquire;
    sw := TStopWatch.StartNew;
    j := 10 + Random(300);
    while b do
    begin
      inc(i);
      b:= (i < j);
      sleep(10);
    end;
    FUML.Call('MainThread','thread3','CreateAnonymousThread',IntToStr(j), 'DONE(' + IntToStr(sw.ElapsedMilliseconds) + 'ms)');
    lock.Release;
  end);

  thread3.FreeOnTerminate := false;
  thread3.Start;

  thread1.WaitFor;
  thread2.WaitFor;
  thread3.WaitFor;
  thread1.Terminate;
  thread1.Free;
  thread2.Terminate;
  thread2.Free;
  thread3.Terminate;
  thread3.Free;

  FUML.Convert;
  Sleep(2000); 
  CheckTrue(FileExists(FUmlFileName + '.jpg'), 'Error, File was not created');
end;

A more realistic approach (Output):

I hope you find this article interesting enough to catch you attention as it is always good when finding tools like this an that are easy enough to integrate with any language and with great results.

Don't forget to leave your comment.

Jordi Corbilla

4 comments:

  1. Great stuff and I plan on implementing a test case right now. Fits perfectly with my current project!!!
    Thanks

    ReplyDelete
  2. Could you please publish the .sd-File of the last diagram? Would be helpful, since sdedit-example-files are rare. Thank you!

    ReplyDelete
    Replies
    1. Hi,
      Check out the source code in my google code page. There you will find the sd files. You can run the code and see what gets generated.
      Jordi

      Delete