Click here to Skip to main content
Click here to Skip to main content

Codeuml - design UML diagrams as fast as you can code

By , 4 Jun 2012
Rate this:
Please Sign up or sign in to vote.

Introduction

Codeuml is a web based UML designer where you code the diagram using a special language and it generates the diagram on the fly. It is faster than using any visual designer where you have to drag & drop diagram elements and use mouse to connect them. Codeuml uses the open source plantuml engine to produce diagram from text. You can produce UML diagrams as fast as you can code.

This web application shows some interesting design and coding challenges. First, it shows you how to build a web based IDE like environment that mimics Windows 8 Metro UI. Second it shows how you can periodically collect data from the website, send to the server in the background asynchronously and get the result generated on the fly. Third and the most important, it shows how you can maintain a server side pool of very expensive resource that you cannot just create on every hit to the server and must have a finite pool that is shared by all your web users.

Get the code

The live site is available at: www.codeuml.com 

Get the code from:  http://code.google.com/p/codeuml/ 

Building the front-end 

The UI is inspired by the metro UI in Windows 8 and it is tablet friendly. You can easily touch the buttons on a tablet. The 3 column resizable panels are built using jQuery Splitter plugin. The text editor is the awesome CodeMirror text editor that has a horrible logo. The ticker is provided by jQuery New Ticker plugin.

The 3 column view is built using the following html:

<div id="MySplitter">
        <div class="SplitterPane unselectable">
            <div id="umlsnippets">
.
.
.
            </div>
        </div>
        <div id="CenterAndRight">
            <div class="SplitterPane">
                <img src="img/ajax-loader.gif" id="ProgressIndicator" />
                <textarea id="umltext" rows="10" cols="40"></textarea>
            </div>
            <div class="SplitterPane">
                <div id="umlimage_container">
                    <img id="umlimage" src="img/defaultdiagram.png" />
                    <div id="ticker">
                        News ticker
                    </div>
                </div>             
            </div>
        </div>
    </div>
First it divides the screen into 2 parts – the left side UML snippet bar and the right side that has the editor and the image. Then it divides the right side into further two parts – the text editor and the diagram image. The following javascript initializes the splitter:
// Main vertical splitter, anchored to the browser window
$("#MySplitter").splitter({
    type: "v",
    outline: true,
    minLeft: 60, sizeLeft: 100, maxLeft: 250,
    anchorToWindow: true,
    resizeOnWindow: true,
    accessKey: "L"
});
// Second vertical splitter, nested in the right pane of the main one.
$("#CenterAndRight").splitter({
    type: "v",
    outline: true,
    minRight: 200, sizeRight: ($(window).width() * 0.6), maxRight: ($(window).width() * 0.9),
    accessKey: "R"
});
$(window).resize(function () {
    $("#MySplitter").trigger("resize");
});
Next it initializes the CodeMirror code editor over the textarea and makes it the awesome text editor.
myCodeMirror = CodeMirror.fromTextArea($('#umltext').get(0),
            {
                onChange: refreshDiagram
            });
myCodeMirror.focus();
myCodeMirror.setCursor({ line: myCodeMirror.lineCount() + 1, ch: 1 });
Then it initializes the left side UML snippet bar. Each button has an associated uml text which is injected to the text editor when clicked. An example of a button:
<div id="scrollable">
	<!-- Sequence diagram -->
	<h2>
		Sequence
	</h2>
	<div class="sequence_diagram">
		<div class="button">
			<div class="icon">
				A&rarr;B</div>
			<div class="title">
				Sync Msg</div>
			<pre class="umlsnippet">A -> B: Sync Message</pre>
		</div>
	</div>

The text that is injected to the text editor is inside the <pre> tag.

You can create as many buttons as you like and just put the uml snippet that needs to be inserted in the <pre> tag with class umlsnippet.

When such buttons are clicked, the following javascript injects the code inside <pre> into the text editor.

$("#umlsnippets").find(".button").click(function () {
	var diagramType = $(this).parent().attr("class");

	if (lastUmlDiagram !== diagramType) {
		if (!confirm("The current diagram will be cleared? Do you want to continue?"))
			return;

		myCodeMirror.setValue("");
	}

	changeDiagramType(diagramType);

	var umlsnippet = $(this).find("pre.umlsnippet").text();
	
	var pos = myCodeMirror.getCursor(true);

	// When replaceRange or replaceSelection is called
	// to insert text, in IE 8, the code editor gets 
	// screwed up. So, it needs to be recreated after this.
	myCodeMirror.replaceRange(umlsnippet, myCodeMirror.getCursor(true));

	// recreate the code editor to fix screw up in IE 7/8
	myCodeMirror.toTextArea();
	myCodeMirror = CodeMirror.fromTextArea($('#umltext').get(0),
	{
		onChange: refreshDiagram
	});

	myCodeMirror.focus();
	myCodeMirror.setCursor(pos);

	refreshDiagram();
});

One tricky thing here is that if I inject text calling replaceRange, the CodeMirror editor stops working. It had to be recreated to make it work again.

Generating diagram as you type

Refreshing the diagram as you type is the most challenging part. The following javascript function gets fired as soon as something changes on the text editor. However, it makes sure it sends the UML to the server only once every second. So, even if you keep typing continuously, it will only send the UML to the server once per second.

function refreshDiagram() {

  if (lastTimer == null) {
    
    lastTimer = window.setTimeout(function () {
      // Remove starting and ending spaces
      var umltext = myCodeMirror.getValue().replace(/(^[\s\xA0]+|[\s\xA0]+$)/g, '');

      var umltextchanged = 
        (umltext !== lastUmlText) 
        && validDiagramText(umltext); 

      if (umltextchanged) {
        $('#ProgressIndicator').show();

        lastUmlText = umltext;

        $.post("SendUml.ashx", { uml: umltext }, function (result) {
          var key = $.trim(result);
          $("#umlimage").attr("src", "getimage.ashx?key=" + key);
        }, "text");

        try {
          var forCookie = $.base64.encode(umltext).replace(/==/, '');

          if (forCookie.length > 3800) {
            alert("Sorry maximum 3800 characters allowed in a diagram");
          }
          else {
            createCookie('uml', forCookie, 30);
            var test = readCookie('uml');

            if (test !== forCookie) {
              createCookie('uml', '', 30);
            }                                
          }
        } catch (e) {
        }
      }
    }, 1000);
  }
  else {
    window.clearTimeout(lastTimer);
    lastTimer = null;
    refreshDiagram();
  }

}

There’s quite some intelligence to this code. First it ensures that it does not send the UML text to the server to go through an expensive image generation process when user is only typing space or hitting enter and there’s really no change in the text that will result in a new image to be rendered. It also does a little bit of validation to prevent half-baked diagram text from being prematurely sent to the server. The more you can catch here, the less useless image generation you can prevent on the server.

First it posts the UML text to an HTTP handler called SendUml.ashx. It remembers the text and returns a GUID. Then that GUID is used to hit the GetImage.ashx which takes care of generating the diagram. The code in SendUml.ashx is very simple:

public class SendUml : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        string uml = context.Request["uml"];
        string key = Guid.NewGuid().ToString();

        context.Cache.Add(key, uml, null, DateTime.Now.AddSeconds(60), System.Web.Caching.Cache.NoSlidingExpiration,
            System.Web.Caching.CacheItemPriority.Default, null);
        
        context.Response.ContentType = "text/plain";
        context.Response.Write(key);
    }

It just stores the text in cache for a short duration as it is expected that the browser will hit the GetImage.ashx immediately after getting the key GUID.

 public void ProcessRequest (HttpContext context) {

	string key = context.Request["key"];
	string umltext = context.Cache[key] as string;
	
	context.Response.ContentType = "image/png";
	context.Response.Cache.SetCacheability(HttpCacheability.Private);
	context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(5));
	
	if (context.Request["saveMode"] == "1")
	{                
		context.Response.AddHeader("Content-Disposition", "attachment; filename=diagram.png");
	}
	
	var connection = PlantUmlConnectionPool.Get(TimeSpan.FromSeconds(15));
	if (connection == null)
		throw new ApplicationException("Connection not found in pool.");

	try
	{
		var uploadFileName = key + ".txt";
		var downloadFileName = key + ".png";
		
		connection.Upload(uploadFileName, 
			"@startuml " + downloadFileName + Environment.NewLine +
			umltext + Environment.NewLine +
			"@enduml");
		
		System.Threading.Thread.Sleep(100);

		using (MemoryStream memoryStream = new MemoryStream())
		{
			connection.Download(downloadFileName, stream =>
			{
				byte[] buffer = new byte[0x1000];
				int bytesRead;
				while ((bytesRead = stream.Read(buffer, 0, 0x1000)) > 0)
				{
					memoryStream.Write(buffer, 0, bytesRead);
				}

			});  

First it reads the key from query string and then loads the UML text from cache. Then it gets a connection to PlantUml FTP server (explanation coming soon in next section) and uploads the UML text as a file to the FTP server. Plantuml then generates the diagram image and makes it available for download. The handler then downloads the image from the FTP server. It then adds a watermark to the image and sends back to the browser.

using (Bitmap b = Bitmap.FromStream(memoryStream, true, false) as Bitmap)
using (Bitmap newBitmap = new Bitmap(b.Width, b.Height + 20))
using (Graphics g = Graphics.FromImage(newBitmap))
{
	// Put the original image on the top left corner.
	g.FillRectangle(Brushes.White, 0, 0, newBitmap.Width, newBitmap.Height);
	g.DrawImage(b, 0, 0);
	
	// Add the watermark
	SizeF size = g.MeasureString(WatermarkText, _font);
	g.DrawString(WatermarkText, _font, Brushes.Black, newBitmap.Width - size.Width, newBitmap.Height - 15);

	// Save the image to the response stream directly.
	newBitmap.Save(context.Response.OutputStream, System.Drawing.Imaging.ImageFormat.Png);
}

context.Response.Flush();

Once done, it returns the connection back to the pool:

PlantUmlConnectionPool.Put(connection); 

That’s it from the front end side

Generating Diagram using Plantuml

Plantuml is a Java application that can run as a FTP server where you can upload diagram text as a file and it generates a diagram image that you can download. Since it runs as a FTP server, I have to maintain a pool of running FTP servers. I cannot just start the FTP server and then generate the image. It will be too slow. So, I have to launch couple of instances of FTP servers during application startup and then maintain a pool of connections to FTP server. Whenever a hit to getimage.ashx comes in order to generate the diagram, it gets one connection from the pool, serves the request and then returns the connection to the pool. This is a common pattern you can use when you have to share a finite number of expensive resources across many demanding customers.

First I maintain a pool of running Plantuml instances. During Application_Start event, the following code launches couple of Plantuml FTP servers and prepares a pool of connections.

public static class PlantUmlProcessManager
{
    private static readonly List<Process> _processes = new List<Process>();

    public static void Startup()
    {
        if (_processes.Count > 0)
            Shutdown();

        var javaPath = ConfigurationManager.AppSettings["java"];

        if (!File.Exists(javaPath))
            throw new ApplicationException("Java.exe not found: " + javaPath);

        var host = ConfigurationManager.AppSettings["plantuml.host"];
        var startPort = Convert.ToInt32(ConfigurationManager.AppSettings["plantuml.start_port"]);
        var instances = Convert.ToInt32(ConfigurationManager.AppSettings["plantuml.instances"]);
            
        var plantumlPath = ConfigurationManager.AppSettings["plantuml.path"];
        if (!File.Exists(plantumlPath))
            throw new ApplicationException("plantuml.jar not found in " + plantumlPath);

        for (int i = 0; i < instances; i++)
        {
            var argument = "-jar " + plantumlPath + " -ftp:" + (startPort + i);
            ProcessStartInfo pInfo = new ProcessStartInfo(javaPath, argument);

            pInfo.CreateNoWindow = true;
            pInfo.UseShellExecute = false;
            pInfo.RedirectStandardInput = true;
            pInfo.RedirectStandardError = true;
            pInfo.RedirectStandardOutput = true;
                
            Process process = Process.Start(pInfo);
            Thread.Sleep(5000);
            _processes.Add(process);

            PlantUmlConnection connection = new PlantUmlConnection();
            connection.Connect(host, startPort + i);
            PlantUmlConnectionPool.Put(connection);
        }
    }

The connection pool is defined as following:

 public static class PlantUmlConnectionPool
{
    private readonly static Queue<PlantUmlConnection> _connectionPool = new Queue<PlantUmlConnection>();
    private readonly static ManualResetEvent _availableEvent = new ManualResetEvent(false);

    public static PlantUmlConnection Get(TimeSpan timeout)
    {
        if (_connectionPool.Count == 0)
        {
            _availableEvent.Reset();
            if (_availableEvent.WaitOne(timeout))
            {
                return _connectionPool.Dequeue();
            }
            else
            {
                return null;
            }
        }
        else
        {
            lock (_connectionPool)
            {
                if (_connectionPool.Count == 0)
                    return null;
                else
                    return _connectionPool.Dequeue();                    
            }
        }
    } 

The algorithm is as following:

  • Check if there’s a free connection available in the pool.
  • If not, then wait for a fixed duration until some connection becomes available.
  • If no connection is available after waiting the timeout period, then return null.

Putting connection back to the pool is very simple:

    public static void Put(PlantUmlConnection connection)
    {
        lock (_connectionPool)           
        _connectionPool.Enqueue(connection);
       
    <span class="Apple-tab-span" style="white-space: pre; ">	</span>_availableEvent.Set();
    }  

In order to maintain a connection ready to the running FTP servers, I have used Alex Pilotti’s FTP client.

public class PlantUmlConnection : IDisposable
{
    private FTPSClient client = new FTPSClient();
    private string _host;
    private int _port;
    public void Connect(string host, int port)
    {
        _host = host;
        _port = port;
        Debug.WriteLine("Connecting to FTP " + host + ":" + port);
        client.Connect(host, port,
            new NetworkCredential("yourUsername","yourPassword"),
            ESSLSupportMode.ClearText,
            null,
            null,
            0,
            0,
            0,
            3000,
            true,
            EDataConnectionMode.Active
        );
        Debug.WriteLine("Connection successful " + host + ":" + port);
    }  

During the initialization of FTP servers, for each instance of FTP server, one instance of this connection class establishes an open connection.

When a diagram needs to be generated, it uploads a text file to the FTP server containing the diagram text. Then the Plantuml engine kicks in and generates the image.

public void Upload(string remoteFileName, string content)
    {
        Debug.WriteLine("Uploading to " + _host + ":" + _port + "/" + remoteFileName);
        using (var stream = client.PutFile(remoteFileName))
        {
            byte[] data = Encoding.UTF8.GetBytes(content);
            stream.Write(data, 0, data.Length);
        }
        Debug.WriteLine("Successfully uploaded " + _host + ":" + _port + "/" + remoteFileName);
    }

Then you can download the image using Download function:

public void Download(string remoteFileName, Action<Stream> processStream)
    {
        Debug.WriteLine("Downloading from " + _host + ":" + _port + "/" + remoteFileName);
        using (var stream = client.GetFile(remoteFileName))
        {
            processStream(stream);
        }

        Debug.WriteLine("Successfully downloaded " + _host + ":" + _port + "/" + remoteFileName);
            
    }

That’s all about managing PlantUML server.

Setting up codeuml on your own

You can install codeuml on your own server. In that case, please follow the readme file carefully. It requires some very careful setting in order to get Plantuml engine to work. I will paste the readme file for your convenience but do keep checking the latest code and read me file.

There are several pre-requisits before you run this website. 

1. Install Java 
===============
Download and install latest Java. Make sure you know where
you are installing java. Usually it will be:
"c:\Program Files\Java\jre6\bin" 

1. Configure Graphviz
=============================================================
First, you have to install graphviz. 
http://www.graphviz.org/
Once you have installed, create a SYSTEM environment variable
called GRAPHVIZ_DOT which points to the dot.exe found in the 
graphviz bin folder. Usually it is:
c:\Program Files\Graphviz2.26.3\bin\dot.exe
Once you have done so, start a new command line window and run
this:
set graphviz_dot
If this shows you:
GRAPHVIZ_DOT=c:\Program Files\Graphviz2.26.3\bin\dot.exe
Then it is ok.

2. Installing on IIS 7+
=============================================================
If you are hosting this on a Windows Server, there are various
steps you need to do:
* First create a new app pool. 
* Create a new website or virtual directory that points to this
website.
* Give the app pool user (IIS AppPool\YourAppPoolName or NETWORK
SERVICE)
Read & Execute permission on the:
      ** Java folder. Eg. "c:\Program Files\Java\jre6\bin" 
      ** Graphviz bin folder: Eg c:\Program Files\Graphviz2.26.3\bin
      ** Within this website:
plantuml folder. 

3. Configuring web.config
==============================================================
You must fix the following entries before you can run:
<add key="java" value="c:\Program Files\Java\jre6\bin\java.exe" />
<add key="plantuml.path" value="C:\Dropbox\Dropbox\OSProjects\PlantUmlRunner\plantuml\plantuml.jar"/>
These are both absolute paths. No relative path allowed. 

4. Running and testing the website
============================================================
Run the Manage.aspx. 

It will take a while to start the page as it tries to launch java
and run the plantuml engine at the application_start event.
Once the site is up and running, click on Test button to test
a UML generation. If it works, you have configured everything 
properly.
Disable the Manage.aspx on production.

Conclusion

Codeuml as a web application is small but it shows how to build highly responsive AJAX front-end that mimics Visual Studio like IDE and generates output form the server using some very expensive pool of finite resource. It shows you how you can implement a pool of expensive resource on your own.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author


Comments and Discussions

 
QuestionAs usual. Pinmembermark merrens3-Oct-12 12:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140415.2 | Last Updated 4 Jun 2012
Article Copyright 2012 by Omar Al Zabir
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid