Monday, February 15, 2010

Playing with Jersey/JAX-RS Method Designators

There is a part of me that likes to try things just to see what happens. If I read something that says, "Never do this," I have a hard time not doing just that thing if I believe doing so comes at no significant cost. There can be value to this approach when I learn about the results associated with an improper action and can then more readily recognize the underlying cause when I inadvertently cause the same problem myself or see a colleague dealing with the same effects. In this blog post, I look at how JAX-RS 1.1 (specifically the Jersey implementation) handles some of the "don't do that" rules related to resource methods.

One JAX-RS Resource Method Designation Per Java Method

JAX-RS literature warns that only one JAX-RS method designation annotation is allowed per method in a Java class resource. In other words, I should only be able to apply @GET or @POST or @PUT or @DELETE to a method, but never apply more than one of them to the same method. This actually seems sensible, but it prevents some of the "trickery" one commonly saw in servlet applications where the doPost and doGet methods were set up so that one called the other or they both called the same piece of code.

To find out what the negative consequences are of specifying multiple JAX-RS method designators on the same method, I took the method addMovieOfTheDay from my previous blog post (JAX-RS and the REST Uniform Interface) and added the @POST annotation on top of its existing @PUT annotation. The altered code is shown next:

Two JAX-RS Method Designation Annotations for a Single Method

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}


The above code compiles without incident. This is not surprising because, as the JAX-RS 1.1 specification states (Section 3.3 Resource Models, emphasis added), "A request method designator is a runtime annotation that is annotated with the @HttpMethod annotation." However, when I try to deploy it following the steps outlined in my blog post JAX-RS with Jersey: An Introduction, I see an error in the GlassFish v3 web-based Administrative Console. A screen snapshot of that is shown next.



The output states that an IllegalStateException has been thrown and suggests seeing the logs for additional details. There is a very lengthy stack trace in the logs, but the most significant piece of it is this:


[#|2010-02-14T19:43:38.495-0700|SEVERE|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=25;_ThreadName=Thread-1;|A (sub-)resource method, public java.lang.String rmoug.td2010.rest.MovieOfTheDay.addMovieOfTheDay(java.lang.Integer,java.lang.Integer,java.lang.String), should have only one HTTP method designator. It currently has the following designators defined: [@javax.ws.rs.PUT(), @javax.ws.rs.POST()]|#]


With the context of the situation in mind, this is pretty straightforward: "only one HTTP method designation" is allowed for a resource method and the method "[addMovieOfTheDay(Integer,String)] currently has the designators ... @PUT ... @POST [defined]." Okay, so that is bad. Note to self: The advice to only use one method designator per class resource method should be heeded.

As I wrote this post, I verified that NetBeans 6.8 continues the tradition of creating new Java servlets with the automatically generated doPost and doGet methods calling the same protected method (processRequest in NetBeans 6.8, though I seem to recall this method had a different name previously, such as doProcess()). I decided the next thing to try was annotating two separate methods with respective annotations, but have them call the same underlying method. My assumption was that this would work fine. The code for this is shown in the next listing.

Two Designators/Two Methods But with Single Common Method

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}

/**
* "Common" method for adding/updating a movie of the day that is intended
* to support PUT and POST.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
private String addMovie(final Integer month, final Integer date, final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}


This code compiles without incident because I appended "Post" to the method name of the added method. However, I "forgot" to change the annotation to @Post for the new method and this became obvious when I tried to deploy to GlassFish. I saw the same general error as before in the console, but the logs had details on the new problem:


[#|2010-02-14T20:43:06.557-0700|SEVERE|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=25;_ThreadName=Thread-1;|A resource, class rmoug.td2010.rest.MovieOfTheDay, has ambiguous sub-resource method for HTTP method PUT, URI path template /{month}/{date}/{title}, and output mime-type: text/html. The problematic mime-type sets (as defined by @Produces annotation at Java methods addMovieOfTheDay and addMovieOfTheDayPost) are [text/html] and [text/html]|#]


This SEVERE-level log message is telling us that the two methods annotated with @PUT are ambiguous because they have the same URI path and same MIME type. One of these needs to be different to allow the JAX-RS provider to differentiate them. For our purposes, I simply change the new method addMovieOfTheDayPost to have the @POST annotation rather than the @PUT annotation.

Method Intended for POST Annotated with @POST

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}


The code again compiles and even deploys this time. I don't show it here, but using RESTClient to access the resource via PUT and via POST works for both HTTP methods. In short, it does appear that one can delegate functionality from two different Java methods associated with different HTTP methods to the same common method.

The last thing I wanted to try in this area was having one method call the other rather than two methods calling a third, common method. To try this, I changed the method added for POST support to simply call the method for PUT:

POST-Supporting Method Calling PUT-Supporting Method

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovieOfTheDay(month, date, title);
}


This approach compiled successfully, deployed successfully, and I was able to invoke both the PUT-supporting and POST-supporting Java methods successfully. So, the approach of having one method directly call the other seems to work as well if a developer wants to get around the limitation of not having multiple HTTP method designations on the same Java method.


Only Public Methods Allowed as Resource Methods

Section 3.3.1 ("Visibility") of the JAX-RS 1.1 specification states: "Only public methods may be exposed as resource methods. An implementation SHOULD warn users if a
non-public method carries a method designator or @Path annotation."

Let's see what happens with Jersey in this situation.

To test this one out, I change the just-added method supporting POST to be package scope by removing its public modifier.

Package-level @POST-annotated Method

/**
* Add a new entry or update an existing entry representing a movie of the day
* via HTTP POST.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
/*public*/ String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovieOfTheDay(month, date, title);
}


In this case, the code compiles and the WAR deploys without apparent incident, but things go less smoothly when a client attempts to use POST to add/change a movie. The package-level method is not available and this is made evident in the RESTClient GUI as shown in the next screen snapshot.



As the above image demonstrates, an HTTP response code of 405 ("Method Not Allowed") is returned when there is no public method associated with the specified HTTP method. As the specification suggests, Jersey does warn of this condition with the following WARNING-level logged statement:


[#|2010-02-14T21:13:31.226-0700|WARNING|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=28;_ThreadName=Thread-1;|A sub-resource method, java.lang.String rmoug.td2010.rest.MovieOfTheDay.addMovieOfTheDayPost(java.lang.Integer,java.lang.Integer,java.lang.String), MUST be public scoped otherwise the method is ignored|#]



Conclusion

The messages Jersey logs related to problems associated with resource method designations are relatively clear and straightforward. That is good news because it means that when these errors are unintentionally caused, they are more likely to be readily identified and addressed.

3 comments:

jlorenzen said...

Thanks. Keep up the great Jersey articles.

Upi Weston said...

That's a nice little job you did there. Very interesting.

I'm trying to do something similar with two methods having the same @Path but a different request; namely
a @Get and a @Put.

like this:
@POST
@Path("/{month}/{date}/{title}")
public String addMovieOfTheDay(...

@GET
@Path("/{month}/{date}/{title}")
public String getMovieOfTheDay( ...

Weird - but I get a 405 (Method not found)

Do you know if this is supposed to work?

Upi Weston said...

Good article.
I'm doing something similar but running into a 405
Surely this is not ambiguous?


@PUT
@Path("/{month}/{date}/{title}")
public void addMovieOfTheDay(

@GET
@Path("/{month}/{date}/{title}")
public String getMovieOfTheDay(

Do you happen to know if this is supposed to be ambiguous?