Goolge app engine + java + spring + REST + JSON + Flex – Part 2

www_1

Objective

Once you are finished with this article, you will be able to store application data in Google App Engine Data store and consume REST service using flex client.

Common Problems and Solutions

In my personal opinion flash player security model is the biggest piece of bullshit I ever encountered. If you sit down and thing just for 10 seconds about how it is implemented you will have to agree that it provides NO security at all. It just makes impossible so do certain things, that’s all. Adobe needs to drop it now.

Here is the problem that we have with REST and Google app engine: Adobe’s implementation of HTTPService only supports GET and POST, to consume REST we need at least 3 methods: GET, POST and DELETE. We can live without PUT – if id is null JDO will create a new persisted object, if it is set JDO will update it. But we still do not have support for DELETE.

If you are implementing Air Application there are good news: as3httpclientlib – a socket implementation of http client that can do all that we need: HEAD, GET, POST, PUT, DELETE. It depends on as3corelib and as3crypto, so grab those too.

If you are implementing a web application in google app engine, you are out of luck with as3httpclientlib. The reason is that adobe has this perverted understanding about security: since as3httpclientlib utilizes the Socket to implement http client adobe flash player tries to “protect” the server that socket is trying to connect by sending
“<policy-file-request/>” to the 843 port to get a scoket policy file, it that fails it tries the port the socket is trying to connect to. Since this is not a http request, google app engine ignores it, and you can not create sockets in google app engine – they are not supported, so you can not write a simple class to send back what flash player wants. The conclusion: Adobe Flex has not yet made it to Web 2.0, it can not talk REST to the server. But there are workarounds and here is mine:

Step one – Add persistence management code to the project we created in part 1

First we need to modify our User.java class to support persistence:

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")
public class User {

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;
    @Persistent
    private String email;
    @Persistent
    private String name;

    // Getter and setters go here......
}

As you can see we have instructed JDO that we will store User objects at application level, added a primary key and its generation strategy and annotated members to be persisted.

Next we need to get hold of a persistence manager factory, since it is a very very costly operation, we wan to do it only once in the lifetime of our application. For this purpose lets create a singleton which will hold this instance for us:

package com.lureto.rjf;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

public class PMF {

    private static final PersistenceManagerFactory pmfInstance = 
JDOHelper.getPersistenceManagerFactory("transactions-optional");

    private PMF() {}

    public static PersistenceManager getManager() {
        return pmfInstance.getPersistenceManager();
    }

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }

}

The last step is to modify our UserContrller.java to actually store and retrieve the User objects from the data store.

package com.lureto.rjf;

import java.io.IOException;
import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/user")
public class UserController {

	private static Logger logger = Logger.getLogger( UserController.class );

	@SuppressWarnings("unchecked")
	@ModelAttribute("users")
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public List listUsers() throws IOException {
        	PersistenceManager pm = PMF.getManager();
		Query query = pm.newQuery(User.class);
		List users;
		try {
			users = (List) query.execute();
		} finally {
			query.closeAll();
		}
		return users;
	}

	@ModelAttribute("user")
	@RequestMapping(value = "/", method = RequestMethod.POST)
	public User saveUser( @RequestBody User user ) throws IOException {
		logger.debug(user);
        	PersistenceManager pm = PMF.getManager();
	        try {
        	    pm.makePersistent(user);
	        } finally {
	            pm.close();
	        }
		return user;
	}

	@RequestMapping(value = "/{userId}", method = RequestMethod.DELETE )
	public void deleteUser( @PathVariable String userId ) throws IOException {
		deleteUserIntenral(userId);
	}
	
	@RequestMapping(value = "/{userId}/delete", method = RequestMethod.POST )
	public void deleteUser2( @PathVariable String userId ) throws IOException {
		deleteUserIntenral(userId);
	}

	private void deleteUserIntenral( String userId ) throws IOException {
		logger.debug("Deleting user id > "+userId);
        PersistenceManager pm = PMF.getManager();
        try {
        	Long id = Long.parseLong(userId);
        	User user = pm.getObjectById( User.class, id ); 
            pm.deletePersistent(user);
        } finally {
            pm.close();
        }		
	}
}

As you can see from the code above, saving and getting a list of Users is pretty trivial coding task. You may also notice that we added deleteUser(@PathVariable String userId) method to delete the user when a delete request is made with user id added to the end of the path. To delete the user object first we need to fetch it from the data store and only then ask persistence manager to delete it. We hav two ways to delte the user – sending DELETE requests and sending POST to “/api/user/{userId}/delete”. The post method is there to get around the shortcomings of Actions Scrip 3.

And one last change we need to make to support not so mature flex – to add crossdomain.xml file to /war of our project so flash player security implementation stops throwing security exceptions. Like I said before this adds nothing to the security, but annoys the hell out of the developers. Here is the contents:

<?xml version="1.0"?><!DOCTYPE cross-domain-policySYSTEM 
            "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <allow-access-from domain="*" to-ports="*"/>
    <allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

Step two- Create Flex project to consume the service we just upgraded

First create a Flex Web application project:
project_step_1

and then layout the UI to look like this:
project_step_2

And if you lazy like and want it all right now, here is the complete sudo code mxml file:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" 
      layout="absolute" creationComplete="init()">
  
  <mx:Script>
    <![CDATA[
      import mx.rpc.events.ResultEvent;
      import mx.rpc.events.FaultEvent;
      import mx.rpc.http.HTTPService;
      import mx.managers.PopUpManager;
      import json.JParser;
      import mx.controls.Alert;
      import mx.controls.List;
      
      [Bindable]
      private var users:Array;
      private var uriBase: String = "http://localhost:8080/api/user/";
      private var popupLabel:Label;

      private function init() : void {
        var myUrl : String = Application.application.url;
        if( myUrl.indexOf( "file://" ) != 0 ) 
          uriBase = myUrl.substring( 0, myUrl.lastIndexOf( "/" ) ) + "/api/user/" ;
        refreshUsers()      
      }
      
      private function addNew() : void {
        this.user_id.text = "";
        this.user_email.text = "";
        this.user_name.text = "";
      }

      private function updateEdit() : void {
        this.user_id.text =  this.usersList.selectedItem.id;
        this.user_name.text =  this.usersList.selectedItem.name;
        this.user_email.text =  this.usersList.selectedItem.email;
      }

      private function deleteUser() : void {
        
        if( usersList.selectedItem == null )
          return;
          
        showPopupWait();

        var service: HTTPService = new HTTPService();
        service.contentType = "application/json";
        service.method = "POST";
        service.resultFormat = "text";
        service.url = uriBase + usersList.selectedItem.id + "/delete" ;
        service.useProxy = false;

        service.addEventListener( FaultEvent.FAULT , function(event:FaultEvent):void {
                removePopup();
                Alert.show( event.message.toString() );
            } )      

        service.addEventListener( ResultEvent.RESULT , function(event:ResultEvent):void {
              refreshUsers();           
            } )      

        service.send();

      }
      
      private function saveUser() : void {
        showPopupWait();

        var service: HTTPService = new HTTPService();
        service.contentType = "application/json";
        service.method = "POST";
        service.resultFormat = "text";
        service.url = uriBase;
        service.useProxy = false;

        service.addEventListener( FaultEvent.FAULT , function(event:FaultEvent):void {
                removePopup();
                Alert.show( event.message.toString() );
            } )      

        service.addEventListener( ResultEvent.RESULT , function(event:ResultEvent):void {
              var response: Object = JParser.decode( event.message.body.toString() );
              user_id.text = response.user.id;
              refreshUsers();           
            } )      


        var user:Object = new Object();
        if( this.user_id.text.length > 0 )
          user.id = this.user_id.text
        user.name = this.user_name.text;
        user.email = this.user_email.text;
        
        var json:String = JParser.encode( user );
        var jsonData:ByteArray = new ByteArray();
        jsonData.writeUTFBytes(json);
        jsonData.position = 0;
        var contentType:String = "application/json";

        //client.post(uri, jsonData, contentType);
        
        service.send( jsonData );
      }

      private function refreshUsers() : void {
        showPopupWait();
        
        var service: HTTPService = new HTTPService();
        service.contentType = "application/json";
        service.method = "GET";
        service.resultFormat = "text";
        // add random number to make browser grab new list.... sux, but works.
        service.url = uriBase+"?"+ (new Date).getTime();
        service.useProxy = false;
        
        service.addEventListener( FaultEvent.FAULT , function(event:FaultEvent):void {
                removePopup();
                Alert.show( event.message.toString() );
            } )      

        service.addEventListener( ResultEvent.RESULT , function(event:ResultEvent):void {
              var userList: Object = JParser.decode( event.message.body.toString() );
              users = userList.users as Array ;
                removePopup();
            } )      
        
        service.send();        
      }

      public function showPopup( message:String ) : void {
        if( this.popupLabel == null ) {
          this.popupLabel = new Label();
          this.popupLabel.text =  message ;
          this.popupLabel.setStyle( "fontSize" , "22" );
          this.popupLabel.setStyle( "fontWeight" , "bold" );
          PopUpManager.addPopUp( this.popupLabel, this, true );
          PopUpManager.centerPopUp( this.popupLabel );
        }
      }
      public function showPopupWait() : void {
        this.showPopup( "Please wait.....");
      }
      public function removePopup() : void {
        if( this.popupLabel != null )
          PopUpManager.removePopUp( this.popupLabel );
        this.popupLabel = null;
      }

    ]]>
  </mx:Script>
  
  <mx:DataGrid x="10" y="35" id="usersList" dataProvider="{users}" 
       width="469" height="159" itemClick="updateEdit()">
    <mx:columns>
      <mx:DataGridColumn headerText="ID" dataField="id"/>
      <mx:DataGridColumn headerText="Email" dataField="email"/>
      <mx:DataGridColumn headerText="Name" dataField="name"/>
    </mx:columns>
  </mx:DataGrid>
  <mx:Label x="10" y="10" text="Users" fontSize="14" fontWeight="bold"/>
  <mx:Button x="487" y="59" label="Add New" click="addNew()"/>
  <mx:Button x="487" y="89" label="Delete" width="76" click="deleteUser()"/>
  <mx:Button x="487" y="119" label="Refresh" width="76" click="refreshUsers()"/>
  <mx:Form x="10" y="202" width="469">
    <mx:FormItem label="User ID">
      <mx:TextInput id="user_id" width="353" editable="false"/>
    </mx:FormItem>
    <mx:FormItem label="User Name">
      <mx:TextInput id="user_name" width="353"/>
    </mx:FormItem>
    <mx:FormItem label="User Email">
      <mx:TextInput id="user_email" width="353"/>
    </mx:FormItem>
    <mx:FormItem>
      <mx:Button label="Save" width="96" click="saveUser()"/>
    </mx:FormItem>
  </mx:Form>
  
</mx:Application>

To serialize objects to and from the server we need a JSON library. I chose JSwoof.swc – JSON serializer and parser. I found it to be a little better qauality then the one in as3corelib, but you can use either one.

Once you get the project compiling, start your google app engine project locally, mine is configured to listen on port 8080, and then you can run the flex project. For the lazy (like me) here is the full project all ready to go.

Conclusion

Adding persistence support in google app engine project is not hard, you just need to read documentation carefully.

The harder part is to get flex talking to these services. Like any closed source solution you are at mercy of company making it, like we see in this particular case. Unfortunately there are no better alternative to FLex for building nice looking af functional web application, we just need to deal with its shortcomings.

You can check out the working application at rest-json-flex.appspot.com

In step 3 we will add session management, user authentication and authorization to our google app engine REST application.

8 thoughts on “Goolge app engine + java + spring + REST + JSON + Flex – Part 2

  1. Perfect! we were stuck sending the json object to the rest service. We were not doing the setcontent(“application/json”) part. Thanks Dude!

  2. Hi,
    Great application!!!!

    I have a problem……When I try to build your file mxml (JsonRestClient.mxml) the builder notify some error on JParser : JParser.decode line 82 and 122, JParser.enconde lines 94 (“access undefined property”)

    Could you help me?

Leave a Reply

Your email address will not be published. Required fields are marked *