Servlet-Based Web Services Under GlassFish

GlassFish distinguishes between servlet-based and EJB-based web services. Servlet-based services include REST-style and SOAP-based services of the sort published earlier with Tomcat, Jetty, or a command-line publisher. EJB-based services also may be REST-style or SOAP-based services implemented as Session EJBs. For example, a JAX-RS service might be implemented as a Session EJB. Yet GlassFish and other JASes make it especially attractive to implement legacy @Stateless EJBs as SOAP-based web services because this requires only an additional annotation, @WebService. For servlet-based services under the JAX-WS umbrella, @WebService and @WebServiceProvider instances, the deployment under GlassFish is simpler than the deployment under Tomcat because GlassFish includes, among its libraries, the full Metro implementation of JAX-WS; hence, the Metro JAR files need not be (indeed, should not be) packaged in the deployed WAR file. GlassFish can handle JAX-WS out of the box.

Among the services in the previous chapters deployed with Tomcat or Jetty, all would count as servlet-based in GlassFish terms. They can be deployed, as is, to GlassFish as servlet-based services. Here is a sample selection of services from Chapters 2 and 5. This review focuses on what needs to be included in a WAR file for GlassFish deployment of servlet-based services.

predictions2

This is the predictions RESTful service implemented as an HttpServlet. Here, for review, are the contents of the deployed WAR file under GlassFish:

WEB-INF/web.xml
WEB-INF/classes/predictions2/Prediction.class
WEB-INF/classes/predictions2/Predictions.class
WEB-INF/classes/predictions2/PredictionsServlet.class
WEB-INF/data/predictions.db
WEB-INF/lib/json.jar

None of the source code needs to change; indeed, this WAR file can be deployed, as is, under Tomcat, Jetty, or GlassFish. For review, the service supports all of the CRUD operations and responds with either XML (the default) or JSON (if the HTTP header includes the key/value pair Accept: application/json). The json.jar file in the deployed WAR generates the JSON.

There is a subtle difference between the URLs used in calls against the Tomcat or Jetty deployment, on the one side, and the GlassFish deployment, on the other side. For example, for Tomcat/Jetty, the curl call:

% curl localhost:8080/predictions2?id=31

returns in XML format the Prediction with an id value of 31. Against the GlassFish deployment, the URL becomes:

% curl localhost:8081/predictions2/?id=31  ;; /?id=31 instead of ?id=31

Under GlassFish deployment, a slash, /, occurs after the WAR filename. In this example, the port number for Tomcat is 8080 as usual. For the sample runs in this section, the assumption is that GlassFish is started after Tomcat was already running; hence, GlassFish awaits HTTP connections on port 8081.

The predictions2 service does not require an interceptor servlet that acts as the intermediary between client requests and the web service. In the case of the predictions2 service, the implementation class is PredictionsServlet, a subclass of HttpServlet. In short, the service instance is itself a servlet. In this sense, the predictions2 service is the least complicated implementation among the REST-style and SOAP-based implementations of the predictions and adages services in Chapter 2. The SOAP-based predictionsSOAP implementation uses the Metro WSServlet as the interceptor. The JAX-RS implementation, predictions3, uses the Jersey ServletContainer as the interceptor, and the Restlet implementation of the adages RESTful service relies upon the ServerServlet as the interceptor.

predictions3

This a RESTful version of the service using JAX-RS. The WAR file can be deployed, as is, under Tomcat, Jetty, or GlassFish. Here, for review, are the contents of the deployed WAR file:

WEB-INF/web.xml
WEB-INF/classes/predictions3/Prediction.class
WEB-INF/classes/predictions3/PredictionsList.class
WEB-INF/classes/predictions3/PredictionsRS.class
WEB-INF/classes/predictions3/RestfulPrediction.class
WEB-INF/data/predictions.db
WEB-INF/lib/asm.jar
WEB-INF/lib/jackson-annotations.jar
WEB-INF/lib/jackson-core.jar
WEB-INF/lib/jackson-databind.jar
WEB-INF/lib/jersey-core.jar
WEB-INF/lib/jersey-server.jar
WEB-INF/lib/jersey-servlet.jar

The various jackson JAR files provide the JSON support and the jersey JARs are the RI implementation of JAX-RS. The predictions3 service also supports all of the CRUD operations. The syntax of the CRUD calls changes deliberately in order to highlight the JAX-WS @Path annotation. For example, the curl call:

% curl http://localhost:8081/predictions3/resourcesP/json/31

would return, in JSON format, the Prediction with the id value 31.

adages2

This is a Restlet implementation of the RESTful adages service. Nothing in the WAR file changes from the Tomcat or Jetty deployments. The service supports all of the CRUD operations with intuitive URIs such as /create to create a new Adage or /delete/9 to delete the Adage with the id value of 9. For review, here are the contents of the deployed WAR file:

WEB-INF/web.xml
WEB-INF/classes/aphorism2/Adage.class
WEB-INF/classes/aphorism2/Adages.class
WEB-INF/classes/aphorism2/AdagesApplication$1.class
WEB-INF/classes/aphorism2/AdagesApplication.class
WEB-INF/classes/aphorism2/CreateResource.class
WEB-INF/classes/aphorism2/JsonAllResource.class
WEB-INF/classes/aphorism2/PlainResource.class
WEB-INF/classes/aphorism2/UpdateResource.class
WEB-INF/classes/aphorism2/XmlAllResource.class
WEB-INF/classes/aphorism2/XmlOneResource.class
WEB-INF/lib/org.json.jar
WEB-INF/lib/org.restlet.ext.json.jar
WEB-INF/lib/org.restlet.ext.servlet.jar
WEB-INF/lib/org.restlet.ext.xml.jar
WEB-INF/lib/org.restlet.jar

This service, like the others, deploys straightforwardly to GlassFish.

predictionsSOAP

This is the SOAP-based implementation of the predictions service, which also supports all of the CRUD operations but in this case with four methods annotated with @WebMethod. The implementation includes a service-side handler that verifies a security credential sent with a request. For the GlassFish deployment, the interceptor servlet is the Metro WSServlet, but the Metro JAR files are not in the deployed WAR file because GlassFish comes with the Metro libraries. Here, for review, are the contents of the deployed WAR file:

WEB-INF/web.xml
WEB-INF/classes/predictions/DataStore.class
WEB-INF/classes/predictions/Prediction.class
WEB-INF/classes/predictions/Predictions.class
WEB-INF/classes/predictions/PredictionsSOAP.class
WEB-INF/classes/predictions/ServiceHashHandler.class
WEB-INF/classes/predictions/VerbosityException.class
WEB-INF/data/predictions.db
WEB-INF/lib/commons-codec.jar
WEB-INF/serviceHandler.xml
WEB-INF/sun-jaxws.xml

The upshot of this review is that services deployed under Endpoint, Tomcat, or Jetty should deploy either as is or with very small changes (for instance, removing Metro JARs from the WAR file) to GlassFish.

The next section picks up a theme from Chapter 3, which focused on clients against REST-style services. The idea is to illustrate how a service-side API (in this case, Restlet) can be combined with a different client-side API (in this case, the JAX-WS Dispatch interface). In the world of JASes, mixed APIs are more the rule than the exception. The Restlet/JAX-WS combination is not without complication, however; the section is thus an opportunity to review, in the context of GlassFish deployment, the challenges of RESTful services before moving on to SOAP-based services under GlassFish.

JAX-RS, Restlet, and JAX-WS with @WebServiceProvider have service-side and client-side APIs. Chapter 3 includes an example of the JAX-RS client-side API. This section introduces a client-side API designed specifically for @WebServiceProvider services, but this API will be used, as an illustration, against the Restlet adages2 service deployed with GlassFish. In summary, the service-side API is Restlet and the client-side API is JAX-WS, in particular the Dispatch client-side API targeted at @WebServiceProvider services.

A RESTful Service as a @WebServiceProvider of Chapter 2 covers the REST-style adages3 service, which is implemented as a @WebServiceProvider. The implementation class begins as follows:

public class AdagesProvider implements Provider<Source> {

The Provider<Source> provides XML documents: a Source is a source of XML specifically. To implement the Provider interface, the AdagesProvider class defines the method:

public Source invoke(Source request); // declaration

The invoke method on the service side expects an XML Source, perhaps null, and returns an XML Source, which also could be null. In the usual case, the response Source is not null; the request Source would be null on HTTP bodyless requests such as GETs and DELETEs. A Source can serve as the source of a transformation, which yields a Result. For example, a Source of XML might be transformed into an HTML, plain text, or some other MIME type of document. The standard JAX-P Transformer class encapsulates a transform method that takes two arguments: the first is a Source of XML and the second is a Result (see Figure 7-3).

The Provider interface used on the service side has, as a client-side counterpart, the Dispatch interface. A Dispatch object, which serves as dynamic service-proxy, likewise encapsulates an invoke method that expects a Source (perhaps null) and returns a Source (perhaps null but typically not). The details of a Dispatch and Provider interaction can be summed up as follows (see Figure 7-4):

  • A client calls the Dispatch method invoke with an XML document as the Source. If the request does not require such a document as an argument, the Source can be null.
  • The client request is dispatched, on the service side, to the invoke method in a Provider. The Source argument passed to the service-side invoke corresponds to the Source argument passed to the client-side invoke.
  • The service transforms the Source into an appropriate Result—for instance, a DOM tree that can be searched for content of interest or an HTML document suitable for display in a browser.
  • The service returns an XML Source as a response; the response is typically not null.
  • The client receives the Source from the service as the return value of the Dispatch method invoke. The client then transforms this Source, as needed, into an appropriate Result for client-side processing.

The DispatchClient (see Example 7-1) uses the invoke method in a Dispatch service-proxy to make CRUD calls against the Restlet adages2 service. This client creates a Service instance (line 1), whose identity is a QName, in this case uri:restlet. The name is arbitrary but should be unique. The Restlet service, written with an altogether different API, has no invoke method to pair up with the Dispatch method named invoke. Nonetheless, the communication between the two is mostly trouble-free. The trouble spot is clarified next.

Example 7-1. The DispatchClient against the Restlet adages2 service

import javax.xml.ws.Service;
import javax.xml.namespace.QName;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.Dispatch;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.ws.handler.MessageContext;
import java.util.Map;
import java.io.StringReader;

public class DispatchClient {
    private static final String baseUrl = "http://localhost:8081/aphorisms2/";

    public static void main(String[ ] args) {
        new DispatchClient().callRestlet();
    }
    private void callRestlet() {
        QName qname = getQName("uri", "restlet"); // service's name
        Service service = Service.create(qname);                                   1
        runTests(service);
    }
    private void runTests(Service service) {
        // get all -- plain text
        Dispatch<Source> dispatch = getDispatch(service, getQName("get", "All"),   2
                                                baseUrl);
        setRequestMethod(dispatch, "GET");                                         3
        Source result = dispatch.invoke(null);                                     4
        stringifyAndPrintResponse("Result (plaintext):", result);
        // get all -- xml
        dispatch = getDispatch(service, getQName("get", "AllXml"), baseUrl + "xml");
        setRequestMethod(dispatch, "GET");
        result = dispatch.invoke(null);
        stringifyAndPrintResponse("Result (xml):", result);
        // get all -- json
        dispatch =
            getDispatch(service, getQName("get", "AllJson"), baseUrl + "json");
        setRequestMethod(dispatch, "GET");
        result = dispatch.invoke(null);
        stringifyAndPrintResponse("Result (json):", result);
        // get one -- xml
        dispatch =
            getDispatch(service, getQName("get", "OneXml"), baseUrl + "xml/2");
        setRequestMethod(dispatch, "GET");
        result = dispatch.invoke(null);
        stringifyAndPrintResponse("Result (one--xml):", result);
        // delete
        dispatch =
            getDispatch(service, getQName("delete", "One"), baseUrl + "delete/3");
        setRequestMethod(dispatch, "DELETE");
        result = dispatch.invoke(null);
        stringifyAndPrintResponse("Result (delete):", result);
        // post -- failure
        dispatch =
            getDispatch(service, getQName("post", "Create"), baseUrl + "create");
        setRequestMethod(dispatch, "POST");
        String cargo = "<a>words=This test will not work!</a>"; // minimal XML     5
        StringReader reader = new StringReader(cargo);
        result = dispatch.invoke(new StreamSource(reader));
        stringifyAndPrintResponse("Result (post):", result);
    }
    private Dispatch<Source> getDispatch(Service service, QName portName,
        String url) {
        service.addPort(portName, HTTPBinding.HTTP_BINDING, url);
        return service.createDispatch(portName,
                                      Source.class,
                                      javax.xml.ws.Service.Mode.MESSAGE);
    }
    private void setRequestMethod(Dispatch<Source> dispatcher, String method) {
        Map<String, Object> rc = dispatcher.getRequestContext();
        rc.put(MessageContext.HTTP_REQUEST_METHOD, method);
    }
    private QName getQName(String ns, String ln) {
        return new QName(ns, ln);
    }
    private void stringifyAndPrintResponse(String msg, Source result) {
        String str = null;
        if (result instanceof StreamSource) {
            try {
                StreamSource source = (StreamSource) result;
                byte[ ] buff = new byte[1024]; // adages are short
                source.getInputStream().read(buff);
                str = new String(buff);
            }
            catch(Exception e) { throw new RuntimeException(e); }
        }
        System.out.println("\n" + msg + "\n" + str);
    }
}

After the setup, the DispatchClient makes six calls against the Restlet service (lines 2 through 4 illustrate), which can be summarized as follows:

getAllPT

The first call gets all of the Adages in plain text. The response, extracted from the Source, is:

 1: What can be shown cannot be said. -- 7 words
 2: If a lion could talk, we could not understand him. -- 10 words
 ...

This call uses a GET request with the default URI /, the slash.

For all of the calls against the Restlet service, the Source of XML returned as a response is sent to the stringifyAndPrintResponse method. This method first checks whether the Source is, in fact, a StreamSource and, if so, extracts the bytes from the InputStream encapsulated in the StreamSource. These bytes then are fed to a String constructor, which produces a string. The resulting string may be plain text, as in this first sample call, or XML and JSON, as in later sample calls.

getAllXml
This call becomes another GET request but the URI is now /xml. The response has the same informational content as getAllPT but the format is XML.
getAllJson
This call also results in a GET request but with a URI of /json. The response is in JSON format.
getOne
This call results in yet another GET request with the URI /xml/2, which specifies the Adage with an id of 2. The response is an XML document.
deleteOne
This call becomes a DELETE request with the URI /delete/3, which specifies the Adage with an id of 3 as the one to remove from the list of Adages. The response is a plain text confirmation of the deletion.
create

This call fails. The response is an error message:

No words were given for the adage.

The problem arises because, with a POST request against the URI /create, the Source argument to the invoke method cannot be null but, rather, must contain the words in the Adage to be created. The Restlet service expects a simple HTML-like form in the body of the POST request, and this form has key/value pairs such as:

words=This is the way the world ends

The Restlet service searches on the key words to get the value, in this case the line from the T. S. Eliot poem “The Hollow Men.” On the client side, however, the call to the create operation uses a POST request against the URI /create, and the contents of the POST body are given as a Source instance—of XML. When the underlying XML libraries parse the non-XML string above, the parser throws an exception. The fix is to turn the string into XML, for example, the minimalist XML document (line 5):

<a>words=This is the way the world ends</a>

When this document is turned into a StreamSource argument to the Dispatch method invoke, the XML parser is satisfied. The problem now shifts to the service side because the Restlet service expects a simple key/value pair, not a key/value pair embedded as text in an XML element. As a result, the Restlet service complains that it cannot find the lookup key words in the body of the POST request.

The example reinforces a hard lesson in programming and many other activities: the devil is in the details. The fix to the problem would be relatively straightforward; perhaps the easiest fix would be to make the Restlet service flexible enough to handle POST requests whose cargo is either plain text or XML. The example also underscores that the JAX-WS @WebServiceProvider API is XML-centric. In this API, a Dispatch client against a Provider service such as the adages3 service (see Section 2.5 in Chapter 2) would be natural because each side would be dealing with XML Source arguments and return values.