July 15, 2009

JAX-RS, @Path, @PathParam and Optional Parameters

In a recent Java project I needed to develop and provide to external clients a RESTful Web Services interface to an internal system. After some research I found that using JAX-RS and its open-source implementation Jersey with Spring and Hibernate as back-end will be great technologies stack for this project. Seems easy but unfortunately I found that JAX-RS does not support optional path parameters.

Customer requested each service to have an optional path parameter called “format” that specifies the output format. All services were required to support multiple output formats: XML, plain text, CSV, JSON, PDF, etc. For example if I request this URL: http://myserver.com/services/location/3/format/xml, the output should be XML, but if I request just http://myserver.com/services/location/3 without “format” parameter, the result should be plain text.

Mandatory Path Parameters

Using a path pattern like this:

@Path("users/{userId}/format/{format}")

makes the parameter “format” mandatory. If we skip it, the request will not match the path.

Optional @Path Parameters in JAX-RS

Using regular expressions and a simple hack can overcome this limitation in JAX-RS. The following example defines two optional path parameters “format” and “encoding”:

@GET
@Path("/user/{id}{format:(/format/[^/]+?)?}{encoding:(/encoding/[^/]+?)?}")
public Response getUser(
  @PathParam("id") int id,
  @PathParam("format") String format,
  @PathParam("encoding") String encoding) {
 String responseText = "";

if (format.equals("")) {
  // Optional parameter "format" not specified
  responseText += "No format specified.";
 } else {
  // Optional parameter "format" has looks like "/format/pdf" -> get it's value only
  format = format.split("/")[2];
  responseText += "Format=" + format;
 }

if (encoding.equals("")) {
  // Optional parameter "encoding" not specified
  responseText += " No encoding specified";
 } else {
  // Optional parameter "encoding" has looks like "/encoding/utf8" -> get it's value only
  encoding = encoding.split("/")[2];
  responseText += " Encoding=" + encoding;
 }

return Response.status(200).type("text/plain").entity(responseText).build();
}

Requesting http://localhost:8080/services/user/3, will return “No format specified. No encoding specified”.
Requesting http://localhost:8080/services/user/3/format/pdf/encoding/utf8, will return “Format=pdf Encoding=utf8″.
Requesting http://localhost:8080/services/user/3/encoding/utf8, will return “No format specified. Encoding=utf8″.

Flexible @Path Parameters in JAX-RS

If we need more flexibility, we can match the entire path ending in the REST request and map it in key-value style (HashMap< String, String >):

@GET
@Produces({"application/xml", "application/json", "plain/text"})
@Path("/location/{locationId}{path:.*}")
public Response getLocation(
  @PathParam("locationId") int locationId,
  @PathParam("path") String path) {
 Map&lt; String, String&gt; params = parsePath(path);
 String format = params.get("format");
 if ("xml".equals(format)) {
  String xml = "<location></location><id></id>" + locationId + "";
  return Response.status(200).type("application/xml").entity(xml).build();
 } else if ("json".equals(format)) {
  String json = "{ 'location' : { 'id' : '" + locationId + "' } }";
  return Response.status(200).type("application/json").entity(json).build();
 } else {
  String text = "Location: id=" + locationId;
  return Response.status(200).type("text/plain").entity(text).build();
 }
}

private Map&lt; String, String &gt; parsePath(String path) {
 if (path.startsWith("/")) {
  path = path.substring(1);
 }
 String[] pathParts = path.split("/");
 Map&lt; String, String &gt; pathMap = new HashMap&lt; String, String &gt;();
 for (int i=0; i &lt; pathParts.length/2; i++) {
  String key = pathParts[2*i];
  String value = pathParts[2*i+1];
  pathMap.put(key, value);
 }
 return pathMap;
}

Requesting http://localhost:8080/services/location/3, will return “Location: id=3″.
Requesting http://localhost:8080/services/location/3/format/json, will return “{ ‘location’ : { ‘id’ : ’3′ } }”.

Enjoy!

Tags: , , , , , , , , ,

1 Comment »

  1. JAX-RS simply isn’t meant for the type of flexibility you’re describing… I’d probably stick with a servlet in the case you describe. Also, if you need the kind of flexibility you’re describing from an “interface” standpoint, then HTTP headers and query parameters seem much more friendly than path params

    Comment by Solomon Duskis — December 3, 2010 @ 18:42

RSS feed for comments on this post. TrackBack URL

Leave a comment