Mark Wilson I am the creator of TopXML. I am available for international and local (Australia) contracts. I am a Solution Architect/Business Analyst. I have worked in IT in several countries (NZ, Australia, South Africa, UK) building and training teams for government and very large non-governmental organizations. I am ex-Microsoft Consulting Services. I wrote the first book on Microsoft XML published in 2000 called XML Programming with VB and ASP. Most recently I have been building tools for the SEO industry. Ask me for a 37 point SEO health-checkup for your website.
First posted :
08/21/2003
Times viewed :
442
Programming Web Services with XML-RPC
By Simon St.Laurent, Joe Johnston & Edd Dumbill
June 2001
0-596-00119-3, Order Number: 1193
230 pages, $34.95
Chapter 3: Client-Server
Communication: XML-RPC in Java
Java was built from the ground up as a network-centric
development environment. As a Java developer, XML-RPC offers you an
opportunity to extend that foundation in a structured way. Adding XML-RPC to
your toolkit makes it easier to integrate a Java application with an
application built using another environment or simply to establish lightweight
point-to-point communication between Java programs running on different
computers. Although XML-RPC goes against the grain of much Java network
programming (and even against some of the fundamental principles of
object-oriented development), its alternative approach can be useful in many
relatively common scenarios.
You already have a wide variety of Java-based XML and HTTP
tools to choose from, but you can also take advantage of a prepackaged set of
XML-RPC tools. Although understanding the foundations of XML-RPC is very
useful for debugging and for establishing connections between systems in
different environments, you can treat XML-RPC much like you do any other Java
feature. There's some setup work to do, especially for XML-RPC servers, but
most of this work is simple and needs to be done only once in the course of a
program.
This chapter looks at how XML-RPC fits into Java's many
network options. It demonstrates how to build a variety of different XML-RPC
clients, servers, and handlers. Some of these examples take advantage of
built-in functionality for setting up simple XML-RPC servers and handlers;
others explore the possibilities opened up by handling more of the processing
directly. The examples cover different styles of XML-RPC programming, from
simple library function calls to more complex calls that manipulate
information on the server.
Why XML-RPC for Java?
Java is already an extremely network-aware environment,
complete with its own mechanisms for remote communication and coordination of
objects on multiple systems. Remote Method Invocation (RMI) and RMI's support
for the broader world of CORBA-based systems provide built-in support for
distributing applications across multiple systems.[1]
In many ways, Java is well ahead of its competitors, and its network support
extends far beyond the simple request-response cycle of XML-RPC.
Despite Java's built-in network computing prowess, XML-RPC
offers a few kinds of functionality that Java can't match. XML-RPC is far more
lightweight than Java's built in RMI support, passing only parameters rather
than objects. Java programs can use XML-RPC to connect directly to any other
system supporting XML-RPC, rather than limiting connections to fellow RMI
systems or having to use complex (and expensive) CORBA object request brokers.
As illustrated in Figure 3-1,
XML-RPC can bring the direct connections RMI makes possible for strictly Java
applications to applications that integrate multiple environmentsXML-RPC's use
of HTTP as a transport substrate makes it relatively simple to integrate
XML-RPC with the web-enabled applications that are already spreading across
the computing landscape. At the same time, XML-RPC uses such a tiny subset of
HTTP that Java applications can easily avoid the overhead of full-scale HTTP
processing, working with a more minimal--and more efficient--driver that takes
advantage of Java's built-in understanding of TCP/IP.
XML-RPC also offers you a shorter compilation and testing
cycle. Unlike RMI, which requires recompilation of interfaces to register
method signatures, XML-RPC allows the client to specify which method it wants
to use and then looks for a handler. Because the reference is done by name,
there aren't any stubs to manage or include, and changes can be made much more
easily at runtime.
On the other hand, XML-RPC is definitely not appropriate in
plenty of Java application situations. Much of the Enterprise JavaBeans (EJB)
work already relies on RMI, and rewriting it to use XML-RPC would be a
remarkable waste of time. Although a snippet of XML-RPC code might be useful
as a simple bridge between an EJB-based application and code written for other
environments, XML-RPC isn't designed explicitly to support the many demands of
complex enterprise-scale design. Similarly, if you need to pass objects,
rather than parameters, betweensystems, you should look into a more
sophisticated set of tools than XML-RPC. XML-RPC lets you pass sets of
parameters, not complex nested structures with associated method information,
back and forth. As explained later in this chapter, XML-RPC's approach doesn't
match cleanly with JavaBeans, either.
Figure 3-1. XML-RPC
makes it possible to connect a wide array of programming environments
Although most Java programs aren't designed for use in the
procedural framework that XML-RPC uses, an enormous amount of code in the
typical Java program could conceivably be exposed as an XML-RPC procedure,
with or without some degree of modification. Although Java is very
object-focused, it retains enough links with procedural environments for
developers to take advantage of "traditional" features, such as
function calls, in the midst of complex object interactions. Although some of
XML-RPC's rules, like its lack of support for void return values, make
integrating XML-RPC with Java difficult, most of the integration process is
pretty simple, limited only by the narrow range of data types and structures
XML-RPC supports.
The XML-RPC library for Java does not require the methods it
uses be static, but in some ways static methods fit the basic architecture of
XML-RPC very well. Static methods are the closest things Java offers to
procedural function calls, commonly used for utility functions (such as those
in the Math class) for which the function itself
is important, but there may not be any surrounding context. If you've built
libraries of these kinds of methods, implementing key algorithms for
processing arguments, you may find it especially easy to convert your old work
to XML-RPC handlers.
You can also use XML-RPC servers in a larger Java framework to
answer client requests while using those requests to modify their own
information set. Rather than thinking about XML-RPC handlers as
"mere" procedures, you can treat XML-RPC handlers as regular Java
methods, limited only by difficulties in transferring objects between the
routine making the method call and the routine performing processing. In every
other way, XML-RPC can become a natural part of Java programming, providing
yet another way to get information into and out of Java environments. Using
XML-RPC can make connecting Java programs to programs written in other
environments much simpler, and may be appropriate for some simple Java-to-Java
cases, as well.
The XML-RPC Java Library
Although you could use the wide variety of XML resources
available in Java to create your own XML-RPC package, Hannes Wallnöfer has
already built a set of classes that provide XML-RPC capabilities for both
clients and servers, including a small XML-RPC server that lets you work
without the overhead of a full-scale Web server. Most examples in this chapter
rely on his package.
As of this writing, the XML-RPC library for Java in still in
beta, at Version 1.0 beta 4. Although it is unlikely that there will be major
changes to the API, you should check the index.html
file and documentation that come with the library if you encounter problems.
The XML-RPC Library for Java web site is http://classic.helma.at/hannes/xmlrpc/,
and additional resources (including a mailing list with archives) are also
available there. The examples in the current chapter use the helma.xmlrpc
library, which is available at that site.
In addition to core XML-RPC functionality, the helma.xmlrpc
package includes:
Classes for quick XML-RPC client and server creation
A micro-web server useful for setting up XML-RPC on
systems that don't already have a web server running or don't want to use
the existing server
A sample of Java servlet integration
A standalone set of classes used for building
lightweight XML-RPC applets
The components included in the XML-RPC library include client-
and server-specific classes used for creating requests and responses, as well
as a more generic core that controls how the library handles HTTP processing
and XML parsing.
Installing the helma.xmlrpc Library
The helma.xmlrpc library is
available for free download as a zip archive at http://classic.helma.at/hannes/xmlrpc/.
You'll need an unzipping utility to open the archive, which contains
documentation, examples, source code, and three Java archive (jar)
files. The files provide the executables you'll need to put XML-RPC into your
Java environment.
The most critical of the jar files
(all of which are stored in the lib directory) is xmlrpc.jar,
which contains the core logic for implementing XML-RPC. The library also
includes a jar file for the OpenXML parser, which is supported by default. You
don't have to use the OpenXML parser, but it's very helpful if you install
XML-RPC on a system without its own XML parser already installed. The last jar
file, xmlrpc-applet.jar, includes code that lets you
build applets that handle XML-RPC client transactions inside a browser and
that can be controlled by JavaScript.
If you already have an XML parser installed, you only need to
add xmlrpc.jar to your Java CLASSPATH environment
variable, though you'll need to specify which parser you want to use in your
XML-RPC client and server initialization code. If you don't have an XML parser
already installed, or you just want to rely on the choice of helma.xmlrpc's
creator, add the openxml-1.2.jar file to your
CLASSPATH in addition to xmlrpc.jar. Although you
may want to add xmlrpc-applet.jar to your CLASSPATH
for development convenience, it's designed to be used in a browser and doesn't
have to be installed on client computers.
You can distribute the helma.xmlrpc
package with your own code, though the author requests that the license be
distributed with the package and that any modifications be clearly documented.
General XML-RPC Library Configuration
The helma.xmlrpc class includes a
set of static methods used to configure your XML-RPC processing. Because they
are static methods, they affect all XML-RPC processing. You can't specify that
some groups of XML-RPC methods should use a different parser from others, nor
can you specify that debugging information should only be reported for certain
groups of methods. This isn't normally a liability, however--it's very
difficult to imagine a situation in which using different parsers for
different methods might be justified, for example.
The setDriver( ) method lets you
choose an XML parser for processing XML-RPC requests as they arrive. By
default, the XML-RPC library uses the OpenXML parser, but developers can
change that to any SAX-compliant parser. If your application uses a different
XML parser for some other aspect of processing, it probably makes sense to use
a single parser--it's easier to manage and cuts down on the size of the
distribution.
The setDriver( ) method takes a
single argument, the name of the parser to be used. It's probably best to
enclose this method in a try/catch exception
handler to handle the ClassNotFoundException the
method will throw if the Java environment can't find the class:
try {
// Use the Microstar AElfred parser for XML-RPC processing
XmlRpc.setDriver("com.microstar.xml.SAXDriver");
} catch (ClassNotFoundException e) {
// If no AElfred, provide an intelligible error message
System.out.println("Could not locate AElfred. Please check your
classpath for com.microstar.xml.SAXDriver.");
}
The XML-RPC package also provides shortcut names for some
commonly used parsers. For the most current list of shortcuts, see
"Choosing the XML parser" in the documentation that comes with the
distribution. In this case, we could have used aelfred
instead of com.microstar.xml.SAXDriver as the
argument to XmlRpc.setDriver( ).
By default, all XML-RPC messages are sent using the ISO-8859-1
character encoding, but the setEncoding( ) method
allows you to choose alternate encodings. Encodings must be specified from the
list of available Java encodings, available at http://java.sun.com/j2se/1.3/docs/guide/intl/encoding.doc.html.
To send out requests using the UTF-8 character set, you can
write:
XmlRpc.setEncoding("UTF8"); //Java identifies UTF-8 as UTF8 without dash
The setDebug( ) method lets you
watch your XML-RPC method registrations and request processing much more
closely, providing information to the system console about the structure of
the document received, the parameters extracted, and the result returned. To
start debugging output, you'll need to pass a value of true
to the setDebug( ) method:
XmlRpc.setDebug(true); //turn on verbose debugging output
When you no longer need debugging information (which can pile
up very quickly), pass a value of false to setDebug(
):
XmlRpc.setDebug(false); //turn off verbose debugging output
The helma.xmlrpc package provides
a lot of information about what happens inside a transaction. The following
output, for example, describes a server-side transaction involving the anyArea
handler that is created later in this chapter. Content marked in bold was
generated by the code used to build the XML-RPC server, but all the rest was
generated by the helma.xmlrpc package itself:
Attempting to start XML-RPC Server...
Started successfully.
Target object is class AreaHandler
Registered AreaHandler class to area.
Now accepting requests. (Halt program to stop.)
POST / HTTP/1.0
User-Agent: helma XML-RPC 1.0
Host: localhost:8899
Content-Type: text/xml
Content-Length: 296
startElement: methodCall
startElement: methodName
endElement: methodName
startElement: params
startElement: param
startElement: value
startElement: struct
startElement: member
startElement: name
endElement: name
startElement: value
startElement: double
endElement: double
endElement: value
endElement: member
startElement: member
startElement: name
endElement: name
startElement: value
endElement: value
endElement: member
endElement: struct
endElement: value
endElement: param
endElement: params
endElement: methodCall
Spent 211 millis parsing
method name is area.anyArea
inparams = [{radius=3.0, type=circle}]
Searching for method: anyArea
Parameter 0: class java.util.Hashtable = {radius=3.0, type=circle}
outparam = 28.274333882308138
Spent 231 millis in request
In this case, the client requested a calculation of the area
of a circle with a radius of 3, and received a response of 28.274.... after
231 milliseconds of processing on my 233MHz system. (This was an initial
request, adding about 200 milliseconds while the classes loaded. Caching
reduces the time per request significantly.)
The version field of the XmlRpc
class may be useful for developers writing code that depends on
version-specific features. At this point, the interface of the class appears
to be stable, and developers should have control over the code they deploy,
but this might be worth checking in situations when CLASSPATH
conflicts and other hazards of shared systems could come into play.
Data Types and Java XML-RPC
The helma.xmlrpc package supports
all XML-RPC data types (plus an extra, nil),
representing them as built-in Java types. Because Java supports, and sometimes
requires, object wrappers around its primitive types, the XML-RPC package can
be flexible with XML-RPC clients and sometime with XML-RPC servers.
The helma.xmlrpc package can
automatically map XML-RPC types to Java types, as shown in Table
3-1.
XML-RPC clients may pass arguments to the helma.xmlrpc
package using either of the choices above, when there is a choice. (Because
Java won't accept primitives inside Vectors and Hashtables,
the wrapper classes are sometimes necessary.) Similarly, XML-RPC handlers may
use either choice for their return values. Because i4
and int are considered identical by the XML-RPC
specification, the helma.xmlrpc package accepts
either of them in incoming requests. The helma.xmlrpc
package handles all encoding and decoding needed by the dateTime.iso8601
and base64 types.
On the other hand, XML-RPC handlers that use the automatic
registration feature of helma.xmlrpc must
use the simplest Java type available to describe the parameters they accept.
The examples in the next few sections detail how this works and show some of
the occasional extra work required to map complex types to simpler types.
If you're reusing existing code that takes the wrapper class,
rather than the primitive, as an argument, it is possible to create an XML-RPC
processor that supports the wrapper argument. However, you either have to
write extra code that manages the conversion of the primitives to the wrappers
before calling the existing code or build your own set of tools for handling
XML-RPC requests. Writing a middleware handler might seem ungainly, but it's
probably the easier route and isn't that difficult with the helma.xmlrpc.XmlRpcHandler
interface.
Building XML-RPC Clients
Building XML-RPC clients with the helma.xmlrpc
package is a relatively simple operation, involving the creation of an XmlRpcClient
object, assigning parameters, and making a call to the XmlRpcClient's
execute method. There are a number of exceptions that can be thrown, and there
may be delays in getting a response, but for the most part, calling XML-RPC
routines requires only a small amount of extra coding, much of which actually
deals with exception handling.
The constructor and the execute( )
methods are the core of the XmlRpcClient class.
The easiest way to handle them is in a try/catch
structure, though you can encapsulate them in methods that throw the
exceptions to a higher-level handler. The constructor may throw a MalformedURLException,
a subclass of IOException; the execute(
) method may throw either IOException
(when connections are refused, impossible, etc.) or XmlRpcException,
which is issued when the XML-RPC server reports an error. The constructor
accepts a String object representing a URL, a URL
object, or the combination of a String for the
hostname and an int for the port.
XmlRpcClient objects are
reusable, though they only connect to the server originally specified when
they were constructed. Applications that establish repeated connections to the
same server may want to reuse the objects, but many applications just create
the client, call a method, and disappear. In these cases, the constructor and execute(
) method may appear inside a single try/catch
statement. When the constructor and execute( )
method appear together, the MalformedURLException
may be treated as just another IOException,
making it one fewer exception to catch.
The following example creates a client that can connect to
port 8899 on the computer with the IP address 192.168.126.42. It sends a double
(retrieved from user input through the args[]
array) to a method identified as area.circleArea,
expecting to get back the area of a circle whose radius is the double sent as
the parameter. This code doesn't do anything with the result; it just sends
the request and handles any exceptions that might be thrown.
try {
// Create the client, identifying the server
XmlRpcClient client =
new XmlRpcClient("http://192.168.126.42:8899/");
// Create the request parameters using user input
Vector params = new Vector( );
params.addElement(new Double(args[0]));
// Issue a request
Object result =
client.execute("area.circleArea", params);
} catch (IOException e) {
System.out.println("IO Exception: " + e.getMessage( ));
} catch (XmlRpcException e) {
System.out.println("Exception within XML-RPC: " + e.getMessage( ));
}
//Continue processing using result object...
//result object will contain one of the XML-RPC data types
Depending on the response sent by the XML-RPC server, the
result object may be any of the types described previously in the section
"Data Types." In many cases, it is just a single typed value, but
the helma.xmlrpc classes return Vector
or Hashtable objects for XML-RPC responses that
return arrays or structs.
As of Version 1.0 Beta 4, the XmlRpcClient
class provides support for basic HTTP authentication. To use basic
authentication, developers need to add only one method call to their client
setup. The setBasicAuthentication( ) method takes
two strings as arguments. The first is a username, the second is the password.
To add a username and password to the previous request, you
simply need to add the code shown in bold in the following example:
try {
// Create the client, identifying the server
XmlRpcClient client =
new XmlRpcClient("http://192.168.126.42:8899/");
XmlRpcClient.setBasicAuthentication("myUsername", "myPassword");
// Create the request parameters using user input
Vector params = new Vector( );
params.addElement(new Double(args[0]));
// Issue a request
Object result =
client.execute("area.circleArea", params);
} catch (IOException e) {
System.out.println("IO Exception: " + e.getMessage( ));
} catch (XmlRpcException e) {
System.out.println("Exception within XML-RPC: " + e.getMessage( ));
}
//Continue processing using result object...
//result object will contain one of the XML-RPC data types
Although HTTP basic authentication isn't especially secure,
code containing usernames and passwords in plain text is also a serious
security risk. If you use this feature, you should consider combining it with
other security tools (like the "paranoid" mode described for the
XML-RPC server, later in this chapter) and protecting your code from
distribution beyond the group of people authorized to work with those
passwords.
The XmlRpcClient class uses
Java's built-in support (java.net.URLConnection)
for HTTP requests, giving it the ability to handle proxies automatically and
to deal with a variety of server situations. Developers who need a
lighter-weight XML-RPC client can use the XmlRpcClientLite
class, which implements more minimal HTTP support. The two classes are
identical, except in XmlRpcClient's support for
more advanced HTTP functionality, including basic HTTP authentication.
Building XML-RPC Servers
Building XML-RPC servers is a bit more complex than building
clients. In addition to building the core logic of your application, you need
to publish your services to the XML-RPC library so it can manage requests.
Your application may do this in the context of a servlet running within a
larger Web server, or it may use the library's built-in WebServer
class to create a minimal server handling only XML-RPC requests.
The simplest way to build an XML-RPC server relies on the WebServer
class and uses the XmlRpcServer class's built-in
ability to recognize Java classes and methods during the registration process.
If you just need to publish methods whose parameters conform to the XML-RPC
data types described earlier in this chapter, this is usually straightforward.
Using the WebServer Class
The helma.xmlrpc package includes
a simple WebServer class that makes it easy to
set up XML-RPC on systems that don't have a web server installed previously or
to add an extra server listening on a nonstandard port. The WebServer
class provides only the core of HTTP functionality used by XML-RPC, not the
full set of functionality used to distribute web pages. This limitation should
reduce the fears of network administrators who don't want to install internal
web sites, while giving Java developers a small-footprint approach to adding
XML-RPC to computers that aren't intended to be web servers.
Creating a new web server that uses the built-in WebServer
class requires calling its constructor with a port number. For example, to
create a web server that listens for XML-RPC requests on port 9876, you can
call its constructor as follows:
WebServer server = new WebServer(9876);
If the WebServer can't start on
the specified port, it throws a java.io.IOException.
Depending on how you structure the program, you may want to enclose the
constructor (and subsequent registrations) in a try/catch
statement, or you may want the method managing the XML-RPC interface to pass
the exception on to another handler.
Once you've set up the WebServer
object, you can add and remove XML-RPC handlers to it using the functionality
described in the section "Creating Handlers," later in this chapter.
WebServer itself is a fairly small wrapper of
functionality around the XmlRpcServer class, so
you can use it the same way. The WebServer class
does support HTTP Basic Authentication, but you'll need to create and register
classes that implement the AuthenticatedXmlRpcHandler
interface.
In addition to registering and processing XML-RPC handlers,
the WebServer class provides a basic security
model in the form of a "paranoid" mode. By default, WebServer
accepts requests from all IP addresses. In paranoid mode, the server only
accepts requests from specified IP addresses. It provides two methods,
acceptClient(
) and denyClient( ), for building lists of
approved and rejected clients. By default, no client connections are accepted
when setParanoid( ) has been called with an
argument of true. You'll need to use the acceptClient(
) method to add approved IP addresses, and can use denyClient(
) to trim that list. Once an IP address has been put on the denied
list, acceptClient( ) can't bring it
back--denials are more permanent than acceptances. (You'll have to restart the
XML-RPC sever to reopen it.) Both methods accept the asterisk (*)
for wildcarding, a feature convenient for dealing with groups of addresses
without resorting to loops.
If your Java XML-RPC server is only communicating with clients
on the same computer--to bridge Java and another environment, most likely--you
may want to shut down all IP addresses except localhost, 127.0.0.1.
The server won't even consider requests made from other systems if you take
the following approach:
WebServer server = new WebServer(9876);
server.setParanoid(true);
server.addClient("127.0.0.1");
Using this setup, only requests directed to http://127.0.0.1:9876
will be considered, and because of the unique nature of the localhost
address, they'll have to originate from the same machine as the XML-RPC
server.
Another common setup permits an entire IP subnet to access the
XML-RPC server, but exclude a few systems, perhaps gateways to other networks
that might be hijacked. If an XML-RPC server was in the 192.168.137.x
private network and the gateway router was 192.168.137.55, that server could
be made available to all hosts on the local network except
the gateway using the following code snippet:
WebServer server = new WebServer(9876);
server.setParanoid(true);
server.addClient("192.168.137.*");
server.denyClient("192.168.137.55");
Although filtering messages based on IP addresses isn't a
complete security model by any means, it may be enough to make XML-RPC usable
in a wide variety of contexts. If you need more security than this, you should
consider using the XmlRpcServer class in a richer
web server context, using the HTTPS and certificates facilities available on
larger-scale web servers.
Using XmlRpcServer Without WebServer
If you integrate XML-RPC with existing web servers or you need
more security than the IP filtering model of the WebServer
class, you should use the XmlRpcServer class.
Although the WebServer class accepts HTTP POST
requests and feeds them directly to handlers, XmlRpcServer
acts as an intermediary, accepting request information from servlets or other
sources. Although XmlRpcServer doesn't handle the
connection management end of HTTP, it does process all textual information
sent over HTTP.
XmlRpcServer is simpler than WebServer
because it leaves network details and security to its surrounding environment.
The two classes use identical approaches for registering and unregistering
classes containing XML-RPC methods, but XmlRpcServer
requires more assistance than WebServer. The WebServer
class listens for requests and processes them automatically; XmlRpcServer
needs another class to listen for requests.
The XmlRpcServer class uses an
XML-in, XML-out model for handling requests, leaving the rest to the
supporting environment. Servlets can process the header information of XML-RPC
requests, but they pass those requests' XML content to the XmlRpcServer's execute( ) method as
InputStreams.
(Optionally, they can include a username and password derived from basic
authentication.) The execute( ) method returns a String
containing the response, which the supporting environment then wraps with
appropriate HTTP header information.
For example, the WebServer class
wraps its calls to the XmlRpcServer's execute
method as follows:
WebServer takes a very basic
approach, reading and writing the HTTP requests as streams of textual bytes.
Within a servlet, developers can take advantage of a slightly higher level of
abstraction, as shown in the Servlet that comes
in the XML-RPC package:
In either case, XmlRpcServer
accepts an XML-RPC request as an XML document and returns an XML-RPC response
document. The supporting environment has to handle the rest of the
transaction.
The XmlRpcServer class also
provides support for the AuthenticatedXmlRpcHandler
interface. You can also pass information using a three-argument version of
execute, which allows the XmlRpcServer class to
send authentication information to the handler. In addition to the InputStream,
you'll need to add the username and password strings. Because the
authentication header, which includes both the username and password, is sent
using base64 encoding and the servlet package
doesn't provide a simple means of reaching the password, extracting the
username and password requires a few extra steps. These steps are highlighted
in bold in the following example:
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
//get authorization header from request
String auth=req.getHeader("Authorization");
if (!auth.toUpperCase( ).startsWith("BASIC")) {
throw ServletException("wrong kind of authorization!");
}
//get encoded username and password
String userPassEncoded=auth.substring(6);
//create a base64 decoder using Sun utility class
sun.misc.BASE64Decoder dec=new sun.misc.BASE64Decoder( );
String userPassDecoded=new String(dec.decodeBuffer(userPassEncoded));
//split decoded username and password
StringTokenizer userAndPass=new StringTokenizer(userPassDecoded,":");
String username=userAndPass.nextToken( );
String password=userAndPass.nextToken( );
//send input stream, username, and password to xmlrpc.execute
String result = xmlrpc.execute (req.getInputStream ( ), username, password);
res.setContentType("text/xml");
res.setContentLength (result.length ( ));
PrintWriter writer = res.getWriter( );
writer.write (result);
writer.flush ( );
}
The WebServer class already
includes this decoding, providing a more transparent means of handling
authenticated transactions.
Creating XML-RPC Handlers
Both WebServer and XmlRpcServer
employ the same set of methods for registering and unregistering classes whose
methods can be used through XML-RPC. Java classes whose methods accept the
standard data types (described earlier in this chapter) can use the WebServer
and XmlRpcServer classes' built-in logic for
mapping XML-RPC calls to Java methods; other classes can be extended with a
single execute( ) method for mapping XML-RPC
requests to Java methods. In both cases, the addHandler(
) and removeHandler( ) methods are used
the same way to add and remove classes that can handle XML-RPC methods.
Creating Handlers Using Automatic Registration
If the methods you want to use for processing XML-RPC requests
are written so that they only accept and return the data types described, you
can use the automatic registration process. The following code sample
demonstrates these methods using a simple testing procedure that accepts two
strings and returns a new string that concatenates the strings in the reverse
order of how they were received:
public class testHandler {
public String nameTester(String first, String last) {
return "Reversed: " + last + ", " + first;
}
}
Making the nameTester( ) method
available as an XML-RPC procedure requires two steps. First, set up a web
server to handle the HTTP transactions. Second, register this method with that
server to make it available via XML-RPC. Once the server object has been
created, the testHandler can be registered with
the server using the addHandler( ) method. The
server then accepts and processes requests for the method:
WebServer server = new WebServer(9876);
server.addHandler("test", new testHandler( ));
The server examines the testHandler
class and extracts its method signatures for mapping to XML-RPC requests.
XML-RPC clients may now send requests for the test.nameTester
method, sending two strings as parameters. They'll receive a single string
back, which begins with the text "Reversed:" and then concatenates
the strings in reverse order.
If that method needs to be disengaged at some later point, the
removeHandler( ) method can be called:
server.removeHandler("test");
Now the test.nameTester( ) method
is no longer available. Although turning XML-RPC methods on and off
dynamically might create serious chaos for a lot of stable service-oriented
applications, it can be very useful for managing XML-RPC methods in
conjunction with some kind of control panel. Among other things, it lets you
update the classes used to handle an XML-RPC request without having to stop
and restart the XML-RPC server or servlet.
Creating Handlers Using Explicit Registration
If you prefer to manage the mappings between classes and
methods more directly, you can register classes that implement the helma.xmlrpc.XmlRpcHandler
interface. When the server encounters these classes, it defers to their
mapping from XML-RPC method names and parameters to Java methods. Instead of
trying to pass arguments directly to Java methods inside a class, the XML-RPC
server passes the method name and parameters (as a Vector)
to the execute method, the single method required by the XmlRpcHandler
interface.
The default behavior of helma.xmlrpc
handles most situations, but mapping methods directly is appropriate in a
number of cases. You may have a set of classes you're retrofitting to XML-RPC
that expect their arguments to arrive as objects rather than primitives. You
would prefer to handle that packaging in a single method rather than by
creating method-by-method front ends. You may want to hide the internal
structures of your processing, a reasonable strategy when exchanging
information with potential competitors. Finally, you may be creating methods
that are minor variations on a theme, where the method name differentiates
only a small change, such as the expected return value type.
Implementing the XmlRpcHandler
interface requires only one method, execute( ).
If a class implements the XmlRpcHandler
interface, all XML-RPC calls are directed to the execute method,
short-circuiting the automatic method mapping of the XmlRpcServer
class. The execute( ) method takes a String
and a Vector as arguments and returns an Object.
The String is the method named by the XML-RPC
request; the Vector contains all parameters that
were sent with that request.
The execute( ) method can be used
in several different ways. The logic inside the execute(
) method may just ship the Vector containing the parameters to other
methods, leaving them to unpackage and process the parameters. Some execute(
) methods may emulate the helma.xmlrpc.XmlRpc
package's own processing, mapping the method name and parameter set to
appropriate handlers. More sophisticated execute( )
methods might read the parameters inside the Vector
and create objects based on those parameters, which are then shipped to the
appropriate target method. In any of these cases, the execute(
) method acts as a gateway.
Example
3-1 uses the execute( ) method to pass
information to the nameTester( ) method used in
the automatic registration example. Note that the nameTester(
) method is now private, accessible only through the execute(
) method. This isn't required, but it illustrates that the execute(
) method has taken over from the XML-RPC package's native registration.
import java.util.Vector;
import helma.xmlrpc.XmlRpcHandler;
public class testHandler implements XmlRpcHandler {
public Object execute(String methodName, Vector parameters) throws java.lang.Exception {
if (methodName=="nameTester") {
String first=(String) parameters.elementAt(0);
String last=(String) parameters.elementAt(1);
return nameTester(first, last);
} else {
throw new Exception("No such method!");
}
}
private String nameTester(String first, String last) {
return "Reversed: " + last + ", " + first;
}
}
Classes that implement the AuthenticatedXmlRpcHandler
interface instead of XmlRpcHandler can process
the username and password pairs from basic authentication as well as the
method names and parameters, giving them additional gateway functionality.
You'll want to implement a more sophisticated password checking mechanism--and
perhaps move it to a separate class to be shared among various handlers. The testHandler
class in Example
3-2, however, demonstrates how a handler supporting authentication might
work.
Example 3-2: Creating
an XML-RPC handler with authentication
import java.util.Vector;
import helma.xmlrpc.*;
public class testHandler implements AuthenticatedXmlRpcHandler {
//authenticated execute
public Object execute(String methodName, Vector parameters,
String username, String password) throws Exception {
if (checkPassword(username, password)) {
return execute(methodName, parameters);
} else {
throw new Exception("Unauthorized user");
}
}
//unauthenticated execute - called by authenticated
protected Object execute(String methodName, Vector parameters)
throws Exception {
if (methodName=="nameTester") {
String first=(String) parameters.elementAt(0);
String last=(String) parameters.elementAt(1);
return nameTester(first, last);
} else {
throw new Exception("No such method!");
}
}
e boolean checkPassword(String username, String password) {
//password checking logic should be more sophisticated!
if (username.equals(password)) {
return true;
} else {
return false;
}
}
private String nameTester(String first, String last) {
return "Reversed: " + last + ", " + first;
}
}
The XmlRpcServer class checks the
type of class it works with and passes the correct set of parameters to the execute(
) method. Classes that implement XmlRpcHandler
receive two parameters--method name and the Vector
containing the parameters--while classes that implement AuthenticatedXmlRpcHandler
receive four parameters: method name, the Vector
containing parameters, username, and password.
Three Practical Examples
Although XML-RPC itself is very simple, it can be applied to a
number of programming styles. The approach that most directly fits
"remote procedure calls" is one that calls procedures across a
network, as shown in the library function example in the next section.
Procedure calls can also be used easily for client-to-server reporting, shown
in an example that logs error reports from clients. Finally, just as Java
itself uses get and set methods to manipulate the properties of objects,
XML-RPC can extend that functionality to expose those methods to other systems
on the network.
Library Functions
This example creates a simple Java library that performs
mathematical calculations without any side effects on the server. The
calculations performed--determining the areas of circles and squares--aren't
very complex, but this same approach could be used for much more intensive
algorithms. Although the average applet is certainly capable of calculating
the area of a square on its own, many fields of computing rely on mathematical
tools that demand extraordinary amounts of processing power.
As shown in Example
3-3, the AreaHandler class has two methods
and no properties. The first method takes two arguments, length
and width, and returns the area of a rectangle;
the second method takes a single argument, radius,
and returns the area of a circle.
Example 3-3: Library
function implemented as XML-RPC handler
public class AreaHandler {
public Double rectArea(double length, double width) {
return new Double(length*width);
}
public Double circleArea(double radius) {
double value=(radius*radius*Math.PI);
return new Double (value);
}
}
This code is run as an XML-RPC client on the server,
with a simple client to pass it values. The code for the server builds
on the generic code created in the previous section, using the helma.xmlrpc
library's built-in WebServer class, as
shown in Example
3-4.
import java.io.IOException;
import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;
public class AreaServer {
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Usage: java AreaServer [port]");
System.exit(-1);
}
try {
// Start the server, using built-in version
System.out.println("Attempting to start XML-RPC Server...");
WebServer server = new WebServer(Integer.parseInt(args[0]));
System.out.println("Started successfully.");
// Register our handler class as area
server.addHandler("area", new AreaHandler( ));
System.out.println(
"Registered AreaHandler class to area.");
System.out.println("Now accepting requests. (Halt program to stop.)");
} catch (IOException e) {
System.out.println("Could not start server: " +
e.getMessage( ));
}
}
}
To start this server and the servers in the rest of
the examples, call the Java runtime with the name of the class and
an argument of 8899, in this case:
D:\xmlrpc\example1a>java AreaServer 8899
A simple client might call one of these methods and
return its result. Most of the code in Example
3-5 provides an interface between the command line and the
XML-RPC request itself, but it works well as test code.
Example 3-5: An XML-RPC client that calls the area
function
import java.io.IOException;
import java.util.Vector;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;
public class AreaClient {
public static void main(String args[]) {
if (args.length < 1) {
System.out.println(
"Usage: java AreaClient [radius]");
System.exit(-1);
}
try {
// Create the client, identifying the server
XmlRpcClient client =
new XmlRpcClient("http://localhost:8899/");
// Create the request parameters using user input
Vector params = new Vector( );
params.addElement(new Double(args[0]));
// Issue a request
Object result =
client.execute("area.circleArea", params);
// Report the results
System.out.println("The area of the circle would be: " + result.toString( ));
} catch (IOException e) {
System.out.println("IO Exception: " + e.getMessage( ));
} catch (XmlRpcException e) {
System.out.println("Exception within XML-RPC: " + e.getMessage( ));
}
}
}
When this XML-RPC client is run from the command
line, it's possible to send a radius and receive back an area:
D:\xmlrpc\example1a>java AreaClient 3
The area of the circle would be: 28.274333882308138
Although this library works well in its current
form, it's possible to add more flexibility to the way in which
the arguments are sent by using a struct
and named parameters instead of the direct approach. To make
this work, the AreaHandler class
needs a new method for processing the Hashtable
used when structs are sent, and then needs to route requests to
the appropriate method based on a type
value in the struct. The new method is shown in Example
3-6.
Example 3-6: A method for routing XML-RPC requests
Most of the code handles conversions from
the generic Object types stored
in the Hashtable to the
primitive types needed by the actual circleArea(
) and rectArea( )
methods. Although this code still returns the same results
as the simpler methods it calls and adds an extra layer of
processing overhead, you may find this approach useful if
you need to create libraries that produce different results
based on different types of named inputs.
Now the client code looks a little different
because it has to assemble a Hashtable,
not just simple parameters. Differences are highlighted in
bold in Example
3-7.
import java.io.IOException;
import java.util.Vector;
import java.util.Hashtable;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;
public class AreaClient {
public static void main(String args[]) {
if (args.length < 1) {
System.out.println(
"Usage: java AreaClient [radius]");
System.exit(-1);
}
try {
// Create the client, identifying the server
XmlRpcClient client =
new XmlRpcClient("http://localhost:8899/");
// Create a double from the user argument
Double radius=new Double(args[0]);
// Create a hashtable and add a circle request
Hashtable requestHash = new Hashtable( );
requestHash.put("type", "circle");
requestHash.put("radius", radius);
// Create the request parameters using user input
Vector params = new Vector( );
params.addElement(requestHash);
// Issue a request
Object result =
client.execute("area.anyArea", params);
// Report the results
System.out.println("The area of the circle would be: " + result.toString( ));
} catch (IOException e) {
System.out.println("IO Exception: " + e.getMessage( ));
} catch (XmlRpcException e) {
System.out.println("Exception within XML-RPC: " + e.getMessage( ));
}
}
}
The call and the results will look the
same--users do not need to be aware of the extra
flexibility they have available:
D:\xmlrpc\example1b>java AreaClient 5.6
The area of the circle would be: 98.5203456165759
Also, unless the circleArea(
) and rectArea( )
are changed to become private methods, direct XML-RPC
requests for those methods will continue to work.
Although this struct
approach is somewhat like implementing the XmlRpcHandler
interface and directing XML-RPC requests yourself, it
isn't nearly as demanding because you control how much
you use this approach. You can redirect some methods,
but let XmlRpcServer figure
out simpler methods. You could also implement something
very similar using a Java Vector/XML-RPC
array to send information between client and server,
relying on order rather than labeling.
Reporting
The procedures called by XML-RPC
requests don't have to be library routines used to
retrieve information. Clients may just send servers
information that they record--perhaps in a database,
just in a file, or even on the system console. The
example in this section builds a simple logging
application that collects client exceptions and records
them to the system console. This kind of tiny
application can be very useful for debugging distributed
applications, giving you an easy way to centralize
information from multiple systems running concurrently.
This application first requires the
design of a static method that captures exceptions and
reports them via XML-RPC to a central server. As shown
in Example
3-8, the first parameter is the IP address of the
client sending the request (that information isn't
passed to the XML-RPC handler), and the second is a String
containing the message from the exception.
Note that methods that process Java
exceptions have to be static methods. In addition,
because the report( )
method itself has some possible exceptions due to the
XML-RPC call, the entire body of the method is contained
inside a try/catch
statement.
import java.io.IOException;
import java.net.*;
import java.util.Vector;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;
public class XLogClient {
public static void main(String args[]) {
try {
throw new Exception("help");
} catch (Exception e) {
report (e);
}
}
public static void report(Exception eReport) {
try {
// Create the client, identifying the server
XmlRpcClient client =
new XmlRpcClient("http://192.168.124.14:8899/");
//get local hostname and IP address
InetAddress address=InetAddress.getLocalHost( );
String ipAddress=address.toString( );
// Create the request parameters using user input
Vector params = new Vector( );
params.addElement(ipAddress);
params.addElement(eReport.getMessage( ));
// Issue a request
Object result =
client.execute("XLog.XLogReport", params);
// Report the results - this is just for the example
// In production, the 'ack' will be thrown away.
// Alternatively, the log system could be more interactive
// and the result might have meaning.
System.out.println("Reponse was: " + result.toString( ));
//If we can't report to server, report locally
} catch (IOException e) {
System.out.println("IO Exception: " + e.getMessage( ));
} catch (XmlRpcException e) {
System.out.println("Exception within XML-RPC: " + e.getMessage( ));
}
}
}
The main( )
method in Example
3-8 is very short--it throws an exception with a
message of "help" and then catches it,
sending it to our report( )
method. The report( )
method sends the address of this system along with
the exception message to the XML-RPC server. If
something goes wrong with that transmission, the report(
) method prints its error messages to
standard output.
The server that hosts this reporting
system only needs to set up a web server and
register one class for monitoring incoming exception
reports. It builds on the same basic framework used
for previous examples, as shown in Example
3-9.
import java.io.IOException;
import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;
public class XLogServer {
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Usage: java AreaServer [port]");
System.exit(-1);
}
try {
// Start the server, using built-in version
System.out.println("Attempting to start XML-RPC Server...");
WebServer server = new WebServer(Integer.parseInt(args[0]));
System.out.println("Started successfully.");
// Register our handler class as area
server.addHandler("XLog", new XLogHandler( ));
System.out.println(
"Registered XLogHandler class to XLog.");
System.out.println("Now accepting requests. (Halt program to stop.)");
} catch (IOException e) {
System.out.println("Could not start server: " +
e.getMessage( ));
}
}
}
For this demonstration, and
often in practice, only a basic exception log
handler is necessary. The handler defined in Example
3-10 just takes the addresses and messages
and reports them to standard output.
Example
3-10: A simple
XML-RPC handler for reporting messages
public class XLogHandler {
public String XLogReport(String address, String message) {
System.out.println("From: " + address);
System.out.println("Message: " + message);
return "ack";
}
}
The results of the test are
simple, but could be effective if they
represented real crises in need of
attention:
D:\xmlrpc\example2>java XLogServer 8899
Attempting to start XML-RPC Server...
Started successfully.
Registered XLogHandler class to XLog.
Now accepting requests. (Halt program to stop.)
From: javalab1/192.168.124.12
Message: help
From: javalab5/192.168.124.17
Message: help
From: javalab6/192.168.124.19
Message: help
From: javalab27/192.168.124.141
Message: help
More sophisticated handling
might filter through the messages to flag
especially important alerts or save the
messages to a file, but the basic report(
) method and the server already
provide a strong foundation for future
development. Building a more sophisticated
log tracking facility would involve sending
the information to something more permanent
(and searchable) than screen output, like a
database or even a file.
A get and set Approach
Although the previous
example featured a relatively active client
and a passive server, XML-RPC can be used to
create more controlling clients, as well.
XML-RPC's procedural approach fits fairly
well with the common JavaBeans approach of
getProperty(
) and setProperty(
) methods, though simple JavaBeans
doesn't work in XML-RPC. Why? The set
methods return void,
and all XML-RPC methods have to return a
value of some kind. On a relatively
fundamental level, XML-RPC and JavaBeans are
mismatched.
It isn't that difficult to
write an execute( )
method that does the mapping, or even to
modify helma.xmlrpc
to return an empty value on methods that
return void.
However, the current section illustrates a
simpler approach, building a controlling
client and a server that maintains state
between requests. Although this pattern
isn't very sophisticated, it can be combined
with other patterns to build more
sophisticated applications. XML-RPC could be
used throughout those applications, or it
could just be one part of many.
To satisfy XML-RPC's need
for return values, this example returns the
current value of the property from the
server to the client. This is duplicate
information to some extent, but at least
that information might be useful to verify
that the change was made. Empty strings
would be slightly more efficient, but would
still incur overhead to no benefit.
The current example uses
XML-RPC client requests to get and set a
value on a Java object. Although the example
used here is simple, it isn't difficult to
extend it into more complex terrain using
the same basic framework.
The key to this example lies
in the handler class, shown in Example
3-11, which supports the get
and set of its
own value property.
Example
3-11: An
XML-RPC handler that manipulates a
variable
public class GetSetHandler {
protected int value;
public GetSetHandler(int initialValue) {
value=initialValue;
}
public int getValue(String requester) {
return value;
}
public int setValue(String requester, int newValue) {
value=newValue;
return value;
}
}
The value
property here is an integer, and the
framework looks much like a JavaBeans
component, but with the added return
values and arguments noted earlier.
As shown in Example
3-12, the server code is much like
that used by earlier examples--this is
just another handler, and state
management is up to the handler, not the
server code wrapping it. You don't need
to create a static variable to host the
handler object because the GetSetHandler
object is bound to the XML-RPC handling
code.
import java.io.IOException;
import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;
public class GetSetServer {
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Usage: java GetSetServer [port]");
System.exit(-1);
}
try {
// Start the server, using built-in version
System.out.println("Attempting to start XML-RPC Server...");
WebServer server = new WebServer(Integer.parseInt(args[0]));
System.out.println("Started successfully.");
// Register our handler class as area
server.addHandler("getSet", new GetSetHandler(20));
System.out.println(
"Registered GetSetHandler class to getSet.");
System.out.println("Now accepting requests. (Halt program to stop.)");
} catch (IOException e) {
System.out.println("Could not start server: " +
e.getMessage( ));
}
}
}
Example
3-13 shows the client used to
test this code. It is more complex,
largely because it needs to manage
both get and set possibilities. In
production code, most of this
complexity can be ignored because
programs calling functions are
generally more predictable than
human input. On the other hand, this
interface can be very useful during
the debugging cycle.
Example
3-13: Client
for manipulating values on the
server
import java.io.IOException;
import java.net.*;
import java.util.Vector;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;
public class GetSetClient {
public static void main(String args[]) {
if (args.length < 1) {
System.out.println(
"Usage: java GetSetClient [get | set] [value]");
System.exit(-1);
}
String getOrSet=new String(args[0]);
if (!((getOrSet.equals("get")) || (getOrSet.equals("set")))) {
System.out.println(
"First argument must be get or set");
System.exit(-1);
}
try {
// Create the client, identifying the server
XmlRpcClient client =
new XmlRpcClient("http://localhost:8899/");
//get local host IP address
InetAddress address=InetAddress.getLocalHost( );
String ipAddress=address.toString( );
// Create the request parameters using user input
Vector params = new Vector( );
params.addElement(ipAddress);
if (getOrSet.equals("set")) {
Integer newValue=new Integer(args[1]);
params.addElement(newValue);
}
// Issue a request
Object result=null;
if (getOrSet.equals("set")) {
result = client.execute("getSet.setValue", params);
} else {
result = client.execute("getSet.getValue", params);
}
// Report the results
System.out.println("The response was: " + result.toString( ));
} catch (IOException e) {
System.out.println("IO Exception: " + e.getMessage( ));
} catch (XmlRpcException e) {
System.out.println("Exception within XML-RPC: " + e.getMessage( ));
}
}
}
After you start
the server, you can test the
implementation from the command
line:
D:\xmlrpc\example3>java GetSetClient get
The response was: 20
D:\xmlrpc\example3>java GetSetClient set 21
The response was: 21
D:\xmlrpc\example3>java GetSetClient get
The response was: 21
D:\xmlrpc\example3>java GetSetClient set 200
The response was: 200
D:\xmlrpc\example3>java GetSetClient get
The response was: 200
D:\xmlrpc\example3>java GetSetClient set 750
The response was: 750
D:\xmlrpc\example3>java GetSetClient get
The response was: 750
The GetSetHandler
object retains the last value it
was set for, starting with the
initial value of 20
it received when first
initialized. Although the
command line here is testing the
setting on a single system,
multiple systems have access to
both setting and retrieving that
value, which could make such a
drop-box useful as a central
point for information
distribution, provided that
security isn't that important an
issue.
This kind of
get-set mechanism can be used
for all kinds of programming
tasks, allowing clients to
control server properties and
behavior. It's a common feature
in interfaces used to administer
a wide variety of systems and
can be used both to tweak values
occasionally and to send long
lists of orders that must be
carried out over time. This can
be an easy way to control Java
systems from programs running in
other environments because
XML-RPC provides the glue and
the get-set mechanism is a
common pattern in Java
programming.
Moving Toward
Cross-Platform Peer-to-Peer
The three
preceding examples have moved
from clients using servers as
remote processors to clients
reporting information to servers
to clients actually controlling
servers. XML-RPC's basic
client-server foundation makes
exchanging information in a wide
variety of different ways
possible, and its variable
structures are flexible enough
that single methods can have
different behaviors, depending
on the kinds of parameters they
are sent.
The flexibility
you need to move from
client-server XML-RPC to a more
peer-to-peer model is already
there, thanks to arrays and
structs. Clients can send any
quantity of information that the
application can then pick
through, and servers can return
any quantity of information for
the client. Some clients may be
capable of sending more
information than others; servers
can send extra information to
clients, understanding that only
some of their targets will use
the entire set of information.
There's no need for clients to
play a purely client role or for
servers to play purely server
roles. Every program can be both
a client and a server, if and
when that seems appropriate.
As long as that
flexibility is sufficient for
your needs, the helma.xmlrpc
package can provide you with a
foundation for communication
with non-Java systems with which
you can define the roles of
client and server as you find
appropriate.
1.
The Common Object Request Broker
Architecture (CORBA), is
designed to facilitate
large-scale exchanges of object
information and processing
between systems that may or may
not use similar environments or
languages.