Friday 9 May 2008

REST in Spring without using Spring Webservices

I quite like using Spring. Especially when using the Spring/Hibernate combo.

About a year ago I started using Spring for a whole heap of REST webservices I was writing. In the process I found it surprisingly easy. Various other colleagues experimented with RESTLET (http://www.restlet.org/) and Jersey (https://jersey.dev.java.net/). But as I wanted to use Hibernate easily and I was using Spring MVC to build the client component of the project I was working on, so I used Spring MVC to build the services. Pretty simple. (Even though I refer to Spring 2.5 in the pom file, I am not using the features of 2.5.)

Here's an example:

I have created a simple webapp using maven. The webapp is called rest-webapp. I have created a stupid service that retrieves the name of an evil programmer.

This means that the request would look like:
http://localhost:8080/rest-webapp/evil/programmers/1

and this would return:
Dr Evil

I wrote a simple class that does this and also returns a 404 error if the request for tne evil programmer does not exist.

This means that the request would look like:
http://localhost:8080/rest-webapp/evil/programmers/6

would return a 404 error with the message: HTTP Status 404 - This evil programmer does not exist!!



pom.xml

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>2.5</version>
</dependency>


web.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.
com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>rest-webapp</display-name>
<description>Test Rest Service</description>
<servlet>
<servlet-name>mytest</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mytest</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>1</session-timeout>
</session-config>
</web-app>


mytest-servlet.xml (Spring Application Context)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/evil/programmers/*">myController</prop>
</props>
</property>
</bean>

<bean id="myController" class="au.bandaid.programming.controller.MyTestController">
</bean>

</beans>



MyTestController.java

package au.bandaid.programming.controller;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import au.bandaid.programming.exception.HttpStatusException;

public class MyTestController implements Controller {

private Hashtable list;

public MyTestController() {
list = new Hashtable();
// Set an arbitary list
list.put("1", "Dr Evil");
list.put("2", "Big Bad Billy");
list.put("3", "Big Bad Billy");
list.put("4", "Stevie Exception");
list.put("4", "Frank Void");
}

public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {

// Determine the type of method
try {
if (request.getMethod().toUpperCase().equals("GET")) {
writeResponse(response, "text/xml", HttpServletResponse.SC_OK, get(request));
}
//else if (method.toUpperCase().equals("POST"))
//else if (method.toUpperCase().equals("DELETE"))
//else if (method.toUpperCase().equals("PUT"))

else {
throw new HttpStatusException(HttpServletResponse.SC_NOT_IMPLEMENTED, "Operation: " + request.getMethod() + " not supported.");
}
}
catch (HttpStatusException e) {
response.sendError(e.getStatusCode(), e.getMessage());
}
catch (Exception e) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.toString());
}

return null;
}

private String get(HttpServletRequest request) throws HttpStatusException {

// Grab some parameters
// I am not using any now
String type = request.getParameter("type");

// Look at the request
// am expecting rest-webapp/programmers/1
Pattern p = Pattern.compile("[1-9]+");
Matcher m = p.matcher(request.getPathInfo());
String index = "";
if (m.find()) {
index = m.group();
}

if (!list.containsKey(index)) {
throw new HttpStatusException(HttpServletResponse.SC_NOT_FOUND, "This evil programmer does not exist!!");
}

return "" + list.get(index) + "";
}

private void writeResponse(HttpServletResponse response, String contentType, int statusCode, String ouput) throws IOException {
response.setContentType(contentType);
response.setStatus(statusCode);
PrintWriter out = new PrintWriter(response.getOutputStream());
out.println(ouput);
out.close();
}

}

Some extra reading:
http://jira.springframework.org/browse/SPR-4419 - Comprehensive REST Support
http://springframework.org/docs/MVC-step-by-step/Spring-MVC-step-by-step.html

3 comments:

Anonymous said...

Any suggestions on how to implement multiple wild cards in the uri? E.g. evile/{java}/programmers/{id}?

Kalpesh Patel said...

This solution will work for simple services but how will you handle complex parameters in your rest service. You will have to bake all that logic in the controller. Spring3 has REST support and that should handle complex cases

Tez said...

I originally made this work way back in 2007 using Spring 2.0. When 2.5 came out I updated some of my services. Version 2.5 has decent support for parameters. I managed to handle wildcards through a mixture of using the Spring Application Context file and parsing the url within the Controller. Not a great solution. But it worked. In truth I have been waiting for Spring 3.0 to come along and provide true Rest support. I suggest giving it a go.