3.6 SOAP with Attachments

   

3.6 SOAP with Attachments

A SOAP message constructed according to the SOAP 1.1 specification can only contain data encoded as XML. The SOAP with attachments specification defines an extension of the SOAP that allows additional data items ” which may or may not be XML ” to be transferred along with a SOAP message. SAAJ supports SOAP with attachments and provides a simple API that allows the transfer of arbitrary data. However, as we'll see, at the detailed level this interface is not quite as easy to use as it could be.

3.6.1 An Example SOAP with Attachments Application

As a demonstration of the use of attachments, the book image web service uses the SAAJ API to return the cover images for one or more of the books that it knows about. To see how this works, start the client using the following command:

 ant run-client-debug 

When the list of book titles appears, select one or more of them by clicking on them with the mouse (hold down the Control key to select more than one), then press the "Fetch" button. The selected images will appear in the user interface, as shown in Figure 3-7, and the message used to deliver them will be written to the command window. Since the message contains the binary data for the images, you'll see some strange characters in the output window. Leaving out the image data itself, a typical message used to transfer three cover images is shown in Figure 3-8.

Figure 3-7. Using SOAP with attachments to transfer images
figs/jwsn_0307.gif
Figure 3-8. A SOAP with attachments message containing three attachments
figs/jwsn_0308.gif

Before we look at the SOAP with attachments message, let's briefly look at the request message that the client sends. This message (which does not have any attachments) contains an element called BookImageRequest that represents an array of strings, where each string is the title of a book. Here is what the body of the SOAP request looks like when requesting the cover images for three books:

 <tns:BookImageRequest 
        xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="xsd:string[]" 
        tns:imageType="image/jpeg">
    <item>J2ME in a Nutshell</item>
    <item>Java Swing</item>
    <item>Java in a Nutshell</item>
</tns:BookImageRequest> 

As you can see, the BookImageRequest element contains the standard xsi:type and SOAP-ENC:arrayType attributes that indicate that it is a string array encoded using the SOAP section 5 encoding rules. It also includes another attribute defined in the book image web service's own namespace, which specifies the format in which the image should be returned:

 tns:imageType="image/jpeg" 

The server supports both JPEG and GIF images, and you can select the type required using the "Use GIF Images" checkbox on the user interface.

Now let's examine the reply message shown in Figure 3-8. As you can see, it is broken down into several parts that map directly to those shown in Figure 3-2. The first part consists of the HTTP headers that wrap the entire SOAP message. These headers are generated when the message is constructed using the saveChanges( ) method, and include a Content-Type header that indicates that the message is constructed according to the rules for a MIME Multipart/ Related message. Any additional MIME headers added by application code to the MimeHeaders object obtained from the getMimeHeaders( ) method of SOAPMessage appear here.

The boundaries between the message parts are indicated using a text string that is automatically generated when the message is constructed and are supplied as the value of the boundary attribute of the Content-Type header, which in this case is:

 ------=_Part_11_4712040.1027772369874 

You can find a complete description of the format of these messages in RFC 2045 and RFC 2387, both of which can be downloaded from http://www.ietf.org.

The second part corresponds to the part of the message represented by the SOAPPart object and includes the SOAP message body. As you can see, this portion of the message also has MIME headers, one of which is the Content-Type header that describes the content as being text/xml . Since the SOAP body is always XML-encoded, this is the only legal value that could appear here. The SOAPPart object has a group of methods that allow application code to set or query the MIME headers that it contains, which are closely related to the methods of the MimeHeaders object described in the previous section of this chapter. These headers are distinct from those associated with the SOAPMessage object and are used only when the message includes attachments.

Following the SOAPPart are the three attachments. Again, each attachment has its own collection of MIME headers that, at minimum, must include a Content-Type header that describes the type of data that the attachment contains. In this case, the header is image/jpeg because all three attachments represent a book cover image encoded in JPEG format. Each attachment also has an additional header that looks like this:

 Content-Id: ID0 

This header is added by the book image servlet and acts as a label that is used to refer to the attachment from within the XML part of the message, which is shown in Example 3-13.

Example 3-13. The array of images returned by the book image service
 <tns:BookImages xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="xsd:anyType[]">
    <item href="cid:ID0"/>
    <item href="cid:ID1"/>
    <item href="cid:ID2"/>
</tns:BookImages> 

By now, you should recognize this as a SOAP-encoded array. In this case, though, the array type is given as xsd:anyType[] . This particular specifier indicates that the element content could be any type ” the application needs to examine each element to work out its actual type. This construct is essentially the same as declaring an array of Java Object s.

Another difference between this array and those that you have already seen is the actual element content is not inline with the array itself. Since the data in this case is not XML, it cannot be included directly in the message part, so we have to provide a reference that will allow the receiving application to locate the data. In this case, we choose to do so by using the href attribute, the value of which maps directly to the Content-Id of the attachment part that contains the image data apart from the inclusion of the cid :, which indicates that the value is a Content-Id . This distinguishes this representation from an alternative choice, available only for XML content, which places the value in the body of the message itself and for which the reference uses a # symbol instead of cid :, as shown here:

 <item href="#ID0"/> 

Here is a typical example where this style of reference is used: [11]

[11] Another point to note is that you could try to avoid the use of href and id attributes by simply adopting the convention that the image data for the first book is in the first attachment, the data for the second is in the second attachment, and so on. However, this might not be a good idea, since although the SAAJ API includes a method that gets all the attachments for a message, its definition does not state that they are returned in the same order in which they appear in the message.

 <?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:enc="http://schemas.xmlsoap.org/soap/encoding/" 
        xmlns:ns0="urn:jwsnut.chapter2.bookservice/types" 
        env:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <env:Body>
        <ans1:getBookInfoResponse xmlns:ans1=
          "urn:jwsnut.chapter2.bookservice/wsdl">  <result href="#ID1"/>  </ans1:getBookInfoResponse>  <ns0:ArrayOfBookInfo id="ID1" xsi:type="enc:Array"
                            enc:arrayType="ns0:BookInfo[12]">   <item href="#ID2"/>  <!-- Additional item elements not shown -->
        </ns0:ArrayOfBookInfo>  <ns0:BookInfo id="ID2" xsi:type="ns0:BookInfo">  <editor xsi:type="xsd:string">Paula Ferguson, Robert Eckstein
            </editor>
            <author xsi:type="xsd:string">David Flanagan</author>
            <price xsi:type="xsd:double">39.95</price>
            <title xsi:type="xsd:string">Java in a Nutshell</title>
        </ns0:BookInfo>
        <!-- More elements not shown -->
    </env:Body>
</env:Envelope> 

This SOAP message is actually created by JAX-RPC, and the data represents an array of BookInfo objects, which were used in the previous chapter. The result element uses the attribute href="#ID1 " to reference the data for the array of BookInfo objects that make up the result of the request, and the element that contains the data, called ArrayOfBookInfo , has an id attribute, the value of which is the referenced identifier. The same technique is used to refer to the array content from within the array.

It is important to note that there is nothing automatic about the use of href and id attributes to cross-reference one part of the message from the other: the references must be included by the application, and the receiver must use the id provided by the href attribute to find the referenced data, either in the SOAP message body as just shown or in an attachment.

3.6.2 Creating and Managing SOAP Attachments

The SAAJ API represents SOAP attachments using an instance of the javax.xml.soap.AttachmentPart class. SOAPMessage provides three methods to create attach-ments:

public AttachmentPart createAttachmentPart( )

Creates an AttachmentPart with no associated content. To add some data to the attachment, use one of the methods described in Section 3.6.3.

public AttachmentPart createAttachmentPart(Object content, String contentType)

Creates an AttachmentPart with the given content and with the Content-Type header set from the second argument. SAAJ makes some assumptions about the data type of the content object passed to it based on the supplied content type, as described in the next section.

public AttachmentPart createAttachmentPart(DataHandler dataHandler)

Creates an AttachmentPart where the data for the attachment is supplied by the given DataHandler . See the next section for a discussion of this method.

These methods do not connect the AttachmentPart to the message. To do this, use the addAttachmentPart( ) method:

 public void addAttachmentPart(AttachmentPart part); 

The number of attachments associated with a SOAPMessage can be obtained by calling the countAttachments( ) method, whereas an Iterator that returns some or all of the attachments can be obtained using the following methods:

 public Iterator getAttachments(  )
public Iterator getAttachments(MimeHeaders headers) 

The Iterator returned by the first of these methods will visit all of the attachments, whereas the one returned by the second method returns only those for which the MIME headers include all of those in the given MimeHeaders object. For example, to get all of the attachments whose data content is a JPEG image, use the following code:

 MimeHeaders headers = new MimeHeaders(  );
headers.addHeader("Content-Type", "image/jpeg");
Iterator iter = soapMessage.getAttachments(headers); 

The SAAJ specification does not explicitly require that the order in which the attachments are returned by the Iterator matches the order in which they appear in the message.

The Iterator returned by these methods can also be used to remove selected attachments from the message. The following code removes all attachments containing JPEG images:

 MimeHeaders headers = new MimeHeaders(  );
headers.addHeader("Content-Type", "image/jpeg");
Iterator iter = soapMessage.getAttachments(headers);
while (iter.hasNext(  )) {
    iter.remove(  );
} 

A quicker way to remove all attachments is to use the removeAllAttachments( ) method.

3.6.3 Attachment Headers and Content

An attachment contains a collection of MIME headers and some data content. This section looks at the API that SAAJ provides for manipulating both of these parts, using the book image web service for illustration purposes.

3.6.3.1 MIME headers

Like the SOAPMessage and the SOAPPart , an AttachmentPart has associated MIME headers. AttachmentPart provides the same API for manipulating these headers (as discussed earlier in Section 3.5.1). An AttachmentPart always has a Content-Type header that reflects the data type of the object that it contains, which is set when the AttachmentPart is created, when content is associated with it using one of the methods that we'll cover shortly, or when explicitly using the setContentType( ) method. If the latter is used, then the value supplied must match the actual type of the data in the attachment.

AttachmentPart also supplies convenience methods for setting two specific MIME headers:

 public void setContentId(String contentId);
public void setContentLocation(String contentLocation); 

Both of these headers are usually used to refer to the attachment from elsewhere using an element with an href attribute. The difference between these two types of identifier is as follows :

  • Content-Id is typically associated with an identifier that has only local scope and is guaranteed to be unique only within the message that contains the attachment. Applications commonly use values such as ID0 , ID1 , etc.

  • The value associated with a Content-Location is a URI (often a URL), which is more likely to be globally meaningful. If a message with an AttachmentPart containing a Content-Location header is received, it can more easily be removed and attached to another message than if Content-Id s is used, since the Content-Id in the AttachmentPart might clash with another already attached to the message.

Refer to RFC 2557 (at http://www.ietf.org/rfc/rfc2557.txt) for a complete discussion of the use of these identifier types. For an example that uses Content-Id s to locate attachments, see Section 3.6.4, later in this chapter.

It is important to realize that setting the Content-Id or the Content-Location header has no actual effect on the data in the AttachmentPart other than attaching a label to it.

3.6.3.2 Associating data with an AttachmentPart

There are four ways to install the data for an AttachmentPart :

  • Use the createAttachmentPart(Object content, String contentType) method of SOAPMessage .

  • Use the setContent(Object content, String contentType) method of AttachmentPart .

  • Use the createAttachmentPart(DataHandler handler) method of SOAPMessage .

  • Use the setDataHandler(DataHandler handler) method of AttachmentPart .

The first two are equivalent, as are the second two, so we'll discuss these two different mechanisms separately.

3.6.3.3 Specifying a value and a content type

This is the easiest way to install content in an attachment. For example, the following code installs a string value in a newly created attachment:

 AttachmentPart part = soapMessage.createAttachment(  );
part.setContent("This is in the attachment", "text/plain");
soapMessage.addAttachmentPart(part); 

You can achieve the same result using an overloaded variant of the createAttachmentPart( ) method:

 AttachmentPart part = soapMessage.createAttachment("This is in the 
attachment", "text/plain");
soapMessage.addAttachmentPart(part); 

However, this mechanism is not always as convenient as it may seem, because SAAJ places some requirements on the type of the content object, depending on the Content-Type that you supply. If these requirements are not met, then a SOAPException is thrown.

The content types that the SAAJ specification requires all implementations to recognize and the expected type of the Java object that must be supplied when creating the attachment are listed in Table 3-3. When the attachment contains a string, it is simple enough to make use of this API, since the content can be supplied as a java.lang.String object. Of course, this is probably among the least likely of data types to find in an attachment, and in all the other cases the developer has to work a little harder.

Table 3-3. Mapping between content type and object type for SOAP attachments

Content type

Required object type

text/plain

java.lang.String

text/xml

javax.xml.transform.stream.StreamSource

image/gif

java.awt.Image

image/jpeg

java.awt.Image

To attach XML, you can't simply read the XML into a string and pass the string value ” in other words, the following does not work:

 soapMessage.createAttachmentPart("<AnElement>Content</AnElement>", 
   "text/xml"); 

Instead, you have to construct a StreamSource object:

 soapMessage.createAttachmentPart(
        new StreamSource(new StringReader(
            "<AnElement>Content</AnElement>")), "text/xml"); 

Although this is not so convenient when you have the XML in the form of a String , it does make the task of including XML from a file or an InputStream very simple:

 soapMessage.createAttachmentPart(new StreamSource(new File(
    "c:\fileName.xml")), "text/xml"); 

As far as images are concerned , in order to attach one to a message, you need to have it in the form of a java.awt.Image object. While this might not seem too onerous a requirement, it probably is not the best design choice for the following reasons:

  • Using the java.awt.Image class implies the use of user interface classes. This is really not a good idea in a server environment, where it should not be necessary to load and initialize any user interface classes.

  • Typically, in order to get the required Image object, you will open a file containing the image encoded in byte form in either GIF or JPEG format, and then load the file content using methods provided by java.awt.Component together with a java.awt.MediaTracker . This is all unnecessary overhead, since the Image is then converted back to a byte stream when the SOAP message is transmitted.

  • Finally, you cannot use this mechanism to attach an image in GIF format to a message, since the process of converting from an Image to a byte stream requires a GIF encoder, which is not supplied as part of the standard Java platform at the time of this writing (although, of course, it is possible to decode a byte stream in GIF format).

For all these reasons, if you want to include an image as an attachment, you need to use a DataHandler , as described in the next section.

3.6.3.4 Using a DataHandler

The javax.activation.DataHandler class is part of the JavaBeans Activation Framework (JAF). It is most commonly used in conjunction with JavaMail to encapsulate access to attachments sent and received with Internet mail, which is essentially the same as handling SOAP message attachments. Whenever you create an AttachmentPart while building an outgoing message, or when an AttachmentPart is created to represent part of a received SOAP message, it has an associated DataHandler .

When a SOAP message is being converted from its internal representation to the byte stream that is ultimately transmitted, the DataHandler is responsible for creating a byte stream representation of the data in the attachment. The way in which this is done depends on the type of data to convert, so the DataHandler uses objects called DataContentHandlers , each of which can perform this conversion for a specific data type. DataContentHandler s are created by an object implementing the DataContentHandlerFactory interface, which returns an appropriate DataContentHandler for a given MIME type, if it has one. The SAAJ reference implementation installs a DataContentHandlerFactory that provides handlers for the data types listed in Table 3-3. Unfortunately, the handlers created by this factory for image/jpeg , image/gif , and text/xml work only when they are asked to convert objects of the types listed in the second column of Table 3-3. When the data you need to attach is already in the form of a byte array, simply using the AttachmentPart setContent( ) method in the obvious way:

 attachmentPart.setContent(imageData, "image/jpeg"); 

cannot be done without also changing the DataContentHandler used to handle objects with this MIME type. The only way to do this is to write and install your own DataContentHandlerFactory . (This is done by using the DataHandler setDataContentHandlerFactory( ) method), but this requires you to implememt handlers for all MIME types that you need to support, since there can only be one factory active at any given time and there is no way to get a reference to the existing factory so that you can delegate to it.

Fortunately, there is a simpler way to solve this problem. A DataHandler can work directly with an object that implements the javax.activation.DataSource interface. A DataSource is used when you have access to the object in a form from which it is simple to create an InputStream and an OutputStream . When you associate a DataSource with a DataHandler , it uses the DataSource to access the underlying data instead of looking for a DataContentHandler . When you have a GIF or JPEG image in the form of an array of bytes, it is natural to use a DataSource instead of a DataContentHandler , because no further data conversion is required. To describe how this can be done, the code that attaches the book cover images to the SOAP reply for the book image service is shown in Example 3-14.

Example 3-14. Attaching images to a SOAP message
 private void handleBookImageRequest(SOAPElement element, SOAPMessage reply) 
                                        throws SOAPException {
  // The request element contains an attribute that holds the 
  // type of image requested and a nested string for each title.
  // The reply body has a BookImages element and a nested item with
    // a reference to the image which is sent as an attachment           
    SOAPBody replyBody = reply.getSOAPPart().getEnvelope(  ).getBody(  );
            
    // Determine whether to use JPEG or GIF images
    String imageType = element.getAttributeValue(IMAGE_TYPE_ATTRIBUTE);
    boolean gif = imageType.equalsIgnoreCase("image/gif");
        
    // Build the BookImages element containing all of the replies
    SOAPBodyElement bodyElement = replyBody.addBodyElement(BOOK_IMAGES_NAME);
    bodyElement.addAttribute(
        soapFactory.createName("type", XMLSCHEMA_INSTANCE_PREFIX, 
           XMLSCHEMA_INSTANCE_URI), SOAP_ENC_PREFIX + ":Array");
    bodyElement.addAttribute(
        soapFactory.createName("arrayType", SOAP_ENC_PREFIX, 
           SOAPConstants.URI_NS_SOAP_ENCODING), XMLSCHEMA_PREFIX + ":anyType[]");
        
    // Index of the next attachment to use
    int index = 0;
        
    // Handle each nested element.
    Iterator iter = element.getChildElements(  );
    while (iter.hasNext(  )) {
        // Get the next child element from the request message
        SOAPElement childElement = (SOAPElement)iter.next(  );
            
        // Get the book title
        String title = childElement.getValue(  );
            
        // Get the image data
        byte[] imageData = BookImageServletData.getBookImage(title, gif);
        if (imageData != null) {
            // Got the data - attach it.  AttachmentPart attach = reply.createAttachmentPart(  );
            attach.setDataHandler(new DataHandler(
                                    new ByteArrayDataSource("Image Data",
                                        imageData,
                                        gif ? "image/gif" : "image/jpeg")));
            attach.setContentId("ID" + index);
            reply.addAttachmentPart(attach);  // Add an element in the reply pointing to the attachment
           bodyElement.addChildElement("item").addAttribute(HREF_ATTRIBUTE,
             "cid:ID" + index); 
                
            // Increment the index
            index++;
        } else {
            // No data - this is a fault.
            // Clear the reply and install the fault
            reply.removeAllAttachments(  );
            bodyElement.detachNode(  );
            createFault(replyBody, "soap-env:Client.Title", "Unknown title",
                                    SERVICE_URI, title);
            return;
        }
    } 
} 

This code loops over all of the book titles in the client request message and adds an element to the body of the reply that points to a corresponding attachment, using an href attribute with the Content-Id of the attachment as the reference value, as shown in Example 3-13. It uses a separate class ( BookImageServletData , not shown here) to get the data for a book with a given title in the form of an array of bytes that is encoded in either GIF or JPEG form. The section of this code that is relevant to this discussion is highlighted in bold. The first step is to create the AttachmentPart :

 AttachmentPart attach = reply.createAttachmentPart(  ); 

To associate the image data with the attachment, we need to replace the default DataHandler with one that uses a DataSource that takes its content from the image data. Unfortunately, there is no DataSource implementation that accepts a byte array as its input, but it is simple enough to create one, as shown in Example 3-15.

Example 3-15. A DataSource that encapsulates access to data in a byte array
 class ByteArrayDataSource implements DataSource {

    private String contentType;
    
    private byte[] data; 
    
    private String name;
        
    ByteArrayDataSource(String name, byte[] data, String contentType) {
        this.name = name;
        this.data = data;
        this.contentType = contentType;
    }
    
    public String getContentType(  ) {
        return contentType;
    }
    
    public InputStream getInputStream(  ) throws IOException {
        return new ByteArrayInputStream(data);
    }
    
    public String getName(  ) {
        return name;
    }
    
    public OutputStream getOutputStream(  ) throws IOException {
        throw new IOException(
            "ByteArrayDataSource cannot support getOutputStream(  )");
    }
} 

From the point of view of this example, the DataSource is only required to implement the

 getContentType( ) 

method (to return the MIME type of the data that it provides) and the getInputStream( ) method (which needs to return an InputStream to provide access to the data passed to it at construction time). This is easily achieved by wrapping the byte array containing the data in a ByteArrayInputStream . It would be simple to generalize this to also allow the data to be modified by implementing the getOutputStream( ) method, but that is not required here.

Given this DataSource , the image data can be associated with the attachment and then added to the SOAP reply message as follows:

 attach.setDataHandler(new DataHandler(
                                    new ByteArrayDataSource("Image Data",
                                        imageData,
                                        gif ? "image/gif" : "image/jpeg")));
            attach.setContentId("ID" + index);
            reply.addAttachmentPart(attach); 

When the completed reply message is being converted into a byte stream for transmission, the content of the attachments are always read from their DataHandler s. Since we constructed the DataHandler for each attachment with a DataSource , instead of trying to find a DataContentHandler for the attachment's associated MIME type, the data is obtained directly from the input stream returned by the getInputStream( ) method of the ByteArrayDataSource . Not only is this more convenient than converting the data into an Image , it is also much more efficient, and better still, it allows GIF images to be supported. Since there is no need for a GIF encoder to be present, the encoding has already taken place when the image data was created. This web service can now support any type of image, since it simply treats the image data as an opaque byte stream. Whether this is useful depends on the capabilities available to the client to decode the data and display it ” obviously, it is not useful to send an image in PNG format to a client that cannot decode PNG images.

This same technique could be used for any type of data for which you have a byte stream representation, for content types that are not listed in Table 3-3, and for which, therefore, the AttachmentPart setContent( ) method will not work. If, for example, you have an array of bytes that represents an audio clip in .wav format (a format that is not directly supported in the reference implementation), you can associate it with an attachment as follows:

 byte[] audioData = ......; // Load sound bytes (not shown)
 attach.setDataHandler(new DataHandler(
                                    new ByteArrayDataSource("Audio Data",
                                        audioData, "audio/wav"))); 

As well as installing your own DataHandler to make use of a custom DataSource , you can also use its other two constructors to attach data to a SOAP message:

 public DataHandler(Object object, String mimeType);
public DataHandler(URL url); 

The first constructor is of little use because it is equivalent to using the AttachmentPart setContent( ) method. The second constructor can be used to import data from a given URL, where it is assumed that the URL getContent( ) method returns an object whose type is consistent with the content types listed in Table 3-3.

3.6.4 Processing Received Attachments

When a SOAP message containing one or more attachments is received, the SOAPMessage object will have one AttachmentPart per attachment. The interpretation placed on these attachments and the way in which they relate to the XML in the body of the message is, of course, application-dependent. In the case of the book image service, as we saw in Example 3-13, each image returned in response to a BookImageRequest has its own entry in the BookImages array that appears in the body of the SOAP message sent by the web service to the client. The array entry is bound to the attachment containing the image data by an href attribute that contains the value of the Content-Id MIME header of the appropriate attachment:

 <item href="cid:ID0"/> 

To extract all of the images, the book image client loops over all of the elements in the array, extracts the href attribute for each, and finds the AttachmentPart that has the corresponding Content-Id , as shown in Example 3-16.

Example 3-16. Handling attachments in a SOAP message
 SOAPBody replyBody = reply.getSOAPPart().getEnvelope(  ).getBody(  );
if (replyBody.hasFault(  )) {
    SOAPFault fault = replyBody.getFault(  );
    throw new SOAPException("Fault when getting book images: " + 
                   fault.getFaultString(  ) +
                    ", actor is [" + fault.getFaultActor(  ) + "]");
}
        
// The body contains a "BookImages" element with a nested
// element for each book title.
Iterator iter = replyBody.getChildElements(BOOK_IMAGES_NAME);
if (iter.hasNext(  )) {
    ArrayList list = new ArrayList(  );
    MimeHeaders headers = new MimeHeaders(  );
    SOAPElement bookImages = (SOAPElement)iter.next(  );
    iter = bookImages.getChildElements(  );
    while (iter.hasNext(  )) {
        SOAPElement element = (SOAPElement)iter.next(  );
        String imageRef = element.getAttributeValue(HREF_ATTRIBUTE);
        if (imageRef != null) {
            // Get the attachment using the Content-Id, having
            // first removed the "cid:" prefix
            imageRef = imageRef.substring(4);
            headers.setHeader("Content-Id", imageRef);
            Iterator attachIter = reply.getAttachments(headers);
            if (attachIter.hasNext(  )) {
                AttachmentPart attach = (AttachmentPart)attachIter.next(  );
                Object content = attach.getContent(  );
                if (content instanceof Image) {
                    list.add(content);
                }                        
            }
        }
    }
    int size = list.size(  );
    Image[] images = new Image[size];
    list.toArray(images);
    return images;            
} else {
    // No BookTitles element was found
    throw new SOAPException("No BookImages element in returned message");
} 

As each child element of the array is found, its href attribute is obtained. Since this is actually in the form cid:ID0 , it is necessary to first strip away the leading cid : to obtain the value that is used for the Content-Id header:

 String imageRef = element.getAttributeValue(HREF_ATTRIBUTE);
        if (imageRef != null) {
            // Get the attachment using the Content-Id, having
            // first removed the "cid:" prefix
            imageRef = imageRef.substring(4); 

As noted earlier, the SAAJ API provides two SOAPMessage methods that return the attachments for the message:

 public Iterator getAttachments(  )
public Iterator getAttachments(MimeHeaders headers) 

There is no direct way to find a single attachment given a content id string. Instead, the second getAttachments( ) method provides a general facility that returns all attachments that have a given set of MIME headers. Here, we use it to locate the correct AttachmentPart by looking for the attachment in which the Content-Id header has the value extracted from the message body:

 MimeHeaders headers = new MimeHeaders(  );
headers.setHeader("Content-Id", imageRef);
Iterator attachIter = reply.getAttachments(headers);
if (attachIter.hasNext(  )) {
     AttachmentPart attach = (AttachmentPart)attachIter.next(  ); 

Given the way that this message is constructed, we expect there to be only one AttachmentPart per element in the message body, but this isn't necessarily the case: it is possible to attach more then one object with the same Content-Id , perhaps to supply the image and an associated sound file containing some marketing information for the book. The two objects would, of course, be distinguished by the content types, which can be obtained from the AttachmentPart using its getContentType( ) method.

Once you have the AttachmentPart , you can get its content using the following method:

 public Object getContent(  ); 

The actual type of the returned object is determined by the attachment's content type and the DataContentHandler s that are installed. In the case of the reference implementation, the mapping from content type to Java object type is as shown in Table 3-3, which means that, in this example, the getContent( ) method should return a java.awt.Image containing the image data extracted from the attachment.

Content types for which a DataContentHandler is not found are returned as an InputStream from which the raw byte data can be read. In the case of the reference implementation, for example, a sound file sent with type audio/wav or a byte stream of type application/octet-stream is returned in this way.

Another way to extract the data from an attachment is to use the DataHandler that is created for each AttachmentPart . Using the DataHandler , you can bypass the DataContentHandlerFactory and get direct access to the raw attachment data, even if the content type is recognized by the SAAJ implementation that you are using. Here, for example, is some code that will create an Image object from any of the image types recognized by the JRE that you are using, even if the SAAJ implementation does not provide a DataContentHandler for it. At the time of this writing, this code works for PNG images as well as those in GIF or JPEG format:

 if (attach.getContentType(  ).startsWith("image/")) {
    javax.activation.DataHandler handler = attach.getDataHandler(  );
    InputStream is = handler.getInputStream(  );
    int count = is.available(  );
    byte[] buffer = new byte[count];
    is.read(buffer, 0, count);
    Image img = java.awt.Toolkit.getDefaultToolkit(  ).createImage(buffer);
} 

Notice that we use the available( ) method of InputStream to find out how much data there is in the attachment, even though AttachmentPart has a getSize( ) method. The reason for this is the AttachmentPart method always seems to return a number that is larger than the actual size of the data.