TN-14: Apex API 5.0 Features and Changes - The salesforce.com CRM On-Demand Platform
Like most Web services, the AppExchange Web service API is based on using SOAP over HTTP. One of the advantages of using HTTP as the transport protocol is that the specification has provisions for compressing the content sent back and forth over the protocol, a feature that is commonly used with web sites, and is useful when sending or retrieve large sets of data from the AppExchange Web service. While most modern browsers attempt to make use compression without any user knowledge, almost none of the modern SOAP stacks make provisions for using compression over HTTP. In the context of the AppExchange Web service, this represents a performance penalty that should not be incurred, especially for large queries, updates or inserts.
For your convenience, a pre-compiled version of Axis that supports compression (per the technique explained below) is available here (axis_c.jar). This jar is NOT supported by salesforce.com nor by the Apache foundation. It is provided as a sample and should be considered as such. The details on how to enable compression within your code are available below, along with a discussion of exactly how and where Axis is modified to support this new capability. With the help of this jar, and a just a few lines of code, you should be able to easily take advantage of the performance benefits SOAP compression provides.
SOAP Message Stages
The basic flow of a SOAP request is that Java objects are passed to a proxy method, the Java objects are serialized into the parts of a SOAP message by the SOAP stack, and the message is sent to the Web service as XML. When the Web services replies, the SOAP message is received and the message is deserialized from XML into Java objects, which are in turn returned by the proxy method to the calling code.
For implementing compression, we need to insert some logic into the process at the right stages of the SOAP process, while ensuring that we don't hamper the serialization/deserialization code. In Apache Axis this point is when the HTTPTransport class writes the serialized message to a socket on request, and reads the serialized message from a socket on response.
Axis implements its own HTTP transport layer (as opposed to using standard Java HTTP classes), which makes the task of hooking into these stages straightforward.
Controlling Compression with Properties
One of the valuable aspects of Axis is the WSDL2Java library, which is used to create the Java proxies, based on a WSDL, for a given Web service. One of the goals in enabling compression in Axis, however, is avoid interfering with that process in a way that would preclude the ability to regenerate the proxy classes for a WSDL. This means that we cannot modify any of the WSDL2Java generated code to enable compression, and instead use a "safe" way of letting the Axis libraries know that we want to use compression.
Axis provides a robust property processing mechanism by which properties bubble up through the class hierarchy. Using the _setProperty method, it is possible to communicate these properties to the SoapBindingStub class, which is the entry point for Web service method invocations created by WSDL2Java. As such, these properties can be managed in your Web service client code, rather than the proxies themselves, which keeps them from being overwritten when WSDL2Java is run.
This is "safe" because the SoapBindingStub extends the org.apache.axis.client.Stub class. We will modify the Stub class in such a way that SoapBindingStub expose the properties required to control the compression process.
Each method that is exposed in the WSDL WSDL2Java creates a corresponding Call object that actually invokes the call on the Web service. The Call object does most of the heavy lifting as far as configuring the inputs, outputs and settings for a given Web service. Specifically, the Call object creates a Message object that contains the actual content of a SOAP request.
Fortunately, properties set on the SoapBindingStub can be automatically propagated to the Call , allowing us to set properties for a Call object without having direct access to it. Similarly, properties are propagated to the Message used within the Call automatically.
Modifying Axis to Support Compression
Having the technique to modify Axis without impacting in-place proxy code, and the mechanism to communicate properties between our client code and the rest of the Axis library, the next step is to modify the Axis code itself. With this approach, the modification will focus on two classes - org.apache.axis.client.Stub, which will define the new properties, and org.apache.axis.client.Call, which will store and propagate the properties for later use at the appropriate stage.
We have chosen to extend the org.apache.axis.client.Stub object to include two new String constants, which in turn will be inherited by the SoapBindingStub object and easily available to our client code. These two constants represent the property names we use to control compression - the snippet below shows these modifications to the org.apache.axis.client.Stub class and represents the only change to that class.
org.apache.axis.client.Stub
gain, when the SoapBindingStub creates a Call object, it transfers any properties that have been set to the Call object, so the next step is to make sure the Call object knows what to do with these values. So far, we have not defined what the values of these properties should be. We will do that now by adding two boolean Class scoped variables to the org.apache.axis.client.Call class. These declarations are added to the section at the top of the class, underneath the declaration of useSOAPAction variable, and also provide setters to maintain consistency with the rest of the Axis code.
org.apache.axis.client.Call
We need these variables so that we can refer to them later during the invoke() method of the Call object.
The SoapBindingStub transfers its properties to the Call object when it is instantiated by invoking the Call object's setProperty method for each of the available properties. So the Call object's setProperty method will be modified to be able to handle the properties created above, and dispatch them accordingly. The code additions to setProperty are shown in gray.
org.apache.axis.client.Call
We now have the ability to set the compression preferences on the Call object. Next, the invoke() method of the Call object will be modified to further propagate those preferences to the HTTP handler at message creation time.
The required modification to the invoke method is shown in the snippet below. Again, the additions are shown in gray.
org.apache.axis.client.Call
You should notice that we are now setting different properties for the MessageContext object - these string constants need to be created. By using string constants in the org.apache.axis.transport.http.HTTPConstants class, we are making these available for future reference and development. The additions to the org.apache.axis.transport.http.HTTPConstantsHTTPConstants class are shown below.
org.apache.axis.transport.http.HTTPConstants
These HTTP headers - HEADER_CONTENT_ENCODING and HEADER_ACCEPT_ENCODING - correspond to the ability to send and receive compressed content; the HEADER_CONTENT_ENCODING_GZIP header specifies the compression algorithm to use in both cases.
The message is actually transmitted using the writeToSocket method of the org.apache.axis.transport.http.HTTPSender class. This represents the last opportunity to do something with the message before it is sent. Part of the task of the writeToSocket method is to properly construct the HTTP headers that have been specified earlier. For the Web service to properly consume our compressed message and know to send us back a compressed message, we need to have the writeToSocket method add the headers that we defined in the org.apache.axis.transport.http.HTTPConstants class.
To do this we need to make a few additions to the writeToSocket method. The first task is to get the properties that have bubbled up to determine if compression is requested. These are now coming off of the MessageContext object. We are also saving Boolean flags to indicate later in the method what the desires where. We will use these flags to add our headers and to actually do the compression
org.apache.axis.transport.http.HTTPSender
Axis provides for user defined header inclusion by using a hashtable called userHeaderTable. After adding all the standard headers for the message, Axis will then iterate the userHeaderTable to include any user defined headers. We add our headers to this table as shown below.
org.apache.axis.transport.http.HTTPSender
At this point we have all of the infrastructure needed to inform Axis of the compression preferences. In the next section, the technique to actually perform the message compression is described.
Compressing the Message
The compression and decompression used in this technique are implemented by the java.util.zip libraries. The imports for the appropriate zip libraries are shown below.
org.apache.axis.transport.http.HTTPSender
Because we need to modify the content length header (as the length has changed due to compression), we will compress the message at the same point in code that Axis typically sets the content length header. We will compress the stream to a ByteArrayOutputStream so that we can later send that stream rather than the raw message and use the length of this byte array for the content length. This modification to the writeToSocket method is shown below.
org.apache.axis.transport.http.HTTPSender
The compressMessage helper function:
org.apache.axis.transport.http.HTTPSender
Adding the following code will start the message transmission on the wire.
org.apache.axis.transport.http.HTTPSender
Finally, we need to send our compressed stream to the Web service. We will do this by reassigning the InputStream defined above to our ByteArrayOutputStream, bufOut.
org.apache.axis.transport.http.HTTPSender
Decompressing the Response
Fortunately, handling a compressed response is far simpler. As you recall we indicated to the Web service that the request is compressed by adding an HTTP header call Content-Encoding. If the server is capable of compressing its response using the compression algorithm that we indicated in the Accept-Encoding HTTP header, then it will have included a Content-Encoding header in its reply with a value of gzip.
We will have a raw stream available to us in the readFromSocket method in the org.apache.axis.transport.http.HTTPSender class. After letting Axis parse out the header information from the stream, all we need to do is inspect the value of the Content-Encoding header to determine if the content has been compressed, and therefore if decompression is required. The code addition to the readFromSocket method is show below.
org.apache.axis.transport.http.HTTPSender
Once we have decompressed the stream and redirected our input stream to the decompressed stream we are done.
Packaging
Once the changes outlined in this tech note have been affected, you can simply compile the code into a new jar file. (Using axis_c.jar is suggested so that it won't be confused with the standard Axis implementation.) In your client application, replace the class path reference to axis.jar to your newly compiled jar.
Resources:
Compiled axis_c jar with compression enabled.
Source code for:
org.apache.axis.transport.http.HTTPConstants.java
org.apache.axis.transport.http.HTTPSender.java
org.apache.axis.Client.Stub.java
org.apache.axis.Client.Call.java
|