Smart GWT + Restful Spring MVC

I’ve been a fan of REST-ful development for a while now. Developing an application with a REST API layer will allow it to integrate effortlessly with other external applications. It also allows for really easy separation of view and server technologies. Usually the best way to accomplish this is using the Model and Controller parts of Spring MVC. You can find a small tutorial on using Spring MVC in a RESTful way here.

On the other hand, I’m also a fan of Smart GWT. It’s suite of widgets makes it a breeze to put together a business application in an environment where the app will be maintained by other Java developers.

It is difficult to integrate Spring with Smart GWT Datasource objects, due to the fact that most Datasource implementations do not use standard REST calls and expect very specific responses. So lets go step by step and get our Smart GWT view talking to our REST-ful Spring Controller.

So first, I assume that the reader already knows how to set up an annotation-driven Spring project. Lets create a controller that interacts with Person objects.

public class Person implements Serializable {
 private Integer id;
 private String name;

 public Integer getId() {
  return id;
 }

 public void setId(Integer id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }
}
@Controller
@RequestMapping("/person")
public class PersonController {
 @RequestMapping(value="/", method=RequestMethod.GET)
 public @ResponseBody List<Person> getAllPersons() {
  // Return a list of Person objects
 }

 @RequestMapping(value="/", method=RequestMethod.PUT)
 public @ResponseBody Person updatePerson(@ModelAttribute Person person) {
  // The person object will only have the fields that are
  // being updated populated + the primary key.
  // The method should return a full object with the same primary key.
 }

 @RequestMapping(value="/", method=RequestMethod.POST)
 public @ResponseBody Person createPerson() {
  // This should create a new person with a new primary key
 }

 @RequestMapping(value="/id/{id}", method=RequestMethod.DELETE)
 public @ResponseBody Person deletePerson(@PathVariable("id") Integer id) {
  // This should delete a person and return the deleted person object
 }
}

Now, once we have a proper controller, lets hook up GWT Datasource to properly interact with our REST-ful Person controller. GWT has a special RestDatasource that is already set up to interact with custom URLs. However, by default it expects very specific URLs and very specific request methods. By default it is not configured to use PUT and DELETE methods. Only POSTs and GETs are supported out of the box. Thankfully, there are also a lot of configuration options that are built into this class, and we can modify it to fit our purposes. Lets extend the GWT RestDataSource to make restful calls to Spring controllers, and abstract that logic away so that we can easily create new datasources that can interact with Spring controllers in REST-ful fashion.

public abstract class AbstractRestDataSource extends RestDataSource {
 public AbstractRestDataSource(String id) {
  setID(id);
  setClientOnly(false);

  //set up FETCH to use GET requests
  OperationBinding fetch = new OperationBinding();  
  fetch.setOperationType(DSOperationType.FETCH);
  DSRequest fetchProps = new DSRequest();
  fetchProps.setHttpMethod("GET");
  fetch.setRequestProperties(fetchProps);

  //set up ADD to use POST requests
  OperationBinding add = new OperationBinding();  
  add.setOperationType(DSOperationType.ADD);  
  add.setDataProtocol(DSProtocol.POSTPARAMS);

  //set up UPDATE to use PUT 
  OperationBinding update = new OperationBinding();  
  update.setOperationType(DSOperationType.UPDATE);  
  DSRequest updateProps = new DSRequest();
  updateProps.setHttpMethod("PUT");
  update.setRequestProperties(updateProps);

  //set up REMOVE to use DELETE
  OperationBinding remove = new OperationBinding();  
  remove.setOperationType(DSOperationType.REMOVE);  
  DSRequest removeProps = new DSRequest();
  removeProps.setHttpMethod("DELETE");
  remove.setRequestProperties(removeProps);  

  //apply all the operational bindings
  setOperationBindings(fetch, add, update, remove);

  init();
 }

 @Override
 protected Object transformRequest(DSRequest request) {
  super.transformRequest(request);

  //now post process the request for our own means
  postProcessTransform(request);

  return request.getData();
 }

 /*
  * Implementers can override this method to create a 
  * different override.
  */
 @SuppressWarnings("rawtypes")
 protected void postProcessTransform(DSRequest request) {
  StringBuilder url = new StringBuilder(getServiceRoot());

  Map dataMap = request.getAttributeAsMap("data");
  if(request.getOperationType() == DSOperationType.REMOVE) {
   //in case of remove, append the primary key
   url.append(getPrimaryKeyProperty()).append("/").
    append(dataMap.get(getPrimaryKeyProperty()));
  } else if(request.getOperationType() == DSOperationType.UPDATE) {
   appendParameters(url, request);
  }

  request.setActionURL(URL.encode(url.toString()));
 }

 /*
  * This simply appends parameters that have changed to the URL
  * so that PUT requests go through successfully. This is usually
  * necessary because when smart GWT updates a row using a form,
  * it sends the data as form parameters. Most servers cannot
  * understand this and will simply disregard the form data
  * sent to the server via PUT. So we need to transform the form
  * data into URL parameters.
  */
 @SuppressWarnings("rawtypes")
 protected void appendParameters(StringBuilder url, DSRequest request) {
  Map dataMap = request.getAttributeAsMap("data");
  Record oldValues = request.getOldValues();
  boolean paramsAppended = false;

  if(!dataMap.isEmpty()) {
   url.append("?");
  }

  for(Object keyObj : dataMap.keySet()) {
   String key = (String) keyObj;
   if(!dataMap.get(key).equals(oldValues.getAttribute(key)) || isPrimaryKey(key)) {
    //only append those values that changed or are primary keys
    url.append(key).append('=').append(dataMap.get(key)).append('&');
    paramsAppended = true;
   } 
  }

  if(paramsAppended) {
   //delete the last '&'
   url.deleteCharAt(url.length()-1);
  }
 }

 private boolean isPrimaryKey(String property) {
  return getPrimaryKeyProperty().equals(property);
 }
 
 /*
  * The implementer can override this to change the name of the
  * primary key property.
  */
 protected String getPrimaryKeyProperty() {
  return "id";
 }

 protected abstract String getServiceRoot();
 protected abstract void init();
}

Now we’ve set up an abstract REST-ful DataSource that will talk to a Spring controller using all 4 REST methods. Now all we need to do is implement it and we can hook it up to any ListGrid or other Smart GWT widgets and ADD, UPDATE, FETCH and REMOVE will correctly correspond to our Spring controller’s methods.

public class PersonDataSource extends AbstractRestDataSource {
 private static PersonDataSource instance = null;
 
 public static PersonDataSource getInstance() {
  if(instance == null) {
   instance = new PersonDataSource("personEditDS");
  }
  
  return instance;
 }
 
 private PersonDataSource(String id) {
  super(id);
 }
 
 protected void init() {
  setDataFormat(DSDataFormat.JSON);
  setJsonRecordXPath("/");
  
  DataSourceField idField = new DataSourceField("id", FieldType.INTEGER, "ID");
  idField.setPrimaryKey(true);
  idField.setCanEdit(false);
  DataSourceField nameField = new DataSourceField("name", FieldType.TEXT, "Name");
  
  setFields(idField, nameField);
 }

 @Override
 protected String getServiceRoot() {
  return "person/";
 }
}

Now that everything is in order, we can simply hook it up to a ListGrid and edit, add, and delete rows and it will work successfully with our controller.

About these ads
This entry was posted in Integration and tagged , , , , , , , . Bookmark the permalink.

16 Responses to Smart GWT + Restful Spring MVC

  1. papirosko says:

    your code doesn’t deal with null values. check
    if(!dataMap.get(key).equals(oldValues.getAttribute(key)) || isPrimaryKey(key)) {
    condition.

    Next. Your appendParameters can be skipped, see
    https://jira.springsource.org/browse/SPR-7030?focusedCommentId=52425&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-52425

    And finally, when posting null values, they are sent as string “null”, causing controller to set pojo field value to string “null”. I’m thinking how to avoid this.

  2. papirosko says:

    @Override
    protected Object transformRequest(DSRequest request) {
    super.transformRequest( request );

    //now post process the request for our own means
    postProcessTransform( request );

    Map dataToSend = new HashMap( );
    Map dataMap = request.getAttributeAsMap( “data” );
    for ( Object keyObj : dataMap.keySet() ) {
    if (dataMap.get( keyObj )!=null) {
    dataToSend.put( keyObj, dataMap.get( keyObj ) );
    }
    }

    return JSOHelper.convertMapToJavascriptObject( dataToSend );
    }

    this code skips null values in submit

  3. Urahara Kisuke says:

    Hy,
    First, thanks a lot for this work.
    The POST method configuration of the AbstractRestDataSource isn’t missing something ?
    I’ve used this code but i’m facing some problems regarding any POST query.

    Thanks,

    • vkubushyn says:

      The POST should actually function exactly as it would by default. Since we’re still doing a POST and stil adding the whole object to be serialized. That’s why there’s no configuration for a post request.

  4. Steve says:

    Thanks for the hugely useful post. However, I am facing issues sending java.util.Date objects from SmartGWT client to the server. The server cannot convert between the String representation of the date (I have tried yyyy-MM-dd – yyyy-MM-dd’T’HH:mm:ssZ – yyyyMMddHHmmssZ ), but it throws

    org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors Field error in object ‘comment’ on field ‘dateAdded’: rejected value [2012-06-27T10:57:47+0100]; codes [typeMismatch.comment.dateAdded,typeMismatch.dateAdded,typeMismatch.java.util.Date,typeMismatch]; arguments

    I have setup a customObjectMapper so I am not transferring milliseconds and I made sure I’m sending the same datetime format between the client and the server.

    Have you or has anyone else faced this issue, as I have been trying to fix it for several days?

    Thanks,
    Steve

    • Steve says:

      I’m still not having any luck with this :-(

      One thing I’ve tried, but didn’t work, was to modify transformRequest in AbstractDataSource to set the String representation of the date object to a format which should be able to create a date on the server side:

      import com.google.gwt.i18n.client.DateTimeFormat;

      public static DateTimeFormat DATE_FORMATTER = DateTimeFormat.getFormat(“yyyy-MM-dd’T’HH:mm:ssZ”); // I’ve tried various different formats here

      @Override
      protected Object transformRequest(DSRequest request) {
      super.transformRequest(request);

      // now post process the request for our own means
      postProcessTransform(request);

      Map dataToSend = new HashMap();
      Map dataMap = request.getAttributeAsMap(“data”);
      for (Object keyObj : dataMap.keySet()) {
      if (dataMap.get(keyObj) != null) {
      if (dataMap.get(keyObj) instanceof java.util.Date)
      dataToSend.put(keyObj, DATE_FORMATTER.format((java.util.Date)dataMap.get(keyObj)));
      else
      dataToSend.put(keyObj, dataMap.get(keyObj));
      }
      }

      return JSOHelper.convertMapToJavascriptObject(dataToSend);
      }

      • vkubushyn says:

        Hey Steve,

        I think the following post will help you line up the Spring Controller’s Date parsing with the way the String is sent over from the GWT client. Create the initBinder method in your controller so that you know exactly the format of the date it expects, and then send it over from your RestDataSource using that format.

        http://linkedjava.blogspot.com/2011/06/spring-controller-with-date-object.html

        Please let me know if that helped or not!

        Thanks,
        Vitaliy.

      • Steve says:

        Thanks Vitaliy – that worked a treat! Awesome! :-D

        I have another query, which I have been trawling the web looking for an answer… I don’t want to chance my luck, but perhaps this is something you have seen?

        I am trying to code good unit tests, so I have a jetty plugin in maven which starts up the application and I have a JUnit class which tries to make Restful calls into the Controller class. The URL is getting hit, but all the fields are NULL. I think it’s to do with my RestTemplate calls. I’ve tried – queryForObject, queryForEntity, postForObject, put, but no luck

        e.g.

        @Test
        public void testAddComment() throws Exception {
        log.info(“Running testAddComment.. “);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        Comment inputComment = new Comment();
        inputComment.setDealRef(DEAL_REF);
        inputComment.setComment(“test comment”);
        inputComment.setUserAdded(“sharper”);
        inputComment.setDateAdded(new Date());

        HttpEntity entity = new HttpEntity(inputComment,headers);
        Comment outputComment = restTemplate.postForObject(BASE_URL, entity, Comment.class);

        assertNotNull(outputComment.getId());
        COMMENT_ID = outputComment.getId();
        assertEquals(DEAL_REF, outputComment.getDealRef());
        assertEquals(“test comment”, outputComment.getComment());
        assertEquals(inputComment.getDateAdded(), outputComment.getDateAdded());
        assertEquals(“sharper”, outputComment.getUserAdded());

        When I run this I get the following:

        Failed tests: testAddComment(com.jpmorgan.creditriskreporting.server.service.R
        estCommentTest): expected: but was:

        And in my log:

        237692 [btpool0-0] INFO com.jpmorgan.creditriskreporting.server.controller.Comme
        ntController – calling createComment with comment: Comment [id=0, dealRef=null,
        comment=null, userAdded=null, dateAdded=null, userModified=null, dateModified=nu
        ll]

        Similar for gets and puts too.

        Thanks again,
        Stephen

      • vkubushyn says:

        Hey Steve,

        So this can be caused by a number of issues. If I were you I’d step into the RestTemplate.postForObject call and see what JSON/XML your controller returns and see why it’s not being converted properly into your Comment class.

        Overall, I usually don’t test Controllers via a test web server. I just write simple unit tests for them to test business logic. My reasoning is that Spring is so extensively tested that you can trust the @Controller annotation to work properly to set up your calls. The only way it could be messed up is with configuration. Having said that, it’s not a bad idea to test the way you do, since the JUnit ensures that no configuration error impacted your controller from standing up properly.

        Sorry to not be of a lot of help, but there is just a whole slew of things that could be causing the behavior you are seeing.

        Thanks,
        Vitaliy.

  5. Steve says:

    Hi Vitaliy,

    Thanks for another quick reply (I’ve been away for a few days so just seen it now). I take your point re not testing Controller classes using test web server. I would rather get it working though, so if anyone else has any pointers please let me know.

    Cheers,
    Stephen

  6. Tom Holmes Jr. says:

    This has been very helpful. I know Spring and a lot of SmartGWT, but have been using it with GWT-RPC for awhile now, and want to do it in the proper REST-ful way. I was looking for a complete CRUD example with Spring MVC, do you have such an example? I am trying to put together an opensource CRUD phonebook framework, and this fits into part of what I am doing.
    Thanks for any help!

  7. khush says:

    Hi,

    getting error
    JSONresponse text is incorrectly formatted as an Array rather than a simple response object.

    while trying to return a list of account from controller. could you please help on this.

    thanks

  8. Good info. Lucky me I found your site by accident (stumbleupon).
    I’ve book-marked it for later!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s