O'Reilly logo

Java Web Services: Up and Running by Martin Kalin

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Implementing RESTful Web Services As HttpServlets

Here is a short review of servlets with emphasis on their use to deliver RESTful services. The class HttpServlet extends the class GenericServlet, which in turn implements the Servlet interface. All three are in the package javax.servlet, which is not included in core Java. The Servlet interface declares five methods, the most important of which is the service method that a web container invokes on every request to a servlet. The service method has a ServletRequest and a ServletResponse parameter. The request is a map that contains the request information from a client, and the response provides a network connection back to the client. The GenericServlet class implements the Service methods in a transport-neutral fashion, whereas its HttpServlet subclass implements these methods in an HTTP-specific way. Accordingly, the service parameters in the HttpServlet have the types HttpServletRequest and HttpServletResponse. The HttpServlet also provides request filtering: the service method dispatches an incoming GET request to the method doGet, an incoming POST request to the method doPost, and so on. Figure 4-2 depicts a servlet container with several servlets.

A servlet container with instances of various servlets

Figure 4-2. A servlet container with instances of various servlets

In the HttpServlet class, the do methods are no-ops (that is, methods with empty bodies) that can be overridden as needed in a programmer-derived subclass. For example, if the class MyServlet extends HttpServlet and overrides doGet but not doPost, then doPost remains a no-op in MyServlet instances.

HttpServlets are a natural, convenient way to implement RESTful web services for two reasons. First, such servlets provide methods such as doGet and doDelete that match up with HTTP verbs, and these methods execute as callbacks that the web container invokes as needed. Second, the HttpServletRequest and HttpServletResponse are the same two arguments to every do method, which encourages a uniform pattern of request processing: client-supplied data are read from the HttpServletRequest map and processed as required; then a response is sent back to the client through the output stream associated with the HttpServletResponse.

The RabbitCounterServlet

The RabbitCounterServlet that follows is a RESTful, servlet-based version of the SOAP-based RabbitCounter service of Chapter 3. The service has a deliberately simple logic to keep focus on what makes the servlet such an attractive implementation of a RESTful service. Here is the source code:

package ch04.rc;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.http.HTTPException;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.beans.XMLEncoder;

public class RabbitCounterServlet extends HttpServlet {
    private Map<Integer, Integer> cache;

    // Executed when servlet is first loaded into container.
    public void init() {
        cache = Collections.synchronizedMap(new HashMap<Integer, Integer>());
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String num = request.getParameter("num");
        
        // If no query string, assume client wants the full list
        if (num == null) {
            Collection<Integer> fibs = cache.values();
            send_typed_response(request, response, fibs);
        }
        else {
            try {
                Integer key = Integer.parseInt(num.trim());
                Integer fib = cache.get(key);
                if (fib == null) fib = -1;
                send_typed_response(request, response, fib);
            }
            catch(NumberFormatException e) {
                send_typed_response(request, response, -1);
            }
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String nums = request.getParameter("nums");
        if (nums == null)
            throw new HTTPException(HttpServletResponse.SC_BAD_REQUEST);

        // Extract the integers from a string such as: "[1, 2, 3]"
        nums = nums.replace('[', '\0');
        nums = nums.replace(']', '\0');
        String[ ] parts = nums.split(", ");
        List<Integer> list = new ArrayList<Integer>();
        for (String next : parts) {
            int n = Integer.parseInt(next.trim());
            cache.put(n, countRabbits(n));
            list.add(cache.get(n));
        }
        send_typed_response(request, response, list + " added.");
    }

    public void doDelete(HttpServletRequest request, HttpServletResponse response) {
        String key = request.getParameter("num");
        // Only one Fibonacci number may be deleted at a time.
        if (key == null)
            throw new HTTPException(HttpServletResponse.SC_BAD_REQUEST);
        try {
            int n = Integer.parseInt(key.trim());
            cache.remove(n);
            send_typed_response(request, response, n + " deleted.");
        }
        catch(NumberFormatException e) {
            throw new HTTPException(HttpServletResponse.SC_BAD_REQUEST);
        }
    }
    public void doPut(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    public void doInfo(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    public void doHead(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    public void doOptions(HttpServletRequest req, HttpServletResponse res) {
        throw new HTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }

    private void send_typed_response(HttpServletRequest request,
                                     HttpServletResponse response,
                                     Object data) {
        String desired_type = request.getHeader("accept");

        // If client requests plain text or HTML, send it; else XML.
        if (desired_type.contains("text/plain"))
            send_plain(response, data);
        else if (desired_type.contains("text/html"))
            send_html(response, data);
        else
            send_xml(response, data);
    }

    // For simplicity, the data are stringified and then XML encoded.
    private void send_xml(HttpServletResponse response, Object data) {
        try {
            XMLEncoder enc = new XMLEncoder(response.getOutputStream());
            enc.writeObject(data.toString());
            enc.close();
        }
        catch(IOException e) {
            throw new HTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
    private void send_html(HttpServletResponse response, Object data) {
        String html_start =
            "<html><head><title>send_html response</title></head><body><div>";
        String html_end = "</div></body></html>";
        String html_doc = html_start + data.toString() + html_end;
        send_plain(response, html_doc);
    }

    private void send_plain(HttpServletResponse response, Object data) {
        try {
            OutputStream out = response.getOutputStream();
            out.write(data.toString().getBytes());
            out.flush();
        }
        catch(IOException e) {
            throw new HTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private int countRabbits(int n) {
        if (n < 0) throw new HTTPException(403);

        // Easy cases.
        if (n < 2) return n;
        // Return cached value if present.
        if (cache.containsKey(n)) return cache.get(n);
        if (cache.containsKey(n - 1) && cache.containsKey(n - 2)) {
           cache.put(n, cache.get(n - 1) + cache.get(n - 2));
           return cache.get(n);
        }
        // Otherwise, compute from scratch, cache, and return.
        int fib = 1, prev = 0;
        for (int i = 2; i <= n; i++) {
            int temp = fib;
            fib += prev;
            prev = temp;
        }
        cache.put(n, fib); 
        return fib;
    }
}     

The RabbitCounterServlet overrides the init method, which the servlet container invokes when the servlet is first loaded. The method constructs the map that stores the Fibonacci numbers computed on a POST request. The other supported HTTP verbs are GET and DELETE. A GET request without a query string is treated as a request to read all of the numbers available, whereas a GET request with a query string is treated as a request for a specific Fibonacci number. The service allows the deletion of only one Fibonacci number at a time; hence, a DELETE request must have a query string that specifies which number to delete. The service does not implement the remaining CRUD operation, update; hence, the doPut method, like the remaining do methods, throws an HTTP 405 exception using the constant:

HttpServletResponse.SC_METHOD_NOT_ALLOWED

for clarity. There are similar constants for the other HTTP status codes.

Requests for MIME-Typed Responses

The RabbitCounterServlet differs from the first RESTful example in being implemented as a servlet instead of as a @WebServiceProvider. The RESTful servlet differs in a second way as well; that is, by honoring the request that a response be of a specified MIME type. Here is a client against the servlet:

import java.util.List;
import java.util.ArrayList;
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.io.IOException;
import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

class ClientRC {
    private static final String url = "http://localhost:8080/rc/fib";

    public static void main(String[ ] args) {
        new ClientRC().send_requests();
    }

    private void send_requests() {
        try {
            HttpURLConnection conn = null;

            // POST request to create some Fibonacci numbers.
            List<Integer> nums = new ArrayList<Integer>();
            for (int i = 1; i < 15; i++) nums.add(i);
            String payload = URLEncoder.encode("nums", "UTF-8") + "=" +
                URLEncoder.encode(nums.toString(), "UTF-8");

            // Send the request
            conn = get_connection(url, "POST");
            conn.setRequestProperty("accept", "text/xml");
            DataOutputStream out = new DataOutputStream(conn.getOutputStream());
            out.writeBytes(payload);
            out.flush();
            get_response(conn);

            // GET to test whether POST worked
            conn = get_connection(url, "GET");
            conn.addRequestProperty("accept", "text/xml");
            conn.connect();
            get_response(conn);

            conn = get_connection(url + "?num=12", "GET");
            conn.addRequestProperty("accept", "text/plain");
            conn.connect();
            get_response(conn);

            // DELETE request
            conn = get_connection(url + "?num=12", "DELETE");
            conn.addRequestProperty("accept", "text/xml");
            conn.connect();
            get_response(conn);

            // GET request to test whether DELETE worked
            conn = get_connection(url + "?num=12", "GET");
            conn.addRequestProperty("accept", "text/html");
            conn.connect();
            get_response(conn);
        }
        catch(IOException e) { System.err.println(e); }
        catch(NullPointerException e) { System.err.println(e); }
    }

    private HttpURLConnection get_connection(String url_string, String verb) {
        HttpURLConnection conn = null;
        try {
            URL url = new URL(url_string);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(verb);
            conn.setDoInput(true);
            conn.setDoOutput(true);
        }
        catch(MalformedURLException e) { System.err.println(e); }
        catch(IOException e) { System.err.println(e); }
        return conn;
    }
    private void get_response(HttpURLConnection conn) {
        try {
            String xml = "";
            BufferedReader reader =
                new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String next = null;
            while ((next = reader.readLine()) != null)
                xml += next;
            System.out.println("The response:\n" + xml);
        }
        catch(IOException e) { System.err.println(e); }
    }
}     

The client sends POST, GET, and DELETE requests, each of which specifies the desired MIME type of the response. For example, in the POST request the statement:

conn.setRequestProperty("accept", "text/xml");

requests that the response be an XML document. The value of the accept key need not be a single MIME type. For instance, the request statement could be:

conn.setRequestProperty("accept", "text/xml, text/xml, application/soap");

Indeed, the listed types can be prioritized and weighted for the service’s consideration. This example sticks with single MIME types such as text/html.

The RabbitCounterServlet can send responses of MIME types text/xml (the default), text/html, and text/plain. Here is the client’s output, slightly formatted and documented for readability:

The response: // from the initial POST request to create some Fibonacci numbers
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_06" class="java.beans.XMLDecoder"> 
<string>
   [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] added.
</string> 
</java>

The response: // from a GET request to confirm that the POST worked
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_06" class="java.beans.XMLDecoder"> 
<string>[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]</string> 
</java>

The response: // from a GET request with text/plain as the desired type
144

The response: // from a DELETE request
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_06" class="java.beans.XMLDecoder"> 
<string>12 deleted.</string> 
</java>

The response: // from a GET to confirm the DELETE with HTML as the desired type
<html><head><title>send_html response</title></head>
<body><div>-1</div></body>
</html>

In the last response, the returned value of –1 signals that the Fibonacci number for 12 is not available. The XML and HTML formats are simple, of course, but they illustrate how RESTful services can generate typed responses that satisfy requests.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required