Friday, August 13, 2010

Using SmartGWT with Jersey RESTful backend on Spring Roo

I decided to give SmartGWT a run, and specifically the RestDataSource functionality using Jersey. To make things easier, I'm using Spring Roo to set up my project and scaffold much of the boilerplate code, configurations etc.

My approach is to have my domain objects exposed as RESTful web services and create a matching SmartGWT DataSource in order to show them in a GUI.

Let's get started, shall we?

First of all, you'll need Spring Roo (unless you just download the project source code and want to run it directly). I'm using the following Roo script to start things off:

project --topLevelPackage com.technowobble
persistence setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY

entity --class ~.domain.Message
field string --fieldName value

controller class --class ~.ws.MessageResource --preferredMapping rest

dependency add --groupId com.sun.jersey --artifactId jersey-server --version 1.3
dependency add --groupId com.sun.jersey.contribs --artifactId jersey-spring --version 1.3
dependency add --groupId com.smartgwt --artifactId smartgwt --version 2.2

gwt setup

You'll need to add the repositories for Jersey/SmartGWT manually in the genereated pom.xml:

<repository>
 <id>smartgwt</id>
 <url>http://www.smartclient.com/maven2</url>
</repository>
<repository>
    <id>maven2-repository.dev.java.net</id>
    <name>Java.net Repository for Maven</name>
    <url>http://download.java.net/maven/2/</url>
</repository>

Unfortunately the jersey-spring dependency depends on Spring 2.5.6, so that needs to be exluded (manually):

<dependency>
    <groupId>com.sun.jersey.contribs</groupId>
    <artifactId>jersey-spring</artifactId>
    <version>1.3</version>
    <!-- jersey-spring depends on Spring 2.5.6, so exluding as we're on 3.0.0X -->
    <exclusions>
 <exclusion>
     <groupId>org.springframework</groupId>
     <artifactId>spring</artifactId>
 </exclusion>
 <exclusion>
     <groupId>org.springframework</groupId>
     <artifactId>spring-core</artifactId>
 </exclusion>
 <exclusion>
     <groupId>org.springframework</groupId>
     <artifactId>spring-beans</artifactId>
 </exclusion>
 <exclusion>
     <groupId>org.springframework</groupId>
     <artifactId>spring-context</artifactId>
 </exclusion>
 <exclusion>
     <groupId>org.springframework</groupId>
     <artifactId>spring-web</artifactId>
 </exclusion>
 <exclusion>
     <groupId>org.springframework</groupId>
     <artifactId>spring-webmvc</artifactId>
 </exclusion>
    </exclusions>
</dependency>

Add the jersey-servlet to web.xml and the corresponding jersey-servlet.xml into webapp/WEB-INF/spring (in accordance to how Spring Roo does it).

<!-- Initialize the Jersey servlet -->
<servlet>
 <servlet-name>jersey</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/jersey-servlet.xml</param-value>
        </init-param>
 <load-on-startup>2</load-on-startup>        
</servlet>

<servlet-mapping>
 <servlet-name>jersey</servlet-name>
 <url-pattern>/rest/*</url-pattern>
</servlet-mapping>  

(The jersey-servlet.xml doesn't contain anything else than a component scan for Jersey web services).

We also have to update the urlrewrite.xml being used by Roo, to handle our servlet mapping of "/rest/*":

<rule enabled="true">
 <from casesensitive="false">/rest/**</from>
 <to last="true" type="forward">/rest/1</to>
</rule>    

In the generated Message domain object, add instructions to represent it in XML using an @XmlRootElement annotation:

package com.technowobble.domain;

import javax.persistence.Entity;
import javax.xml.bind.annotation.XmlRootElement;

import org.springframework.roo.addon.entity.RooEntity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.tostring.RooToString;

@XmlRootElement
@Entity
@RooJavaBean
@RooToString
@RooEntity
public class Message {

    private String value;
}


The only thing we're missing on the backend now is the actual implementation of our service... A RestDataSource e.g. expects a response like the following in response to a "fetch" request (taken from the JavaDoc):

 <response>
    <status>0</status>
    <startRow>0</startRow>
   
 <endRow>76</endRow>
    <totalRows>546</totalRows>
    <data>
     
 <record>
          <field1>value</field1>
          <field2>value</field2>
   
 </record>
      <record>
          <field1>value</field1>
         
 <field2>value</field2>
      </record>
      ... 75 total records ... 
   
 </data>
 </response>

I've created some POJOs for Jersey to marshall/unmarshall that would mimic this format in the com.technowobble.ws.util-package. Essentially, there's two abstract classes (DSRequest and DSResponse) containing all common attributes, and two specialized classes (MessageDSResponse, MessageDSRequest) handling the specific marshalling of each domain object. I'm not posting the entire source here, but ask you to look at it from the source code instead...

Before you comment on this - yes, there are probably a smarter way to do this using generics etc, but that's an improvement to be done in a real-world project!

With the xml-mapping in place, now it's time to look at the Jersey resource implementation:

package com.technowobble.ws;

import java.util.Collection;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.apache.commons.beanutils.BeanUtils;
import org.springframework.stereotype.Component;

import com.technowobble.domain.Message;
import com.technowobble.ws.util.DSResponse;
import com.technowobble.ws.util.MessageDSRequest;
import com.technowobble.ws.util.MessageDSResponse;
import com.technowobble.ws.util.OperationType;

/**
 * Jersey resource for a SmartGWT {@link MessageDS}.
 * <p>
 * 
 * @see http://docs.sun.com/app/docs/doc/820-4867/ggnxo?l=en&a=view
 * @see http://blogs.sun.com/enterprisetechtips/entry/jersey_and_spring
 */
@Component
@Path("/message")
public class MessageResource {
 @Produces( { MediaType.APPLICATION_XML })
 @Consumes( { MediaType.TEXT_XML })
 @POST
 @Path("/add")
 public MessageDSResponse create(MessageDSRequest request) {
  MessageDSResponse response = new MessageDSResponse();
  
  if (request.getOperationType() != OperationType.ADD || request.getMessages().size() != 1) {
   response.setStatus(DSResponse.STATUS_FAILURE);
  } else {
   Message message = request.getMessages().iterator().next();   
 
   try {
    // create the message
    message.persist(); 
    response.addMessage(message);
    
    response.setStatus(DSResponse.STATUS_SUCCESS);
   } catch (Exception e) {
    response.setStatus(DSResponse.STATUS_FAILURE);
    e.printStackTrace();
   }   
  }
  
  return response;
 }

 @Produces( { MediaType.APPLICATION_XML })
 @Consumes( { MediaType.TEXT_XML })
 @POST
 @Path("/update")
 public MessageDSResponse update(MessageDSRequest request) {
  MessageDSResponse response = new MessageDSResponse();
  
  if (request.getOperationType() != OperationType.UPDATE || request.getMessages().size() != 1) {
   response.setStatus(DSResponse.STATUS_FAILURE);
  } else {
   try {
    Message data = (Message) request.getMessages().iterator().next();
    Message message = Message.findMessage(data.getId());
    BeanUtils.copyProperties(message, data);
    message.merge();
    response.addMessage(message);
    
    response.setStatus(DSResponse.STATUS_SUCCESS);
   } catch (Exception e) {
    response.setStatus(DSResponse.STATUS_FAILURE);
    e.printStackTrace();
   }
  }
      
  return response;
 }

 @Produces( { MediaType.APPLICATION_XML })
 @Consumes( { MediaType.TEXT_XML })
 @POST
 @Path("/remove")
 public MessageDSResponse delete(MessageDSRequest request) {
  MessageDSResponse response = new MessageDSResponse();
  
  if (request.getOperationType() != OperationType.REMOVE || request.getMessages().size() != 1) {
   response.setStatus(DSResponse.STATUS_FAILURE);
  } else {
   try {
    Message data = request.getMessages().iterator().next();
    Message message = Message.findMessage(data.getId());
    message.remove();
    response.addMessage(message);
    
    response.setStatus(DSResponse.STATUS_SUCCESS);
   } catch (Exception e) {
    response.setStatus(DSResponse.STATUS_FAILURE);
    e.printStackTrace();
   }
  }
      
  return response;
 }

 @POST
 @Produces( { MediaType.APPLICATION_XML})
 @Consumes( { MediaType.TEXT_XML })
 @Path("/read")
 public MessageDSResponse read(MessageDSRequest request) {
  MessageDSResponse response = new MessageDSResponse();
  
  response.setStartRow(request.getStartRow());
  
  if (request.getOperationType() != OperationType.FETCH) {
   response.setStatus(DSResponse.STATUS_FAILURE);
  } else {
   try {
    Collection<Message> messages = Message.findMessageEntries(request.getStartRow(), 1 + (request.getEndRow() - 

request.getStartRow()));
    long count = Message.countMessages();
    response.setEndRow(response.getStartRow()+messages.size()-1);
    response.setTotalRows((int)count);
    for (Message message : messages) {
     response.addMessage(message);
    }
   } catch (Exception e) {
    response.setStatus(DSResponse.STATUS_FAILURE);
   }
   
   response.setStatus(DSResponse.STATUS_SUCCESS);
  }
  
  return response;
 }
}

As you can see, there's one method per CRUD-reqeust from the SmartGWT RestDataSource (add, update, remove, read). They all basically parse the
incoming DSRequest and answers with a DSResponse, according to the specs of SmartGWT.

Now that the backend is in place, let's create a small GWT application to take it for a test drive!

By using Roo, there are some things to consider when adding a new module to the project, which I have blogged about previously. As with any other module, create a module file (in our case Application.gwt.xml), a host page (Application.html), configure the gwt-maven-plugin to use it (in pom.xml) and add two rules to urlrewrite.xml...

With that in place, let's have a look at the entry-point class:

package com.technowobble.gwt.client;

import com.google.gwt.core.client.EntryPoint;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.data.RestDataSource;
import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.RowEndEditAction;
import com.smartgwt.client.widgets.IButton;
import com.smartgwt.client.widgets.grid.ListGrid;
import com.smartgwt.client.widgets.grid.ListGridField;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import com.smartgwt.client.widgets.layout.HLayout;
import com.smartgwt.client.widgets.layout.VLayout;
import com.technowobble.gwt.client.datasources.MessageDS;

/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class Application implements EntryPoint {
 /**
  * This is the entry point method.
  */
 public void onModuleLoad() {

  VLayout layout = new VLayout(15);
  layout.setAutoHeight();

  RestDataSource dataSource = new MessageDS();

  final ListGrid messageGrid = new ListGrid();
  messageGrid.setHeight(300);
  messageGrid.setWidth(500);
  messageGrid.setTitle("Messages");
  messageGrid.setDataSource(dataSource);
  messageGrid.setAutoFetchData(true);
  messageGrid.setCanEdit(true);
  messageGrid.setCanRemoveRecords(true);
  messageGrid.setListEndEditAction(RowEndEditAction.NEXT);

  ListGridField idField = new ListGridField("id", "Id", 40);
  idField.setAlign(Alignment.LEFT);
  ListGridField messageField = new ListGridField("value", "Message");
  messageGrid.setFields(idField, messageField);

  layout.addMember(messageGrid);

  HLayout hLayout = new HLayout(15);

  IButton updateButton = new IButton("Add message");
  updateButton.addClickHandler(new com.smartgwt.client.widgets.events.ClickHandler() {
   public void onClick(com.smartgwt.client.widgets.events.ClickEvent event) {
    Record message = new ListGridRecord();
    message.setAttribute("value", "...");
    messageGrid.addData(message);
   }
  });
  
  hLayout.addMember(updateButton);

  layout.addMember(hLayout);
  layout.draw();
 }
}

All in all, a ListGrid making use of a customized RestDataSource (MessageDS) - which might be more interesting than the actual code above...

Let's have a look at it:

package com.technowobble.gwt.client.datasources;

import com.smartgwt.client.data.OperationBinding;
import com.smartgwt.client.data.RestDataSource;
import com.smartgwt.client.data.fields.DataSourceTextField;
import com.smartgwt.client.types.DSOperationType;
import com.smartgwt.client.types.DSProtocol;
import com.technowobble.domain.Message;

/**
 * SmartGWT datasource for accessing {@link Message} entities over http in a RESTful manner.
 */
public class MessageDS extends RestDataSource {
 
 public MessageDS() {
  setID("MessageDS");
  DataSourceTextField messageId = new DataSourceTextField("id");
  messageId.setPrimaryKey(true);
  messageId.setCanEdit(false);
  
  DataSourceTextField messageValue = new DataSourceTextField("value");
  setFields(messageId, messageValue);
  
  OperationBinding fetch = new OperationBinding();
  fetch.setOperationType(DSOperationType.FETCH);
  fetch.setDataProtocol(DSProtocol.POSTMESSAGE);
  OperationBinding add = new OperationBinding();
  add.setOperationType(DSOperationType.ADD);
  add.setDataProtocol(DSProtocol.POSTMESSAGE);
  OperationBinding update = new OperationBinding();
  update.setOperationType(DSOperationType.UPDATE);
  update.setDataProtocol(DSProtocol.POSTMESSAGE);
  OperationBinding remove = new OperationBinding();
  remove.setOperationType(DSOperationType.REMOVE);
  remove.setDataProtocol(DSProtocol.POSTMESSAGE);
  setOperationBindings(fetch, add, update, remove);
    
  setFetchDataURL("rest/message/read");
  setAddDataURL("rest/message/add");
  setUpdateDataURL("rest/message/update");
  setRemoveDataURL("rest/message/remove");
 }
}

No surprises here... The RestDataSource is configured to match the operations of the RESTful backend, including the properties of the domain object we're exposing.

Done already? Then download the full source code here and give it a try!

"mvn jetty:run-war" should do the trick, although on my Windows machine I have to force a gwt-compilation using "mvn package gwt:compile jetty:run-war"

Enjoy!