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.
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.
Good points. I think appendParameters is still valuable if you don’t want to resort to the workarounds suggested in the link you provided.
@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
Great catch! Thanks for looking at this
.
thanks for this improvement. i had to switch the order of the following statements before data changes get posted to my desired serviceRoot-URL:
super.transformRequest( request );
processTransform( request );
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,
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.
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
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);
}
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.
Thanks Vitaliy – that worked a treat! Awesome!
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
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.
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