Embedding Tomcat and BlazeDS in an AIR 2.0 Application

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

  1. Download the AIR 2.0 beta runtime here.
  2. Because Tomcat Launcher uses the Native Process API, I had to create native installers:

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>
  • 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

  • Rick

    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

  • John

    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

  • Pingback: AIR 2.0 Web Server using the New Server Socket API()

  • Vniu

    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 :)

  • Pingback: AIR 2 / Flash Player 10.1 Beta Info « Devgirl’s Weblog()

  • O’live

    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 ?

  • MOGIO

    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

  • MOGIO

    SORRY…

    put the extendedDesktop desktop
    into the wrong place
    …works

  • Andrew Reifers

    @ 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.

  • Oleg

    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.

  • miro

    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.

  • Oleg

    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.

  • Zen

    Hi,
    Great blog you have and a great article. I have been looking for something similar in for a while, but havent found it until now.

    However, I have a problem when I try to install and run your example. I install AIR 2 beta 2 runtime, but then when I try to install your program it says I need an AIR update that is not available for my system (and I get a link to the AIR prereqs). I saw you wrote this article in december. Maybe the .exe file you provide doesnt work with the AIR 2 beta 2 runtime? I am using Windows XP SP3.

    Thanks! I will read your other articles as well!

  • Pingback: AIR 2 Native Process with Java « bioRex21's blog()

  • Renu

    Hi Christophe,
    Nice explaination, I am doing similar functionality but facing an issue. Tomcat is running through native process.and the application is running for more than 3-4 hrs and if the system is idle. After sometime, we found Tomcat has automatically gets shut down, without explictly calling shutdown. Can you please help me on this.

css.php