SOAP-Based Web Services and Binary Data

In the examples so far, the SOAP messages contain text that is converted to service-appropriate types such as List<Prediction>. The type conversion is typically automatic, occurring in the JAX-WS infrastructure without application intervention, but handlers could use JAX-B and related technologies for converting text to and from Java types. Even a very simple example illustrates the power of this underlying, automatic conversion. Here is a SOAP response from the RandService, in particular a call to the next1 operation, which returns a randomly generated integer:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <ns2:next1Response xmlns:ns2="http://rand/">
         <return>-1691660782</return>
      </ns2:next1Response>
   </S:Body>
</S:Envelope>

The returned value -1691660782 occurs as text in the SOAP message but is converted automatically to an int for a Java client, which does not need to do any explicit type conversion.

Type conversions come to the forefront in the issue of how binary data such as images, movies, and the like can be arguments passed to or values returned from SOAP-based service operations. SOAP-based services can deal with binary payloads but such payloads raise issues of efficiency. There are two general approaches to dealing with binary data in SOAP-based services:

In the course of SOAP development, there have been three options for attachments. SwA (SOAP with Attachments) is the original specification but does not work well with document-style services, which are the default. Moreover, some frameworks such as DotNet do not support SwA out of the box. DIME (Direct Internet Message Encapsulation) is a lightweight but proprietary encoding scheme, which has received little play outside of Windows. MTOM (Message Transmission Optimization Mechanism), which is based on XOP (XML-Binary Optimized Packaging), has the W3C seal of approval and enjoys widespread support. In short, MTOM is a modern, efficient, and interoperable way to share binary data through SOAP-based services.

To underscore the efficiency of MTOM, the first example uses base64 encoding. The SkiImageService class (see Example 5-11) has two @WebMethod operations: getImage returns a specified image about skiing, for instance, a picture of a nordic skier; GetImageList returns a list of the available skiing images.

Example 5-11. The SkiImageService, which delivers images encoded as base64 text

package images;

import javax.jws.WebService;
import javax.jws.WebMethod;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.awt.Image;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.ImageReader;

@WebService
public class SkiImageService {
    private Map<String, String> photos;

    @WebMethod
    public Image getImage(String name) { return createImage(name);  }
    @WebMethod
    public List<Image> getImages() { return createImageList(); }
    public SkiImageService() {
       photos = new HashMap<String, String>();
       photos.put("nordic", "nordic.jpg");
       photos.put("alpine", "alpine.jpg");
       photos.put("telemk", "telemk.jpg");
    }
    private Image createImage(String name) {
        String fileName = photos.get(name);
        byte[ ] bytes = getRawBytes(fileName);
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        Iterator iterators = ImageIO.getImageReadersByFormatName("jpeg");
        ImageReader iterator = (ImageReader) iterators.next();
        Image image = null;
        try {
            ImageInputStream iis = ImageIO.createImageInputStream(in);
            iterator.setInput(iis, true);
            image = iterator.read(0);
        }
        catch(Exception e) { throw new RuntimeException(e); }
        return image;
    }
    private List<Image> createImageList() {
        List<Image> list = new ArrayList<Image>();
        for (String key : photos.keySet()) {
            Image image = createImage(key);
            if (image != null) list.add(image);
        }
        return list;
    }
    private byte[ ] getRawBytes(String fileName) {
        if (fileName == null) fileName = "nordic.jpg";
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            FileInputStream in = new FileInputStream(fileName);
            if (in == null) in = new FileInputStream("nordic.jpg");
            byte[ ] buffer = new byte[2048];
            int n = 0;
            while ((n = in.read(buffer)) != -1)
                out.write(buffer, 0, n); // append to array
            in.close();
        }
        catch(Exception e) { throw new RuntimeException(e); }
        return out.toByteArray();
    }
}

Most of the code consists of utility methods that read bytes from a file and transform these into a Java Image. This service can be published straightforwardly with Endpoint:

package images;
import javax.xml.ws.Endpoint;
public class SkiImagePublisherBase64 {
    public static void main(String[ ] args) {
        System.out.println("URL: http://localhost:9876/ski");
        Endpoint.publish("http://localhost:9876/ski", new SkiImageService());
    }
}

Here is a Perl client against the service—a client that explicitly consumes the service WSDL before making a request:

#!/usr/bin/perl -w
use SOAP::Lite +trace => 'debug';
use strict;

my $url = 'http://localhost:9876/ski?wsdl';
my $service = SOAP::Lite->service($url);
print $service->getImage("nordic"), "\n"; # base64 string

There is nothing unusual in either the service or the client: a client request results in a SOAP response whose body is text, but in this case it is text that is potentially huge in size because of the base64 encoding. The XML Schema in the service WSDL points to the problem. Here is a slice:

<xs:complexType name="getImageResponse">
 <xs:sequence>
  <xs:element name="return" type="xs:base64Binary" minOccurs="0"></xs:element> 1
 </xs:sequence>
</xs:complexType>

Line 1 shows that the XML Schema type of the response message getImageResponse is, indeed, xs:base64Binary. For dramatic effect, here is a slice of the more than 30K-byte response to a getImage request:

<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
  <S:Body>
    <ns2:getImageResponse xmlns:ns2="http://images/">
      <return>iVBORw0KGgoAAAANSUhEUgAAAHwAAABWCAIAAACCS2W5AABY...</return> 1
    </ns2:getImageResponse>
  </S:Body>
</S:Envelope>

The element tagged return (line 1) contains the base64 encoding of the image. The image itself, nordic.jpg, is just under 3K bytes—and the SOAP response is just over 30K bytes. In this case, the data bloat is ten-fold.

To avoid the data bloat associated with base64 or equivalent encoding, the service can be revised to take advantage of MTOM optimizations. Here are the steps to the revision:

With these changes in place, an efficient Java client against the revised MTOM-based SkiImageService can be built upon the usual wsimport artifacts. The by now familiar command:

% wsimport -p clientMTOM -keep http://localhost:9876/ski?wsdl

generates the support classes for the revised SkiImageClient (see Example 5-12).

Example 5-12. A Java client against the MTOM-based SkiImageService

The Java DataHandler type (lines 1 and 2) binds to the type application/octet-stream. Each image from the service is a DataHandler instance whose properties (for instance, the contentType and content properties shown in lines 3 and 4) are accessible with the familiar get-methods. The output from a sample client run is:

MIME type: application/octet-stream
Content:   java.io.ByteArrayInputStream@3f3e10ce
...
MIME type: application/octet-stream
Content:   java.io.ByteArrayInputStream@ca753f7

The ByteArrayInputSteam instances contain the bytes sent from the MTOM-enabled SkiImageService. Although the transmission is relatively efficient, the client now must deal with these bytes in some application-appropriate way—for instance, by reconstructing the JPG images from the bytes.

In JAX-WS a client, too, can use MTOM to send media attachments to a service. Here is a revision of the SkiImageClient that shows the setup (lines 1 through 3):

SkiImageService port = new SkiImageServiceService().getSkiImageServicePort();
BindingProvider bp = (BindingProvider) port;          1
SOAPBinding binding = (SOAPBinding) bp.getBinding();  2
binding.setMTOMEnabled(true);                         3

SOAP-based web services are at their best when dealing with text payloads, as the SOAP infrastructure then assumes the burden of converting between native language types (for instance, a Java List<Prediction>), on the one side, and XML Schema types, on the other side. This advantage goes away once the SOAP payloads are binary, regardless of whether the payload is base64 encoded text in the SOAP body or SOAP attachments. Can SOAP-based services handle binary payloads? The answer is a firm yes, but this answer invites the further question of whether SOAP-based services are ideally suited for binary payloads. The answer to this second question is no.