65.9K
CodeProject 正在变化。 阅读更多。
Home

Codeuml - 像编码一样快速设计 UML 图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (51投票s)

2012年6月4日

CPOL

6分钟阅读

viewsIcon

151496

downloadIcon

1

Codeuml.com 是一个开源的免费 Web 版 UML 图编辑器。您可以使用一种特殊的语言来描述图表,从而像打字一样快速地编写 UML 图。

引言

Codeuml 是一个基于 Web 的 UML 设计器,您可以使用一种特殊的语言来编写图表,它会实时生成图表。它比使用任何可视化设计器都快,后者需要您拖放图表元素并使用鼠标连接它们。Codeuml 使用开源的 plantuml 引擎从文本生成图表。您可以像编码一样快速地生成 UML 图。

这个 Web 应用程序展示了一些有趣的设计和编码挑战。首先,它展示了如何构建一个模仿 Windows 8 Metro UI 的基于 Web 的 IDE 环境。其次,它展示了如何定期从网站收集数据,在后台异步发送到服务器,并实时获取生成的结果。第三,也是最重要的,它展示了如何维护一个服务器端非常昂贵的资源池,这些资源不能在每次服务器请求时都创建,而必须拥有一个所有 Web 用户共享的有限池。

获取代码

实时站点可在以下网址找到:www.codeuml.com 

从以下网址获取代码: http://code.google.com/p/codeuml/ 

构建前端 

UI 灵感来自 Windows 8 的 Metro UI,并且对平板电脑友好。您可以在平板电脑上轻松触摸按钮。3 列可调整大小的面板使用 jQuery Splitter 插件构建。文本编辑器是出色的 CodeMirror 文本编辑器,它有一个糟糕的 Logo。行情信息由 jQuery New Ticker 插件提供。

3 列视图使用以下 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>
首先,它将屏幕分为两部分——左侧的 UML 片段栏和右侧的编辑器和图像。然后,它将右侧进一步分为两部分——文本编辑器和图表图像。以下 JavaScript 初始化了分割器
// 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");
});
接下来,它在 textarea 上初始化 CodeMirror 代码编辑器,并使其成为出色的文本编辑器。
myCodeMirror = CodeMirror.fromTextArea($('#umltext').get(0),
            {
                onChange: refreshDiagram
            });
myCodeMirror.focus();
myCodeMirror.setCursor({ line: myCodeMirror.lineCount() + 1, ch: 1 });
然后,它初始化左侧的 UML 片段栏。每个按钮都有一个关联的 UML 文本,单击时会注入到文本编辑器中。按钮示例
<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>

注入到文本编辑器中的文本位于 `<pre>` 标签内。

您可以根据需要创建任意数量的按钮,只需将需要插入的 UML 片段放在带有 `umsnippet` 类的 `<pre>` 标签内。

单击这些按钮时,以下 JavaScript 会将 `<pre>` 内的代码注入到文本编辑器中。

$("#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();
});

这里有一个棘手的问题是,如果我注入调用 `replaceRange` 的文本,CodeMirror 编辑器就会停止工作。必须重新创建它才能使其再次工作。

边输入边生成图表

边输入边刷新图表是最具挑战性的部分。以下 JavaScript 函数在文本编辑器中的任何内容发生变化时都会触发。但是,它确保每秒只向服务器发送一次 UML。因此,即使您持续输入,它每秒也只会将 UML 发送一次到服务器。

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();
  }

}

这段代码相当智能。首先,它确保当用户只输入空格或按 Enter 键,并且文本实际上没有会生成新图像的变化时,它不会将 UML 文本发送到服务器进行昂贵的图像生成过程。它还会进行一些验证,以防止不完整的图表文本过早发送到服务器。您在这里能捕获的越多,就能在服务器上避免越多的无用图像生成。

首先,它将 UML 文本发布到一个名为 SendUml.ashx 的 HTTP 处理程序。它会记住文本并返回一个 GUID。然后,该 GUID 用于命中 `GetImage.ashx`,该处理程序负责生成图表。`SendUml.ashx` 中的代码非常简单

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);
    }

它只是将文本缓存很短时间,因为预期浏览器在获得密钥 GUID 后会立即命中 `GetImage.ashx`。

 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);
				}

			});  

首先,它从查询字符串中读取密钥,然后从缓存中加载 UML 文本。然后,它连接到 PlantUml FTP 服务器(稍后将在下一节中解释),并将 UML 文本作为文件上传到 FTP 服务器。Plantuml 然后生成图表图像并使其可供下载。然后,处理程序从 FTP 服务器下载图像。然后,它在图像上添加水印并将其发送回浏览器。

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();

完成后,它将连接返回到池中

PlantUmlConnectionPool.Put(connection); 

这就是前端部分的内容

使用 Plantuml 生成图表

Plantuml 是一个 Java 应用程序,可以作为 FTP 服务器运行,您可以将图表文本作为文件上传,它会生成一个您可以下载的图表图像。由于它作为一个 FTP 服务器运行,我必须维护一个正在运行的 FTP 服务器池。我不能只是启动 FTP 服务器然后生成图像。这会太慢。因此,我必须在应用程序启动期间启动几个 FTP 服务器实例,然后维护到 FTP 服务器的连接池。每当出现一个生成图表的 `getimage.ashx` 请求时,它就会从池中获取一个连接,处理请求,然后将连接返回到池中。当您需要与许多要求苛刻的客户共享有限的昂贵资源时,这是一种常见的模式。

首先,我维护一个 Plantuml 实例池。在 `Application_Start` 事件期间,以下代码会启动几个 Plantuml FTP 服务器并准备连接池。

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);
        }
    }

连接池定义如下

 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();                    
            }
        }
    } 

算法如下

  • 检查池中是否有可用连接。
  • 如果没有,则等待固定时长,直到有连接可用。
  • 如果在等待超时期间没有连接可用,则返回 null。

将连接放回池中非常简单

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

为了维护与正在运行的 FTP 服务器的连接就绪,我使用了 Alex Pilotti 的 FTP 客户端

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);
    }  

在 FTP 服务器初始化期间,对于每个 FTP 服务器实例,这个连接类的一个实例会建立一个打开的连接。

需要生成图表时,它会将包含图表文本的文本文件上传到 FTP 服务器。然后,Plantuml 引擎会启动并生成图像。

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);
    }

然后您可以使用 Download 函数下载图像

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);
            
    }

关于管理 PlantUML 服务器就这些了。

自行设置 Codeuml

您可以在自己的服务器上安装 codeuml。在这种情况下,请仔细遵循 readme 文件。它需要一些非常仔细的设置才能使 Plantuml 引擎正常工作。我将为您粘贴 readme 文件,但请务必检查最新的代码和 readme 文件。

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. 
https://graphviz.cn/
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.

结论

Codeuml 作为一个 Web 应用程序虽然不大,但它展示了如何构建高度响应式的 AJAX 前端,该前端模仿 Visual Studio 风格的 IDE,并使用一些非常昂贵的有限资源池从服务器生成输出。它向您展示了如何自行实现一个昂贵的资源池。

 

© . All rights reserved.