Sep 2, 2017 10:46 AM PT

Web services in Java SE, Part 3: Creating RESTful Web services

Learn how to create RESTful-based Web services

Part 2 of this four-part series on Java SE Web services showed how to use the JAX-WS API to develop SOAP-based Web services. JAX-WS also supports RESTful-based Web services, which this article shows how to develop. It first defines and then builds and runs a library Web service, and next defines, builds, and runs a simple client that accesses this service.

Defining a library web service

The library Web service, which I've named Library, manages a library of books with their authors. It consists of a main Library class along with supporting Book and Author classes.

Exploring the Library class

The Library class manages the four HTTP operations (DELETE, GET, POST, and PUT) that are the hallmark of a RESTful Web service and which, in this example, handle requests to delete a specific book (identified via its ISBN) or all books, get a specific book (identified via its ISBN) or the ISBNs of all books, insert a new book, or update an existing book. Listing 1 presents this class's source code.

Listing 1. Library's endpoint class

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;

import javax.xml.transform.dom.DOMResult;

import javax.xml.transform.stream.StreamSource;

import javax.xml.ws.BindingType;
import javax.xml.ws.Endpoint;
import javax.xml.ws.Provider;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.WebServiceProvider;

import javax.xml.ws.handler.MessageContext;

import javax.xml.ws.http.HTTPBinding;
import javax.xml.ws.http.HTTPException;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.NodeList;

@WebServiceProvider
@ServiceMode(value = javax.xml.ws.Service.Mode.MESSAGE)
@BindingType(value = HTTPBinding.HTTP_BINDING)
public class Library implements Provider<Source>
{
   private final static String LIBFILE = "library.ser";
   @Resource
   private WebServiceContext wsContext;
   private Map<String, Book> library;

   Library()
   {
      try
      {
         library = deserialize();
      }
      catch (IOException ioe)
      {
         library = new HashMap<>();
      }
   }

   @Override
   public Source invoke(Source request)
   {
      if (wsContext == null)
         throw new RuntimeException("dependency injection failed on wsContext");
      MessageContext msgContext = wsContext.getMessageContext();
      switch ((String) msgContext.get(MessageContext.HTTP_REQUEST_METHOD))
      {
         case "DELETE": return doDelete(msgContext);
         case "GET"   : return doGet(msgContext);
         case "POST"  : return doPost(msgContext, request);
         case "PUT"   : return doPut(msgContext, request);
         default      : throw new HTTPException(405);
      }
   }

   private Source doDelete(MessageContext msgContext)
   {
      try
      {
         String qs = (String) msgContext.get(MessageContext.QUERY_STRING);
         if (qs == null)
         {
            library.clear();
            serialize();
            StringBuilder xml = new StringBuilder("<?xml version=\"1.0\"?>");
            xml.append("<response>all books deleted</response>");
            return new StreamSource(new StringReader(xml.toString()));
         }
         else
         {
            String[] pair = qs.split("=");
            if (!pair[0].equalsIgnoreCase("isbn"))
               throw new HTTPException(400);
            String isbn = pair[1].trim();
            library.remove(isbn);
            serialize();
            StringBuilder xml = new StringBuilder("<?xml version=\"1.0\"?>");
            xml.append("<response>book deleted</response>");
            return new StreamSource(new StringReader(xml.toString()));
         }
      }
      catch (IOException ioe)
      {
         throw new HTTPException(500);
      }
   }

   private Source doGet(MessageContext msgContext)
   {
      String qs = (String) msgContext.get(MessageContext.QUERY_STRING);
      if (qs == null)
      {
         Set<String> keys = library.keySet();
         Iterator<String> iter = keys.iterator();
         StringBuilder xml = new StringBuilder("<?xml version=\"1.0\"?>");
         xml.append("<isbns>");
         while (iter.hasNext())
            xml.append("<isbn>" + iter.next() + "</isbn>");
         xml.append("</isbns>");
         return new StreamSource(new StringReader(xml.toString()));
      }
      else
      {
         String[] pair = qs.split("=");
         if (!pair[0].equalsIgnoreCase("isbn"))
            throw new HTTPException(400);
         String isbn = pair[1].trim();
         Book book = library.get(isbn);
         if (book == null)
            throw new HTTPException(404);
         StringBuilder xml = new StringBuilder("<?xml version=\"1.0\"?>");
         xml.append("<book isbn=\"" + book.getISBN() + "\" " +
                    "pubyear=\"" + book.getPubYear() + "\">");
         xml.append("<title>" + book.getTitle() + "</title>");
         for (Author author: book.getAuthors())
            xml.append("<author>" + author.getName() + "</author>");
         xml.append("<publisher>" + book.getPublisher() + "</publisher>");
         xml.append("</book>");
         return new StreamSource(new StringReader(xml.toString()));
      }
   }

   private Source doPost(MessageContext msgContext, Source source)
   {
      try
      {
         DOMResult dom = new DOMResult();
         Transformer t = TransformerFactory.newInstance().newTransformer();
         t.transform(source, dom);
         XPathFactory xpf = XPathFactory.newInstance();
         XPath xp = xpf.newXPath();
         NodeList books = (NodeList) xp.evaluate("/book", dom.getNode(),
                                                 XPathConstants.NODESET);
         String isbn = xp.evaluate("@isbn", books.item(0));
         if (library.containsKey(isbn))
            throw new HTTPException(400);
         String pubYear = xp.evaluate("@pubyear", books.item(0));
         String title = xp.evaluate("title", books.item(0)).trim();
         String publisher = xp.evaluate("publisher", books.item(0)).trim();
         NodeList authors = (NodeList) xp.evaluate("author", books.item(0),
                                                   XPathConstants.NODESET);
         List<Author> auths = new ArrayList<>();
         for (int i = 0; i < authors.getLength(); i++)
            auths.add(new Author(authors.item(i).getFirstChild()
                                        .getNodeValue().trim()));
         Book book = new Book(isbn, title, publisher, pubYear, auths);
         library.put(isbn, book);
         serialize();
      }
      catch (IOException | TransformerException e)
      {
         throw new HTTPException(500);
      }
      catch (XPathExpressionException xpee)
      {
         throw new HTTPException(400);
      }
      StringBuilder xml = new StringBuilder("<?xml version=\"1.0\"?>");
      xml.append("<response>book inserted</response>");
      return new StreamSource(new StringReader(xml.toString()));
   }

   private Source doPut(MessageContext msgContext, Source source)
   {
      try
      {
         DOMResult dom = new DOMResult();
         Transformer t = TransformerFactory.newInstance().newTransformer();
         t.transform(source, dom);
         XPathFactory xpf = XPathFactory.newInstance();
         XPath xp = xpf.newXPath();
         NodeList books = (NodeList) xp.evaluate("/book", dom.getNode(),
                                                 XPathConstants.NODESET);
         String isbn = xp.evaluate("@isbn", books.item(0));
         if (!library.containsKey(isbn))
            throw new HTTPException(400);
         String pubYear = xp.evaluate("@pubyear", books.item(0));
         String title = xp.evaluate("title", books.item(0)).trim();
         String publisher = xp.evaluate("publisher", books.item(0)).trim();
         NodeList authors = (NodeList) xp.evaluate("author", books.item(0),
                                                   XPathConstants.NODESET);
         List<Author> auths = new ArrayList<>();
         for (int i = 0; i < authors.getLength(); i++)
            auths.add(new Author(authors.item(i).getFirstChild()
                                        .getNodeValue().trim()));
         Book book = new Book(isbn, title, publisher, pubYear, auths);
         library.put(isbn, book);
         serialize();
      }
      catch (IOException | TransformerException e)
      {
         throw new HTTPException(500);
      }
      catch (XPathExpressionException xpee)
      {
         throw new HTTPException(400);
      }
      StringBuilder xml = new StringBuilder("<?xml version=\"1.0\"?>");
      xml.append("<response>book updated</response>");
      return new StreamSource(new StringReader(xml.toString()));
   }

   private Map<String, Book> deserialize() throws IOException
   {
      try (BufferedInputStream bis = 
           new BufferedInputStream(new FileInputStream(LIBFILE));
           XMLDecoder xmld = new XMLDecoder(bis))
      {
         @SuppressWarnings("unchecked")
         Map<String, Book> result = (Map<String, Book>) xmld.readObject();
         return result;
      }
   }

   private void serialize() throws IOException
   {
      try (BufferedOutputStream bos
             = new BufferedOutputStream(new FileOutputStream(LIBFILE));
           XMLEncoder xmle = new XMLEncoder(bos))
      {
         xmle.writeObject(library);
      }
   }

   public static void main(String[] args)
   {
      Endpoint.publish("http://localhost:9902/library", new Library());
   }
}

Listing 1 relies on various Java XML APIs such as DOM and XPath. If you're unfamiliar with these APIs, you might want to check out my Java XML and JSON book (advertised at the end of this post).

Annotating library

Listing 1 reveals that the Library class is prefixed with @WebServiceProvider, @ServiceMode, and @Binding annotations.

@WebServiceProvider specifies that Library is a Web service endpoint class implementing the javax.xml.ws.Provider<T> interface (an alternative to a Service Endpoint Interface [SEI] for services that need to work at the XML message level) in terms of its T invoke(T request) method. The actual type argument that you pass to type parameter T identifies the source of request and response data, and is one of the following types: javax.xml.transform.Source, javax.activation.DataSource, or javax.xml.soap.SOAPMessage. For a RESTful Web service provider, you would specify Source or DataSource for T.

When a request is made to the RESTful Web service, the provider class's invoke() method is called with a source of bytes, such as a POST request's XML document. The invoke() method responds to the request in some appropriate way, returning a source of bytes in XML format that form the service's response. This method throws an instance of the javax.xml.ws.WebServiceException runtime exception class or one of its descendent classes (e.g., javax.xml.ws.http.HTTPException) when something goes wrong.

@ServiceMode specifies that Library's invoke() method receives entire protocol messages (instead of message payloads) by having its value() element initialized to javax.xml.ws.Service.Mode.MESSAGE. When this annotation isn't present, value() defaults to javax.xml.ws.Service.Mode.PAYLOAD.

@BindingType specifies that Library's invoke() method receives arbitrary XML messages over HTTP by having its value() element initialized to HTTPBinding.HTTP_BINDING -- the default binding is SOAP 1.1 over HTTP. Unlike @ServiceMode, @BindingType must be specified with this initialization; otherwise, you'll receive a runtime exception when a RESTful client sends a nonSOAP request message to this Web service provider.

Exploring library's fields

Library first declares a LIBFILE constant that identifies the name of the file that stores information about the books in the library. I could have used JDBC to create and access a library database, but decided to use a file to keep Listing 1 from becoming longer.

This string constant is initialized to library.ser, where ser indicates that the file stores serialized data. The stored data is an XML encoding of a map that contains Book and Author instances -- I'll present the map, discuss its encoding/decoding, and present these classes shortly.

The LIBFILE constant declaration is followed by a wsContext field declaration, where wsContext is declared to be of type javax.xml.ws.WebServiceContext and is annotated with @Resource. WebServiceContext is an interface that makes it possible for a Web service endpoint implementation class to access a request message's context and other information. The @Resource annotation causes an implementation of this interface to be injected into an endpoint implementation class, and causes an instance of this implementation class (a dependency) to be assigned to the variable.

A library field declaration follows the wsContext declaration, where library is declared to be of type Map<String, Book>. This variable stores books in a map, where a book's ISBN serves as a map entry's key, and the book's information is recorded in a Book object that serves as the map entry's value.

Exploring library's constructor

Library next declares a noargument constructor whose job is to initialize library. The constructor first attempts to deserialize library.ser's contents to a java.util.HashMap instance by calling the deserialize() method (explained later), and assign the instance's reference to library. If this file doesn't exist, java.io.IOException is thrown and an empty HashMap instance is created and assigned to library.

Exploring library's invoke() method

The invoke() method is now declared. Its first task is to verify that dependency injection succeeded by testing wsContext to determine if it contains the null reference. If so, dependency injection failed and an instance of the java.lang.RuntimeException class is created with a suitable message and thrown.

Continuing, invoke() calls WebServiceContext's MessageContext getMessageContext() method to return an instance of a class that implements the javax.xml.ws.handler.MessageContext interface. This instance abstracts the message context for the request being served at the time this method is called.

MessageContext extends Map<String, Object>, making MessageContext a special kind of map. This interface declares various constants that are used with the inherited Object get(String key) method to obtain information about the request. For example, get(MessageContext.HTTP_REQUEST_METHOD) returns a String object identifying the HTTP operation that the RESTful client wants performed; for example, POST.

At this point, you might want to convert the string's contents to uppercase and trim off any leading or trailing whitespace. I don't perform these tasks because the client that I present later will not allow an HTTP verb to be specified that isn't entirely uppercase and/or is preceded/followed by whitespace.

I use the switch-on-string language feature to simplify the logic for invoking the method that corresponds to the HTTP verb. The first argument passed to each of the doDelete(), doGet(), doPost(), and doPut() helper methods is the MessageContext instance (assigned to msgContext). Although not used by doPost() and doPut(), this instance is passed to these methods for consistency -- I might want to access the message context from doPost() and doPut() in the future. In contrast, invoke()'s request argument is passed only to doPost() and doPut() so that these methods can access the request's source of bytes, which consist of the XML for the book to be inserted or updated.

If any other HTTP verb (such as HEAD) should be passed as the request method, invoke() responds by throwing an instance of the HTTPException class with a 405 response code (request method not allowed).

Exploring library's doDelete() and doGet() methods

The doDelete() method first obtains the query string that identifies the book to delete via its ISBN (as in ?isbn=9781484219157). It does so by calling get(MessageContext.QUERY_STRING) on the msgContext argument passed to this method.

If the null reference returns, there's no query string and doDelete() deletes all entries in the map by executing library.clear(). This method then calls the serialize() method to persist the library map to library.ser, so that the next invocation of this Web service will find an empty library.

If a query string was passed, it will be returned in the form key1 = value1 & key2 = value2 &.... doDelete() assumes that only a single key = value pair is passed, and splits this pair into an array with two entries.

doDelete() first validates the key as one of isbn, ISBN, or any other uppercase/lowercase mix of these letters. When this key is any other combination of characters, doDelete() throws HTTPException with a 400 response code indicating a bad request. This validation isn't essential where a single key is concerned, but if multiple key/value pairs were passed, you would need to perform validation to differentiate between keys.

After extracting the ISBN value, doDelete() passes this value to library.remove(), which removes the ISBN String object key/Book object value entry from the library map. It then calls serialize() to persist the new map to library.ser, and creates an XML response message that is sent back to the client. The message is returned from invoke() as a String object encapsulated in a java.io.StringReader instance that's encapsulated in a javax.xml.transform.stream.StreamSource object.

If doDelete() encounters a problem, it throws an HTTPException instance with response code 500 indicating an internal error.

The doGet() method is similar to doDelete(). However, it responds to the absence or presence of a query string by returning an XML document containing a list of all ISBNs, or an XML document containing book information for a specific ISBN.

Exploring library's doPost() and doPut() methods

The doPost() and doPut() methods also have similar architectures. Each method first transforms the argument passed to its source parameter (which identifies the XML body of the POST or PUT request) to a javax.xml.transform.dom.DOMResult instance. This instance is then searched via XPath expressions, first for a single book element, then for the <book> tag's isbn and pubyear attributes, and finally for the book element's nested title, author, and publisher elements -- multiple author elements might be present. The gathered information is used to construct Author and Book objects, where the Author object(s) is/are stored in the Book object. The resulting Book object is stored in the library map, the map is serialized to library.ser, and a suitable XML message is sent to the client.

As well as providing a slightly different response message, doPost() and doPut() differ in whether or not the book is already recorded (as determined by its ISBN) in the map. If doPost() is called and an entry for the book is in the map, doPost() throws HTTPException with response code 400 (bad request). If doPut() is called and an entry for the book isn't in the map, doPut() throws the same exception.

Exploring library's deserialize() and serialize() methods

The doPut() method is followed by deserialize() and serialize() methods that are responsible for deserializing a serialized library map from library.ser and serializing this map to library.ser, respectively. These methods accomplish their tasks with the help of the java.beans.XMLDecoder and java.beans.XMLEncoder classes. According to their documentation, XMLEncoder and XMLDecoder are designed to serialize a JavaBean component to an XML-based textual representation and deserialize this representation to a JavaBean component, respectively.

After creating the necessary output stream to library.ser and instantiating XMLEncoder via a try-with-resources statement (to ensure proper resource cleanup whether or not an exception is thrown), serialize() invokes XMLEncoder's void writeObject(Object o) method with library as this method's argument so that the entire map will be serialized. The deserialize() method creates the necessary input stream to library.ser, instantiates XMLDecoder, invokes this instance's Object readObject() method, and returns the deserialized object after casting it to Map<String, Book>.

Exploring library's main() method

Lastly, Library declares a main() method that publishes this Web service on path /library of port 9902 of the local host, by executing Endpoint.publish("http://localhost:9902/library", new Library());.

Exploring the Book class

Library manages books via the Book helper class, whose beans store information about individual books. Listing 2 presents this class's source code.

Listing 2. Library's Book class

import java.util.List;

public class Book implements java.io.Serializable
{
   private String isbn;
   private String title;
   private String publisher;
   private String pubYear;
   private List<Author> authors;

   public Book() {} // Constructor and class must be public for instances to
                    // be treated as beans.

   public Book(String isbn, String title, String publisher, String pubYear,
               List<Author> authors)
   {
      setISBN(isbn);
      setTitle(title);
      setPublisher(publisher);
      setPubYear(pubYear);
      setAuthors(authors);
   }

   public List<Author> getAuthors() { return authors; }

   public String getISBN() { return isbn; }

   public String getPublisher() { return publisher; }

   public String getPubYear() { return pubYear; }

   public String getTitle() { return title; }

   public void setAuthors(List<Author> authors) { this.authors = authors; }

   public void setISBN(String isbn) { this.isbn = isbn; }

   public void setPublisher(String publisher) { this.publisher = publisher; }

   public void setPubYear(String pubYear) { this.pubYear = pubYear; }

   public void setTitle(String title) { this.title = title; }
}

Listing 2 reveals that a Book instance stores a book's ISBN, title, publisher, publication year, and list of authors. Various getter methods return this information; various setter methods let you change assorted details.

Exploring the Author class

Listing 2 reveals that Book depends on an Author helper class, whose beans store the names of individual authors, and which is presented in Listing 3.

Listing 3. Library's Author class

public class Author implements java.io.Serializable
{
   private String name;

   public Author() {}

   public Author(String name) { setName(name); }

   public String getName() { return name; }

   public void setName(String name) { this.name = name; }
}

Building and running the library web service

It's easy to build and run Library. Copy Listings 1 through 3 to Library.java, Book.java, and Author.java files that are stored in the same directory. Next, assuming that this directory is current, compile this source code as follows:

javac --add-modules java.xml.ws *.java

You can omit --add-modules java.xml.ws when compiling under Java 8 or earlier.

Assuming successful compilation, run this application as follows:

java --add-modules java.xml.ws Library

Again, you can omit --add-modules java.xml.ws when compiling under Java 8 or earlier.

The Library Web service should run continuously and you should observe no output.

Defining a library client

Now that you understand how the Library Web service is implemented, you need a client to try it out. Listing 4 presents the source code to an application that shows how a client can access Library via the java.net.HttpURLConnection class.

Listing 4. A client for accessing the Library Web service

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

import java.net.HttpURLConnection;
import java.net.URL;

public class LibraryClient
{
   final static String LIBURI = "http://localhost:9902/library";

   public static void main(String[] args) throws Exception
   {
      String book1 = "<?xml version=\"1.0\"?>" +
                     "<book isbn=\"0201548550\" pubyear=\"1992\">" +
                     "  <title>" +
                     "    Advanced C++" +
                     "  </title>" +
                     "  <author>" +
                     "    James O. Coplien" +
                     "  </author>" +
                     "  <publisher>" +
                     "    Addison Wesley" +
                     "  </publisher>" +
                     "</book>";
      doPost(book1);
      String book2 = "<?xml version=\"1.0\"?>" +
                     "<book isbn=\"9781430210450\" pubyear=\"2008\">" +
                     "  <title>" +
                     "    Beginning Groovy and Grails" +
                     "  </title>" +
                     "  <author>" +
                     "    Christopher M. Judd" +
                     "  </author>" +
                     "  <author>" +
                     "    Joseph Faisal Nusairat" +
                     "  </author>" +
                     "  <author>" +
                     "    James Shingler" +
                     "  </author>" +
                     "  <publisher>" +
                     "    Apress" +
                     "  </publisher>" +
                     "</book>";
      doPost(book2);
      doGet(null);
      doGet("0201548550");
      doGet("9781430210450");
      String book1u = "<?xml version=\"1.0\"?>" +
                      "<book isbn=\"0201548550\" pubyear=\"1992\">" +
                      "  <title>" +
                      "    Advanced C++" +
                      "  </title>" +
                      "  <author>" +
                      "    James O. Coplien" +
                      "  </author>" +
                      "  <publisher>" +
                      "    Addison Wesley" +
                      "  </publisher>" +
                      "</book>";
      doPut(book1u);
      doGet("0201548550");
      doDelete("0201548550");
      doGet(null);
   }

   static void doDelete(String isbn) throws Exception
   {
      URL url = new URL(LIBURI + ((isbn != null) ? "?isbn=" + isbn : ""));
      HttpURLConnection httpurlc = (HttpURLConnection) url.openConnection();
      httpurlc.setRequestMethod("DELETE");
      httpurlc.setDoInput(true);
      InputStreamReader isr;
      isr = new InputStreamReader(httpurlc.getInputStream());
      BufferedReader br = new BufferedReader(isr);
      StringBuilder xml = new StringBuilder();
      String line;
      while ((line = br.readLine()) != null)
         xml.append(line);
      System.out.println(xml);
      System.out.println();
   }

   static void doGet(String isbn) throws Exception
   {
      URL url = new URL(LIBURI + ((isbn != null) ? "?isbn=" + isbn : ""));
      HttpURLConnection httpurlc = (HttpURLConnection) url.openConnection();
      httpurlc.setRequestMethod("GET");
      httpurlc.setDoInput(true);
      InputStreamReader isr;
      isr = new InputStreamReader(httpurlc.getInputStream());
      BufferedReader br = new BufferedReader(isr);
      StringBuilder xml = new StringBuilder();
      String line;
      while ((line = br.readLine()) != null)
         xml.append(line);
      System.out.println(xml);
      System.out.println();
   }

   static void doPost(String xml) throws Exception
   {
      URL url = new URL(LIBURI);
      HttpURLConnection httpurlc = (HttpURLConnection) url.openConnection();
      httpurlc.setRequestMethod("POST");
      httpurlc.setDoOutput(true);
      httpurlc.setDoInput(true);
      httpurlc.setRequestProperty("Content-Type", "text/xml");
      OutputStream os = httpurlc.getOutputStream();
      OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
      osw.write(xml);
      osw.close();
      if (httpurlc.getResponseCode() == 200)
      {
         InputStreamReader isr;
         isr = new InputStreamReader(httpurlc.getInputStream());
         BufferedReader br = new BufferedReader(isr);
         StringBuilder sb = new StringBuilder();
         String line;
         while ((line = br.readLine()) != null)
            sb.append(line);
         System.out.println(sb.toString());
      }
      else
         System.err.println("cannot insert book: " + httpurlc.getResponseCode());
      System.out.println();
   }

   static void doPut(String xml) throws Exception
   {
      URL url = new URL(LIBURI);
      HttpURLConnection httpurlc = (HttpURLConnection) url.openConnection();
      httpurlc.setRequestMethod("PUT");
      httpurlc.setDoOutput(true);
      httpurlc.setDoInput(true);
      httpurlc.setRequestProperty("Content-Type", "text/xml");
      OutputStream os = httpurlc.getOutputStream();
      OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
      osw.write(xml);
      osw.close();
      if (httpurlc.getResponseCode() == 200)
      {
         InputStreamReader isr;
         isr = new InputStreamReader(httpurlc.getInputStream());
         BufferedReader br = new BufferedReader(isr);
         StringBuilder sb = new StringBuilder();
         String line;
         while ((line = br.readLine()) != null)
            sb.append(line);
         System.out.println(sb.toString());
      }
      else
         System.err.println("cannot update book: " + httpurlc.getResponseCode());
      System.out.println();
   }
}

LibraryClient is partitioned into a main() method and four do-prefixed methods for performing DELETE, GET, POST, and PUT operations. main() invokes each "do" method to make a request and output a response.

A "do" method first instantiates the java.net.URL class; doDelete() and doGet() attach query strings to their URI arguments when these methods are called with nonnull isbn arguments. The method then invokes URL's URLConnection openConnection() method to return a communications link between the application and URL instance as an instance of a concrete subclass of the abstract java.net.URLConnection class. This concrete subclass is HttpConnection because of the http:// prefix in the argument passed to URL's constructor.

HttpURLConnection's void setRequestMethod(String method) is then called to specify the HTTP verb, which must appear in uppercase with no whitespace. Depending on the "do" method, either void setDoInput(boolean doinput) is called with a true argument, or void setDoInput(boolean doinput) and void setDoOutput(boolean dooutput) are called with true arguments, to signify that an input stream or input and output streams are required to communicate with the Web service.

Each of doPost() and doPut() is required to set the Content-Type request header to text/xml, which it accomplishes by passing this header and MIME type to the void setRequestProperty(String key, String value) method. Forgetting to set the content type to text/xml causes the JAX-WS infrastructure to respond with an internal error response code (500).

doDelete() and doGet() read the XML from the connection's input stream and output this XML content to the standard output stream. Behind the scenes, the JAX-WS infrastructure makes the string of characters encapsulated in the StringReader instance, which is encapsulated in the StreamSource instance returned from invoke(), available on the input stream.

doPost() and doPut() access the connection's output stream and output their XML content to the stream. Behind the scenes, JAX-WS makes this content available to invoke() as an instance of a class that implements the Source interface. Assuming that the Web service responds with a success code (200), each method reads the XML reply from the connection's input stream and outputs this content to the standard output stream.

Building and running the library client

It's easy to build and run the Library client. Copy Listing 4 to a LibraryClient.java file that is stored in the current directory. Next, compile this source code as follows:

javac LibraryClient.java

Assuming successful compilation, and assuming that the Library Web service is running, run this application as follows:

java LibraryClient

You should observe the following output (slightly reformatted for clarity):

<?xml version="1.0" ?><response>book inserted</response>

<?xml version="1.0" ?><response>book inserted</response>

<?xml version="1.0" ?><isbns><isbn>9781430210450</isbn><isbn>0201548550</isbn></isbns>

<?xml version="1.0" ?><book isbn="0201548550" pubyear="1992"><title>Advanced C++</title>
   <author>James O. Coplien</author><publisher>Addison Wesley</publisher></book>

<?xml version="1.0" ?><book isbn="9781430210450" pubyear="2008"><title>Beginning Groovy and Grails</title>
   <author>Christopher M. Judd</author><author>Joseph Faisal Nusairat</author>
   <author>James Shingler</author><publisher>Apress</publisher></book>

<?xml version="1.0" ?><response>book updated</response>

<?xml version="1.0" ?><book isbn="0201548550" pubyear="1992"><title>Advanced C++</title>
   <author>James O. Coplien</author><publisher>Addison Wesley</publisher></book>

<?xml version="1.0" ?><response>book deleted</response>

<?xml version="1.0" ?><isbns><isbn>9781430210450</isbn></isbns>

Run LibraryClient a second time and you should observe that the second <response>book inserted</response> message has been replaced with cannot insert book: 400. This message is output because the library map already contains an entry whose key identifies ISBN 9781430210450.

Conclusion

This article and the first article in this four-part series on Java SE Web services referred to Java's SAAJ API. Part 4 wraps up this series by introducing you to SAAJ and a few other advanced Java SE Web services topics.

download
Get the source code for this post's applications. Created by Jeff Friesen for JavaWorld

The following software was used to develop the post's code:

  • 64-bit JDK 9ea+181

The post's code was tested on the following platform(s):

  • JVM on 64-bit Windows 10