That Java Thing, Part 12: Expanding the Plugin - JAX-RS
Thu Dec 03 15:21:28 EST 2015
- That Java Thing, Part 1: The Java Problem in the Community
- That Java Thing, Part 2: Intro to OSGi
- That Java Thing, Part 3: Eclipse Prep
- That Java Thing, Part 4: Creating the Plugin
- That Java Thing, Part 5: Expanding the Plugin
- That Java Thing, Part 6: Creating the Feature and Update Site
- That Java Thing, Part 7: Adding a Managed Bean to the Plugin
- That Java Thing, Part 8: Source Bundles
- That Java Thing, Part 9: Expanding the Plugin - Jars
- That Java Thing, Part 10: Expanding the Plugin - Serving Resources
- That Java Thing, Interlude: Effective Java
- That Java Thing, Part 11: Diagnostics
- That Java Thing, Part 12: Expanding the Plugin - JAX-RS
- That Java Thing, Part 13: Introduction to Maven
- That Java Thing, Part 14: Maven Environment Setup
- That Java Thing, Part 15: Converting the Projects
- That Java Thing, Part 16: Maven Fallout
- That Java Thing, Part 17: My Current XPages Plug-in Dev Environment
A couple of months back, Toby Samples wrote a series on using Wink in Domino. I'm not going to cover all of that ground here, but it's still worth dipping into the topic a bink, as writing REST services in an OSGi plugin is a great way to not only add capabilities to your XPages apps, but to also start making your data (and programming skills) more accessible from other platforms.
There are two main terms to know here: "JAX-RS" and "Wink".
JAX-RS stands for "Java API for RESTful Web Services". If you're observant, you may notice that there's no "X" in that phrase. This is because the "JAX" part is sort of a legacy hanger-on from the common "JAX" prefix used in JAX-WS and the other Java XML APIs... even though JAX-RS doesn't necessarily involve any XML. In any event, JAX-RS is a specification for a way to write RESTful web services in Java with (mostly) inline configuration using annotations.
Apache Wink is a specific implementation of this standard. For the most part, with these Java standards, the actual implementation shouldn't matter too much, but there are always edge cases. In any event, Wink has historically been the preferred method of doing this on Domino by virtue of the fact that the ExtLib's DAS is built on it and so it ships with Domino, along with some IBM support code.
Setting up JAX-RS services is pretty similar to the addition of the XSP Library and theme provider: we'll need to create the Java classes and add some configuration to the plugin.xml
. In this case, we'll do it in reverse, starting with the configuration first before diving into the servlet class.
There are actually two ways to proceed here. In the past, I've added servlets directly to the plugin.xml
file. This works fine, but Toby's series took a different and more-interesting route: instead of having the plugin provide a servlet alone, it provides a web application which in turn contains a servlet. Doing it this way brings the configuration more in line with what you'd see in a non-OSGi Java web application.
To start with, we'll need to add dependencies on four more plugins. Open the META-INF/MANIFEST.MF
file, go to the "Dependencies" tab, and add "org.apache.wink", "com.ibm.domino.osgi.core", "org.eclipse.equinox.http.registry", and "com.ibm.wink":
One word of caution: when adding "org.eclipse.equinox.http.registry", make sure to add version "1.0.100". Eclipse will likely have a newer version available as well, but that won't be there on Domino. Setting the version requirement too high will cause the plugin to fail to load.
While on this screen, add "lotus.domino" in the "Imported Packages" section of the page. OSGi has two mechanisms for specifying dependencies - until this point, we've used exclusively the "Required Plug-ins" route, which attaches a plugin by name as a dependency. The "Imported Packages" route is a little different: it says "I want this Java package available, but I don't care what plug-in provides it". Outside of the Domino world, the Packages route is very common, but for our needs we usually use Plug-ins to avoid some common nitty-gritty issues with the XPages stack.
The plugin.xml
addition is pretty similar to the services we added before, but with some different tags. We're going to add another extension point to the list, along with a couple lines of configuration. With a minor cleanup to the previous lines, the result should look like this:
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.4"?> <plugin> <extension point="com.ibm.commons.Extension"> <service class="com.example.xsp.ExampleLibrary" type="com.ibm.xsp.Library"/> </extension> <extension point="com.ibm.commons.Extension"> <service class="com.example.xsp.theme.ExampleStyleKitFactory" type="com.ibm.xsp.stylekit.StyleKitFactory"/> </extension> <extension point="org.eclipse.equinox.http.registry.servlets"> <servlet alias="/examplerest" class="com.example.xsp.servlet.ExampleServlet"> <init-param name="applicationConfigLocation" value="/WEB-INF/exampleapplication"/> <init-param name="propertiesLocation" value="/WEB-INF/exampleservlet.properties"/> <init-param name="DisableHttpMethodCheck" value="true"/> </servlet> </extension> </plugin>
The code inside of the extension point is sort of a compressed version of the kind of servlet configuration you see in JEE's web.xml
file.
This is actually an area where Toby's series and these instructions diverge. In his series, he used a different extension point - com.ibm.pvc.webcontainer.application
- to provide his servlet as part of a full web application. That is a very interesting route as well, and opens the door to broader Java application development. Either one will get the job done, but the "just the servlet" route saves a bit of cognitive overhead for now.
Now, to create the files mentioned in the configuration. In the src/main/resources
folder, create a folder named WEB-INF
and add the exampleapplication
and exampleservlet.properties
files, both plain text:
The exampleservlet.properties
file can be left blank for now. It's there to provide Wink-specific initialization properties that are useful as you get further into it, but are not needed now.
The exampleapplication
file lists the classes that will provide the individual REST resources, one per line. For now, we'll just have one:
com.example.xsp.servlet.HelloWorldResource
With the configuration set up, it's time to create two Java classes. First, create a new class named "ExampleServlet" in the "com.example.xsp.servlet" package and have it extend "com.ibm.domino.services.AbstractRestServlet":
This class doesn't actually need to do anything other than exist for our purposes, but it can be a useful point for hooking in behavior that occurs for each servlet request. The actual code will go in the "HelloWorldResource" class mentioned in the application file. So create that class in the "com.example.xsp.servlet" package. Unlike the previous class, this one doesn't need to extend anything:
This one contains the core of our business logic (such as it is):
package com.example.xsp.servlet; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import lotus.domino.NotesException; import lotus.domino.Session; import lotus.domino.Database; import com.ibm.commons.util.StringUtil; import com.ibm.commons.util.io.json.JsonJavaObject; import com.ibm.domino.osgi.core.context.ContextInfo; @Path("/helloworld/{id}") public class HelloWorldResource { @GET @Produces(MediaType.APPLICATION_JSON) public Response get( @Context UriInfo context, @PathParam("id") String id, @QueryParam("echo") String echo ) throws NotesException { JsonJavaObject json = new JsonJavaObject(); json.put("payload", "Hello world"); Session session = ContextInfo.getUserSession(); if (session != null) { json.put("userName", session.getEffectiveUserName()); } Database database = ContextInfo.getUserDatabase(); if(database != null) { json.put("databaseFilePath", database.getFilePath()); } if(StringUtil.isNotEmpty(echo)) { json.put("echo", echo); } if(StringUtil.isNotEmpty(id)) { json.put("id", id); } return Response.ok(json.toString()).build(); } }
There are quite a few things going on here! Let's start with the JAX-RS annotations. JAX-RS, like other "new-era" Java APIs, using annotations heavily in order to put configuration inline and cut down on the number and size of the previous giant blobs of XML. They look a bit unusual if you haven't encountered Java annotations before, but their intent here is hopefully clear.
At the top, the @Path("/helloworld/{id}")
annotation indicates that this class is hooked into "/helloworld" within our servlet, and moreover also expects a path parameter that it will refer to as "id". Then, the @GET
annotation indicates that our get
method will respond to that HTTP method (the name of the method is arbitrary - it could be "foobar" and still work). The @Produces(MediaType.APPLICATION_JSON)
annotation further indicates that the method will supply JSON. There are other ways to accomplish this if the method may return different content types, but we know it's always JSON here.
Inside the method signature, there's a bit more magic going on. We're never going to call this method in any code ourselves, but instead Wink is going to inspect it and its parameter list in order to provide what it requests. By annotating the first parameter with @Context
, we're telling Wink that we want to have that populated with the context information for the servlet - this is similar to ExternalContext
from XSP development. This object isn't actually used in the example (and could be removed), but I've found that it's handy to have it consistently there. The other two parameters use @PathParam("id")
and @QueryParam("echo")
to extract the "id" parameter we specified earlier as well as look for "echo=..." in the query string.
The method itself uses IBM's JSON library to produce a simple JSON object containing the information we passed in, as well as some Domino-specific stuff, using the class ContextInfo
. This class is sort of like the equivalent of ExtLibUtil
for Equinox servlets, at least when it comes to getting access to the current session and database. ExtLibUtil
itself, though available, doesn't function properly in a servlet, and so we use this instead. The current session is straightforward enough and works like you'd expect, but the current database is interesting. When you register a servlet via this method, it becomes available not only via "yourserver.com/examplerest", but also within each NSF, like "yourserver.com/foo.nsf/examplerest". By default, that will just provide the same results, but, if your code uses ContextInfo.getCurrentDatabase()
, you can get the database the URL is requesting. This is how DAS does its thing, and it allows you to write a servlet that can operate in a contextually-appropriate way in each database.
So phew! Now that this is all written, you can build your update site and deploy it to the server. After restarting HTTP, you should be able to go to a URL like "http://yourserver.com/foo.nsf/examplerest/helloworld/someid?echo=hey", and see a result like this:
{ "echo": "Hey", "userName": "CN=Jesse Gallagher/O=IKSG", "id": "someid", "databaseFilePath": "foo.nsf", "payload": "Hello world" }
To finish, it's time to commit the changes.
Next up, it will be time to start taking the plunge into Maven. Don't worry; it'll be fun!