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

Learn how to create RESTful-based Web services

1 2 3 Page 2
Page 2 of 3

@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.

   Book(String isbn, String title, String publisher, String pubYear,
        List<Author> authors)

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

   String getISBN() { return isbn; }

   String getPublisher() { return publisher; }

   String getPubYear() { return pubYear; }

   String getTitle() { return title; }

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

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

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

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

   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() {}

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

   String getName() { return name; }

   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
1 2 3 Page 2
Page 2 of 3