lunes, 3 de octubre de 2011

Using Embed Jetty and BlazeDS Remoting with Adobe AIR

When we need to communicate with server side data from an Adobe AIR application we usually use HTTP services, Web Services or Remoting this works great, but what happens when we need to create an standalone application? We have options like merapi, flerry and transmission...

I want to show you how to embed Jetty Server in an Adobe Air application and use BlazeDS for communication. The use case is simple access a MySQL server directly from Adobe AIR but with this option you might experiment and create better use cases for this.


We need to download the following:
  • Jetty library (i use jetty-all-7.4.0.RC0.jar)
  • Servlet api (i use servlet-api-2.5.jar)
  • Mysql Connector
  • BlazeDS 4

Let's coding:

First we create a simple table for sample purposes:


Java side:
First we create a simple Java Project and add the 2 jars in the classpath.

Product.java

package entities;
import java.math.BigDecimal;
public class Product {
private int productID;
private String productName;
private BigDecimal productPrice;
public int getProductID() {
return productID;
}
public void setProductID(int productID) {
this.productID = productID;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getProductPrice() {
return productPrice;
}
public void setProductPrice(BigDecimal productPrice) {
this.productPrice = productPrice;
}
}
view raw Product.java hosted with ❤ by GitHub
Here i use plain JDBC but you can use JPA, Hibernate, etc. 
ProductDAO.java

package dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import entities.Product;
public class ProductDAO {
private Connection conexion;
public ProductDAO(){
conexion = JDBCHelper.getConexion();
}
public List<Product> getProducts(){
List<Product> products = new ArrayList<Product>();
try{
PreparedStatement pstmt = conexion.prepareStatement("SELECT * FROM Product");
ResultSet rs = pstmt.executeQuery();
while(rs.next()){
Product product = new Product();
product.setProductID(rs.getInt(1));
product.setProductName(rs.getString(2));
product.setProductPrice(rs.getBigDecimal(3));
products.add(product);
}
}catch(SQLException ex){
Logger.getLogger(ProductDAO.class.getSimpleName()).log(Level.SEVERE, ex.getMessage());
}
return products;
}
}
view raw PorductDAO.java hosted with ❤ by GitHub
ProductServices.java

package services;
import java.util.List;
import dao.ProductDAO;
import entities.Product;
public class ProductService {
private ProductDAO productDAO = new ProductDAO();
public List<Product> getProducts(){
return productDAO.getProducts();
}
}
This is a very important class, here we are going to create the Server instance and start it. 
Note: I assume that you know something about embedding servers, if you want some background here you can find a great resource.

BlazeDSServer.java

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.webapp.WebAppContext;
public class BlazeDSServer {
private static Server server = new Server();
public static void main(String[] args) throws Exception {
SocketConnector connetor = new SocketConnector();
connetor.setPort(8080);
server.setConnectors(new Connector[] {connetor});
WebAppContext context = new WebAppContext();
context.setDescriptor("WEB-INF/web.xml");
context.setResourceBase("webapp");
context.setContextPath("/demoapp");
context.setParentLoaderPriority(true);
server.setHandler(context);
server.setStopAtShutdown(true);
MonitorThread monitor = new MonitorThread();
monitor.start();
server.start();
server.join();
}
private static class MonitorThread extends Thread {
private ServerSocket socket;
public MonitorThread(){
setDaemon(true);
setName("StopMonitor");
try{
socket = new ServerSocket(8079, 1, InetAddress.getLocalHost());
Logger.getLogger(BlazeDSServer.class.getSimpleName()).log(Level.INFO, "Close Thread Started");
}catch(Exception ex){
throw new RuntimeException();
}
}
@Override
public void run() {
super.run();
Socket accept;
try{
accept = socket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));
reader.readLine();
server.stop();
accept.close();
socket.close();
Logger.getLogger(BlazeDSServer.class.getSimpleName()).log(Level.INFO, "Server Stopped");
}catch(Exception ex){
throw new RuntimeException(ex);
}
}
}
}
Now we need to extract the BlazeDS.war content. create a folder called webapp inside your src folder with this content, your project structure should look like this.



The last thing you need to do is declare a remote destination on your services-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<service id="remoting-service"
class="flex.messaging.services.RemotingService">
<adapters>
<adapter-definition id="java-object" class="flex.messaging.services.remoting.adapters.JavaAdapter" default="true"/>
</adapters>
<default-channels>
<channel ref="my-amf"/>
</default-channels>
<destination id="productService">
<properties>
<source>services.ProductService</source>
</properties>
</destination>
</service>


Now we code the Air side:
Fist create an asset folder inside your air src folder and copy all the bin folder from your java project

As you can see i made the same copy into the bin-debug folder, i don't know why it doesn't export all the files to the bin-debug.

Now let's code the application, first in the descriptor file enable the extended desktop profile so you can use Native Process




Now  the code:

EmbedJetty.mxml
<?xml version="1.0" encoding="utf-8"?>
<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/mx"
width="500" height="500"
creationComplete="creationCompleteHandler(event)">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.controls.Alert;
import mx.events.FlexEvent;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
[Bindable]
private var products:ArrayCollection;
//The native process
private var nativeProcess:NativeProcess;
private var nativeProcessStartupInfo:NativeProcessStartupInfo;
private var executable:File = new File("C:/Program Files/Java/jdk1.7.0/bin/java.exe");
//The socket
private var socket:Socket;
protected function btnStartServer_clickHandler(event:MouseEvent):void
{
if(nativeProcess == null){
nativeProcess = new NativeProcess();
nativeProcessStartupInfo = new NativeProcessStartupInfo();
}
nativeProcessStartupInfo.executable = executable;
Alert.show(File.applicationDirectory.nativePath);
nativeProcessStartupInfo.workingDirectory = new File(File.applicationDirectory.nativePath +"/assets/");
var args:Vector.<String> = new Vector.<String>();
args.push("-classpath");
args.push(".;lib/jetty-all-7.4.0.RC0.jar;lib/servlet-api-2.5.jar;lib/mysql-connector.jar");
args.push("BlazeDSServer");
nativeProcessStartupInfo.arguments = args;
nativeProcess.start(nativeProcessStartupInfo);
nativeProcess.addEventListener(ProgressEvent.STANDARD_ERROR_DATA, nativeProcess_errorHandler);
}
protected function creationCompleteHandler(event:FlexEvent):void
{
if(!NativeProcess.isSupported){
Alert.show("Native Process not supported");
}
if(!executable.exists){
Alert.show("Java is not found on your machine");
}
}
protected function btnStopServer_clickHandler(event:MouseEvent):void
{
socket = new Socket();
socket.addEventListener(Event.CONNECT, socket_connectHandler);
socket.addEventListener(IOErrorEvent.IO_ERROR, socket_onError);
socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socket_onSecError);
try{
socket.connect("192.168.1.34", 8079);
}catch(e:Error){
Alert.show(e.message);
}
}
protected function btnGetData_clickHandler(event:MouseEvent):void
{
getProductsCR.token = productRO.getProducts();
}
protected function nativeProcess_errorHandler(event:ProgressEvent):void
{
var temp:NativeProcess = event.target as NativeProcess;
var salida:String = temp.standardError.readUTFBytes(temp.standardError.bytesAvailable);
txtLog.text += salida + File.lineEnding;
}
protected function socket_connectHandler(event:Event):void
{
socket.writeUTFBytes("\n");
socket.flush();
}
protected function socket_onError(event:IOErrorEvent):void
{
Alert.show(event.text);
}
protected function socket_onSecError(event:SecurityErrorEvent):void
{
Alert.show(event.text);
}
protected function productRO_faultHandler(event:FaultEvent):void
{
Alert.show(event.fault.message, "Error");
}
protected function getProductsCR_resultHandler(event:ResultEvent):void
{
products = event.result as ArrayCollection;
}
]]>
</fx:Script>
<fx:Declarations>
<s:RemoteObject id="productRO" destination="productService" endpoint="http://192.168.1.34:8080/demoapp/messagebroker/amf"
fault="productRO_faultHandler(event)"/>
<s:CallResponder id="getProductsCR" result="getProductsCR_resultHandler(event)"/>
</fx:Declarations>
<s:layout>
<s:VerticalLayout paddingBottom="10" paddingLeft="10" paddingRight="10" paddingTop="10"/>
</s:layout>
<s:HGroup width="100%" minHeight="100" height="50" >
<s:Button id="btnStartServer" label="Start Server" click="btnStartServer_clickHandler(event)"/>
<s:Button id="btnStopServer" label="Stop Server" click="btnStopServer_clickHandler(event)"/>
</s:HGroup>
<s:HGroup width="100%">
<s:Button id="btnGetData" label="Get Data" click="btnGetData_clickHandler(event)"/>
</s:HGroup>
<s:HGroup width="100%" height="100%">
<s:DataGrid id="dgData" width="100%" height="100%" dataProvider="{products}">
<s:columns>
<s:ArrayCollection>
<s:GridColumn headerText="Product ID" dataField="productID"/>
<s:GridColumn headerText="Product Name" dataField="productName"/>
<s:GridColumn headerText="Product Price" dataField="productPrice"/>
</s:ArrayCollection>
</s:columns>
</s:DataGrid>
</s:HGroup>
<s:Group width="100%">
<s:TextArea id="txtLog" color="red" width="100%" height="100%"/>
</s:Group>
</s:WindowedApplication>
view raw EmbedJetty.mxml hosted with ❤ by GitHub


Now let's take a more closer view to the code:
  • First the executable file will be the Java path so it depends on your machine.
  • I use NativeProcess to start the embed Jetty instance and a socket to shutdown the embed jetty instance.
  • The arguments for the NativeProcessStartupInfo are the necessary classes to make the Java desktop app run. In the application:
args.push("-classpath");
args.push(".;lib/jetty-all-7.4.0.RC0.jar;lib/servlet-api-2.5.jar;lib/mysql-connector.jar");
args.push("BlazeDSServer"); 
  •  Once it connects you can use RemoteObjects to make rpc call to your Java code.
  • You can use messaging too.
Before you run check your ip match with the socket connection ip, and verify the JDBC setting in the Java code.

When you click the start server button, in the text area you see the log comming from the server.

Now click the Get Data button and in my case the screen will be the following:
 

Stop the server by clicking the Stop Server button.

And that's all, hope this will help you, any question or bug please let me know.
Here is the source code for the Java Project
Here is the Source code for the Flex Project

1 comentario:

  1. What if you another executable jar running previously? Air won't be able to run "java" again. At least that's a problem I'm facing currently...

    ResponderEliminar