Java XML-RPC Client with Apache XmlRpcClient

In this post I'll provide a working example of a Java XML-RPC client using the org.apache.xmlrpc.client package and explain how to parse responses.

First, Why XML-RPC?

If you're looking into creating an XML-RPC client, I sincerely hope it's to connect with a legacy system. For the most part XML-RPC APIs have fallen out of favor, replaced with JSON-based REST APIs and what not. But if, like me, you've been tasked with writing a client to consume a legacy API, you don't really have a choice.

Getting Started

Fortunately for Java developers (and most languages that aren't brand-spankin' new), XML-RPC support is pretty easy to come by. For this tutorial, I'll use the Apache library, which can be added to a Maven project with:

<dependency>
<groupId>org.apache.xmlrpc</groupId>
<artifactId>xmlrpc-client</artifactId>
<version>3.1.3</version>
</dependency>

Since what we're really writing is an integration test, I've also added in TestNG:

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.13</version>
</dependency>

Now I can write a simple test class that starts like this: package com.techdagan.xmlrpcsandbox;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Map;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.apache.xmlrpc.client.XmlRpcClientException;
import org.apache.xmlrpc.client.XmlRpcStreamTransport;
import org.apache.xmlrpc.client.XmlRpcTransport;
import org.apache.xmlrpc.client.XmlRpcTransportFactoryImpl;
import org.apache.xmlrpc.common.XmlRpcStreamRequestConfig;
import org.testng.Assert; import org.testng.annotations.Test;
import org.xml.sax.SAXException;

public class XmlRpcClientTest {

@Test public void textXmlRpcClientTest() throws Exception {

// Set up a client config that points no where (we're mocking anyway) XmlRpcClientConfigImpl clientConfig = new XmlRpcClientConfigImpl(); clientConfig.setServerURL(new URL("http://localhost/my/xml-rpc/endpoint"));

// Create our client XmlRpcClient client = new XmlRpcClient(); client.setConfig(clientConfig);

// Use our mock transport factories XmlRpcClientTest.MockXmlRpcTransportFactory transportFactory = new XmlRpcClientTest.MockXmlRpcTransportFactory(client); client.setTransportFactory(transportFactory); transportFactory.setResponse(new ByteArrayInputStream( ("<?xml version=\"1.0\"?>" + "<methodResponse>" + "<params>" + "<param>" + "<value><double>18.24668429131</double></value>" + "</param>" + "</params>" + "</methodResponse>").getBytes()));

Double answer = (Double) client.execute("add", new Object[]{ 1234, 56789}); Assert.assertEquals(answer, (Double)18.24668429131); }

Here's a breakdown of the code above:

  • Client classes are configured using object's implementing XmlRpcClientConfig, namely XmlRpcClientConfigImpl. You can set several properties of the config object, such as username and password (for basic auth, I believe), but you'll need to set the server URL at a minimum. To apply a configuration, simply pass a config object to the client's setConfig() method.
  • The client object will rely on three classes to do the actual work: a worker class, a transport factory, and a type factory. The worker class handles thread pooling, the transport factory generates the transport object (which does the actual server communication), and the type factory parses server's the response into an object. Aside from testing and extraordinary circumstances, the default implementations should be fine.
  • Since this is a test, we don't want to bother with setting up an actual XML-RPC server. Fortunately, the Apache XML-RPC package includes an abstract XmlRpcStreamTransport class that can easily be extended to implement a mock transport, which I've done.
  • Review the mocked response I've assigned. XML-RPC has a very strict format for requests and responses (checkout the Wikipedia article). Basically, each request names the method to call and provides an ordered list of parameters. Each response either define a fault or a method response. In the case of a method response, there is exactly one return value.
  • Finally, executing a request is fairly simple: pass a method name and array of objects to be used as parameters to the client's execute() method and cast the return value to the expected data type.

Return Values

The values returned by execute() have to be cast to the appropriate data type. There are eight, corresponding to the eight XML-RPC data types:

Stringstring

Integerint,i4

Doubledouble

Booleanboolean

Byte[]base64

Map<String,Object> (java.util.map)struct

Object[]array

Date (java.util.Date)dateTime.iso8601

(Okay, there's actually the possibility of 10 more, but you'll probably use these eight.)

In the case of struct and array, you'll need to do additional casting for each member, since there's no requirement other than a member have a name (for structs) and a value of one of the supported data types.

For me, the simple cast requirement was the missing piece. Having worked with more modern APIs and patterns, I was expecting to be able to implement a mapping interface and use generics to retrieve a domain-specific class. I guess I was over thinking it.

Wrapping Up

Perhaps surprisingly, that's really it for the client. Aside from executing the request, handling exceptions (thrown when communication fails or the server returns a fault response), and your own domain-specific mapping (you know, all that stuff), you're done.

Well, mostly, anyway. If you actually want the sample code above to execute, you'll need the mock transport factory and transport classes I created. So here are those:

private class MockXmlRpcTransportFactory extends XmlRpcTransportFactoryImpl {

protected InputStream response; protected XmlRpcClientTest.MockXmlRpcTransport transport;

protected MockXmlRpcTransportFactory(XmlRpcClient pClient) { super(pClient); this.transport = new XmlRpcClientTest.MockXmlRpcTransport(this.getClient()); }

@Override public XmlRpcTransport getTransport() { this.transport = new XmlRpcClientTest.MockXmlRpcTransport(this.getClient()); this.transport.setResponse(this.response); return this.transport; }

public void setResponse(InputStream stream) { this.response = stream; }

public ByteArrayOutputStream getLastRequest() { return this.transport.getLastRequest(); } }

private class MockXmlRpcTransport extends XmlRpcStreamTransport{

protected ByteArrayOutputStream request; protected InputStream response;

public MockXmlRpcTransport(XmlRpcClient client) { super(client); }

public MockXmlRpcTransport(XmlRpcClient client, InputStream response) { super(client); this.setResponse(response); }

public ByteArrayOutputStream getLastRequest() { return this.request; }

public InputStream getResponse() { return this.response; }

public void setResponse(InputStream response) { this.response = response; }

@Override protected void close() throws XmlRpcClientException { return; }

@Override protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) { return false; }

@Override protected InputStream getInputStream() throws XmlRpcException { return this.response; }

@Override protected void writeRequest(ReqWriter pWriter) throws XmlRpcException, IOException, SAXException {

this.request = new ByteArrayOutputStream(); pWriter.write(this.request); } }