Well not 100% true, the first one did just do hello world to check it all worked. as you will have seen in previous posts; but this is the first time I have tried to use JAX-RS in anger.
So the problem I was trying to solve is that whilst our bug system allows you to do lots of static queries they don't give you a feel for what bugs have been updated when. So I wanted to put together some Atom feeds of recently updated bugs that I am associated with to give me a better overview. This seems like a job of JAX-RS.
Now the JAX-RS examples do use the Rome libraries; but I followed the example of this blog and simple generated a JAXB content model from the Atom Schema. This gave me a basis of my application; but I needed to work out what my resource URL looks like. My first guess had:
http://localhost:7101/bugfeed/reportedBy/gdavison
But to my eyes this didn't seem right as reportedBy is a property of the user, and I needed some room for expansion. Also it take some time to swap your thinking around from RPC/OO to REST style of thinking....
http://localhost:7101/bugfeed/user/gdavison/repotedBy
Having decided that I need to create my top level class. You will see that I also created a bug resource which is a simple redirection to the bug web site currently. It would also be possible to return computer readable xml of json objects depending on the content type asked for; but at the moment I am going to assume real human users.
Now you can match complex paths in one step with JAX-RS, for example "user/{username}/reportedBy" but I wanted more resource steps for later flexibility. So we just return a POJO which represents the user.
@Path("/") public class BugDBFeed { @Path("/bug/{bug_num}") @GET public Response getBug(@PathParam("bug_num") String bugNum) { return Response.temporaryRedirect( UriBuilder.fromUri("https://xxxx/query") .queryParam("bugNo",bugNum).build()).build(); } @Path("/user/{username}/") public UserResource getUser( @PathParam("username") String username) { return new UserResource(username); } }
There really isn't anything here, at the moment the user resource doesn't produce anything - it just represents part of the resource path in the URL. It does expose three sub paths which represent different ways I could be associated with a bug. Compare that with the "getBug" method previously where it has a @GET on it to say what HTTP method it accepts.
public class UserResource { private String username; public UserResource(String username) { super(); this.username = username; } @Path("/assigned") public UpdateResource getAssignedBugs() { return new UpdateResource(username, "programmer"); } @Path("/reportedBy") public UpdateResource getReportedByBugs() { return new UpdateResource(username,"rptd_by"); } @Path("/supportContact") public UpdateResource getSuuportedContactBugs() { return new UpdateResource(username,"support_contact"); } }
Now the next level of resource classes is where it becomes really interesting as we start to produce new content. The getBugsAtom method declares that it returns an xml type and JAX-RS will convert the JAXB classes in the background for you. You will notice that there are two parameters injected: the first in UriInfo which gives you a context to build uris that point to other resources; and the second is an optional query parameter on the feed to control the number of days you want the query to go back. You can inject quite a bit of information into parameters, even as far as details such as cookies.
UriInfo is a really powerful class for referencing other resources, in this case we are asking to the root of the application so that we can us that bug indirection used in the top level class. Notice like most object here they use the builder pattern to reduce the number of code steps required.
The rest of the code is straight forward SQL queries and creating the right POJO classes to later marshal into XML. JAX-RS does most of the work for you. I couldn't get the DataSource to inject properly, this might have been because I was only using Jersey 0.9; but most likely it required further integration into the server for this to work. No matter JNDI can do the same work.
public class UpdateResource { // @Resource(name = "jdbc/BugDS") // DataSource ds; private String username; private String property; public UpdateResource(String username, String property) { super(); this.property = property; this.username = username; } @GET @Produces("application/atom+xml") public Feed getBugsAtom( @javax.ws.rs.core.Context UriInfo info, @QueryParam(value = "days") @DefaultValue("10") int days) { try { ObjectFactory fact = new ObjectFactory(); Feed feed = fact.createFeed(); // List<Object> authorOrCategoryOrContributor = feed.getAuthorsAndCategoriesAndContributors(); PersonType personType = fact.createPersonType(); TextType name = fact.createTextType(); name.getContent().add(username); personType.getNamesAndUrisAndEmails().add(fact.createFeedTitle(name)); authorOrCategoryOrContributor.add(fact.createFeedAuthor(personType)); // Put in a title // TextType title = fact.createTextType(); title.getContent().add(info.getAbsolutePath().toASCIIString()); authorOrCategoryOrContributor.add(fact.createFeedTitle(title)); // Put some entries in // Connection otherConnection = getConnection("java:comp/env/jdbc/BugDS"); try { //Connection connection = dbConnection.getConnection(); OraclePreparedStatement prep = (OraclePreparedStatement)otherConnection.prepareCall("..."); try { prep.setStringAtName("nxuser", username.toUpperCase()); prep.setIntAtName("nxdays", days); // Last thirty days ResultSet rs = prep.executeQuery(); try { while (rs.next()) { // Entry entry = fact.createEntry(); authorOrCategoryOrContributor.add(entry); // Get data out of the columns String bugNumber = Integer.toString(rs.getInt(1)); String titleText = rs.getString(2); GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("PST")); Date date = rs.getTimestamp(3, calendar); DateTimeType updatedOn = fact.createDateTimeType(); String status = Integer.toString(rs.getInt(4)); // TextType entryTtitle = fact.createTextType(); entryTtitle.getContent().add(status + " " + titleText + " on " + date); entry.getAuthorsAndCategoriesAndContents() .add(fact.createEntryTitle(entryTtitle)); LinkType entryLink = fact.createLinkType(); UriBuilder toBuild = info.getBaseUriBuilder(); entryLink.setHref(toBuild.path("bug") .path(bugNumber).build().toASCIIString()); entry.getAuthorsAndCategoriesAndContents() .add(fact.createEntryLink(entryLink)); calendar.setTime(date); XMLGregorianCalendar updated = DatatypeFactory.newInstance().newXMLGregorianCalendar(calendar); updatedOn.setValue(updated); entry.getAuthorsAndCategoriesAndContents() .add(fact.createEntryUpdated(updatedOn)); } } finally { rs.close(); } } finally { prep.close(); } } finally { otherConnection.close(); } // return feed; } catch (Exception ex) { throw new RuntimeException(ex); } } private Connection getConnection(String dataSourceLocation) throws NamingException, SQLException { Connection conn = null; // Get a context for the JNDI look up Context ctx = new InitialContext(); // Look up a data source javax.sql.DataSource ds = (javax.sql.DataSource)ctx.lookup(dataSourceLocation); // Create a connection object conn = ds.getConnection(); return conn; } }
So after working with an playing with the service for a little while I was ready to try in Thunderbird where all my other feeds are. It turns out that at least 2.x doesn't like all Atom feed, so I decided that rather than try to find another reader that instead I would also produce a RSS stream. One of the cool things about JAX-RS is that you can very easily overload a give resource with different media types depending in the client accept headers.
I decided that I would much rather use XSL rather than write all that JAXB code again for RSS. So I dug up a link on the internet and started work. It turns out that the JAX-RS method can return a simple source object so all we really needed to do was to marshal the Atom class, transform and return. Thunderbird will now get a RSS feed which at least in my case it seemed to prefer.
@GET @Produces("application/rss+xml") public Source getBugsRSS( @javax.ws.rs.core.Context UriInfo info, @QueryParam(value = "days") @DefaultValue("10") int days) throws ... { // Get the atom version // Feed feed = getBugsAtom(info, days); // Jaxb it DOMResult atomModel = new DOMResult(); JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); jc.createMarshaller().marshal( feed, atomModel); // Transform it // Transformer t = TransformerFactory.newInstance().newTransformer( new StreamSource( UpdateResource.class.getResourceAsStream("atom2rss.xsl"))); DOMResult rssModel = new DOMResult(); t.transform( new DOMSource(atomModel.getNode()), rssModel); // Return RSS version // return new DOMSource(rssModel.getNode()); }
It is worth taking a quick look at the WADL view of the application one it was deployed. It am not sure why the last two paths are not correctly described but can get the general idea. I would be relatively easy to parse this and generate a client of some kind; but this isn't part of the JAX-RS project as yet.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <application xmlns="http://research.sun.com/wadl/2006/10"> <doc xmlns:jersey="http://jersey.dev.java.net/" jersey:generatedBy="Jersey: 0.9-ea 08/22/2008 04:48 PM"/> <resources base="http://xxxxx:7001/bugfeed/"> <resource path="/"> <resource path="/bug/{bug_num}"> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="bug_num"/> <method name="GET" id="getBug"> <response> <representation mediaType="*/*"/> </response> </method> </resource> <resource path="/user/{username}/"> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="username"/> <resource path="/assigned"> <method name="GET" id="getBugsRSS"> <request> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="10" type="xs:int" style="query" name="days"/> </request> <response> <representation mediaType="application/xml+rss"/> </response> </method> <method name="GET" id="getBugsAtom"> <request> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="10" type="xs:int" style="query" name="days"/> </request> <response> <representation mediaType="application/xml+atom"/> </response> </method> </resource> <resource path="/reportedBy"/> <resource path="/supportContact"/> </resource> </resource> </resources> </application>
Well that is the entire application, it is quite simple in that it doesn't put, update or delete resources; but I was trying to solve a problem not write a tutorial! (A nice tutorial is here.) I also haven't taken any care to map exception to anything more user friendly. This can be done easily with the use of @Provider/ExceptionMapper to convert you application specific error to a WebApplicationException instance. But for the moment the default 500 error will have to do.
Update 13 Feb '09 Whoops there was a typo in the @Produces annotation I had "xml+atom" rather than "atom+xml" and the same mix up for rss. I have fixed this typo now. The early version of Jersey I wrote this with didn't really mind; but 1.0.x onwards is more strict. In case you are interested the error message I got back from Jersey when requesting context type application/xml+atom was:
A message body writer for Java type, class org.w3._2005.atom.Feed, and MIME media type, application/xml+atom, was not found
The actual mime type wild card that would match JAXB elements is application/*+xml. Took a little while to work our the mistake; but it was something I have learnt from.
2 comments:
I enjoyed this post. Did you read Subbu's latest REST article on InfoQ? It provided some insights that I had not fully understood before about REST design principles.
http://www.infoq.com/articles/subbu-allamaraju-rest
Jambay,
Actually I didn't see that one, that is really a lot of food for thought. Thanks for the tip,
Gerard
Post a Comment