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:
$("#MySplitter").splitter({
type: "v",
outline: true,
minLeft: 60, sizeLeft: 100, maxLeft: 250,
anchorToWindow: true,
resizeOnWindow: true,
accessKey: "L"
});
$("#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">
<!--
<h2>
Sequence
</h2>
<div class="sequence_diagram">
<div class="button">
<div class="icon">
A→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);
myCodeMirror.replaceRange(umlsnippet, myCodeMirror.getCursor(true));
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 () {
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))
{
g.FillRectangle(Brushes.White, 0, 0, newBitmap.Width, newBitmap.Height);
g.DrawImage(b, 0, 0);
SizeF size = g.MeasureString(WatermarkText, _font);
g.DrawString(WatermarkText, _font, Brushes.Black, newBitmap.Width - size.Width, newBitmap.Height - 15);
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.