The format of SOAP messages is defined by a note submitted to the World Wide Web Consortium (W3C) in May 2000 by a group of companies including Microsoft and IBM. This note, which describes Version 1.1 of SOAP and can be downloaded from http://www.w3.org/TR/SOAP, is not a formally adopted W3C specification but it is, nevertheless, the specification on which all existing SOAP implementations, including SAAJ, are based. The W3C is working on a formal definition of the next revision of SOAP, to be called Version 1.2. At the time of this writing, the SOAP 1.2 specification is available for public review, and Sun plans to include support for SOAP 1.2 in SAAJ (and JAX-RPC) when the specification is finalized.[2]
[2] The acronym SOAP originally stood for "Simple Object Access Protocol." In SOAP Version 1.2, the letters no longer have any meaning. This is probably a good idea, because the original name is arguably not really an accurate description of what SOAP is: although the S and the P could be justified, there is really nothing in SOAP that provides object access, at least not in any sense that would be acceptable to an object-oriented programmer.
SOAP defines a way to wrap information represented in XML so that it can be transmitted between peer entities that know how to interpret that information and, presumably, act on it to provide a service. Other than the fact that the useful content of the message (which is known as the payload) must be encoded in XML, SOAP does not mandate a particular set of XML elements and attributes to be used to represent primitive items of data (such as integers, floating-point numbers, and strings) in terms of XML constructs, although it does specify an encoding mechanism for these data types and others, the use of which is encouraged. In practice, these encoding rules, commonly referred to as "SOAP section 5 encoding," have become a de facto standard and are the default encoding used by JAX-RPC to represent method arguments and return values. Although detailed coverage of these encoding rules is outside the scope this book, there are several examples of their use in this chapter; you'll find a complete discussion of the SOAP encoding rules in Java and SOAP, by Robert Englander (O'Reilly). Applications are free to use privately defined encodings instead of the SOAP section 5 rules if they wish.
What SOAP does specify is the overall structure of a message, together with rules for wrapping SOAP messages when the underlying transport protocol is HTTP. There are two slightly different ways to construct a SOAP message depending on whether the message has any attachments (the meaning of which is defined later). The SOAP Version 1.1 specification covers only the case where there are no attachments and requires that a message be constructed as shown in Figure 3-1. The outermost layer of the message is a protocol-specific wrapper, the nature of which is defined in the specification only for HTTP and which we'll see later in this chapter. Inside this wrapper is the SOAP message itself, consisting of an envelope, a header part, a body part, and optional additional content.
The SOAP envelope is, as its name suggests, a top-level XML element that acts as a container for the rest of the message. The SOAP header is optional but, if present, must be the first element in the envelope. It is intended to be used to carry information that can be used in the processing or routing of the message payload, such as a digital signature to guarantee the integrity of payload data or authentication information to validate the identity of the message sender. There are specifications, referred to in the JAXM documentation as profiles, that define specific SOAP headers to be used to communicate information to applications that are aware of the profile. The reference implementation of JAXM includes messaging providers that can create SOAP messages that have pre-installed headers as required by the SOAP-RP or ebXML TRP specifications. For further information on these two profiles, see Chapter 4.
The SOAP body is the only mandatory part of the envelope and contains the actual payload intended for the ultimate recipient of the message. It must either follow the SOAP header or, if the header is omitted, be the first element in the envelope. Following the body, it is possible to include additional content, the interpretation of which, like the payload itself, is entirely dependent on the sending and receiving entities.
Everything within the SOAP envelope must be encoded in XML. For many applications, this is not an acceptable restriction. For example, an online book store offering a web service interface might want to supply book details that include a photograph of the book's cover, scanned copies of pages from the book, or a sample chapter in the form of a PDF document. In order to enable applications such as this, Microsoft and Hewlett Packard created an additional specification called "SOAP Messages with Attachments," which can be downloaded from http://www.w3.org/TR/SOAP-attachments. Like the SOAP 1.1 document, this specification has been submitted to the W3C, and although it has not been approved by W3C, it has become the de facto standard for packaging SOAP messages that require some element of non-XML content.
A SOAP message that has attachments is formatted as shown in Figure 3-2. Attachments typically contain non-XML data, such as images, audio, or plain text. The first part of the message contains the SOAP envelope and its content, constructed as described by the SOAP 1.1 specification. Each object to be attached is then added as MIME content, and the whole message is packaged as a MIME Multipart/Related message as defined by RFC 2387 (which can be found at http://www.ietf.org/rfc/rfc2387.txt). Each separate message part has its own MIME wrapper that specifies, among other things, the type of data that it contains � it may contain an identifier that can be used within the SOAP envelope to refer to it. Using SAAJ, you can construct SOAP messages with attachments that contain any type of data that has a MIME encoding. This topic is discussed in more detail in Section 3.6, later in this chapter, where you'll also see exactly what a typical message with attachments looks like when bound to the HTTP protocol.
In order to allow vendors to supply their own implementations, almost all of the SAAJ API is made up of interfaces and abstract classes. In particular, the SOAPMessage class, which represents a SOAP message, is abstract and therefore cannot be directly instantiated. To create a SOAP message, you need to use a MessageFactory. MessageFactory, which is part of the javax.xml.soap package, is itself an abstract class, an instance of which can be obtained using its static newInstance( ) method:
public static MessageFactory newInstance( ) throws SOAPException;
To permit vendors to plug in their own implementations of MessageFactory, this method looks at the system property javax.xml.soap.MessageFactory and in a couple of other places to select the subclass of MessageFactory that will be used. Refer to the description of javax.xml.soap.MessageFactory in the reference section of this book for a complete description of the steps taken to find a MessageFactory.
If a problem is encountered while instantiating the MessageFactory, a SOAPException is thrown. SOAPException, which is one of only four concrete classes in the javax.xml.soap package, is a checked exception that is thrown by many of the methods in the SAAJ API. It contains a message describing the reason for the exception and, for cases where the root cause is an exception thrown from a lower level, provides a getCause( ) method to provide access to the original exception.
MessageFactory has two methods that create SOAP messages:
public SOAPMessage createMessage( ) throws SOAPException; public SOAPMessage createMessage(MimeHeaders headers, InputStream is) throws SOAPException;
The second of these two methods is used typically in a servlet to deserialize a message received from an input stream, an example of which we'll see in Section 3.3.2, later in this chapter. To create a SOAP message for transmission, you need to use the first method:
MessageFactory messageFactory = MessageFactory.newInstance( ); SOAPMessage message = messageFactory.createMessage( );
The message that createMessage( ) returns does not contain anything useful, but rather than pause here to introduce the API needed to add some content to it, let's skip ahead a little and look at how to transmit the skeleton message. In order to send a message, you need an instance of the SOAPConnection class, which, since it is abstract, must be obtained from a factory:
SOAPConnectionFactory connFactory = SOAPConnectionFactory.newInstance( ); SOAPConnection conn = connFactory.createConnection( );
Just like MessageFactory, SOAPConnectionFactory is an abstract class that vendors can supply their own implementations of. The actual class that the newInstance( ) method uses is determined by a procedure that is virtually identical to that used by the MessageFactory class, and which is described in the reference section for javax.xml.soap.SOAPConnectionFactory, later in this book. If all else fails, the reference implementation returns its own implementation of SOAPConnectionFactory (or throws a SOAPException if this class is not available).[3]
[3] Despite its name, SOAPConnection does not actually represent a connection to a message receiver. Instead, think of it as representing a connection between application code and the SAAJ runtime that can be used to send SOAP messages. In Chapter 4, we'll see that JAXM has a similar class, called ProviderConnection, which provides an association between a JAXM application and a messaging provider, but similarly does not imply that an immediate network connection is made.
Once you have a SOAPConnection, you can use its call( ) method to transmit a message:
public abstract SOAPMessage call(SOAPMessage request, Object destination) throws SOAPException;
The destination object determines where the message will be sent. The SAAJ specification requires that the following types be supported as valid destination arguments:
An instance of the java.net.URL class.
An instance of String, provided that its value can be converted to a valid URL using its URL(String url) constructor.
An instance of the class javax.xml.messaging.URLEndpoint. This class, which wraps a URL, is part of the JAXM API and is therefore not likely to be used by pure SAAJ applications. It is accepted as a valid destination because Version 1.0 of the JAXM specification, which did not have SAAJ separated out as a freestanding API, defined the destination parameter of this method as being of type javax.xml.messaging.Endpoint. This is an abstract base class of which URLEndpoint is the only concrete implementation; therefore, supporting a destination of type URLEndpoint provides backward-compatibility.
The reference implementation supports all of these possibilities; vendor implementations are free to add their own destination types as required. All of these destination types resolve to a URL, but the structure of this URL depends entirely on the implementation of the receiver and the environment in which it is hosted. We'll see a typical example in Section 3.3.2, later in this chapter.
As noted earlier in this section, a SAAJ client can only use a synchronous request/response programming model; therefore, the call( ) method blocks having sent the message until a reply sent to the message is received, or until an error causes it to throw a SOAPException. The reply is returned to the method caller. Having received a reply, if you don't need to make further use of the SOAPConnection object, you should use its close( ) method to release it; this method can be called only once. Once it is, any further invocations of call( ) method result in a SOAPException.
The example source code for this chapter includes a client that creates a SOAP message, prints its content, and then sends it to a servlet that echoes it straight back. To run this example, start your web server or application server, open a command window and make chapter3\echoservice your working directory, and then use the following command to build and deploy the servlet:
ant deploy
Next, compile and run the client application using the command:
ant compile-client run-client
In the command window, you'll see quite a lot of output, including the XML for the SOAPMessage that was transmitted (which has been reformatted to make it more readable):
1 <soap-env:Envelope xmlns:soap-env= "http://schemas.xmlsoap.org/soap/envelope/"> 2 <soap-env:Header/> 3 <soap-env:Body/> 4 </soap-env:Envelope>
This is what the basic message returned by the createMessage( ) method looks like if you don't make any changes to it. Lines 1 and 4 contain the XML elements that represent the SOAP message envelope. As you can see, the element name is Envelope and it is qualified using the namespace prefix soap-env, which is associated with the URL http://schemas.xmlsoap.org/soap/envelope. This URL identifies the entire SOAP message as being formatted according to the rules of SOAP Version 1.1. If a SOAP message whose envelope is qualified with any other namespace is received, then it should be treated as a SOAP version mismatch, and the receiver is required to reject the message by generating a SOAP fault (as described in Section 3.4, later in this chapter).
Inside the envelope are the header and body elements, for which the element names are Header and Body, respectively. These two elements are also qualified with the same namespace tag as the envelope. In this case, since we didn't actually add anything to the message returned by the MessageFactory createMessage( ) method, the header and body parts are both empty. Under normal circumstances, if you do not need any header content, you completely remove the header part. You'll see how to do this later in this chapter.
The SOAPConnection class provides the call( ) method to allow a SOAP message to be transmitted, but there is no corresponding API that takes care of receiving a message. The example just shown uses a servlet as the target of the message. This is convenient because the SOAPConnection class uses HTTP as the default protocol when transmitting messages, and the servlet API contains everything that you need to handle a payload delivered over an HTTP connection, including a convenient API to handle the HTTP headers.
The java.xml.messaging package includes a servlet called JAXMServlet that can be used to receive and handle SOAP messages. However, since this package is part of the JAXM API, using it would introduce a dependency on JAXM as well as SAAJ, which is not desirable because JAXM is not part of the J2EE 1.4 platform. To avoid this, the servlet used in the echoservice example is based on a slightly simpler version of JAXMServlet that is provided in the sample code supplied with the JWSDP. The source code for this servlet can be found in the file chapter3\servlet\ora\jwsnut\saaj\SAAJServlet.java relative to the installation directory of the example code for this book. Most of the code is also shown in Example 3-1.
package ora.jwsnut.saaj; import java.io.IOException; import java.io.OutputStream; import java.util.Enumeration; import java.util.Iterator; import java.util.StringTokenizer; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.soap.MessageFactory; import javax.xml.soap.MimeHeader; import javax.xml.soap.MimeHeaders; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; /** * A servlet that can be used to host a SAAJ * service within a web container. This is based * on ReceivingServlet.java in the JWSDP tutorial * examples. */ public abstract class SAAJServlet extends HttpServlet { /** * The factory used to build messages */ protected MessageFactory messageFactory; /** * Initialisation - create the MessageFactory */ public void init(ServletConfig config) throws ServletException { super.init(config); try { messageFactory = MessageFactory.newInstance( ); } catch (SOAPException ex) { throw new ServletException("Failed to create MessageFactory", ex); } } /** * Handles a POST request from a client. The request is assumed * to contain a SOAP message with the HTTP binding. */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // Get all the HTTP headers and convert them to a MimeHeaders object MimeHeaders mimeHeaders = getMIMEHeaders(request); // Create a SOAPMessage from the content of the HTTP request SOAPMessage message = messageFactory.createMessage(mimeHeaders, request.getInputStream( )); // Let the subclass handle the message SOAPMessage reply = onMessage(message); // If there is a reply, return it to the sender. if (reply != null) { // Set OK HTTP status, unless there is a fault. boolean hasFault = reply.getSOAPPart().getEnvelope( ) .getBody( ).hasFault( ); response.setStatus(hasFault ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : HttpServletResponse.SC_OK); // Force generation of the MIME headers if (reply.saveRequired( )) { reply.saveChanges( ); } // Copy the MIME headers to the HTTP response setHttpHeaders(reply.getMimeHeaders( ), response); // Send the completed message OutputStream os = response.getOutputStream( ); reply.writeTo(os); os.flush( ); } else { // No reply - set the HTTP status to indicate this response.setStatus(HttpServletResponse.SC_NO_CONTENT); } } catch (SOAPException ex) { throw new ServletException("SOAPException: " + ex); } } /** * Method implemented by subclasses to handle a received SOAP message. * @param message the received SOAP message. * @return the reply message, or <code>null</code> if there is * no reply to be sent. */ protected abstract SOAPMessage onMessage(SOAPMessage message) throws SOAPException; // HEADER HANDLING CODE NOT SHOWN..... }
When SAAJServlet receives a SOAP message, it hands it to the abstract onMessage( ) method and sends the reply message returned by this method to the message sender. To provide specific message handling, subclass SAAJServlet and implement the required processing in the onMessage( ) method.
The doPost( ) method of this servlet demonstrates how to receive a SOAP message from an HTTP connection. When transmitted over HTTP, the protocol-specific wrapper shown in Figure 3-1 and Figure 3-2 is represented as HTTP headers, and the rest of the message is written out as a stream of XML or, if the message has attachments, as a Multipart/Related MIME payload containing the XML and the encoded attachment data. Example 3-2 shows what an empty SOAP message looks like when bound into an HTTP request message.
Content-Length: 17 1 SOAPAction: "" User-Agent: Java1.4.0 Host: localhost:5050 Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Connection: keep-alive <?xml version="1.0" encoding="UTF-8"?> <soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"> <soap-env:Header/> <soap-env:Body/> </soap-env:Envelope>
When this HTTP request message is delivered to a servlet, the HTTP headers are automatically stripped off and made available via the HttpServletRequest object, while the rest of the data, namely the XML itself, can be obtained by reading the InputStream provided by the HttpServletRequest getInputStream( ) method. To convert the XML back into a SOAPMessage, the servlet uses a MessageFactory that it creates when it is initialized, and calls the second variant of the createMessage( ) method that we saw in Section 3.3.1 earlier in this chapter, passing it the HTTP headers and the input stream containing the XML-encoded SOAP message:[4]
[4] In case you are wondering why the createMessage( ) method needs access to the HTTP headers as well as the XML content, it is because it uses the Content-Type header to decide whether the SOAP message has attachments, and decodes the rest of the HTTP request appropriately.
// Create a SOAPMessage from the content of the HTTP request SOAPMessage message = messageFactory.createMessage(mimeHeaders, request. getInputStream( ));
The first argument supplied to this method is an object of type javax.xml.soap.MimeHeaders, which encapsulates the HTTP headers that were received as part of the HTTP binding. We'll look at this class and the way in which it is populated from the HTTP request in Section 3.5. later in this chapter.
Once the SOAPMessage is created, it is passed to the onMessage( ) method. If this method returns a reply, it needs to be sent back to the caller in the servlet's HTTP response message. Naturally, the reply needs to be wrapped in HTTP in the same way that the request was when it was transmitted by the sender. The original message was created as a result of calling the SOAPConnection call( ) method, which opens an HTTP connection to the servlet and takes care of building the HTTP wrapper for the message. Here, however, we can't use this method because we want to return the message using the HTTP connection originally created by the client, so we have to create the HTTP binding for the reply message ourselves. This requires three steps:
Get any HTTP headers to be sent with the reply message from the message itself. This can be done by calling the SOAPMessage getMimeHeaders( ) method, which returns them in the form of a MimeHeaders object.
Use the methods of the servlet's HttpServletResponse object to install the HTTP headers in the reply.
Get the XML representation of the SOAPMessage itself and write it to the OutputStream obtained from the HttpServletResponse getOutputStream( ) method.
Before we can start this process, however, we need to ensure that the HTTP headers for the SOAPMessage have been created. As you'll see later, a newly created SOAPMessage object does not actually contain any XML or any of the HTTP headers that will eventually be used when transmitting it over HTTP. Instead, it consists of a hierarchy of objects that represents the envelope, the header, the body, and so on, in much the same way as a DOM model represents an XML document. In order to force the headers to be created, we use the following code:
if (reply.saveRequired( )) { reply.saveChanges( ); }
The saveChanges( ) method creates not only the headers, but also the XML representation of the message itself. The saveRequired( ) method is used to discover whether it is actually necessary to perform this step � once saveChanges( ) is called, it does not need to be called again, and saveRequired( ) returns false until some change is made to the SOAPMessage that causes either the headers or the XML to need to be updated. The actual code that gets the HTTP headers from the reply SOAPMessage and installs them in the HTTP response is shown in Section 3.5.
The XML representation of a SOAPMessage can be written to an OutputStream using the following SOAPMessage method:
public void writeTo(OutputStream os);
It is not necessary to call saveChanges( ) before invoking this method, since writeTo( ) calls it for itself (if necessary). Hence, the following code is all that is necessary to write the reply message back to the caller:
// Send the completed message OutputStream os = response.getOutputStream( ); reply.writeTo(os); os.flush( );
Incidentally, the writeTo( ) method is a useful debugging aid because you can use code like the following to dump the XML represented by a SOAPMessage to the standard output stream:
message.writeTo(System.out);
Note, however, that this method writes only the XML � it does not include the MIME headers, which must be obtained separately by calling getMimeHeaders( ).
This leaves only one open issue regarding the handling of SOAP messages in a servlet: what destination URL should the client application supply to the SOAPConnection call( ) method to arrange for the message to be delivered to the servlet? The simple answer is that it depends on how the servlet is deployed. In the case of the echoservice example used here, the message echoing is provided by a simple servlet that is derived from SAAJServlet and overrides its onMessage( ) method to return the SOAPMessage that it is called with, having first written its content to the web containers log for debugging purposes, as shown in Example 3-3.
public class EchoServlet extends SAAJServlet { /** * Output stream used to save a SOAP message * for logging. */ private ByteArrayOutputStream os = new ByteArrayOutputStream( ); /** * Handles a received SOAP message by simply * returning it. */ public SOAPMessage onMessage(SOAPMessage message) { // Convert the message to string representation // and log it. try { message.writeTo(os); log("Received SOAP message:\n" + os.toString( )); os.reset( ); } catch (Exception ex) { log("Exception", ex); } // Return the received message to the caller. return message; } }
This servlet (together with its base class, SAAJServlet) is wrapped in a WAR file and then deployed as a web application called SAAJEchoService. The web application's web.xml file looks like this:
<web-app> <display-name>SAAJ Echo Service</display-name> <description>SAAJ Message Echo Service</description> <servlet> <servlet-name>EchoService</servlet-name> <display-name>Servlet for the SAAJ Message Echo Service </display-name> <servlet-class>ora.jwsnut.chapter3.echoservice.EchoServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>EchoService</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
The url-pattern element means that all URLs that map to the web application SAAJEchoService are directed to the servlet. Assuming that the web application is deployed in a web server listening at port 8000 on your local machine, the client application may therefore use the following URL to send SOAP messages to the servlet:
If the url-pattern element looks like this:
<url-pattern>/EchoServlet/*</url-pattern>
the result is that the required URL is:
If you are writing a SAAJ client that is intended to communicate with an existing server, then the URL that you need to use should be obtained from the service provider. In some cases, the address of a service is not fully specified by a URL. An example of this might be a server implementation where all requests are initially handled by a single servlet and then routed internally to the appropriate web service based on something in the message itself. In order to aid with such an arrangement, the SOAP 1.1 specification defines an HTTP header called SOAPAction that can be used to provide a string that holds further information that can be used to identify the target web service. If a service does not require SOAPAction, its value should be set to the empty string. This is, in fact, the default value for the SOAPAction header when you create a new SOAPMessage, as you can see from Example 3-2. If the service you need to communicate with requires a specific SOAPAction value, you can install it using the MimeHeaders setHeader( ) method:
message.getMimeHeaders( ).setHeader("SOAPAction", "ServiceInfo");
Although a SOAP message is transmitted as a set of characters encoded in UTF-8, holding the message in character form would make it difficult to manipulate. Instead, SOAPMessage contains a hierarchy of objects that represent the various parts of the message and the XML elements and text (if any) that it contains. The message is kept in this form until it is ready to be transmitted, at which point a UTF-8 byte stream is created from its content. Figure 3-3 shows a typical hierarchy of elements that might make up a SOAPMessage object.
A SOAPMessage object consists of a single SOAPPart and zero or more AttachmentParts. The default message returned by the MessageFactory createMessage( ) method contains a SOAPPart but no attachments. AttachmentParts, if they are required, must be explicitly created and added to create a SOAP with attachments message. The SOAPPart, in turn, contains a SOAPEnvelope object, which represents the envelope shown in Figure 3-1 and Figure 3-2. Within the envelope is the SOAP message header, represented by an object implementing the SOAPHeader interface and the SOAP message body in the form of a SOAPBody object. Both the body and the header contain XML markup that constitutes, respectively, the message payload and any information required to qualify or process the payload, represented by SOAPElements or Text nodes.
SOAPMessage is an abstract class that represents the whole SOAP message. It contains methods that deal with the following:
When a SOAP message is wrapped in a MIME-aware transport protocol such as HTTP or SMTP, MIME-related information must be included to identify the data being carried as an XML message. A SOAPMessage object supplies default values for the MIME Content-Type, Content-Length, and SOAPAction headers, but it also provides a method that returns the underlying MimeHeaders object that contains the associated MIME headers. You can use this object to retrieve the headers for a received message or to add additional ones for an outgoing message. See Section 3.5 later in this chapter, for details.
SOAPMessage also provides a convenience method to set or retrieve the value of the optional Content-Description header, which is used to attach descriptive text to the message.
A SOAP message is created and manipulated by application code as a tree of objects like that shown in Figure 3-3. For transmission, however, it must be converted to a byte stream that represents the XML itself. The saveChanges( ) method creates a byte array that represents the object tree and stores it within the SOAPMessage object ready to be written by the writeTo( ) method. Since the creation of the byte representation is an expensive process, it should be performed only if it has not already been done, or if the content of the object tree has been modified since saveChanges( ) was last called. The saveRequired( ) method can be used to determine whether it is necessary to call saveChanges( ). The writeTo( ) method calls both saveRequired( ) and saveChanges( ) as necessary, so it is rarely necessary for client code to do anything other than call writeTo( ).
The SOAPMessage getSOAPPart( ) method returns a reference to the SOAPPart that contains the XML parts of the message:
public SOAPPart getSOAPPart( ) throws SOAPException;
Note that it is not possible to replace the SOAPPart associated with a SOAPMessage.
A SOAPMessage can have any number of attachments, each of which may contain data of any type that has a MIME encoding. SOAPMessage provides the methods necessary to create the AttachmentPart objects that represent individual attachments, add them to the message, retrieve them, and remove them. Attachments must be used if it is necessary to include non-XML content in a message; their use is described in detail in Section 3.6, later in this chapter.
The SOAPPart object is a wrapper for the SOAP envelope and therefore contains the part of the message that must be represented in XML. For simple SOAP messages, SOAPPart does not really add anything other than the ability to access the SOAPEnvelope object, but for a SOAP message with attachments, it provides the ability to manipulate the content of the MIME wrapper for the SOAP part of the message, as shown in Figure 3-2.
For most practical purposes, the getEnvelope( ) method is the most important feature of SOAPPart:
public SOAPEnvelope getEnvelope( ) throws SOAPException;
There is no direct way with the SAAJ API to create your own SOAPEnvelope object and use it to replace the one installed when a SOAPMessage is created. However, you can replace the entire content of the SOAPPart, as described next, which has the same effect but is less convenient.
Most of the SOAPPart API is concerned with creating, retrieving, and manipulating MIME headers. These headers, however, are not related to those associated with the SOAPMessage � they are used only when the message has one or more attachments, and become part of the MIME wrapper for the SOAP part of the message. Although you can still use this API for a message that does not have attachments, it will have no practical effect on the message that is finally transmitted.
The getContent( ) and setContent( ) methods allow access to and replacement of the content of the SOAPPart in a form that can be manipulated by the JAXP APIs. The setContent( ) method in particular can be used to replace the SOAP envelope with an alternative that is represented by a DOM tree or the content of an input stream encoded in XML. While this might have some practical applications, it would be more useful to be able to import XML in either of these forms into the SOAP body, but there is currently no API that supports this.[5]
[5] The only way to import XML into the SOAP body is to use the SOAPPart getContent( ) method to get access to the content of the SOAPPart, transform it into a DOM tree, import the XML into the DOM tree, and then use setContent( ) to replace the entire SOAP part with the modified content. If you really need to do this, you'll find the necessary code in Java Web Services, by David Chappell and Tyler Jewell (O'Reilly).
This object represents the SOAP envelope, which must contain a single SOAPBody object and may contain a SOAPHeader and additional XML elements. If these parts are present, they must appear in the following order: header, body, and additional elements. Like SOAPPart, SOAPEnvelope is of little direct use to most applications, except as a means of accessing the header and body parts.
SOAPEnvelope provides methods that allow references to the header and body objects that it contains to be retrieved:
public SOAPBody getBody( ) throws SOAPException; public SOAPHeader getHeader( ) throws SOAPException;
The message created by the MessageFactory createMessage( ) method contains empty header and body parts.
It is possible to create and add a new header or a new body part to a SOAP message. This is useful when modifying the content of a received message in order to forward it or to use it as the basis for the reply to the sender:
public SOAPBody addBody( ) throws SOAPException; public SOAPHeader addHeader( ) throws SOAPException;
These methods create, install, and return an empty SOAPBody or SOAPHeader object, respectively. These methods represent the only way to directly create instances of these objects. You can only install a new SOAPHeader or a new SOAPBody if you have already removed the previous one. There are, however, no methods named removeHeader( ) or removeBody( ) that allow you to do this. Instead, you have to rely on the fact that both SOAPBody and SOAPHeader (like SOAPEnvelope) are derived from the javax.xml.soap.Node interface, which provides the detachNode( ) method to allow it to be removed from whatever it is contained in. Here, for example, is how you would remove the SOAPHeader from a SOAPMessage and install a new, empty one:
// Remove the message header SOAPEnvelope envelope = message.getSOAPPart( ).getEnvelope( ); envelope.getHeader( ).detachNode( ); // Add a new header SOAPHeader header = envelope.addHeader( );
Nodes are discussed in Section 3.3.4, later in this chapter.
For reasons of backwards-compatibility with Version 1.0 of JAXM (which contained the API that is now known as SAAJ),
SOAPEnvelope
includes factory methods that create Name objects for use with XML elements within the envelope. In SAAJ 1.1, the preferred way to create Name objects is by using the SOAPFactory class. Name objects are described in Section 3.3.4, later in this chapter.
SOAPHeader is a container for headers that determines the way in which the message payload is processed or interpreted. There are standardized uses of SOAP, such as SOAP-RP or ebXML-TRP, that specify the format of certain headers that are of meaning to receivers that implement these standards, but applications are free to define and use their own private headers.[6] SOAP itself does not specify any standard headers, and therefore the API provided by SOAPHeader is restricted to methods that add a new header entry or allow existing header entries to be accessed. SOAP headers are discussed in Section 3.7, later in this chapter.
[6] The terminology can get a little confusing when discussing headers. A SOAP message can only contain a single SOAPHeader, but a SOAPHeader may contain any number of XML fragments that represent application-defined headers. When the context does not make it clear whether "header" refers to one such XML fragment or the entire SOAPHeader, we use the term "header entry" instead.
SOAPBody is the container that holds the real payload of the SOAP message. Logically, the payload consists of one or more XML document fragments. However, in the SAAJ API, the payload is constructed as a hierarchy of elements and text nodes, represented by the SOAPElement and Node interfaces (described in the next section). The payload is typically included in the body by creating a SOAPBodyElement to represent the root element of each fragment and then adding nested elements and text as necessary:
public SOAPBodyElement addBodyElement(Name name) throws SOAPException;
The body may also contain a SOAPFault object that reports a failure to properly process either a message header or the message payload itself. SOAPBody provides methods to add a SOAPFault object to the body and to handle one in a message that has been received. This API is covered in Section 3.4, later in this chapter.
The SAAJ interfaces that we have seen so far provide the framework within which SOAP messages can be built, but the actual message content is constructed by using four more basic interfaces that are described in this section. Here's a brief description of these interfaces, which we'll expand on by reference to an example in the rest of this section:
In XML terms, a Name object represents a qualified element name (or QName) � that is, a local name together with an optional namespace prefix and the associated namespace URI. For example, in the case of the XML tag that represents the SOAP envelope:
<soap-env:Envelope xmlns:soap-env= "http://schemas.xmlsoap.org/soap/envelope/">
Envelope is the local name of the element, soap-env is the namespace prefix, http://schemas.xmlsoap.org/soap/envelope/ is the URI that uniquely identifies the namespace, and the combination soap-env:Envelope is the qualified element name (qualified because it contains a namespace prefix).
Node is the base interface for the nodes in the object tree that make up the content of a SOAP message. It provides the methods necessary to link itself to or remove itself from its parent node as well as to allow application code to discover its parent node. However, this interface does not provide a way to add child nodes and therefore represents a leaf in the object tree. SAAJ applications usually do not deal directly with Nodes � instead, they handle objects of the derived types SOAPElement and Text.
Text is a type of Node that holds a text string. Text is always a leaf node and its content may represent an XML comment.
SOAPElement is a subinterface of Node that adds the methods required to attach child nodes, and therefore need not always be a leaf in the object tree.
SOAPElement has a number of subinterfaces that represent entities that can appear within the SOAPPart. These subinterfaces and their relationships to SOAPElement and Node are shown in Figure 3-4.
It is interesting to note that SOAPEnvelope, SOAPBody, and SOAPHeader are all SOAPElements, which implies that you can add content directly to them. This is, of course, true, although we'll see later that both SOAPBody and SOAPHeader have their own specific SOAPElement variants (SOAPBodyElement and SOAPHeaderElement) that are used as their immediate children and take special action when you attempt to add an arbitrary SOAPElement instead. We'll look at how a typical SOAP message is constructed by examining an example application that gets a list of O'Reilly book titles from a server and displays them in list form in a Swing-based user interface. Later in this chapter, we'll extend this example so that it can fetch images of the front covers of these books from the server, as an illustration of the use of non-XML SOAP message attachments.
Before running this example, you need to compile and deploy the service implementation, which is based on SAAJServlet. To do so, start your web server or application server, open a command window and make chapter3\bookimageservice your working directory, and then use the following command to build and deploy the servlet:
ant deploy
Next, compile and run the client application using the command:
ant compile-client run-client
Once the client starts, you'll see the list of books that it has obtained from the server, as shown in Figure 3-5.
The SOAP message that is sent to get the list of book titles is shown in Example 3-4.
1 <?xml version="1.0" encoding="UTF-8"?> 2 <soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" 3 xmlns:tns="urn:jwsnut.bookimageservice"> 4 <soap-env:Body> 5 <tns:BookList/> 6 </soap-env:Body> 7 </soap-env:Envelope>
There are three differences between this message and the empty one shown in Example 3-2:
The SOAP header part has been removed because headers are not required by this application.
The addition of a namespace declaration on the Envelope element.
The SOAP body contains an application-defined element called BookList.
The BookList element is, of course, not defined by the SOAP 1.1 specification � it is a private element that is recognized by the web service as a request to send the list of book titles that it knows about. Since this request does not need any parameters, all that is required is an empty element. However, since the BookList element is private, we define an XML namespace for the web service, assign a prefix to it, and associate the prefix with the element name, so that there is no ambiguity about the meaning of the request.
The namespace itself is defined by the xmlns attribute on the Envelope element, as shown on line 3 of Example 3-4. In order to declare a namespace, we have to assign a prefix and associate it with the namespace's URI. In this case, we chose to use the prefix tns. Although arbitrary, it is quite common to use this particular prefix for the namespace that refers to the service's private elements, since the letters tns serve as a useful mnemonic for "this namespace," as distinct from the namespace used for the elements that are part of the SOAP envelope itself. The namespace URI is, again, arbitrary, although it should be unique, and the same value must be used by both the client and the web service implementation.[7] Here, we choose to use a URN with a string value that identifies the service as the book image service from this book, but we could have used any other valid URI.
[7] Although the client and the service implementation must use exactly the same URI for the namespace, they do not have to use the same prefix, because the prefix serves only as a shorthand for the URI itself.
The SOAP 1.1 specification does not actually require elements in the body of the SOAP message to be namespace-qualified, although it recommends that they are. From the point of view of the service implementation, it is just as well to require that the client supplies the namespace so it can unambiguously check that if it receives a BookList element from a client, the element actually represents the BookList request that it provides, rather than a similarly named request for an entirely different service. You'll see shortly how the service implementation makes this check. Incidentally, you don't have to associate a namespace prefix with every SOAP element. You can, if you wish, declare a default namespace on an element, like this:
<?xml version="1.0" encoding="UTF-8"?> <soap-env:Envelope xmlns:soap-env= "http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:jwsnut.bookimageservice"> <soap-env:Body> <BookList/> </soap-env:Body> </soap-env:Envelope>
An xmlns attribute without an associated namespace prefix specifies the namespace that will be associated with any element, such as BookList, that does not have an explicit namespace prefix. This default is in operation for the scope of the element to which the xmlns attribute is attached.
Now let's look at the code that created the SOAPMessage shown in Example 3-4. This code is shown in Example 3-5.
1 // Build the message 2 SOAPMessage message = messageFactory.createMessage( ); 3 4 // Remove the message header 5 SOAPEnvelope envelope = message.getSOAPPart( ).getEnvelope( ); 6 envelope.getHeader( ).detachNode( ); 7 8 // Set up the namespace declaration 9 envelope.addNamespaceDeclaration(SERVICE_PREFIX, SERVICE_URI); 10 11 // Add the element for the book list request 12 SOAPBody soapBody = envelope.getBody( ); 13 soapBody.addBodyElement(BOOK_LIST_NAME);
The first part of this code (lines 2 to 6) creates an empty message, gets a reference to the SOAPEnvelope, and then removes the empty header part, which this application does not use. Line 9 adds the declaration of the private namespace used by this service using the SOAPElement addNamespaceDeclaration( ) method:
public SOAPElement addNamespaceDeclaration(String prefix, String uri) throws SOAPException;
In this case, the prefix and the URI supplied are declared as static strings within the class definition:
// The URI used to qualify elements for this service private static final String SERVICE_URI = "urn:jwsnut.bookimageservice"; // The namespace prefix used in elements for this service private static final String SERVICE_PREFIX = "tns";
The addNamespaceDeclaration( ) method can be used to attach a namespace declaration to any SOAPElement. Its effect is scoped to that element and its child elements, and returns a reference to the SOAPElement on which it is invoked. In this case, we apply the declaration to the SOAPEnvelope itself, so that it applies to the entire message.
Lines 12 and 13 are responsible for adding the BookList element to the SOAP body, using the SOAPBody addBodyElement( ) method:
public SOAPElement addBodyElement(Name name) throws SOAPException;
The Name object passed to this method determines the element name and the namespace within which it is defined. As you'll see later in this chapter, there are several ways to create a Name object. In this case, the Name is created using one of the methods provided by the SOAPFactory class:
// The name of the element used to request a book name list private static Name BOOK_LIST_NAME; // SOAPFactory for message pieces private static SOAPFactory soapFactory; // Create the BookList element soapFactory = SOAPFactory.newInstance( ); BOOK_LIST_NAME = soapFactory.createName("BookList", SERVICE_PREFIX, SERVICE_URI);
SOAPFactory is a factory for objects that can be added to SOAP messages, including SOAPElements, Detail objects (which are used in connection SOAP faults), and Names.[8] A Name object is a representation of a fully qualified element name and therefore requires the following attributes:
[8] SOAPFactory is a new class added to SAAJ Version 1.1. The earlier API (JAXM 1.0) used the SOAPEnvelope createName( ) methods, which require access to the SOAPEnvelope object, to create Name objects. In many cases, however, an application will want to construct part of a SOAP message without having to have access to the SOAPEnvelope within which it will eventually be enclosed, so the context-free SOAPFactory class was added to make this possible. SOAPFactory is a generalization of the JAXM 1.0 SOAPElementFactory class, which is deprecated as of SAAJ Version 1.1.
The local name of the element � in this case, BookList
The URI of the namespace within which the name is defined
The prefix used to represent the namespace (tns in this example)
It is also possible to have Names that are not namespace-qualified. In these cases, the URI and namespace prefix are both null.
Once the Name is created, the SOAPBody addBodyElement( ) method creates the actual SOAPBodyElement and installs it in the body. Since this element does not have any nested elements, it ends up looking like this:
<tns:BookList/>
You can see that the namespace prefix and the local name that were used to create the Name object become part of the element.
The book image service is implemented as a servlet derived from SAAJServlet, much of the code for which is shown in Example 3-6.
/** * A servlet that uses SAAJ attachments to * serve images to a client. */ public class BookImageServlet extends SAAJServlet { // The XML Schema namespace private static final String XMLSCHEMA_URI = "http://www.w3.org/2001/XMLSchema"; // The XML Schema instance namespace private static final String XMLSCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance"; // Namespace prefix for XML Schema private static final String XMLSCHEMA_PREFIX = "xsd"; // Namespace prefix for XML Schema instance private static final String XMLSCHEMA_INSTANCE_PREFIX = "xsi"; // The namespace prefix used for SOAP encoding private static final String SOAP_ENC_PREFIX = "SOAP-ENC"; // The URI used to qualify elements for this service private static final String SERVICE_URI = "urn:jwsnut.bookimageservice"; // The namespace prefix used in elements for this service private static final String SERVICE_PREFIX = "tns"; // MessageFactory for replies from this service private static MessageFactory messageFactory; // SOAPFactory for message pieces private static SOAPFactory soapFactory; // The name of the element used to request a book name list private static Name BOOK_LIST_NAME; // The name of the element used to reply to a book name list request private static Name BOOK_TITLES_NAME; // The name of the element used to request a book image private static Name BOOK_IMAGE_REQUEST_NAME; // The name of the element used to respond to a book image request private static Name BOOK_IMAGES_NAME; // The name of the attribute used to hold the image encoding private static Name IMAGE_TYPE_ATTRIBUTE; // The name of the href attribute private static Name HREF_ATTRIBUTE; /** * Handles a received SOAP message. */ public SOAPMessage onMessage(SOAPMessage message) throws SOAPException { if (messageFactory == null) { // Create all static data on first call messageFactory = MessageFactory.newInstance( ); soapFactory = SOAPFactory.newInstance( ); BOOK_LIST_NAME = soapFactory.createName("BookList", SERVICE_PREFIX, SERVICE_URI); BOOK_TITLES_NAME = soapFactory.createName("BookTitles", SERVICE_PREFIX, SERVICE_URI); BOOK_IMAGE_REQUEST_NAME = soapFactory.createName("BookImageRequest", SERVICE_PREFIX, SERVICE_URI); BOOK_IMAGES_NAME = soapFactory.createName("BookImages", SERVICE_PREFIX, SERVICE_URI); IMAGE_TYPE_ATTRIBUTE = soapFactory.createName("imageType", SERVICE_PREFIX, SERVICE_URI); HREF_ATTRIBUTE = soapFactory.createName("href"); } // Create the reply message and define the namespace // and encoding for the elements used in the reply. SOAPMessage reply = messageFactory.createMessage( ); SOAPEnvelope replyEnvelope = reply.getSOAPPart( ).getEnvelope( ); replyEnvelope.getHeader( ).detachNode( ); replyEnvelope.addNamespaceDeclaration(SERVICE_PREFIX, SERVICE_URI); replyEnvelope.addNamespaceDeclaration(SOAP_ENC_PREFIX, SOAPConstants.URI_NS_SOAP_ENCODING); replyEnvelope.addNamespaceDeclaration(XMLSCHEMA_PREFIX, XMLSCHEMA_URI); replyEnvelope.addNamespaceDeclaration(XMLSCHEMA_INSTANCE_PREFIX, XMLSCHEMA_INSTANCE_URI); replyEnvelope.setEncodingStyle(SOAPConstants.URI_NS_SOAP_ENCODING); SOAPBody replyBody = reply.getSOAPPart().getEnvelope( ).getBody( ); // There are two requests - one for the list of // book titles, the other for the image for a book. SOAPBody requestBody = message.getSOAPPart().getEnvelope( ).getBody( ); Iterator iter = requestBody.getChildElements( ); if (iter.hasNext( )) { // The child element contains the request SOAPElement element = (SOAPElement)iter.next( ); Name elementName = element.getElementName( ); if (elementName.equals(BOOK_LIST_NAME)) { handleBookListRequest(replyBody); } else if (elementName.equals(BOOK_IMAGE_REQUEST_NAME)) { handleBookImageRequest(element, reply); } else { // Unrecognized request - this is a fault. createFault(replyBody, "soap-env:Client.UnknownRequest", "Unrecognized request", SERVICE_URI, elementName. getLocalName( )); } } else { // No request - this is a fault createFault(replyBody, "soap-env:Client.MissingRequest", "Missing request", SERVICE_URI, "No request found"); } return reply; } // HELPER METHODS NOT SHOWN... }
The first part of the onMessage( ) method is concerned with creating objects such as Names that will be used to build a reply message the first time the service is used, and then creating the envelope for the reply message, which we'll look at more closely very shortly. Before the reply can be constructed, however, the received message must be examined to identify the client request that it contains. To do this, it is necessary to find the first child element of the SOAP body and extract its element name. Here is the code that does this:
SOAPBody requestBody = message.getSOAPPart( ).getEnvelope( ) .getBody( ); Iterator iter = requestBody.getChildElements( ); if (iter.hasNext( )) { // The child element contains the request SOAPElement element = (SOAPElement)iter.next( );
Although SOAPBody does not provide any methods for accessing its content, the SOAPElement interface from which it is derived does provide this facility. In particular, the getChildElements( ) method returns an Iterator that can be used to step through all of the immediate child elements of the
SOAPElement
to which it is applied. In this case, we expect to find exactly one child element; if we don't find any, then the message has been constructed incorrectly and the request will not be processed. Instead, a reply message containing a SOAP fault will be returned to the client, as described in Section 3.4, later in this chapter.
Assuming that we find a child element, the next step is to find out what it represents. So far, we have only seen the BookList request, but in fact, this servlet can also handle a request for a cover image for a book with a given title, the implementation of which we'll look at when we discuss how to use SOAP attachments. It is therefore necessary to work out which of these two requests has been received:
Name elementName = element.getElementName( ); if (elementName.equals(BOOK_LIST_NAME)) { handleBookListRequest(replyBody); } else if (elementName.equals(BOOK_IMAGE_REQUEST_NAME)) { handleBookImageRequest(element, reply); } else { // Unrecognized request - this is a fault. createFault(replyBody, "soap-env:Client.UnknownRequest", "Unrecognized request", SERVICE_URI, elementName.getLocalName( )); }
The SOAPElement getElementName( ) method returns the Name object that identifies the element, which contains the local element name and the URI for its associated namespace. To determine which request has been received, we compare the element name against two fixed values, BOOK_LIST_NAME and BOOK_IMAGE_REQUEST_NAME, which are constructed (using the SOAPFactory class) with the appropriate local names and the private URN associated with this service. This is, of course, the same URN (urn:jwsnut.bookimageservice) used by the client. Notice that the comparison is performed using the equals( ) method, which, for Name objects, returns true if both the local names and the namespace URIs are the same.[9] Depending on the result of the test, the handleBookListRequest( ) or the handleBookImageRequest( ) method is called, or, if the element name does not match either of the expected values, a SOAP fault is returned to the client. Here, we are interested only in the handleBookListRequest( ) method. Before we look at its implementation, however, let's examine the SOAP message that the service sends in reply to the client's BookList request, which is shown in Example 3-7.
[9] In the case of Names that do not have namespace qualifiers, this test still works because the namespace URI for such a Name is null.
<?xml version="1.0" encoding="UTF-8"?> <soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="urn:jwsnut.bookimageservice" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" soap-env:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <soap-env:Body> <tns:BookTitles xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="xsd:string[]"> <item>Java in a Nutshell</item> <item>J2ME in a Nutshell</item> <item>Java 2D Graphics</item> <item>Java I/O</item> <item>JavaServer Pages</item> <item>Java Internationalization</item> <item>Java Foundation Classes in a Nutshell</item> <item>Java Performance Tuning</item> <item>Creating Effective JavaHelp</item> <item>Enterprise JavaBeans</item> <item>Java Servlet Programming</item> <item>Java Swing</item> </tns:BookTitles> </soap-env:Body> </soap-env:Envelope>
If you compare this to the BookList request message shown in Example 3-4, you'll see that the Envelope of the reply message declares many more namespaces than the request. Each of these namespaces, which are listed in Table 3-2, is added to the SOAPEnvelope object using the addNamespaceDeclaration( ) method that we saw earlier; therefore, they apply to the whole message.
URI |
Description |
---|---|
Associated with the SOAP message envelope. This namespace is used on all of the standard envelope elements. |
|
Definitions relating to the W3C XML Schema standard. Typically assigned the prefix xsd and used to indicate built-in datat ypes defined by this standard, such as xsd:string. |
|
Another namespace associated with W3C XML Schema, this is typically assigned the prefix xsi and is attached to attributes such as xsi:type that define the data types of elements in the SOAP message. |
|
A namespace that indicates definitions taken from the SOAP section 5 encoding rules. |
The implementation shown in Example 3-6 defines constants to represent both the URIs and the prefixes that will be associated with them when constructing SOAP messages. The javax.xml.soap.SOAPConstants interface includes constant values (SOAPConstants.URI_NS_SOAP_ENCODING and SOAPConstants.URI_NS_SOAP_ENVELOPE) for the URIs associated with the SOAP encoding rules and the envelope, but does not define the other URIs, even though they are likely to be used just as often.
The Envelope element also has an attribute that you haven't seen before:
soap-env:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
The value of this attribute is a URI that indicates the encoding rules used to determine the representation of the data within the element to which it is attached. In this case, the message is encoded using SOAP section 5 encoding rules. It is, of course, necessary for both the sender and receiver of the message to understand the encoding rules that are actually used within the message, which is why the SOAP 1.1 specification includes these rules that applications are encouraged (but not absolutely required) to use.
Since the encodingStyle attribute is attached to the Envelope element, it applies to everything within the envelope unless explicitly overridden at the level of a nested element, in which case that element and its child elements can be encoded according to different rules:
<OuterElement soap-env:encodingStyle= "http://schemas.xmlsoap.org/soap/encoding/"> <!-- SOAP section 5 encoding rules apply here --> <InnerElement soap-env:encodingStyle="urn:MyPrivateStyle"> <!-- Private encoding rules apply here --> </InnerElement> <!-- SOAP section 5 encoding rules apply again --> </OuterElement>
Nested encodings such as this might be convenient if the message contains all or part of an XML document that uses a different representation than that used by the parties exchanging SOAP messages and that ultimately will probably be processed by a different application.
The body of the reply message is simply an array of strings, where each string represents the title of a book. In order for the client to understand the reply, it must know how the server will represent an array of strings. Fortunately, the SOAP section 5 encoding rules provide a standard way of representing an array of objects in XML. For simple types such as strings, the canonical form looks like this:
<tns:ElementName xsi:type= "SOAP-ENC:Array" SOAP-ENC:arrayType= "arrayType">
<item>itemValue1</item>
<item>itemValue2</item>
<!-- and so on -->
</tns:ElementName>
The attribute xsi:type is defined by the W3C XML Schema standard and indicates that the element contains an array as defined by the SOAP encoding rules, since the namespace prefix for the Array attribute is SOAP-ENC, which is mapped, in this example, to the URI for these encoding rules. The arrayType attribute specifies the type of the array. For an array of strings, this will typically have the value:
SOAP-ENC:arrayType="xsd:string[]"
Here, the value xsd:string is the XML Schema attribute value that defines a string,[10] and the square brackets obviously indicate an array. In this case, the square brackets are empty, which means that the number of elements in the array should be determined by counting the child elements. It is also possible to specify fixed bounds by composing the attribute like this, to indicate a string array with 12 elements:
[10] You can see that xsd:string is an attribute defined by XML Schema because the xsd prefix in this example (represented in Example 3-6 by the constant value XMLSCHEMA_PREFIX) is mapped to the XML Schema URI (represented by the constant value XMLSCHEMA_URI).
SOAP-ENC:arrayType="xsd:string[12]"
The elements of the array are all represented as nested elements, the names of which (commonly referred to as accessor names) must all be the same but can otherwise be arbitrary. In this example, we use the element name item, but any legal XML name will suffice. In the actual reply message shown in Example 3-7, the array element is actually called BookTitles and, not surprisingly, is qualified with the namespace associated with this web service. Notice, however, that the array elements use an element name (item) that is not namespace-qualified. This is explicitly allowed by the SOAP encoding rules.
Now let's look at the code in the handleBookListRequest( ) method that creates this representation of a string array, which is shown in Example 3-8.
/** * Handles a request for list of book names. */ private void handleBookListRequest(SOAPBody replyBody) throws SOAPException { // Create a BookTitles element containing an entry // for each book title. SOAPBodyElement bodyElement = replyBody.addBodyElement(BOOK_TITLES_NAME); // Add 'xsi:type = "SOAP-ENC:Array"' bodyElement.addAttribute( soapFactory.createName("type", XMLSCHEMA_INSTANCE_PREFIX, XMLSCHEMA_INSTANCE_URI), SOAP_ENC_PREFIX + ":Array"); // Add 'SOAP-ENC:arrayType = "xsd:string[]" bodyElement.addAttribute( soapFactory.createName("arrayType", SOAP_ENC_PREFIX, SOAPConstants.URI_NS_SOAP_ENCODING), XMLSCHEMA_PREFIX + ":string[]"); // Add an array entry for each book String[] titles = BookImageServletData.getBookTitles( ); for (int i = 0; i < titles.length; i++) { SOAPElement titleElement = bodyElement.addChildElement("item"); titleElement.addTextNode(titles[i]); } }
We're not going to show how the book titles themselves are obtained � the logic for this is hidden in a separate class called BookImageServletData, which provides a method that returns all of the titles in the form of an array of strings. Instead, we're going to concentrate on how to build the SOAP representation of the titles list. The first step is to create the BookTitles element, which is done using the SOAPBody addBodyElement( ) method, using the qualified name of the element as its argument. This is the same way that the client created the BookList request. Next, we need to add to this element the xsi:type and SOAP-ENC:arrayType attributes required by the SOAP-encoding rules to indicate that the element represents an array of strings. To add an attribute to a SOAPElement, the addAttribute( ) method is used:
public SOAPElement addAttribute(Name name, String value);
name is the qualified name of the attribute, and value is the value to be associated with it. As before, in this example, we use the SOAPFactory class to create Name objects for the attributes, which creates the Name object for the attribute xsi:type, which is then passed to the addAttribute( ) method:
// Add 'xsi:type = "SOAP-ENC:Array"' bodyElement.addAttribute( soapFactory.createName("type", XMLSCHEMA_INSTANCE_PREFIX, XMLSCHEMA_INSTANCE_URI), SOAP_ENC_PREFIX + ":Array");
The addAttribute( ) method returns a reference to the
SOAPElement
to which the attribute was added, which makes it possible to add multiple attributes by chaining calls:
bodyElement.addAttribute(...).addAttribute(...).addAttribute( );
Having installed the BookTitles element in the reply body, we now need to add a child element for each book title. The SOAPElement interface provides five methods that allow a nested element to be added. In this case, since the element is part of a SOAP-encoded array definition, its name does not need to be namespace-qualified, so we can use the variant of the addChildElement( ) method that requires a string to specify the local name of the element to be created and returns a reference to the SOAPElement itself:
SOAPElement titleElement = bodyElement.addChildElement("item");
At this point, we have SAAJ message structures that amount to the following XML:
<tns:BookTitles xsi:type="SOAP-ENC:Array" SOAP- ENC:arrayType="xsd:string[]"> <item/> <tns:BookTitles>
What is missing here is the book title within the item element. Unlike item itself, the book title should not be represented by an XML element � it is, in fact, just a text string. To add text to a SOAPElement, we need to create and add a Text node, using the SOAPElement addTextNode( ) method:
public SOAPElement addTextNode(String text) throws SOAPException;
You may be surprised to see that this method is defined to return a SOAPElement instead of an object of type Text, despite its name (especially since Text is not derived from SOAPElement; see Figure 3-4). In fact, this method returns a reference to the SOAPElement on which it was invoked, not the Text object that was created. In most cases, you don't actually need to get access to the Text object itself; the only way to do so is to use the SOAPElement getChildElements( ) method to get an Iterator over all of the element's children and search for the one that implements the Text interface. Adding the following method call:
titleElement.addTextNode(titles[i])
finally completes the array element for the book title, so that now the message body contains the following:
<tns:BookTitles xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="xsd:string[]"> <item>Creating Effecttive JavaHelp</item> <tns:BookTitles>
The rest of the loop in the handleBookListRequest( ) method adds item elements for each of the books that the service knows about. When this method completes, the reply message is completely built and is returned from the servlet's onMessage( ) method, to be sent back to the client.
|
The processing performed by the client when it receives the reply to its BookList request is very simple � all it has to do is loop over the SOAP array in the message body and extract the book titles from each nested element. Example 3-9 shows the implementation.
SOAPBody replyBody = reply.getSOAPPart().getEnvelope( ).getBody( ); if (replyBody.hasFault( )) { SOAPFault fault = replyBody.getFault( ); throw new SOAPException("Fault when getting book titles: " + fault.getFaultString( )); } // The body contains a "BookTitles" element with a nested // element for each book title. Iterator iter = replyBody.getChildElements(BOOK_TITLES_NAME); if (iter.hasNext( )) { ArrayList list = new ArrayList( ); SOAPElement bookTitles = (SOAPElement)iter.next( ); iter = bookTitles.getChildElements( ); while (iter.hasNext( )) { list.add(((SOAPElement)iter.next( )).getValue( )); } }
The first part of this code checks whether the server detected an error with the request by looking for a SOAP fault element in the reply body. SOAPBody provides the convenience methods hasFault( ), which returns true if there is a fault present, and getFault( ), which looks for and returns the SOAPFault element. Since the presence of a fault indicates either an error in the client code that constructed the request or a problem at the server, there is no reasonable recovery possible in this case and a SOAPException is thrown.
If there is no fault, then the body of the reply contains a BookTitles element. Earlier, we saw that SOAPElement has a getChildElements( ) method that returns all of the children of the element that it is invoked on, and which we used previously to get all of the top-level elements in the body of a SOAP message. Here, we use an overloaded variant of that method, which accepts a Name as its argument:
public Iterator getChildElements(Name name) throws SOAPException;
This method returns only those elements whose name matches the one given. In this case, we get back an Iterator over all of the BookTitles elements in the SOAPBody � there should be exactly one. Once we have a reference to this element, we invoke the getChildElements( ) method on it to gain access to all of the nested array elements. In this case, we use the variant of getChildElements( ) that returns all of the children, both because we know that we need to process all of them and because the name associated with the element is arbitrary (and therefore we cannot construct an appropriate Name object). Finally, we iterate over all of the nested elements and extract the title by using the getValue( ) method, which returns the text associated with a Node, if there is any.
The example that you have just seen shows some of the ways in which a SOAPElement can be created. It also demonstrates some of its API. In this section, we take a closer look at how to create and use SOAPElements.
So far, you have seen how to create new SOAPElements and incorporate them into the node tree for a SOAPMessage in a single step. To create a SOAPElement that appears directly in the SOAPBody, in a SOAPHeader, or in a SOAPFault, use convenience methods provided by these classes, of which the one provided by SOAPBody is typical:
public SOAPBodyElement addBodyElement(Name name) throws SOAPException;
This method returns not a SOAPElement but an instance of SOAPBodyElement, which is derived from SOAPElement. All top-level elements in the SOAP body must be SOAPBodyElements, rather than simply
SOAPElement
s. Similarly, top-level elements inside the SOAPHeader must be of type SOAPHeaderElement, and top-level elements in a SOAPFault will be of type SOAPFaultElement. As far as the SAAJ API is concerned, there is no real difference between a SOAPElement and a SOAPBodyElement or a SOAPHeaderElement, since neither of these interfaces add any methods in addition to those defined by SOAPElement.
Once you have a SOAPElement, you can add child elements to it using one of the five overloaded variants of its addChildElement( ) method, all of which return the newly created SOAPElement:
This variant adds a new SOAPElement for which the name is obtained by the supplied Name argument. If the Name is a qualified name, then the element as written to the XML output stream has both a local name and an associated namespace prefix; otherwise, it has an unqualified local name.
Creates a new SOAPElement with the given unqualified name and adds it to the child list of the element on which this method is invoked.
Creates a new SOAPElement with a fully qualified name created from the supplied arguments and adds it to the child list of the element on which this method is invoked. Using this method has the same result as creating a Name using the supplied arguments and then calling the addChildElement(Name name) variant.
This is similar to the previous variant, except that it does not provide the URI for the namespace that the localName argument is associated with. However, if the supplied prefix can be resolved to a namespace URI by examining the SOAPElement on which this method or its ancestors are invoked, then that URI is used when creating the SOAPElement. If this is not possible, then a SOAPException is thrown. For example, consider the following code, where soapBody is a reference to a SOAPBody object:
Name name = soapFactory.createName("bodyElement", "tns", "urn:service"); SOAPBodyElement bodyElement = soapBody.addBodyElement(name); SOAPElement childElement = bodyElement.addChildElement("childElement", "tns");
Here, the addChildElement( ) call to create childElement does not specify the namespace URI. However, since the namespace prefix tns supplied as the second argument is defined by the parent node, a mapping to the URN given in that element is inferred and childElement is associated with this URI.
Requests that the given SOAPElement be added to the list of children of the element on which it is invoked. Depending on the type of the parent element, however, a different SOAPElement may be added to the parent.
Although the methods all declare the return value to be of type SOAPElement, in reality they may return a subinterface of SOAPElement. In particular, calling addChildElement( ) on a SOAPBody object (which is possible because SOAPBody is itself a SOAPElement) will always return a SOAPBodyElement. Consider the following code:
SOAPElement bodyElement = soapBody.addBodyElement(name); // "name" is of type Name SOAPElement secondElement = soapBody.addChildElement("localName");
Here, bodyElement is obviously of type SOAPBodyElement because addBodyElement( ) is defined to return an object of that type. However, secondElement is also a SOAPBodyElement, since it is a top-level element within a SOAPBody. Similar results are obtained when addChildElement( ) is invoked on an element of type SOAPHeader or SOAPFault.
Now suppose that element is an existing SOAPElement called TopElement, and then consider the following code, in which soapBody again refers to an object of type SOAPBody:
SOAPElement childElement = soapElement.addChildElement("ChildElement"); SOAPBody addedElement = body.addChildElement(element);
This attempts to add an existing SOAPElement, complete with a child element of its own, to a SOAPBody. There seems to be a problem here, since the immediate child of a SOAPBody must be a SOAPBodyElement, but the element being added is actually a SOAPElement. What actually happens is that a new SOAPBodyElement is created as a copy of the supplied SOAPElement, added to the SOAPBody instead of it, and returned from the addChildElement( ) method. The copied element also has as its child a copy of the child element associated with the original.
Since all of the SOAPElements that you have seen so far have been created as children of other SOAPElements, you might wonder how we could create the unattached SOAPElement used in the example just shown. To create a freestanding SOAPElement, use one of the following methods of SOAPFactory:
public SOAPElement createElement(Name name) throws SOAPException; public SOAPElement createElement(String localName) throws SOAPException; public SOAPElement createElement(String localName, String prefix, String uri) throws SOAPException;
The SOAPElementFactory class also has identical methods to those shown here. However, SOAPElementFactory is deprecated as of SAAJ 1.1 and the SOAPFactory methods (to which those in SOAPElementFactory actually delegate) should be used instead.
These methods can be used, as shown in a small way in the previous example, to build a message fragment as a freestanding entity and then add it to a SOAPMessage, without having to supply a reference to any part of the message to the code that builds the fragment.
Although SOAPElement does not provide a method that allows any of its child elements to be removed, the Node interface, from which SOAPElement is derived, has a detachNode( ) method that removes the Node on which it is invoked from its parent. For example, the following code shows how to use this method to remove the first child of a SOAPElement:
SOAPElement parent = soapFactory.createElement("Element"); parent.addChildElement("Child1"); parent.addChildElement("Child2"); parent.addChildElement("Child3"); parent.addChildElement("Child4"); // Get the first child and remove it Iterator iter = parent.getChildElements( ); SOAPElement child = (SOAPElement)iter.next( ); child.detachNode( );
Incidentally, it is not possible to remove more than one SOAPElement at a time by using an Iterator in this way, since the next( ) method throws a ConcurrentModificationException when called after removal of the first child element. To remove more than one SOAPElement, use the Iterator remove( ) method instead. The following code shown next, for example, removes all the children of a SOAPElement.
Iterator iter = parent.getChildElements( ); while (iter.hasNext( )) { iter.remove( ); }