In my previous post, I demonstrated how to create a simple Tomcat Launcher using the new AIR 2.0 Native Process API. The assumption in that sample was that Tomcat was already installed on your machine.
In this new sample application, I’m taking that idea a little further by embedding Tomcat as part of the native installer. The first time you run the application, Tomcat is automatically copied to your applicationStorageDirectory from where the AIR application starts it. In this sample, I still provide a simple console for the user to start and stop Tomcat manually, but in a real life application, you would probably want to start Tomcat automatically when the AIR application starts.

Why would you want to embed Tomcat as part of an AIR application? First of all, I’m not saying that you should… This is just part of my exploration of new capabilities in AIR 2.0. That being said, I can think of a number of use cases where this could make sense. For example:
- Tighter communication with Java code using BlazeDS locally as a way to directly invoke methods on remote Java classes.
- Provide an offline mode for an application that closely mimics the online infrastructure.
Installation Instructions
- Download the AIR 2.0 beta runtime here.
- Because Tomcat Launcher uses the Native Process API, I had to create native installers:
- To install the application on Windows, download and run AIRAppWithEmbeddedTomcat.exe.
- To install the application on a Mac, download and run AIRAppWithEmbeddedTomcat.dmg.
After starting the application, enter the path to your Java home folder, then click the Start button. The application will remember the folder you entered the next time you use the application. Try the “Embedded JSP Example” and “Embedded RemoteObject Example”. If you get a BindingException when you start Tomcat, make sure it isn’t already started. You can also click the Stop button to stop a running instance.
You can download the project here: AIRAppWithEmbeddedTomcat.fxp.zip. If you just want to take a quick peak at the code, here is AIRAppWithEmbeddedTomcat.mxml:
<?xml version="1.0" encoding="utf-8"?>
<!--
Christophe Coenraets http://coenraets.org
-->
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/halo"
title="AIR Application with Embedded Tomcat"
height="600" width="800"
applicationComplete="init()">
<s:layout>
<s:VerticalLayout paddingLeft="8" paddingRight="8" paddingBottom="8" paddingTop="8"/>
</s:layout>
<fx:Script>
<![CDATA[
import mx.controls.Alert;
private var startTomcatProcess:NativeProcess;
private var stopTomcatProcess:NativeProcess;
private var tomcatHomeDir:File;
public function init():void
{
if (!NativeProcess.isSupported)
{
Alert.show("NativeProcess not supported");
}
tomcatHomeDir = File.applicationStorageDirectory.resolvePath("tomcat");
if (!tomcatHomeDir.exists)
{
var tomcatOriginalDir:File = File.applicationDirectory.resolvePath("tomcat");
trace("Copying tomcat to appStorageDirectory...");
tomcatOriginalDir.copyTo(tomcatHomeDir);
}
tomcatHome.text = tomcatHomeDir.nativePath;
// Read the last home used to start Tomcat (if any)
var xml:XML = readConfig();
if (xml != null)
{
javaHome.text = xml.javaHome;
return;
}
// If no known last home, present default/sample values
if (Capabilities.os.toLowerCase().indexOf("win") > -1)
{
// default/sample values for Windows users
javaHome.text = "C:\\Program Files\\Java\\jdk1.6.0";
}
else if (Capabilities.os.toLowerCase().indexOf("mac") > -1)
{
// default/sample values for Mac users
javaHome.text = "/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0";
}
}
public function startTomcat():void
{
// Write the Java home path to config.xml so that it can be presented
// to the user the next time the application is started
writeConfig();
log.text = "Starting Tomcat..." + File.lineEnding;
startTomcatProcess = new NativeProcess();
execute(startTomcatProcess, "start");
}
public function stopTomcat():void
{
log.text = "Stopping Tomcat..." + File.lineEnding;
stopTomcatProcess = new NativeProcess();
execute(startTomcatProcess, "stop");
}
public function execute(process:NativeProcess, arg:String):void
{
// Get a file reference to the JVM
var file:File = new File(javaHome.text);
if (Capabilities.os.toLowerCase().indexOf("win") > -1)
{
file = file.resolvePath("bin/javaw.exe");
}
else
{
file = file.resolvePath("Home/bin/java");
}
// Start the process
try
{
var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
nativeProcessStartupInfo.executable = file;
nativeProcessStartupInfo.workingDirectory = tomcatHomeDir.resolvePath("bin");
var processArgs:Vector.<String> = new Vector.<String>();
processArgs[0] = "-Dcatalina.home="+tomcatHomeDir.nativePath;
processArgs[1] = "-classpath";
processArgs[2] = tomcatHomeDir.resolvePath("bin/bootstrap.jar").nativePath;
processArgs[3] = "org.apache.catalina.startup.Bootstrap";
processArgs[4] = arg;
nativeProcessStartupInfo.arguments = processArgs;
startTomcatProcess = new NativeProcess();
startTomcatProcess.start(nativeProcessStartupInfo);
startTomcatProcess.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA,
outputDataHandler);
startTomcatProcess.addEventListener(ProgressEvent.STANDARD_ERROR_DATA,
errorOutputDataHandler);
}
catch (e:Error)
{
Alert.show(e.message, "Error");
}
}
public function outputDataHandler(event:ProgressEvent):void
{
var process:NativeProcess = event.target as NativeProcess;
var data:String = process.standardOutput.readUTFBytes(process.standardOutput.bytesAvailable);
log.text += data;
}
public function errorOutputDataHandler(event:ProgressEvent):void
{
var process:NativeProcess = event.target as NativeProcess;
var data:String = process.standardError.readUTFBytes(startTomcatProcess.standardError.bytesAvailable);
log.text += data;
}
private function readConfig():XML
{
var file:File = File.applicationStorageDirectory.resolvePath("config.xml");
if (file.exists)
{
var fileStream:FileStream = new FileStream();
fileStream.open(file, FileMode.READ);
var xml:XML = XML(fileStream.readUTFBytes(fileStream.bytesAvailable));
fileStream.close();
return xml;
}
else
{
return null;
}
}
private function writeConfig():void
{
var xml:String = '<?xml version="1.0" encoding="utf-8"?>' + File.lineEnding;
xml += "<config>" + File.lineEnding;
xml += "<javaHome>" + javaHome.text + "</javaHome>" + File.lineEnding;
xml += "<tomcatHome>" + tomcatHome.text + "</tomcatHome>" + File.lineEnding;
xml += "</config>" + File.lineEnding;
var file:File = File.applicationStorageDirectory.resolvePath("config.xml");
var fileStream:FileStream = new FileStream();
fileStream.open(file, FileMode.WRITE);
fileStream.writeUTFBytes(xml);
fileStream.close();
}
private function openWithDefaultEditor():void
{
var file:File =
File.createTempDirectory().resolvePath("tomcat_launcher_console.txt");
var fileStream:FileStream = new FileStream();
fileStream.open(file, FileMode.WRITE);
fileStream.writeUTFBytes(log.text);
fileStream.close();
file.openWithDefaultApplication();
}
]]>
</fx:Script>
<fx:Declarations>
<s:RemoteObject id="srv" destination="contacts"
endpoint="http://localhost:8080/messagebroker/amf"/>
</fx:Declarations>
<mx:TabNavigator width="100%" height="100%">
<mx:VBox width="100%" height="100%" label="Console" paddingLeft="8" paddingRight="8" paddingBottom="8">
<mx:Form width="100%">
<mx:FormItem label="Java Home" width="100%">
<s:TextInput id="javaHome" width="100%"/>
</mx:FormItem>
<mx:FormItem label="Embedded Tomcat Home" width="100%">
<s:TextInput id="tomcatHome" width="100%" enabled="false"/>
</mx:FormItem>
</mx:Form>
<s:HGroup>
<s:Button label="Start" click="startTomcat()"/>
<s:Button label="Stop" click="stopTomcat()"/>
<s:Button label="Clear Console" click="log.text=''"/>
<s:Button label="Open in Default Text Editor"
click="openWithDefaultEditor()"/>
</s:HGroup>
<s:TextArea id="log" width="100%" height="100%"/>
</mx:VBox>
<mx:VBox label="Embedded JSP Example" paddingLeft="8" paddingRight="8" paddingBottom="8">
<mx:HTML id="html" width="100%" height="100%"/>
<s:Button label="Load JSP" click="html.location='http://localhost:8080/hello.jsp'"/>
</mx:VBox>
<mx:VBox label="Embedded Remote Object Example" paddingLeft="8" paddingRight="8" paddingBottom="8">
<mx:DataGrid width="100%" height="100%" dataProvider="{srv.findAll.lastResult}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Id"/>
<mx:DataGridColumn dataField="firstName" headerText="First Name"/>
<mx:DataGridColumn dataField="lastName" headerText="Last Name"/>
<mx:DataGridColumn dataField="city" headerText="City"/>
</mx:columns>
</mx:DataGrid>
<s:Button label="Get Data" click="srv.findAll()"/>
</mx:VBox>
</mx:TabNavigator>
</s:WindowedApplication>


17 Comments
Very Good , Cool !!!!!!!!!!!
Very good, I was wondering between Merapi and Tomcat for a project, this is the solution for the future! Thanks!
This is just Great stuff.. Ever imagined.
Wow, this looks *very* interesting. I’ve been struggling with how to package such our Tomcat/BlazeDS/Air application for a while now.
Thanks big time.
Stu
Hi,
great example. In principle could a device driver be installed in a similar way to ensure that AIR can successfully communicate with a device connected via USB?
Many thanks,
Rick
Nice post.
Obviously the next step would be using that local tomcat to allow offline blazeds. Out of my head I see the following problems:
- How would you reuse Airs’ SQLlite, when the server uses another database?
- How can you automate the offline synchronisation?
- How does this impact installation size? Tomcat isn’t small and neither are the libraries neccessary for blazeDS leave alone spring+blazeDS.
Clearly this has a lot of potential for distributing blazeds applications offline – but we are nowhere close to this yet. It’s all very exciting though and I can see why you started work on it. :-)
Best, John
That is wicked. It adds a lot to Air having Tomcat running along with the app.
I just don’t like the packaging process of those native apps and switching OS to publish it. Do you think it will be easier with the final version of AIR to publish to multiple platforms from a single one?
Also, I was wondering if your example could be simplified a bit.
Basically if you have JVM installed you are going to have the path added to envirionment vars.
Going to command line and typing java, should write out java usage, etc.
That means that you don’t actually need to know where JVM is, you only need to know if it is installed.
Just simple check if “win” file = “javaw.exe” else file=”java” would be your nativeProcess.executable
If the startup fails it means there is no JVM and you can prompt to download it.
Would that work?
Tom
Nice post.It’s creative and cool.
In this sample, I still provide a simple console for the user to start and stop Tomcat manually, but in a real life application, you would probably want to start Tomcat automatically when the AIR application starts.
Can you confirm this is possible with AIR 2.0, ie. create an application that can start Tomcat and then the AIR application? I tried this with AIR 1.x but ran into too many issues. I haven’t checked out the NativeProcess stuff yet but a simple yes/no answer would help a lot :)
provide you with a variety of replica watches, these replica watches are high quality and are sold at a cheap price.
Thanks for this post.
I managed to make it work, but I had to copy WEB-INF/classes and WEB-INF/flex directories from the src (in the project) to the applicationStorageDir.
Flash Builder compilation process didn’t create these directories in the bin-debug directory (only lib under WEB-INF was created).
I am on Mac OS X Leopard / Flash Builder Beta 2 / SDK 4.0 (+ AIR beta 2).
Has anyone else experienced this strange behavior ?
Nice Example….
just installed the beta 2….
I’m working with your project in FlashBuilder….
Got a general question… Can I debug an air project which uses a native process within FlashBuilder or do I have to use the adl?
In debug mode FlashBuilder tells me that the nativeProcess is not availabe
THX
SORRY…
put the extendedDesktop desktop
into the wrong place
…works
@ O’live I also experienced these same issues. Thanks for the heads up on how to fix the problem. Can anyone offer advice on automatically copy these files to the proper output directory? I am hoping to convert a more complex BLAZEDS application to an air desktop application and it would be very helpful if I could configure my build properties properly.
I ran into a problem with AIR2 installing that application:
when I start your bundled EXE file, got error: “This application requires an update to Adobe AIR that is not available on your system”. Just a minute before that I installed AIR2 Beta2 (2.0.0.11670).
Not sure, maybe it is because my PC has Win7 64 bit installed ? Any idea how to fix that ?
Also, I have a use case for you having AIR app bundled with Tomcat – when you need a server part with Java on your PC. In my case, I am developing AIR app which reads/writes Excel 2007 files (uses Java POI 3.6 framework), since Office 2007 OpenXML format not supported in Flex AS3XLS lib.
A year ago i have done something similar with air 1.5 and jetty witch is a bit lighter then tomcat. I had to use a custom installer and a lot of magic to make it work. I definitely think that air 2.0 will be much more useful. Thanks for the example.
It would be very useful if that issue of copying ALL WEB-INF subdirectories is fixed, that gives a lot of grief. A few more possible enhancements:
1) Is it possible for AIR2 app to read JAVA_HOME variable from environment ?
You do not want users to figure that out on their PCs, especially since it only gives “Error #3214″ in some cases.
2) How to check whether Tomcat is Up from Flex? If Yes, do not start, or shut it down and Start again. Again, you do not want users to stumble there.
3) Where in “src” do you put files to be deployed in Tomcat webapps [not under ROOT], like WAR files ?
4) Please mention how to create EXE out of AIR in your post:
adt -package -target native MyApp.exe MyApp.air
I think, FB4 should have an option of creating EXE files for desktop apps.
2 Trackbacks
[...] Christophe Coenraets Rich Internet Applications, Flex, AIR, Java Skip to content BioUsing Flex with Spring « Embedding Tomcat and BlazeDS in an AIR 2.0 Application [...]
[...] Christophe Coenraets – Embedding Tomcat using NativeProcess APIs [...]