Earlier this week, Hiram Chirino released RestyGWT, a GWT generator for REST services and JSON encoded data transfer objects. You can read more about it in Hiram's post RestyGWT, a Better GWT RPC??. First of all, I'm impressed with RestyGWT because provides something I've always wanted with GWT: the ability to call RESTful services and get a populated POJO in your callback, much like AsyncCallback provides for RPC services.
RestyGWT also allows you to easily create services using only interfaces and JAX-RS annotations. For example:
import javax.ws.rs.POST;
...
public interface PizzaService extends RestService {
@POST
public void order(PizzaOrder request, MethodCallback<OrderConfirmation> callback);
}
After taking a brief look at RestyGWT, I thought it'd be interesting to share how I develop and test GWT client services.
Developing GWT Client Services
Writing services in a GWT application can be helpful when you're using MVP, especially since you can EasyMock them in a test. On my GWT projects, I've often used overlay types because they allow me to write less code and they make parsing JSON super simple.
I've had issues testing my presenters when using overlay types. The good news is I think I've figured out a reasonable solution, but it does require using GWTTestCase. If RestyGWT supported overlay types, there's a good chance I'd use it, especially since its integration tests seem to require GWTTestCase too.
Rather than using callbacks in my presenters, I try to only use them in my service implementations. That way, my presenters don't have to worry about overlay types and can be tested in a JUnit-only fashion. The callbacks in my services handle JSON parsing/object population and fire events with the populated objects.
GWT's RequestBuilder is one option for communicating with RESTful services. The Development Guide for HTTP Requests explains how to use this class. To simplify REST requests and allow multiple callbacks, I'm using a RestRequest class, and a number of other utility classes that make up a small GWT REST framework (created by a former colleague). RestRequest wraps RequestBuilder and provides a Fluent API for executing HTTP requests. Another class, Deferred, is a GWT implementation of Twisted's Deferred.
As part of my service implementation, I inject an EventBus (with GIN) into the constructor and then proceed to implement callbacks that fire Events to indicate loading, saving and deleting has succeeded. Here's an example service:
public class ConversationServiceImpl implements ConversationService {
private EventBus eventBus;
@Inject
public ConversationServiceImpl(EventBus eventBus) {
this.eventBus = eventBus;
}
public void getConversation(String name) {
Deferred<Representation> d =
RestRequest.get(URLs.CONVERSATION + "/" + URL.encode(name)).build();
d.addCallback(new Callback<Representation>() {
public void onSuccess(Representation result) {
Conversation conversation = convertResultToConversation(result);
eventBus.fireEvent(new ResourceLoadedEvent<Conversation>(conversation));
}
});
d.run();
}
public void saveConversation(Conversation conversation) {
Deferred<Representation> d = RestRequest.post(URLs.CONVERSATION)
.setRequestData(conversation.toJson()).build();
d.addCallback(new Callback<Representation>() {
public void onSuccess(Representation result) {
Conversation conversation = convertResultToConversation(result);
eventBus.fireEvent(new ResourceSavedEvent<Conversation>(conversation));
}
});
d.run();
}
public void deleteConversation(Long id) {
Deferred<Representation> d =
RestRequest.post(URLs.CONVERSATION + "/" + id).build();
d.addCallback(new Callback<Representation>() {
public void onSuccess(Representation result) {
eventBus.fireEvent(new ResourceDeletedEvent());
}
});
d.run();
}
/**
* Convenience method to populate object in one location
*
* @param result the result of a resource request.
* @return the populated object.
*/
private Conversation convertResultToConversation(Representation result) {
JSOModel model = JSOModel.fromJson(result.getData());
return new Conversation(model);
}
}
In the saveConversation() method you'll notice the conversation.toJson() method call. This method uses a JSON class that loops through an objects properties and constructs a JSON String.
public JSON toJson() {
return new JSON(getMap());
}
Testing Services
In my experience, the hardest part about using overlay types is writing your objects so they get populated correctly. I've found that writing tests which read JSON from a file can be a great productivity boost. However, because of overlay types, you have to write a test that extends GWTTestCase. When using GWTTestCase, you can't simply read from the filesystem. The good news is there is a workaround where you can subclass GWTShellServlet and overwrite GWT's web.xml to have your own servlet that can read from the filesystem. A detailed explanation of how to do this was written by Alex Moffat in Implementing a -noserver flag for GWTTestCase.
Once this class is in place, I've found you can easily write services using TDD and the server doesn't even have to exist. When constructing services, I've found the following workflow to be the most productive:
- Create a file with the expected JSON in src/test/resources/resource.json where resource matches the last part of the URL for your service.
- Create a *ServiceGwtTest.java and write tests.
- Run tests to make sure they fail.
- Implement the service and run tests to ensure JSON is getting consumed/produced properly to/from model objects.
Below is the code for my JsonReaderServlet.java:
public class JsonReaderServlet extends GWTShellServlet {
public void service(ServletRequest servletRequest, ServletResponse servletResponse)
throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
String uri = req.getRequestURI();
if (req.getQueryString() != null) {
uri += "?" + req.getQueryString();
}
if (uri.contains("/services")) {
String method = req.getMethod();
String output;
if (method.equalsIgnoreCase("get")) {
// use the part after the last slash as the filename
String filename = uri.substring(uri.lastIndexOf("/") + 1, uri.length()) + ".json";
System.out.println("loading: " + filename);
String json = readFileAsString("/" + filename);
System.out.println("loaded json: " + json);
output = json;
} else {
// for posts, return the same body content
output = getBody(req);
}
PrintWriter out = resp.getWriter();
out.write(output);
out.close();
resp.setStatus(HttpServletResponse.SC_OK);
} else {
super.service(servletRequest, servletResponse);
}
}
private String readFileAsString(String filePath) throws IOException {
filePath = getClass().getResource(filePath).getFile();
BufferedReader reader = new BufferedReader(new FileReader(filePath));
return getStringFromReader(reader);
}
private String getBody(ServletRequest request) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
return getStringFromReader(reader);
}
private String getStringFromReader(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder();
char[] buf = new char[1024];
int numRead;
while ((numRead = reader.read(buf)) != -1) {
sb.append(buf, 0, numRead);
}
reader.close();
return sb.toString();
}
}
This servlet is mapped to <url-pattern>/*</url-pattern>
in a web.xml file in src/test/resources/com/google/gwt/dev/etc/tomcat/webapps/ROOT/WEB-INF.
My Service Test starts by getting an EventBus from GIN and registering itself to handle the fired events.
public class ConversationServiceGwtTest extends AbstractGwtTestCase
implements ResourceLoadedEvent.Handler, ResourceSavedEvent.Handler, ResourceDeletedEvent.Handler {
ConversationService service;
ResourceLoadedEvent<Conversation> loadedEvent;
ResourceSavedEvent<Conversation> savedEvent;
ResourceDeletedEvent deletedEvent;
@Override
public void gwtSetUp() throws Exception {
super.gwtSetUp();
DesigntimeGinjector injector = GWT.create(MyGinjector.class);
EventBus eventBus = injector.getEventBus();
service = new ConversationServiceImpl(eventBus);
eventBus.addHandler(ResourceLoadedEvent.ENGINE, this);
eventBus.addHandler(ResourceSavedEvent.ENGINE, this);
eventBus.addHandler(ResourceDeletedEvent.ENGINE, this);
}
@SuppressWarnings("unchecked")
public void onLoad(ResourceLoadedEvent event) {
this.loadedEvent = event;
}
@SuppressWarnings("unchecked")
public void onSave(ResourceSavedEvent event) {
this.savedEvent = event;
}
public void onDelete(ResourceDeletedEvent event) {
this.deletedEvent = event;
}
}
After this groundwork has been done, a test can be written that loads up the JSON file and verifies the objects are populated correctly.
public void testGetConversation() {
service.getConversation("test-conversation");
Timer t = new Timer() {
public void run() {
assertNotNull("ResourceLoadedEvent not received", loadedEvent);
Conversation conversation = loadedEvent.getResource();
assertEquals("Conversation name is incorrect","Test Conversation", conversation.getName());
assertNotNull("Conversation has no channel", conversation.getChannel());
assertEquals("Conversation has incorrect task size", 3, conversation.getTasks().size());
convertToAndFromJson(conversation);
finishTest();
}
};
delayTestFinish(3000);
t.schedule(100);
}
private void convertToAndFromJson(Conversation fromJsonModel) {
Representation json = fromJsonModel.toJson();
assertNotNull("Cannot convert empty JSON", json.getData());
// change back into model
JSOModel data = JSOModel.fromJson(json.getData());
Conversation toJsonModel = new Conversation(data);
verifyModelBuiltCorrectly(toJsonModel);
}
private void verifyModelBuiltCorrectly(Conversation model) {
assertEquals("Conversation name is incorrect", "Test Conversation", model.getString("name"));
assertEquals("Conversation has incorrect task size", 3, model.getTasks().size());
assertEquals("Conversation channel is incorrect", "Web", model.getChannel().getString("type"));
}
For more information on the usage of the Timer, finishTest() and delayTestFinish(), see GWTTestCase's javadoc.
The tests for saving and deleting a resource look as follows:
public void testSaveConversation() {
Conversation conversation = new Conversation().setName("Test").setId("1");
List<Task> tasks = new ArrayList<Task>();
for (int i = 1; i < 4; i++) {
tasks.add(new Task().setName("Task " + i));
}
conversation.setTasks(tasks);
System.out.println("conversation.toJson(): " + conversation.toJson());
assertTrue(conversation.toJson().toString().contains("Task 1"));
service.saveConversation(conversation);
Timer t = new Timer() {
public void run() {
assertNotNull("ResourceSavedEvent not received", savedEvent);
finishTest();
}
};
delayTestFinish(3000);
t.schedule(100);
}
public void testDeleteConversation() {
service.deleteConversation(1L);
Timer t = new Timer() {
public void run() {
assertNotNull("ResourceDeletedEvent not received", deletedEvent);
finishTest();
}
};
delayTestFinish(3000);
t.schedule(100);
}
Summary
This article has shown you how I develop and test GWT Client Services. If RestyGWT supported overlay types, there's a good chance I could change my service implementation to use it and I wouldn't have to change my test. Robert Cooper, author of GWT in Practice, claims he has a framework that does this. Here's to hoping this article stimulates the GWT ecosystem and we get a GWT REST framework that's as easy to use as GWT RPC.
Update: Today I enhanced this code to use Generics-based classes (inspired by Don't repeat the DAO!) for the boiler-plate CRUD code in a service. In a nutshell, a service interface can now be written as:
public interface FooService extends GenericService<Foo, String> {
}
The implementation class is responsible for the URL and converting the JSON result to an object:
public class FooServiceImpl extends GenericServiceImpl<Foo, String> implements FooService {
@Inject
public FooServiceImpl(EventBus eventBus) {
super(eventBus, "/services/foo");
}
@Override
protected Foo convertResultToModel(Representation result) {
return new Foo(JSOModel.fromJson(result.getData()));
}
}
I'm sure this can be further enhanced to get rid of the need to create classes altogether, possibly leveraging GIN or some sort of factory. The parent classes referenced in this code can be viewed at the following URLs:
There's also a GenericServiceGwtTest.java that proves it all works as expected.