Hugh Winkler holding forth on computing and the Web

Wednesday, August 15, 2007

Restful Servlets + JSP: My framework

Stefan and Bill asked, so here's how I do it (with a tip of the hat to Django).

It is a micro framework. You subclass RestServlet and declare some URL patterns to match, and handlers for them. The base class parses the URI, sets attributes in the ServletRequest object based on the URI pattern, and invokes your handlers.

So here's how a simple BlogServlet would look:

[updated: fixed path to JSP, added a note above about attributes].
[update: added Apache license]

Copyright 2007 Wellstorm Development, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.



public class BlogServlet extends RestServlet{


/**
* GET an entry. The base class will populate the "entryId" attribute before calling invoke.
* We told it to do so below, when we defined entryIdentifier.
*/
RequestHandler entryGetHandler = new RequestHandler(){

public void invoke(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
// The base class has parsed the URI and populated the request with
// the "entryId" attribute
String entryId = (String)request.getAttribute("entryId");
String forward = "/WEB-INF/entry.jsp";
request.setAttribute("entryHTML", getEntryHTML(entryId));
request.setAttribute("entryTitle", getEntryTitle(entryId));

request.getRequestDispatcher(forward).forward(request, response);
} catch (Exception e) {
response.sendError(404);
}
}
};


/**
* POST a new entry to the collection URI
*/
RequestHandler collectionPostHandler = new RequestHandler(){

public void invoke(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
String entryId = saveEntry(request, response);

response.setHeader("Location", buildEntryUri(entryId));
response.setStatus(201);
//write out some HTML or maybe the entry itself.
String forward = "/WEB-INF/entry.jsp";
request.setAttribute("entryHTML", getEntryHTML(entryId));
request.setAttribute("entryTitle", getEntryTitle(entryId));
request.getRequestDispatcher(forward).forward(request, response);
} catch (Exception e) {
response.sendError(404);
}
}
};

//Stubbing these in...
RequestHandler entryPutHandler =null;
RequestHandler entryDeleteHandler = null;
RequestHandler collectionGetHandler = null;

//
// One ResourceIdentifier per URI pattern.
// Each one tells us how to parse the pattern into attributes, and handlers
// for HTTP methods on the resources it identifies.
//
ResourceIdentifier entryIdentifier = new ResourceIdentifier(
"^/(\\d+)$", // URI pattern
new String[] {"entryId"}, // match pattern and insert named request attributes
entryGetHandler, // GET handler for entry URIs
null, // no POST handler for entries
entryPutHandler, // PUT an entry
entryDeleteHandler); // DELETE an entry


ResourceIdentifier collectionIdentifier = new ResourceIdentifier(
"^/$", // URI pattern
collectionGetHandler, // GET handler for collection would list entries
collectionPostHandler); // POST handler for collection will add and entry



@Override
/**
* Here's how we tell our base class how to map URIs to handlers:
*/
protected ResourceIdentifier[] resourceIdentifiers() {
return new ResourceIdentifier[]{entryIdentifier, collectionIdentifier};
}

// these are just stubs... exercise for the reader.
private String buildEntryUri(String entryId) {
return null;
}

private String saveEntry(HttpServletRequest request,
HttpServletResponse response) {
return null;
}

private Object getEntryTitle(String entryId) {
return null;
}
private Object getEntryHTML(String entryId) {
return null;
}

}




Here's the base RestServlet class. In real life this class also has convenience methods to send redirects, and other standard HTTP stuff.



public abstract class RestServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

private static Logger logger = Logger
.getLogger(RestServlet.class.getName());

protected RestServlet() {
super();
}

protected abstract ResourceIdentifier[] resourceIdentifiers();

/** try calling doGet, doPost, or whatever, on each ResourceIdentifier, until one succeeds.
Uses reflection to reduce bloat.
*/

private void doMethod(String methodName, HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
boolean did = false;
try {
// The method must be like e.g.
// boolean doGet(HttpServletRequest request, HttpServletResponse response)
Method method = ResourceIdentifier.class.getMethod(methodName, new Class[]{HttpServletRequest.class, HttpServletResponse.class});
Object [] params = new Object[]{request, response};
for (ResourceIdentifier rid : resourceIdentifiers()) {
if (did = (Boolean)method.invoke(rid, params)) {
break;
}
}
if (!did) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception in doPost", e);
throw new ServletException(e);
}

}

@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doMethod ("doPost", request, response);

}

@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doMethod ("doGet", request, response);
}

@Override
protected void doPut(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doMethod ("doPut", request, response);
}

@Override
protected void doDelete(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doMethod ("doDelete", request, response);
}



Here's the ResourceIdentifier class. It tells us how to map URI patterns to handlers, and maps matched pattern groups to attribute names

public class ResourceIdentifier {

private final Pattern pattern;
private final String[] attributeNames;
private final RequestHandler getHandler;
private final RequestHandler putHandler;
private final RequestHandler postHandler;
private final RequestHandler deleteHandler;

public ResourceIdentifier(String regex, String[] attributeNames, RequestHandler getHandler, RequestHandler postHandler,
RequestHandler putHandler, RequestHandler deleteHandler) {
this.pattern = Pattern.compile(regex);
this.attributeNames = attributeNames;
this.getHandler = getHandler;
this.postHandler = postHandler;
this.putHandler = putHandler;
this.deleteHandler = deleteHandler;
}


public ResourceIdentifier(String regex, RequestHandler supportsGet) {
this(regex, new String[] {}, supportsGet, null, null, null);
}

public ResourceIdentifier(String regex, String[] attributeNames, RequestHandler supportsGet) {
this(regex, attributeNames, supportsGet, null, null, null);
}

public ResourceIdentifier(String regex, RequestHandler supportsGet, RequestHandler supportsPost) {
this(regex, new String[] {}, supportsGet, supportsPost, null, null);
}

public ResourceIdentifier(String regex, String[] attributeNames, RequestHandler supportsGet, RequestHandler supportsPost) {
this(regex, attributeNames, supportsGet, supportsPost, null, null);
}

public boolean doGet(HttpServletRequest request, HttpServletResponse response)
throws Exception {

return doMethod(this.getHandler, request.getPathInfo(), request, response);
}

public boolean doPost(HttpServletRequest request, HttpServletResponse response)
throws Exception {

return doMethod(postHandler, request.getPathInfo(), request, response);
}

public boolean doPut (HttpServletRequest request, HttpServletResponse response)
throws Exception {

return doMethod(putHandler, request.getPathInfo(), request, response);
}

public boolean doDelete(HttpServletRequest request, HttpServletResponse response)
throws Exception {

return doMethod(deleteHandler, request.getPathInfo(), request, response);
}

/**
* Test the uri against our pattern. If matched, dispatch to the handler.
*/
private boolean doMethod(RequestHandler handler, String uri, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (uri == null){
uri = "";
}
Matcher matcher = pattern.matcher(uri);
boolean bDid;
if (matcher.matches()) {
if (handler == null) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
bDid = true;
} else {
dispatch(matcher, request, response, handler);
bDid = true;
}
} else {
bDid = false;
}
return bDid;
}


private void dispatch(Matcher matcher, HttpServletRequest request, HttpServletResponse response,
RequestHandler listener) throws Exception {

// The regex matched. Extract all the named attributes from the URL and
// set them as attributes on the request. Then invoke RequestHandler.
int n = matcher.groupCount() ;
if (n != attributeNames.length) {
throw new RuntimeException("must have same number of matches as attribute names");
}
for (int i = 0; i < n; i++) {
request.setAttribute(attributeNames[i], matcher.group(i + 1));
}
listener.invoke(request, response);
}
}


The RequestHandler interface is the merest slip of a thing:


public interface RequestHandler {
void invoke(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

2 comments:

Benjamin Good said...

I am thinking about implementing this solution in my new java app-engine project. I like the clean, flexible, django-like pattern for urls and the apparently low overhead to get it running.
2 years later, do you think this is still a productive way forward? If not, would you suggest another java web framework instead?
thanks for the informative post.

hughw said...

@Benjamin thanks for your comment. We still use this structure almost exactly as I wrote here two years ago. And we use it a lot. We add new restful servlets pretty frequently, and these classes makes that pretty painless.

It occurs to me I ought to add some licensing boilerplate, so I'll edit the post to grant Apache 2 license.