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!

13 comments:

  1. I'm in the process of trying out smartgwt with grails but roo is also on my wish-list. This will be a good help to get starting. Thanks for posting!

    ReplyDelete
  2. I am currently exploring the idea to use SmartGWT, Spring restful, and Spring roo for my project. This post is great which give me an example on how other people are doing it. However, I have couple questions:
    Would you consider to use the JSON instead of XML as your message protocol? Spring 3 has JSON restful support out of box. I see that you are creating controller class with Spring roo command. Can you just have multiple @RequestMapping in the controller to handle additional urls?

    Or maybe I am missing something important in your example.

    Thanks.

    ReplyDelete
  3. Yes, I would definitely consider json, but I guess you'd still need to create a mapping that fits the structure of a JSONified RestDataSource from SmartGWT (if there is one).

    Regarding the controller class, I'm only creating one controller that acts as a Jersey resource, with different @Path-annotation. It would be exactly the same approach with @RequestMapping-annotations. If you're thinking of depending on Roo, the only thing you get for free is a json-representation of you business objects, which won't map to what a RestDataSource in SmartGWT is expecting.

    Anyway, good luck with the experiment!

    ReplyDelete
  4. Hi Mattias,
    I'm very much new to Springsource & Roo.
    And I am starting a project with smartgwt & gae. But came across certain hindrances when running both gwt & gae. I have the frontend SmartGWT app working independently by itself. But there's not backend capabilities to it yet, for CRUD operations :(

    Hence I'm looking for alternatives & found ROO as shown in google i/o

    The tutorial looks very helpful, thanks.

    But I'm not able to run the project nor know how to go about when I do go thru this article step by step. Can you please mail me a direct project which I can import into STS which has the whole project & how to run the app. So i can get a starting point & tailor my existing UI.

    All I need a simple CRUD operation with SmartGWT

    I would really appreciate the help, cause this project is very much important as a major part of my school work.

    My contact details is on my website
    http://www.supreeth.net

    Hope to hear from you soon :)

    Thanks,
    Supreeth

    ReplyDelete
  5. Hi, I can't really give you free consultancy hours, nor do I have the time to do so.

    My advise is to learn how to use Maven and download the source code from the link in my post.

    You might also be able to import the source code into STS using "Import Maven project".

    ReplyDelete
  6. i'm working on a project where i want a rest backend that i can use for web presentation and as a service-like layer for an android app. except for the addition of spring-security and sprind data jpa this is exactly what i'm going for.

    you saved me a lot of time, thanks :-)

    /janne

    ReplyDelete
  7. Neat. Looks to be something I am trying to implement as well (SmartGWT over Roo).
    One Question-
    I've never worked with Jersey, so please help me- what did you gain by having Jersey being thrown into the mix? To avoid using both GWT and MVC controllers?

    Also, the link to your source code is broken (at least to me).

    ReplyDelete
  8. Hi, I've decided to use Jersey to give it a spin, and due to the fact that it's a standard framework for creating RESTful applications.

    The link works for me.

    ReplyDelete
  9. this is NOTHING! LOOK FOR X7CLOUD!

    ReplyDelete
  10. can you give a simple example for smartgwt+spring?

    ReplyDelete
  11. No, sorry. It shouldn't be too hard to port this to a 100% Spring backend, instead of Jersey, but I really don't have the time to do this.

    ReplyDelete
  12. Thank you this is a nice post. I've tried this approach and it work well as long as on the smartgwt client side you have grid.setAutoSaveEdits(true);
    Because if you don't, the xml serialized POST request to the server won't be parsed by JAXB since the struct of the xml will ressembling this:



    KLJKLJLKJ

    DummyDS
    add
    isc_ListGrid_1




    UIYUIYUI

    DummyDS
    add
    isc_ListGrid_1



    The annoying thing is that if you edit a single object you won't the transaction wrapping so the XXXXDSRequest cannot deal with the two different cases.
    I wonder whether there is a solution to allow
    grid.setAutoSaveEdits(false);

    ReplyDelete