Chapter 6
Sending binary files
What's in this chapter?
In this chapter you'll learn how to receive and return binary files in your web service.
Providing the image of a product
Suppose that you'd like to have a web service to allow people to upload the image (jpeg) of a product (identified by a product id). The SOAP message may be like below. But how to represent the binary image data? The problem is that SOAP uses XML and XML uses text to represent the data:
One way to do it is to encode the binary data into a text format. For example, one could encode byte 0 as character 'A', encode byte 1 as character 'B' and etc. One of such an encoding is the base64 encoding. Then the SOAP message will be like:
<Envelope>
<Body>
<uploadImage>
<productId>p01</productId>
<image>kdubn87kamlndy...</image>
</uploadImage>
</Body>
</Envelope>
The problem is that the base64 encoded data will be much larger than the binary version. This wastes processing time, network bandwidth and transmission time. In fact, if the image is huge, then many XML parsers may not be able to handle it properly. To solve this problem, instead of always representing an XML document as text, people state that it can be represented as a MIME message. For example, the above XML document (SOAP envelope) can be represented as below without changing its meaning:
<Envelope>
<Body>
<uploadImage>
<productId>p01</productId>
<image>???</image>
</uploadImage>
</Body>
</Envelope> How to send the binary image data?
The key is that now the XML document (represented as a MIME message) can have textual parts and binary parts. Therefore it can represent binary data efficiently.
To implement this idea, create a new project named ImageService as usual.
Rename the WSDL file as ImageService.wsdl and modify it as:
Content-Type: Multipart/Related --MIME boundary
Content-Type: text/xml
<Envelope>
<Body>
<uploadImage>
<productId>p01</productId>
<image>
<xop:Include
xmlns:xop="http://www.w3.org/2004/08/xop/include"
href="cid:abc"/>
</image>
</uploadImage>
</Body>
</Envelope>
--MIME boundary
Content-Type: image/jpeg Content-ID: abc
...binary data here...
...
--MIME boundary
Binary data is allowed in a MIME part.
Refer to the actual data by content id.
This is a MIME message. It can contain multiple parts. Here it contains 2 parts.
This MIME message represents the XML document (the SOAP envelope).
A part that contains the "core" of the XML document as text.
A part that contains binary data (the image).
This is the xop namespace.
xop stands for XML-binary optimized packaging.
Although this is not required, it uses the wrapped convention. Next, update the CodeGenerator class:
public class CodeGenerator {
public static void main(String[] args) { WSDLToJava.main(new String[] {
"-server",
Generate the code again. Check the SEI:
@WebService(targetNamespace = "urn:ttdev.com:service/img", name = "ImageService")
@XmlSeeAlso( { ObjectFactory.class }) public interface ImageService {
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
<xsd:element name="productId" type="xsd:string" />
<xsd:element name="image" type="xsd:base64Binary" />
</xsd:sequence>
<wsdl:part name="parameters" element="tns:uploadImage" />
</wsdl:message>
<wsdl:portType name="ImageService">
<wsdl:operation name="uploadImage">
<wsdl:input message="tns:uploadImageRequest" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="ImageServiceSOAP" type="tns:ImageService">
<soap:binding style="document"
transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="uploadImage">
<soap:operation soapAction="urn:ttdev.com:service/img/uploadImage" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="ImageService">
<wsdl:port binding="tns:ImageServiceSOAP" name="p1">
<soap:address location="http://localhost:8080/is/p1" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
It will contain binary data. It is basically to be encoded using base64. Later you will tell the code generator to use XOP for it.
The operation doesn't return anything, so there is no output message.
Use a urn as the target namespace.
@Oneway
@RequestWrapper(
localName = "uploadImage",
targetNamespace = "urn:ttdev.com:service/img", className = "com.ttdev.service.img.UploadImage")
@WebMethod(action = "urn:ttdev.com:service/img/uploadImage") public void uploadImage(
@WebParam(
name = "productId",
targetNamespace = "") java.lang.String productId,
@WebParam(
name = "image",
targetNamespace = "") byte[] image);
}
Note that the binary image data is presented as a byte array. You are NOT using XOP yet. You're just getting the service up and running. Create a ImageServiceImpl class:
package com.ttdev.service.img;
...@WebService(endpointInterface = "com.ttdev.service.img.ImageService") public class ImageServiceImpl implements ImageService {
@Override
public void uploadImage(String productId, byte[] image) { try {
FileOutputStream out = new FileOutputStream(productId+".jpg");
out.write(image);
out.close();
} catch (IOException e) { throw new RuntimeException(e);
} } }
It simply saves the image data into a p01.jpg file if the product is p01. Next, create an ImageClient product as usual. Copy any .jpg file in your computer into the src/main/resources folder as sample.jpg. Then modify the ImageService_P1_Client class:
public final class ImageService_P1_Client {
...public static void main(String args[]) throws Exception { ...
ImageService_Service ss = new ImageService_Service(wsdlURL, SERVICE_NAME);
ImageService port = ss.getP1();
{
System.out.println("Invoking uploadImage...");
java.lang.String _uploadImage_productId = "p01";
FileInputStream in = new FileInputStream(
"src/main/resources/sample.jpg");
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
for (;;) {
int noBytesRead = in.read(buf);
if (noBytesRead == -1) { break;
}
out.write(buf, 0, noBytesRead);
}
Run the service and then run the client. Refresh the ImageService project and }
you should see a p01.jpg file there. Open it with a browser to verify that it is a copy of your sample.jpg.
To verify that it is NOT using XOP, use the TCP Monitor and adjust the client:
public final class ImageService_P1_Client { ...
public static void main(String args[]) throws Exception { ...
ImageService_Service ss = new ImageService_Service(wsdlURL, SERVICE_NAME);
ImageService port = ss.getP1();
{
BindingProvider bp = (BindingProvider) port;
bp.getRequestContext().put(
BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
"http://localhost:1234/is/p1");
System.out.println("Invoking uploadImage...");
java.lang.String _uploadImage_productId = "p01";
FileInputStream in = new FileInputStream(
"src/main/resources/sample.jpg");
ByteArrayOutputStream out = new ByteArrayOutputStream();
...
port.uploadImage(_uploadImage_productId, out.toByteArray());
out.close();
in.close();
}
System.exit(0);
} }
The message captured should be like below. That is, a lot of binary data encoded as base64:
POST /is/p1 HTTP/1.1
Content-Type: text/xml; charset=UTF-8
SOAPAction: "urn:ttdev.com:service/img/uploadImage"
Accept: */*
User-Agent: Apache CXF 2.2.5 Cache-Control: no-cache Pragma: no-cache Host: 127.0.0.1:1234 Connection: keep-alive Transfer-Encoding: chunked ff9
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:uploadImage xmlns:ns2="urn:ttdev.com:service/img">
<productId>p01</productId>
<image>/9j/4AAQSkZJRgABAgEASABIAAD/7QlCUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAAB AASAAAAAEAAgBIAAAAAQACOEJJTQQNAAAAA...zr/2Q==</image>
</ns2:uploadImage>
</soap:Body></soap:Envelope>0
To enable the use of XOP, modify the WSDL file in the client project:
Generate the code again. Then data type of the binary data will be changed from byte[] to DataHandler:
@WebService(targetNamespace = "urn:ttdev.com:service/img", name = "ImageService")
@XmlSeeAlso( { ObjectFactory.class }) public interface ImageService {
@Oneway
@RequestWrapper(...)
@WebMethod(action = "urn:ttdev.com:service/img/uploadImage") public void uploadImage(
@WebParam(...) java.lang.String productId,
@WebParam(...) javax.activation.DataHandler image);
}
How's a DataHandler differ from a byte array? A DataHandler can provide an InputStream on demand, which means that the program doesn't need to load all the data into memory. In addition, a DataHandler can tell you the content type of the data.
As the SEI has changed, you need to modify the ImageService_P1_Client class to pass a DataHandler:
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="urn:ttdev.com:service/img"
xmlns:xmime="http://www.w3.org/2005/05/xmlmime"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="ImageService"
targetNamespace="urn:ttdev.com:service/img">
<wsdl:types>
<xsd:schema targetNamespace="urn:ttdev.com:service/img"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="uploadImage">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="productId" type="xsd:string" />
<xsd:element name="image" type="xsd:base64Binary"
xmime:expectedContentTypes="application/octet-stream"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</wsdl:types>
</wsdl:definitions>...
It states that this element should be sent as a MIME part those content type is application/octet-stream which means a generic (no particular meaning) byte stream.
This XML MIME namespace defines XML attributes used for representing XML in a MIME message.
However, this is not enough. You need to enable this special packaging in the client:
Run the client again. In the TCP Monitor, you should see:
import javax.xml.ws.Binding;
import javax.xml.ws.soap.SOAPBinding;
...public static void main(String args[]) throws Exception { ...
ImageService_Service ss = new ImageService_Service(wsdlURL, SERVICE_NAME);
ImageService port = ss.getP1();
{
BindingProvider bp = (BindingProvider) port;
bp.getRequestContext().put(
BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
"http://localhost:1234/is/p1");
Binding binding = bp.getBinding();
SOAPBinding soapBinding = (SOAPBinding) binding;
soapBinding.setMTOMEnabled(true);
System.out.println("Invoking uploadImage...");
java.lang.String _uploadImage_productId = "p01";
FileDataSource ds = new FileDataSource("src/main/resources/sample.jpg");
port.uploadImage(_uploadImage_productId, new DataHandler(ds));
}
System.exit(0);
}
Get the binding from the binding provider (the port). Cast it to a SOAP binding as you're using SOAP.
Enable MTOM which stands for Message Transmission Optimization Mechanism.
It simply means sending a SOAP message using XOP.
public static void main(String args[]) throws Exception {
...ImageService_Service ss = new ImageService_Service(wsdlURL, SERVICE_NAME);
ImageService port = ss.getP1();
{
BindingProvider bp = (BindingProvider) port;
bp.getRequestContext().put(
BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
"http://localhost:1234/is/p1");
System.out.println("Invoking uploadImage...");
java.lang.String _uploadImage_productId = "p01";
FileDataSource ds = new FileDataSource("src/main/resources/sample.jpg");
port.uploadImage(_uploadImage_productId, new DataHandler(ds));
}
System.exit(0);
}
Create a DataSource from this file. A DataSource can also provide an InputStream and a content type (here it will guess from the file extension .jpg to conclude that the content type is image/jpeg).
Let the DataHandler get the data from this DataSource. In addition to providing the data and the content type, a DataHandler can also suggest a list of actions that can be performed on that data (something like when you right click on a file and you'll see a list of menu commands).
Note that even though you haven't done anything in the service, it can already successfully decode the SOAP message sent using MTOM. This is designed for good compatibility: The receiver can receive all kinds of formats, while explicit configuration determines which format to be initiated by the sender.
Enabling MTOM in the service
For the moment, it is your client that needs to send a file. If it was your web service that needed to do that, you would need to enable MTOM in the service.
To do that, copy the WSDL file from the client into the service and generate the code again. Then modify your implementation class to use a DataHandler:
POST /is/p1 HTTP/1.1
Content-Type: multipart/related; type="application/xop+xml";
boundary="uuid:9223456b-e0fb-4d04-bf15-52793bea1426";
start="<[email protected]>"; start-info="text/xml"
SOAPAction: "urn:ttdev.com:service/img/uploadImage"
Accept: */*
User-Agent: Apache CXF 2.2.5 Cache-Control: no-cache
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml";
Content-Transfer-Encoding: binary
x8BIM� 8BIM
8BIM' ...
--uuid:9223456b-e0fb-4d04-bf15-52793bea1426--0
Refer to the binary data using cid (content id).
MIME message (multipart/related)
The binary data
public class ImageServiceImpl implements ImageService {
@Override
public void uploadImage(String productId, DataHandler image) { try {
FileOutputStream out = new FileOutputStream(productId+".jpg");
image.writeTo(out);
out.close();
} catch (IOException e) { throw new RuntimeException(e);
} } }
To initiate MTOM, modify the ImageService_P1_Server class:
public class ImageService_P1_Server {
protected ImageService_P1_Server() throws Exception { System.out.println("Starting Server");
Object implementor = new ImageServiceImpl();
String address = "http://localhost:8080/is/p1";
Endpoint endpoint = Endpoint.publish(address, implementor);
SOAPBinding soapBinding = (SOAPBinding) endpoint.getBinding();
soapBinding.setMTOMEnabled(true);
}...
}
The way to enable MTOM is very similar to the client: you get the SOAP binding and enable MTOM. For the client you get it from the port, for the service you get it from the endpoint.
Doing it in Axis2
To do it in Axis2, copy the Axis2SimpleService and paste it as Axis2ImageService, copy the Axis2SimpleClient and paste it as Axis2ImageClient. Copy the WSDL file and XSD file from the ImageService project into both of these two new projects (and delete the existing WSDL files).
Next, modify the CodeGenerator class in the Axis2BizService project as shown below. Note that the two package mappings are separated by a comma:
public class CodeGenerator {
public static void main(String[] args) throws Exception { WSDL2Code.main(new String[] {
"-ss",
"-uri", "src/main/resources/ImageService.wsdl" });
System.out.println("Done!");
} }
Do the same thing in the Axis2ImageClient project:
public class CodeGenerator {
public static void main(String[] args) throws Exception { WSDL2Code.main(new String[] {
"-uw",
"-S", "src/main/java",
"-R", "src/main/resources/META-INF",
"-ns2p", "urn:ttdev.com:service/img=com.ttdev.is",
"-uri", "src/main/resources/ImageService.wsdl" });
System.out.println("Done!");
} }
Delete the com.ttdev.ss package and the src/main/resources/META-INF folder in both projects. Then run CodeGenerator in both projects. Fill in the code in the ImageServiceSkeleton class:
public class ImageServiceSkeleton {
public void uploadImage(java.lang.String productId, javax.activation.DataHandler image) {
try {
FileOutputStream out = new FileOutputStream(productId + ".jpg");
image.writeTo(out);
out.close();
} catch (IOException e) { throw new RuntimeException(e);
} }
<operation name="uploadImage" mep="http://www.w3.org/ns/wsdl/in-only"
namespace="urn:ttdev.com:service/img">
Modify the pom.xml file to use a new artifact ID:
<project ...> axis2/repository/services folder) as ImageService.aar.
Then, in the client project, create an ImageClient class:
package com.ttdev.is;
...
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import org.apache.axis2.Constants;
public class ImageClient {
public static void main(String[] args) throws RemoteException {
ImageServiceStub service = new ImageServiceStub(
"http://localhost:1234/axis2/services/ImageService");
service._getServiceClient().getOptions().setProperty(
Constants.Configuration.ENABLE_MTOM, "true");
FileDataSource ds = new FileDataSource("src/main/resources/sample.jpg");
service.uploadImage("p01", new DataHandler(ds));
}
The way to enable MTOM in Axis2 is very similar to that in CXF (which actually }
is the standard JAX-WS API).
Copy the sample.jpg file into the client package in the src/main/resources folder.
Run the client and observe the message in TCP Monitor. It should be packaged in a MIME message.
Interoperability
If you need to send binary files to others, make sure the other side supports MTOM. For example, for .NET, MTOM is supported with WSE (Web Services Enhancements) 3.0 or later.
Summary
XOP stores XML elements that is of the type xsd:base64Binary as MIME parts and represents the whole XML document as a MIME message. When the XML document is a SOAP envelope, it is called MTOM.
To receive a binary file using MTOM, if the receiver is written with CXF or Axis2, for maximum interoperability, it can always handle incoming messages using MTOM without any configuration.
To send a binary file using MTOM, indicate the content type in the schema and set an option to enable MTOM in the sender.