|
|
|
|
Programming Web Services with XML-RPCBy Simon St.Laurent, Joe Johnston & Edd DumbillJune 2001 0-596-00119-3, Order Number: 1193 230 pages, $34.95 |
Chapter 3
Client-Server Communication: XML-RPC in JavaJava 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
Mathclass) 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 thehelma.xmlrpclibrary, which is available at that site.In addition to core XML-RPC functionality, the
helma.xmlrpcpackage 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.xmlrpclibrary 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.xmlrpcpackage 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.xmlrpcclass 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 atry/catchexception handler to handle theClassNotFoundExceptionthe method will throw if the Java environment can't find the class:try {// Use the Microstar AElfred parser for XML-RPC processingXmlRpc.setDriver("com.microstar.xml.SAXDriver");} catch (ClassNotFoundException e) {// If no AElfred, provide an intelligible error messageSystem.out.println("Could not locate AElfred. Please check yourclasspath 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
aelfredinstead ofcom.microstar.xml.SAXDriveras the argument toXmlRpc.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 dashThe
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 oftrueto thesetDebug( )method:XmlRpc.setDebug(true); //turn on verbose debugging outputWhen you no longer need debugging information (which can pile up very quickly), pass a value of
falsetosetDebug( ):XmlRpc.setDebug(false); //turn off verbose debugging outputThe
helma.xmlrpcpackage provides a lot of information about what happens inside a transaction. The following output, for example, describes a server-side transaction involving theanyAreahandler 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 thehelma.xmlrpcpackage itself:Attempting to start XML-RPC Server...Started successfully.Target object is class AreaHandlerRegistered AreaHandler class to area.Now accepting requests. (Halt program to stop.)POST / HTTP/1.0User-Agent: helma XML-RPC 1.0Host: localhost:8899Content-Type: text/xmlContent-Length: 296startElement: methodCallstartElement: methodNameendElement: methodNamestartElement: paramsstartElement: paramstartElement: valuestartElement: structstartElement: memberstartElement: nameendElement: namestartElement: valuestartElement: doubleendElement: doubleendElement: valueendElement: memberstartElement: memberstartElement: nameendElement: namestartElement: valueendElement: valueendElement: memberendElement: structendElement: valueendElement: paramendElement: paramsendElement: methodCallSpent 211 millis parsingmethod name is area.anyAreainparams = [{radius=3.0, type=circle}]Searching for method: anyAreaParameter 0: class java.util.Hashtable = {radius=3.0, type=circle}outparam = 28.274333882308138Spent 231 millis in requestIn 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
versionfield of theXmlRpcclass 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 whenCLASSPATHconflicts and other hazards of shared systems could come into play.Data Types and Java XML-RPC
The
helma.xmlrpcpackage 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.xmlrpcpackage can automatically map XML-RPC types to Java types, as shown in Table 3-1.
Table 3-1: XML-RPC versus Java data types XML-RPC type
Simplest Java type
More complex Java type
i4
int
java.lang.Integer
int
int
java.lang.Integer
boolean
boolean
java.lang.Boolean
string
java.lang.String
java.lang.String
double
double
java.lang.Double
dateTime.iso8601
java.util.Date
java.util.Date
struct
java.util.Hashtable
java.util.Hashtable
array
java.util.Vector
java.util.Vector
base64
byte[]
byte[]
nil (extension)
null
null
XML-RPC clients may pass arguments to the
helma.xmlrpcpackage using either of the choices above, when there is a choice. (Because Java won't accept primitives insideVectors andHashtables, the wrapper classes are sometimes necessary.) Similarly, XML-RPC handlers may use either choice for their return values. Becausei4andintare considered identical by the XML-RPC specification, thehelma.xmlrpcpackage accepts either of them in incoming requests. Thehelma.xmlrpcpackage handles all encoding and decoding needed by thedateTime.iso8601andbase64types.On the other hand, XML-RPC handlers that use the automatic registration feature of
helma.xmlrpcmust 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.XmlRpcHandlerinterface.Building XML-RPC Clients
Building
XML-RPCclients with thehelma.xmlrpcpackage is a relatively simple operation, involving the creation of anXmlRpcClientobject, assigning parameters, and making a call to theXmlRpcClient'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 theXmlRpcClientclass. The easiest way to handle them is in atry/catchstructure, though you can encapsulate them in methods that throw the exceptions to a higher-level handler. The constructor may throw aMalformedURLException, a subclass ofIOException; theexecute( )method may throw eitherIOException(when connections are refused, impossible, etc.) orXmlRpcException, which is issued when the XML-RPC server reports an error. The constructor accepts aStringobject representing a URL, a URL object, or the combination of aStringfor the hostname and anintfor the port.
XmlRpcClientobjects 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 andexecute( )method may appear inside a singletry/catchstatement. When the constructor andexecute( )method appear together, theMalformedURLExceptionmay be treated as just anotherIOException, 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 theargs[]array) to a method identified asarea.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 serverXmlRpcClient client =new XmlRpcClient("http://192.168.126.42:8899/");// Create the request parameters using user inputVector params = new Vector( );params.addElement(new Double(args[0]));// Issue a requestObject 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 typesDepending 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.xmlrpcclasses returnVectororHashtableobjects for XML-RPC responses that return arrays or structs.As of Version 1.0 Beta 4, the
XmlRpcClientclass provides support for basic HTTP authentication. To use basic authentication, developers need to add only one method call to their client setup. ThesetBasicAuthentication( )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 serverXmlRpcClient client =new XmlRpcClient("http://192.168.126.42:8899/");XmlRpcClient.setBasicAuthentication("myUsername", "myPassword");// Create the request parameters using user inputVector params = new Vector( );params.addElement(new Double(args[0]));// Issue a requestObject 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 typesAlthough 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
XmlRpcClientclass 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 theXmlRpcClientLiteclass, which implements more minimal HTTP support. The two classes are identical, except inXmlRpcClient's support for more advanced HTTP functionality, including basicHTTPauthentication.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-inWebServerclass to create a minimal server handling only XML-RPC requests.The simplest way to build an XML-RPC server relies on the
WebServerclass and uses theXmlRpcServerclass'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.xmlrpcpackage includes a simpleWebServerclass 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. TheWebServerclass 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
WebServerclass 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
WebServercan't start on the specified port, it throws ajava.io.IOException. Depending on how you structure the program, you may want to enclose the constructor (and subsequent registrations) in atry/catchstatement, 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
WebServerobject, you can add and remove XML-RPC handlers to it using the functionality described in the section "Creating Handlers," later in this chapter.WebServeritself is a fairly small wrapper of functionality around theXmlRpcServerclass, so you can use it the same way. TheWebServerclass does support HTTP Basic Authentication, but you'll need to create and register classes that implement theAuthenticatedXmlRpcHandlerinterface.In addition to registering and processing XML-RPC handlers, the
WebServerclass 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( )anddenyClient( ), for building lists of approved and rejected clients. By default, no client connections are accepted whensetParanoid( )has been called with an argument oftrue. You'll need to use theacceptClient( )method to add approved IP addresses, and can usedenyClient( )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
XmlRpcServerclass 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 theWebServerclass, you should use theXmlRpcServerclass. Although theWebServerclass accepts HTTP POST requests and feeds them directly to handlers,XmlRpcServeracts as an intermediary, accepting request information from servlets or other sources. AlthoughXmlRpcServerdoesn't handle the connection management end of HTTP, it does process all textual information sent over HTTP.
XmlRpcServeris simpler thanWebServerbecause 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, butXmlRpcServerrequires more assistance thanWebServer. TheWebServerclass listens for requests and processes them automatically;XmlRpcServerneeds another class to listen for requests.The
XmlRpcServerclass 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 theXmlRpcServer'sexecute( )method asInputStreams. (Optionally, they can include a username and password derived from basic authentication.) Theexecute( )method returns aStringcontaining the response, which the supporting environment then wraps with appropriate HTTP header information.For example, the
WebServerclass wraps its calls to theXmlRpcServer's execute method as follows:ServerInputStream sin = new ServerInputStream (input, contentLength);byte result[] = xmlrpc.execute (sin, user, password);output.write (httpversion.getBytes( ));output.write (ok);output.write (server);output.write (conclose);output.write (ctype);output.write (clength);output.write (Integer.toString (result.length).getBytes( ));output.write (doubleNewline);output.write (result);output.flush ( );
WebServertakes 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 theServletthat comes in the XML-RPC package:public void doPost(HttpServletRequest req, HttpServletResponse res)throws ServletException, IOException {byte[] result = xmlrpc.execute (req.getInputStream ( ));res.setContentType("text/xml");res.setContentLength (result.length);OutputStream output = res.getOutputStream( );output.write (result);output.flush ( );}In either case,
XmlRpcServeraccepts 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
XmlRpcServerclass also provides support for theAuthenticatedXmlRpcHandlerinterface. You can also pass information using a three-argument version of execute, which allows theXmlRpcServerclass to send authentication information to the handler. In addition to theInputStream, you'll need to add the username and password strings. Because the authentication header, which includes both the username and password, is sent usingbase64encoding 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 requestString auth=req.getHeader("Authorization");if (!auth.toUpperCase( ).startsWith("BASIC")) {throw ServletException("wrong kind of authorization!");}//get encoded username and passwordString userPassEncoded=auth.substring(6);//create a base64 decoder using Sun utility classsun.misc.BASE64Decoder dec=new sun.misc.BASE64Decoder( );String userPassDecoded=new String(dec.decodeBuffer(userPassEncoded));//split decoded username and passwordStringTokenizer userAndPass=new StringTokenizer(userPassDecoded,":");String username=userAndPass.nextToken( );String password=userAndPass.nextToken( );//send input stream, username, and password to xmlrpc.executeString 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
WebServerclass already includes this decoding, providing a more transparent means of handling authenticatedtransactions.Creating XML-RPC Handlers
Both
WebServerandXmlRpcServeremploy 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 theWebServerandXmlRpcServerclasses' built-in logic for mapping XML-RPC calls to Java methods; other classes can be extended with a singleexecute( )method for mapping XML-RPC requests to Java methods. In both cases, theaddHandler( )andremoveHandler( )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, thetestHandlercan be registered with the server using theaddHandler( )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
testHandlerclass and extracts its method signatures for mapping to XML-RPC requests. XML-RPC clients may now send requests for thetest.nameTestermethod, 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.XmlRpcHandlerinterface. 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 aVector) to the execute method, the single method required by theXmlRpcHandlerinterface.The default behavior of
helma.xmlrpchandles 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
XmlRpcHandlerinterface requires only one method,execute( ). If a class implements theXmlRpcHandlerinterface, all XML-RPC calls are directed to the execute method, short-circuiting the automatic method mapping of theXmlRpcServerclass. Theexecute( )method takes aStringand aVectoras arguments and returns anObject. TheStringis the method named by the XML-RPC request; theVectorcontains all parameters that were sent with that request.The
execute( )method can be used in several different ways. The logic inside theexecute( )method may just ship the Vector containing the parameters to other methods, leaving them to unpackage and process the parameters. Someexecute( )methods may emulate thehelma.xmlrpc.XmlRpcpackage's own processing, mapping the method name and parameter set to appropriate handlers. More sophisticatedexecute( )methods might read the parameters inside theVectorand create objects based on those parameters, which are then shipped to the appropriate target method. In any of these cases, theexecute( )method acts as a gateway.Example 3-1 uses the
execute( )method to pass information to thenameTester( )method used in the automatic registration example. Note that thenameTester( )method is now private, accessible only through theexecute( )method. This isn't required, but it illustrates that theexecute( )method has taken over from the XML-RPC package's native registration.
Example 3-1: Creating an XML-RPC handler 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
AuthenticatedXmlRpcHandlerinterface instead ofXmlRpcHandlercan 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. ThetestHandlerclass 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 executepublic 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 authenticatedprotected 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
XmlRpcServerclass checks the type of class it works with and passes the correct set of parameters to theexecute( )method. Classes that implementXmlRpcHandlerreceive two parameters--method name and theVectorcontaining the parameters--while classes that implementAuthenticatedXmlRpcHandlerreceive four parameters: method name, theVectorcontaining 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
AreaHandlerclass has two methods and no properties. The first method takes two arguments,lengthandwidth, 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.xmlrpclibrary's built-inWebServerclass, as shown in Example 3-4.
Example 3-4: Hosting the area function 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 versionSystem.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 areaserver.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 8899A 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 serverXmlRpcClient client =new XmlRpcClient("http://localhost:8899/");// Create the request parameters using user inputVector params = new Vector( );params.addElement(new Double(args[0]));// Issue a requestObject result =client.execute("area.circleArea", params);// Report the resultsSystem.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 3The area of the circle would be: 28.274333882308138Although 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
structand named parameters instead of the direct approach. To make this work, theAreaHandlerclass needs a new method for processing theHashtableused when structs are sent, and then needs to route requests to the appropriate method based on atypevalue in the struct. The new method is shown in Example 3-6.
Example 3-6: A method for routing XML-RPC requests public Double anyArea(Hashtable arguments) {Double value;value=new Double(0);String requestType=(String) arguments.get("type");if (requestType.equals("circle")) {Double radius=(Double) (arguments.get("radius"));value=circleArea(radius.doubleValue( ));}if (requestType.equals("rectangle")) {Double length=(Double) (arguments.get("length"));Double width=(Double) (arguments.get("width"));value=rectArea(length.doubleValue(), width.doubleValue( ));}return value;}Most of the code handles conversions from the generic
Objecttypes stored in theHashtableto the primitive types needed by the actualcircleArea( )andrectArea( )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.
Example 3-7: A client that calls the router 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 serverXmlRpcClient client =new XmlRpcClient("http://localhost:8899/");// Create a double from the user argumentDouble radius=new Double(args[0]);// Create a hashtable and add a circle requestHashtable requestHash = new Hashtable( );requestHash.put("type", "circle");requestHash.put("radius", radius);// Create the request parameters using user inputVector params = new Vector( );params.addElement(requestHash);// Issue a requestObject result =client.execute("area.anyArea", params);// Report the resultsSystem.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.6The area of the circle would be: 98.5203456165759Also, unless the
circleArea( )andrectArea( )are changed to become private methods, direct XML-RPC requests for those methods will continue to work.Although this
structapproach is somewhat like implementing theXmlRpcHandlerinterface 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 letXmlRpcServerfigure out simpler methods. You could also implement something very similar using a JavaVector/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
Stringcontaining the message from the exception.
Note that methods that process Java exceptions have to be static methods. In addition, because thereport( )method itself has some possible exceptions due to the XML-RPC call, the entire body of the method is contained inside atry/catchstatement.
Example 3-8: Exception reporting XML-RPC client 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 serverXmlRpcClient client =new XmlRpcClient("http://192.168.124.14:8899/");//get local hostname and IP addressInetAddress address=InetAddress.getLocalHost( );String ipAddress=address.toString( );// Create the request parameters using user inputVector params = new Vector( );params.addElement(ipAddress);params.addElement(eReport.getMessage( ));// Issue a requestObject 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 ourreport( )method. Thereport( )method sends the address of this system along with the exception message to the XML-RPC server. If something goes wrong with that transmission, thereport( )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.
Example 3-9: A host for the logging handler 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 versionSystem.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 areaserver.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 8899Attempting to start XML-RPC Server...Started successfully.Registered XLogHandler class to XLog.Now accepting requests. (Halt program to stop.)From: javalab1/192.168.124.12Message: helpFrom: javalab5/192.168.124.17Message: helpFrom: javalab6/192.168.124.19Message: helpFrom: javalab27/192.168.124.141Message: helpMore 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 afile.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 ofgetProperty( )andsetProperty( )methods, though simple JavaBeans doesn't work in XML-RPC. Why? The set methods returnvoid, 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 modifyhelma.xmlrpcto return an empty value on methods that returnvoid. 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
getandsetof 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
valueproperty 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 theGetSetHandlerobject is bound to the XML-RPC handling code.
Example 3-12: XML-RPC host for GetSetHandler 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 versionSystem.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 areaserver.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 serverXmlRpcClient client =new XmlRpcClient("http://localhost:8899/");//get local host IP addressInetAddress address=InetAddress.getLocalHost( );String ipAddress=address.toString( );// Create the request parameters using user inputVector params = new Vector( );params.addElement(ipAddress);if (getOrSet.equals("set")) {Integer newValue=new Integer(args[1]);params.addElement(newValue);}// Issue a requestObject result=null;if (getOrSet.equals("set")) {result = client.execute("getSet.setValue", params);} else {result = client.execute("getSet.getValue", params);}// Report the resultsSystem.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 getThe response was: 20D:\xmlrpc\example3>java GetSetClient set 21The response was: 21D:\xmlrpc\example3>java GetSetClient getThe response was: 21D:\xmlrpc\example3>java GetSetClient set 200The response was: 200D:\xmlrpc\example3>java GetSetClient getThe response was: 200D:\xmlrpc\example3>java GetSetClient set 750The response was: 750D:\xmlrpc\example3>java GetSetClient getThe response was: 750The
GetSetHandlerobject retains the last value it was set for, starting with the initial value of20it 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.xmlrpcpackage 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.
Back to: Programming Web Services with XML-RPC
© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com