In Chapter 3, you saw that SOAP messages can carry information either in the body part or in separate attachments. Typically, you would use an attachment when you need to include data of a type that cannot be placed in the SOAP body, which is restricted to valid XML. You might also use an attachment to carry plain text or an XML document or document fragment in cases where it would be difficult or inconvenient to embed it within the body part itself. Whereas SAAJ requires the use of a specific API to create SOAP messages with attachments, JAX-RPC transparently places the values of method arguments and return values with any of the following Java types into SOAP attachments:
java.awt.Image
javax.activation.DataHandler
javax.mail.internet.MimeMultipart
javax.xml.transform.Source
Arguments and return types that are arrays of these data types are handled by creating one attachment for each array entry.
Since JAX-RPC handles the creation of attachments transparently, neither the client nor the service implementation needs to be aware that some of the data they are exchanging may be carried in an attachment. This is far more convenient than the lower-level API provided by SAAJ, which requires the programmer to explicitly manage each attachment and to create references from the message body to any associated data in an attachment if necessary.
|
In order to illustrate the use of attachments, the example source code for this book includes a JAX-RPC web service that is similar to the book image web service implemented in Chapter 3 using the SAAJ API. The endpoint interface definition for this service, which you can find in the file chapter6\extendedbookservice\interface\ora\jwsnut\chapter6\extendedbookservice\EBookQuery.java, is shown in Example 6-11.
package ora.jwsnut.chapter6.extendedbookservice; import java.awt.Image; import java.rmi.Remote; import java.rmi.RemoteException; import javax.activation.DataHandler; import javax.mail.internet.MimeMultipart; import javax.xml.transform.Source; public interface EBookQuery extends Remote { //Gets the number of books known to the service public abstract int getBookCount( ) throws RemoteException; // Gets the set of book titles. public abstract String[] getBookTitles( ) throws RemoteException; // Gets the images for books with given titles public abstract Image[] getImages(String[] titles, boolean gif) throws EBookServiceException, RemoteException; // Gets book images in the form of a DataHandler public abstract DataHandler[] getImageHandlers(String[] titles, boolean gif) throws EBookServiceException, RemoteException; // Gets the book images in MimeMultipart form public abstract MimeMultipart getAllImages( ) throws EBookServiceException, RemoteException; // Gets XML details for a given list of books. public abstract Source[] getBookDetails(String[] titles) throws EBookServiceException, RemoteException; }
The service implementation has a list of books, keyed by book title, for each of which it holds a cover image in both GIF and JPEG form as well as an XML document fragment that contains details of the book. The methods of the EBookQuery interface allow a client application to get the complete list of book titles and then to retrieve the cover images and the XML information for any number of them. You can build and install the web service for this example by opening a command window, making chapter6\extendedbookservice your working directory and then typing the following:
ant deploy
The client application is a variant of the Swing GUI client that was used in Chapter 3. To compile and run it, type the following commands:
ant compile-client ant run-client
When the user interface appears, you will see that it contains a list of the book titles that the web service knows about, together with a blank area in which the cover images will be displayed and a set of checkboxes that determine which of the service methods will be used to retrieve them, as shown in Figure 6-1.
The following sections use the client and server implementations for this example to illustrate how to exchange the various different types of data that JAX-RPC places into attachments. Although the methods of the EBookQuery interface are defined in such a way that the attachments are all associated with method return values, it is perfectly possible for an attachment to be sent from the client to the server. For example, a method defined like this:
public void addImage(String title, Image image);
results in the image data being placed in an attachment in the SOAP request message sent to the server.
You can exchange images that are encoded using formats supported by the JRE by declaring a java.awt.Image object as a method argument or return value. Having done this, the sender simply has to create the Image object, and the receiver uses the copy that it receives in the normal way. Neither the sender nor the receiver has to be concerned about how the image is encoded in the SOAP message.
Example 6-12 shows the server-side implementation of the getImages( ) method from the EBookQuery interface. The helper class EBookServiceServantData holds book cover image data in the form of two byte arrays for each book � one encoded in GIF format, and the other in JPEG format. The method arguments specify the titles of the books for which the cover images are required and whether the GIF or JPEG encoding should be returned.[4] The code in Example 6-12 gets the correct set of image data from the helper class, converts each item of the set into an Image object, and returns an array containing an entry for each of them. None of this code uses any JAX-RPC API, but it is fairly typical of code that handles images.
[4] As of Java 2 Version 1.3, PNG images are also supported. In Java 2 Version 1.4, it is also possible to plug in encoders and decoders for other image formats. For simplicity, this example uses only GIF and JPEG images.
public Image[] getImages(String[] titles, boolean gif) throws EBookServiceException { int length = titles.length; Image[] images = new Image[length]; for (int i = 0; i < length; i++) { byte[] imageData = EBookServiceServantData.getImageData(titles[i], gif); if (imageData == null) { throw new EBookServiceException("Unknown title: [" + titles[i] + "]"); } if (tracker == null) { tracker = new MediaTracker(new Component( ) {}); } Image image = Toolkit.getDefaultToolkit( ).createImage(imageData); tracker.addImage(image, 0); images[i] = image; } try { tracker.waitForAll( ); for (int i = 0; i < length; i++) { tracker.removeImage(images[i]); } } catch (InterruptedException ex) { } return images; }
The client code that uses this method, shown in Example 6-13, is also very straightforward � the Image objects that are returned are used to create Swing ImageIcons that are then displayed in the user interface using JLabels. Once again, as you might expect, there is no JAX-RPC-specific code involved.
Image[] images = bookQuery.getImages(selectedTitles, isGif.isSelected( )); for (int i = 0; i < images.length; i++) { imagePanel.add(new JLabel(new ImageIcon(images[i]))); }
To trigger this code, start the Swing client, select one or more book titles from the list on the lefthand side of the window, make sure that the "DataHandler" checkbox is not selected, then press the button labeled "Fetch." After a short delay, you should see the cover images for the books that you selected displayed in the righthand side of the window.
Although this seems very straightforward, this code probably does not behave in quite the way you might expect, although you're unlikely to notice the problem simply by looking at the results. The issue revolves around the way in which the image is transferred from the web service. The second argument of the getImages( ) method in Example 6-12 specifies whether to use the GIF- or JPEG-encoded version of the image data. The value that is used depends on whether the "GIF images" checkbox in the user interface is selected. This value is correctly conveyed to the server, which will build Image objects from the appropriately encoded image data. Once the server's getImage( ) method returns the
Image
objects, the JAX-RPC runtime has to encode them in byte form for inclusion in an attachment.[5] However, as noted during the discussion of attachments in Chapter 3, the JRE includes an encoder for JPEG images but not for GIF images. As a result, the JAX-RPC runtime included in the reference implementation cannot produce a GIF-encoded byte stream for an image. It actually always creates an attachment in which the content type is image/jpeg. Therefore, although you might request a GIF image, and the service implementation class might return an Image object created from GIF-encoded data, the client nevertheless receives an Image object created from a JPEG-encoding of that image. The same is true for PNG images, or any other type of image that the server might support. In practice, this is not really very important, since the Image class does not provide a means of finding out how the image was originally encoded and therefore there is no way for the client to be affected by this implementation quirk. There is really only a problem if the JRE used a JPEG encoding algorithm that noticably reduced the quality of the image.
[5] This is, of course, rather counter-productive, since we already had the image data in byte form and then converted it to an Image object for the sake of the method call. As you'll see in the next section, there is a more efficient way to return image data to the client.
As you saw in Chapter 3, the DataHandler class, which is part of the JavaBeans Activation Framework, can be used to encapsulate any kind of data that has a MIME encoding. It therefore represents an extremely flexible way to transport data � such as images, sound files, movies, and so on � from a web service to an application client. Since the programming interface is independent of the encapsulated data, the client does not need to be specifically coded to handle a specific set of possible data types, but can instead deal only with a DataHandler object and use its getContentType( ) method to determine the type of data that it has received. Processing could then be delegated to an appropriate helper, which is most likely configured and selected based on the content type.
In the case of the EBookQuery interface, the getImageHandlers( ) method is provided as an alternative to getImages( ), to return a DataHandler for each book's cover image instead of an Image. The server-side implementation of this method is shown in Example 6-14.
public DataHandler[] getImageHandlers(String[] titles, boolean gif) throws EBookServiceException { int length = titles.length; DataHandler[] handlers = new DataHandler[length]; for (int i = 0; i < length; i++) { byte[] imageData = EBookServiceServantData.getImageData(titles[i], gif); if (imageData == null) { throw new EBookServiceException("Unknown title: [" + titles[i] + "]"); } handlers[i] = new DataHandler(new ByteArrayDataSource("Image Data", imageData, gif ? "image/gif" : "image/jpeg")); } return handlers; }
As you can see, this code is much simpler than that of the getImage( ) method in Example 6-12. All that is necessary is to get the encoded image data and then create a DataHandler to encapsulate it. A DataHandler typically is created from a Java object representing the data itself and a string that represents the content type of the data (such as image/jpeg). However, in this case, we associate the DataHandler with the data through a custom DataSource class called ByteArrayDataSource. The rationale for doing this, together with the implementation of the ByteArrayDataSource class, is covered in detail in Section 3.6.3.4 (which you should read before proceeding, if you have not already done so).
On the client side, an application that calls the getImageHandlers( ) method can use the DataHandler getContent( ) method to get a Java object that represents the encapsulated data. The code in the Swing GUI client that uses this method is shown in Example 6-15.
DataHandler[] handlers = bookQuery.getImageHandlers(selectedTitles, isGif.isSelected( )); for (int i = 0; i < handlers.length; i++) { imagePanel.add(new JLabel(new ImageIcon((Image)handlers[i].getContent( )))); }
This code uses the getContent( ) method to extract the data from each returned DataHandler in object form, which it expects to be an Image object. This assumption is justified because the JAX-RPC specification requires that implementations of the getContent( ) method return objects of specific types based on the content type of the data, which can itself be obtained by calling the DataHandler getContentType( ) method. Table 6-5 shows the mapping from MIME type to Java types required by the specification.
MIME type |
Java data type |
---|---|
image/gif |
java.awt.Image |
image/jpeg |
java.awt.Image |
text/plain |
java.lang.String |
text/xml |
javax.xml.transform.Source |
application/xml |
javax.xml.transform.Source |
multipart/* |
javax.mail.internet.MimeMultipart |
In the case of this example, since the data always has content type image/gif or image/jpeg, the object obtained from the getContent( ) method can be assumed to be of type Image. To return objects of the other types, the server implementation creates a DataHandler in which the content type is set appropriately. For example, to convey plain text in an attachment, the server does the following:
DataHandler handler = new DataHandler("Some plain text", "text/plain");
while XML can be incorporated by supplying a javax.xml.transform.Source object and setting the content type to text/xml:
Source source = new javax.xml.transform.stream.StreamSource( new StringReader("<detail><name>Fred</name></detail>")); DataHandler handler = new DataHandler(source, "text/xml");
MimeMultipart and XML data can also be explicitly passed via method arguments and the return value, as will be described in Section 6.5.3 and Section 6.5.4, later in this chapter. For content types that are not listed in Table 6-5, the DataHandler getContent( ) method may return an appropriate Java object, or an instance of
java.io.InputStream
that can be used to access the byte stream representation of the object. For all content types, you can use the DataHandler getInputStream( ) method to get an InputStream instead of a Java object representation, should you need to do so.
The example client calls the getImageHandlers( ) method when you select one or more book titles and check the "DataHandler" checkbox. Select or unselect the "GIF images" checkbox to request GIF or JPEG images.
Leaving aside the fact that the intent is slightly less clear, there are two main advantages to be gained by using a DataHandler in preference to Image objects when defining remote method signatures:
As shown in Example 6-14, the server needs only to keep the image in byte form, exactly as it would be read from a file. This avoids the use of Image objects, which are more appropriate to a client application than a server, and take up memory resources as well as the time required to create them.
When a DataHandler is used, the JAX-RPC runtime does not need to perform the time-consuming process of converting the content of an Image back to a byte stream, almost certainly reversing the process used by the service implementation to create the Image object in the first place. The fact that no conversion is involved also means that images handled using a DataHandler are encoded in SOAP message attachments using their correct MIME type � in other words, GIF (and PNG) images are not converted to JPEG form first.
A javax.mail.internet.MimeMultipart object is a convenient wrapper that can be used to move one or more items of MIME-encoded data between the client and the server. Although there is no requirement for the data encapsulated within the MimeMultipart object to be all of the same MIME type, the getAllImages( ) method of the EBookQuery interface always returns a MimeMultipart in which each part is a JPEG image. The implementation of this method is shown in Example 6-16.
public MimeMultipart getAllImages( ) throws EBookServiceException { String[] titles = EBookServiceServantData.getBookTitles( ); Image[] images = getImages(titles, false); try { MimeMultipart mp = new MimeMultipart( ); for (int i = 0; i < images.length; i++) { MimeBodyPart mbp = new MimeBodyPart( ); mbp.setContent(images[i], "image/jpeg"); mbp.addHeader("Content-Type", "image/jpeg"); mp.addBodyPart(mbp); } return mp; } catch (MessagingException ex) { throw new EBookServiceException("Failed building MimeMultipart: " + ex.getMessage( )); } }
To add an item to a MimeMultipart object, first create a MimeBodyPart, then associate it with the data by calling its setContent( ) method, which also specifies the MIME data.[6] In this case, the data is the Image object for a book cover image. Alternatively, the data can be supplied in the form of a DataHandler using the setDataHandler( ) method. This technique could be used to reimplement the getAllImages( ) method using code similar to that shown in Example 6-14, thereby avoiding the need to use intermediate Image objects.
[6] It is also necessary to use the MimeBodyPart addHeader( ) method to add a MIME header for the content type. This seems a little strange given that the content type has already been specified by the setContent( ) method, but, at least in Sun's implementation, if you fail to call addHeader( ), the part of the MimeMultipart attachment that contains the data does not have a MIME Content-Type header. This typically results in the data being interpreted as if it had type text/plain.
A MimeBodyPart is incorporated in the MimeMultipart using the addBodyPart( ) method. Once the MimeBodyPart for each image is added, getAllImages( ) returns the image to the JAX-RPC runtime.
The client code that uses a MimeMultipart object is essentially the reverse of the server implementation, as shown in Example 6-17. The example application executes this code when you press the "Fetch" button when there are no book titles selected in the list on the lefthand side of Figure 6-1. This results in the cover images for all of the books being obtained in JPEG form, ignoring the settings of the "GIF images" and "DataHandler" checkboxes.
MimeMultipart mp = bookQuery.getAllImages( ); count = mp.getCount( ); for (int i = 0; i < count; i++) { BodyPart bp = mp.getBodyPart(i); Image img = (Image)bp.getContent( ); imagePanel.add(new JLabel(new ImageIcon(img))); }
To handle the images, each BodyPart is obtained by calling the MimeMultipart getBodyPart( ) method.[7] The simplest way to get the encapsulated image data is, as shown here, to use the BodyPart getContent( ) method, which returns a Java object whose type is determined by the MIME type of the data, as listed in Table 6-5. In this case, the returned object is an Image. Alternatively, you can access the data by calling the getDataHandler( ) method to retrieve a DataHandler, from which you can then either obtain an InputStream or the same Java object that is returned by the BodyPart getContent( ) method.
[7] BodyPart is an abstract base class from which MimeBodyPart is derived. The server needs to explicitly use a MimeBodyPart when creating the MimeMultipart object, but the client does not need to be aware exactly which subclass of BodyPart has been used and therefore is not required to cast the return value of the getBodyPart( ) method.
You can use an XML document (or an array of them) in a JAX-RPC method by defining the argument or return type to be of type javax.xml.transform.Source. The EBookQuery interface uses this facility to allow a client application to retrieve an XML fragment that describes one or more books:
public abstract Source[] getBookDetails(String[] titles) throws EBookServiceException, RemoteException;
A Source object can contain XML in the form of a DOM model, a SAX event stream, or an input stream. On receiving an attachment for which the content type is text/xml (or application/xml), the JAX-RPC runtime creates a Source object of one of these types (i.e., DOMSource, SAXSource, or StreamSource), where the choice is implementation-dependent. The implementation of this method in the example web service is shown in Example 6-18.
public Source[] getBookDetails(String[] titles) throws EBookServiceException { int length = titles.length; Source[] sources = new Source[length]; for (int i = 0; i < length; i++) { String data = EBookServiceServantData.getXMLDetails(titles[i]); if (data == null) { throw new EBookServiceException("Unknown title: [" + titles[i] + "]"); } try { data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + data; sources[i] = new StreamSource(new ByteArrayInputStream( data.getBytes("utf-8"))); } catch (UnsupportedEncodingException ex) { ex.printStackTrace( ); } } return sources; }
This method gets the XML data for a specific book based on its title, in the form of a String, then wraps it in a StreamSource object�which implements the Source interface�by interposing a ByteArrayInputStream (since the StreamSource constructors do not directly accept XML in the form of a String). If you have an XML document in the form of a DOM model, you can use a similar pattern to include some or all of it as an attachment by using a DOMSource instead of a StreamSource:
Document doc = ....... ; // Get XML document as a DOM model Element rootElement = doc.getDocumentElement( ); // Using the "rootSource" object would result in the entire // document being included in the attachment. DOMSource rootSource = new DOMSource(rootElement); // Using the "firstChild" object would result in only that // part of the document at and below the first child element // being included in the attachment Node firstChild = rootElement.getFirstChild( ); DOMSource childSource = new DOMSource(firstChild);
The example client requests XML data when you check the "Get XML" checkbox and then press the "Fetch" button. Once it retrieves all of the image data, it calls the getBookDetails( ) method to get the XML details for the same set of books, and writes the result to its standard output. Here is a typical result:
<?xml version="1.0" encoding="UTF-8"?> <detail><author>Kim Topley</author><editor>Robert Eckstein</editor><price> 29.95</price></detail> ------------------------ <?xml version="1.0" encoding="UTF-8"?> <detail><author>Robert Eckstein et al</author><editor>Mike Loukides</editor> <price>44.95</price></detail> ------------------------
The client code that calls the getBookDetails( ) method is shown in Example 6-19.
Source[] sources = bookQuery.getBookDetails(selectedTitles); if (transformer == null) { transformer = TransformerFactory.newInstance( ).newTransformer( ); streamResult = new StreamResult(System.out); } for (int i = 0; i < sources.length; i++) { transformer.transform(sources[i], streamResult); System.out.println("\n------------------------"); }
Most of this code is concerned with getting a Transformer object that will extract the XML from the Source objects returned by the server and write it in a readable form to the standard output stream. Even though the server provided the XML to the JAX-RPC runtime in the form of an array of StreamSource objects, you cannot assume that the client will receive a StreamSource object with the data extracted from the received attachment, since the choice between DOMSource, SAXSource, and StreamSource depends on the implementation of the client-side JAX-RPC runtime. You can, of course, use a Transformer to convert whatever you get into whichever type you want. For example, the following code gets a DOM model from any kind of Source object:
// Convert the source to a DOM model DOMResult domResult = new DOMResult( ); transformer.transform(sources[i], domResult); // Get the root node of the DOM model Node node = domResult.getNode( );