As
instructional as our Hello
example has been in
demonstrating how to use XML-RPC with Java, it isn’t very
realistic. In addition to being a trivial example, the server is not
very flexible and the handler itself doesn’t give any
indication of how a practical XML-RPC handler might operate. Here we
try to give examples of using XML-RPC in a production environment by
increasing the usefulness of the handler and the usability of the
server. These, while still not code you might want to immediately add
to your current project, should at least begin to demonstrate to you
how XML-RPC might be of use in your future projects, and how to build
applications that can use XML-RPC but are not limited by it.
Our
HelloHandler
class was simple but useless in a
practical application. Remember that we said most XML-RPC uses relate
to letting events occur on a server that is more suited for complex
tasks, while allowing a thin client to request procedures be executed
and use the returned results. In addition, it is possible that part
or even all of the computations needed to respond to a request can be
done in advance; in other words, the handler class may be running
tasks and ensuring that results are already available when a method
call comes in. As a Java coder, threads and shared instance data
should leap to your mind. Here we take a look at a very simple
Scheduler
class to illustrate these principles.
Our
scheduler should allow clients to add and remove events. We also want
to be able to query our scheduler for a list of all events in the
queue. To make this a little more practical (and to have a task for
our server to perform later), we want the result of an events query
to return the events sorted by the time they occur. These events for
our example will simply be a String
event name and
a time for the event (in a java.util.Date
format).
Though this is not a complete scheduler implementation, it can
demonstrate how to let the server do some behind-the-scenes work for
us.
First we will code our addEvent( )
and
removeEvent( )
methods. Because these are both
client-triggered events, there is nothing particularly remarkable
about them; what is worth thinking about is how to store these events
in our Scheduler
class. Although our XML-RPC
server will instantiate this class, and that instance will be used
for all XML-RPC calls that come into that server, it is possible and
even probable that other classes or even XML-RPC servers may interact
with our scheduler as well. If we store a list of events as a member
variable of our class, multiple instances will not be able to share
data. To solve this problem in our example, we will make our storage
static, causing it to be shared across all
Scheduler
class instances. To store both an event
name and an event time, a
Hashtable
would seem appropriate, allowing the use of key-value pairs. In
addition to this Hashtable
, we store the names of
the events in a
Vector
.
Although this uses some extra storage space (and memory in the Java
Virtual Machine), we can sort our Vector
and not
have to deal with sorting our Hashtable
; the
advantage here is that we can swap the event names in our
Vector
(a single swap) and not have to swap the
event times in our Hashtable
(two swaps for each
exchange). Let’s code the skeleton of this class, and add these
first two methods to allow addition and removal of events. For now,
we add our storage as well, but we leave the implementation of the
retrieval and sorting of events for later. Example 10.6 is a code listing for this new handler.
Example 10-6. The Scheduler Class
import java.util.Date; import java.util.Hashtable; import java.util.Vector; /** * <b><code>Scheduler</code></b> is a class that allows * addition, removal, and retrieval of a list of events, sorted * by their occurrence time. * * @version 1.0 */ public class Scheduler { /** List of event names (for sorting) */ private static Vector events = null; /** Event details (name, time) */ private static Hashtable eventDetails = null; /** * <p> * This will initialize the storage. * </p> */ public Scheduler( ) { events = new Vector( ); eventDetails = new Hashtable( ); } /** * <p> * This will add the requested event. * </p> * * @param eventName <code>String</code> name of event to add. * @param eventTime <code>Date</code> of event. * @return <code>boolean</code> - indication of if event was added. */ public boolean addEvent(String eventName, Date eventTime) { // Add this event to the list of events if (!events.contains(eventName)) { events.addElement(eventName); eventDetails.put(eventName, eventTime); } return true; } /** * <p> * This will remove the requested event. * </p> * * @param eventName <code>String</code> name of event to remove. * @return <code>boolean</code> - indication of if event was removed. */ public synchronized boolean removeEvent(String eventName) { events.remove(eventName); eventDetails.remove(eventName); return true; } }
Our addEvent( )
method adds the name of the event to both
storage objects, and the time to the Hashtable
.
Our removeEvent( )
method does the converse. Both methods return
a boolean
value. Although in the example this
value is always true
, in a more complex
implementation, this value could be used to indicate problems in the
addition or removal of events.
With the ability to add and remove
events, we now need to add a method that
returns a list of events. This method returns all events added to the
event store, regardless of what client or application added those
events; in other words, these could be events added by a different
XML-RPC client, a different XML-RPC server, another application, or a
standalone implementation of this same scheduler. Since we have to
return a single Object
result, we can return a
Vector
of formatted String
values that contain the name of each event and its time. Certainly,
in a more useful implementation this might return the
Vector
of events, or some other form of the events
in a typed format (with the date as a Date
object,
etc.). This method acts more as a view of the data, though, and does
not allow the client to further manipulate it. To return this list of
events, we use the event store and the
java.text.SimpleDateFormat
class, which allows textual formatting of
Date
objects. Iterating through all events, a
String
is created with the event name and the time
it is set for; each String
is inserted into the
Vector
result list, and this list is then returned
to the client. Let’s add the required import statement and the
code to return the events in the store to the scheduler code:
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Hashtable; import java.util.Vector; .../**
* <p>
* This returns the current listing of events.
* </p>
*
* @return <code>Vector</code> - list of events.
*/
public Vector getListOfEvents( ) {
Vector list = new Vector( );
// Create a Date Formatter
SimpleDateFormat fmt =
new SimpleDateFormat("hh:mm a MM/dd/yyyy");
// Add each event to the list
for (int i=0; i<events.size( ); i++) {
String eventName = (String)events.elementAt(i);
list.addElement("Event "" + eventName +
"" scheduled for " +
fmt.format(
(Date)eventDetails.get(eventName)));
}
return list;
}
...
At
this point, we could use this class as an XML-RPC handler without any
problems. However, the point of this exercise is to look at how work
can be done by the server while the client is performing other tasks.
The getListOfEvents( )
method assumes that the
event list (the Vector
variable
events
) is ordered in the correct way when this
method is called; this means that sorting has already occurred. We
haven’t written code to sort our events yet, but more
importantly, we haven’t written code to trigger this sorting.
Furthermore, as the event store gets large, sorting it can be very
time-consuming, and this task should not cause the client to wait for
it to complete. First we must add a method that our class can use to
sort the events. For simplicity, a bubble sort is used; discussion of
sorting algorithms is beyond the scope of this book, so this code is
presented without any explanation of its workings. At the end of the
method, though, the Vector
variable
events
is sorted in order of the time the
events within it occur. For information on this and other
sorting algorithms, you should
refer to Algorithms in Java
, by Robert Sedgewick
and Tim Lindholm (Addison-Wesley). The algorithm and method to handle
sorting of the events is presented here, and should be added to your
code:
import java.text.SimpleDateFormat; import java.util.Date;import java.util.Enumeration;
import java.util.Hashtable; import java.util.Vector; /** * <b><code>Scheduler</code></b> is a class that allows * addition, removal, and retrieval of a list of events, sorted * by their occurrence time. * * @author <a href="mailto:[email protected]">Brett McLaughlin</a> * @version 1.0 */ public class Scheduler { /** List of event names (for sorting) */ private static Vector events = null; /** Event details (name, time) */ private static Hashtable eventDetails = null;/** Flag to indicate if events are sorted */
private static boolean eventsSorted;
// Other existing method implementations
/**
* <p>
* Sort the events in the current list.
* <p>
*/
private synchronized void sortEvents( ) {
if (eventsSorted) {
return;
}
// Create array of events as they are (unsorted)
String[] eventNames = new String[events.size( )];
events.copyInto(eventNames);
// Bubble sort these
String tmpName;
Date date1, date2;
for (int i=0; i<eventNames.length - 1; i++) {
for (int j=0; j<eventNames.length - i - 1; j++) {
// Compare the dates for these events
date1 = (Date)eventDetails.get(eventNames[j]);
date2 = (Date)eventDetails.get(eventNames[j+1]);
if (date1.compareTo(date2) > 0) {
// Swap if needed
tmpName = eventNames[j];
eventNames[j] = eventNames[j+1];
eventNames[j+1] = tmpName;
}
}
}
// Put into new Vector (ordered)
Vector sortedEvents = new Vector( );
for (int i=0; i<eventNames.length; i++) {
sortedEvents.addElement(eventNames[i]);
}
// Update the global events
events = sortedEvents;
eventsSorted = true;
}
... }
In addition to the core algorithm, we import the
java.util.Enumeration
class and add a
boolean
member variable,
eventsSorted
. This flag allows short-circuiting of
the execution of the sorting when the events are already ordered.
Although we have not yet added code to update this flag, we can
easily do so. Our sorting method already indicates that events are
sorted at its completion. Our constructor should initially set this
value to true
, indicating that all events are in
order. It is only when events are added that the list may become
unordered, so in our addEvents( )
method we need
to set this flag to false
if an event is added.
This will let our Scheduler
class know that
something should occur that will trigger the sort. Then when the
getListOfEvents( )
method is invoked, the events
will be ordered and ready for retrieval. Let’s add code to our
constructor and the method for adding events that will update this
flag:
/**
* <p>
* This will initialize the storage.
* </p>
*/
public Scheduler( ) {
events = new Vector( );
eventDetails = new Hashtable( );
eventsSorted = true;
}
/**
* <p>
* This will add the requested event.
* </p>
*
* @param eventName <code>String</code> name of event to add.
* @param eventTime <code>Date</code> of event.
* @return <code>boolean</code> - indication of if event was added.
*/
public boolean addEvent(String eventName, Date eventTime) {
// Add this event to the list of events
if (!events.contains(eventName)) {
events.addElement(eventName);
eventDetails.put(eventName, eventTime);
eventsSorted = false;
}
return true;
}
We do not need to make any changes to the removeEvent( )
method, as removing an entry does not affect the order of
the events. The ideal mechanism to handle server-side processing
while freeing the client for further action is a thread that sorts
events. With this thread started in the JVM, client processing can
continue without waiting for the thread to complete. This is
particularly important in a multi-threaded environment where
synchronization and
threads waiting for
object locks would be in use. In this example, we avoid those issues
(this is a chapter about XML-RPC, not threading), but you can add the
relevant code to handle these issues fairly easily. In our example,
we want to create an inner class that extends
Thread
, and does nothing but invoke the
sortEvents( )
method. We then add to our
addEvents( )
method code that creates and starts
this thread when events are added. This results in the addition of
events triggering a re-sorting of the events, but allows the client
to continue with its actions (which might include adding additional
events, which in turn starts more threads to sort the data). When the
client does request the list of events, the events should be sorted
when returned, all without the client ever waiting on this action to
occur, or having to spend processing power to make it happen. The
addition of the inner class to sort, and code to run that class as a
thread in our addEvents( )
method rounds out the
Scheduler
class:
public class Scheduler { ... public boolean addEvent(String eventName, Date eventTime) { // Add this event to the list of events if (!events.contains(eventName)) { events.addElement(eventName); eventDetails.put(eventName, eventTime); eventsSorted = false;// Start thread on server sorting
SortEventsThread sorter = new SortEventsThread( );
sorter.start( ); } return true; } .../**
* <p>
* This inner class handles starting the sorting as
* a <code>Thread</code>.
*/
class SortEventsThread extends Thread {
/**
* <p>
* Start the sorting.
* </p>
*/
public void run( ) {
sortEvents( );
}
}
}
You can now compile the modified source code, and we have a threaded scheduler that performs the process-intensive task of sorting on the server, allowing any clients to work uninterrupted while that sorting occurs. This is still a simple example of using a handler class properly, but it does introduce the concepts of resource distribution and letting a server handle the workload when possible. To complement this more advanced handler class, we next look at building a more robust XML-RPC server implementation.
Our XML-RPC server class still needs some work. The current version requires us to specifically add our handler classes to the server in the code. This means that the addition of a new handler class requires coding and recompilation. Not only is this undesirable from a change control perspective, but it is annoying and time-consuming. Obtaining the newest code from a source control system, adding the change, and testing to add one or two handlers is not practical, and won’t win you friends among your management. What is preferred is to have a robust server that can read this sort of information from a configuration file and load the needed classes at runtime. We can build a lightweight server to do this now.
To begin, we create a new server class. You can either start from
scratch, or copy and paste from the HelloServer
class given earlier in this chapter. We start by setting up our
framework, adding the required import statements, and instantiating
our server, similar to the earlier example; however, we do not add
any code that registers handlers, as we will write a helper method to
load the needed information from a file. The one change from our
earlier version is that we require an additional command-line
parameter; this parameter should be the name of a file. We will read
this file in our methods later to add handlers to the server. You can
create the LightweightXmlRPcServer
class (part of
the com.oreilly.xml
utility package), which
continues to use the thin WebServer
helper class,
with the code shown in Example 10.7. The complete
com.oreilly.xml
package is also available for download at http://www.oreilly.com/catalog/javaxml or
http://www.newInstance.com.
Example 10-7. The LightweightXmlRpcServer Class
package com.oreilly.xml; import java.io.IOException; import helma.xmlrpc.XmlRpc; import helma.xmlrpc.WebServer; /** * <b><code>LightweightXmlRpcServer</code></b> is a utility class * that will start an XML-RPC server listening for HTTP requests * and register a set of handlers, defined in a configuration file. * * @author * <a href="mailto:[email protected]">Brett McLaughlin</a> * @version 1.0 */ public class LightweightXmlRpcServer { /** The XML-RPC server utility class */ private WebServer server; /** Port number to listen on */ private int port; /** Configuration file to use */ private String configFile; /** * <p> * This will store the requested port and configuration file * for the server to use. * </p> * * @param port <code>int</code> number of port to listen to * @param configFile <code>String</code> filename to read for * configuration information. */ public LightweightXmlRpcServer(int port, String configFile) { this.port = port; this.configFile = configFile; } /** * <p> * This will start up the server. * </p> * * @throws <code>IOException</code> when problems occur. */ public void start( ) throws IOException { try { // Use Apache Xerces SAX Parser XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser"); System.out.println("Starting up XML-RPC Server..."); server = new WebServer(port); // Register handlers } catch (ClassNotFoundException e) { throw new IOException("Error loading SAX parser: " + e.getMessage( )); } } /** * <p> * Provide a static entry point. * </p> */ public static void main(String[] args) { if (args.length < 2) { System.out.println( "Usage: " + "java com.oreilly.xml.LightweightXmlRpcServer " + "[port] [configFile]"); System.exit(-1); } LightweightXmlRpcServer server = new LightweightXmlRpcServer(Integer.parseInt(args[0]), args[1]); try { // Start the server server.start( ); } catch (IOException e) { System.out.println(e.getMessage( )); } } }
There is really nothing remarkable here. We ensure that the required parameters are passed in and start the server on the requested port. We now need to add in methods to load our handlers from a file, and then add those handlers one by one to our server.
Because each handler needs a name and an associated class, we can
create a configuration file that has these two pieces of information.
With Java, it is easy to load and instantiate a class with its
complete package and name. This means we can completely represent a
new handler with a pair of textual values. Within this file, we can
add both our original HelloHandler
class as well
as our new Scheduler
class. Since we are writing
the file parser as well, we can arbitrarily decide to use commas as
delimiters and the pound sign (#
) as a comment
marker. In fact, you can use whatever format you wish as long as you
write code that uses your conventions in parsing the file. Create the
configuration file shown in Example 10.8 that will
add the HelloHandler
class under the class
identifier “hello” and the Scheduler
class under the class identifier “scheduler,” and save it
as xmlrpc.conf.
Example 10-8. XML-RPC Handler Configuration File
# Hello Handler: sayHello( ) hello,HelloHandler # Scheduler: addEvent(), removeEvent(), getEvents( ) scheduler,Scheduler
For documentation purposes, we specify the methods available to each handler in our comments. This allows future maintainers of our code to know what methods are available for each handler.
Java’s I/O classes make it easy to load this file and read its
contents. We can create a helper method that reads the specified file
and stores the pairs of values in a Java
Hashtable
. This object can then be passed on to
another helper that loads and registers each handler. This example
method does not do extensive error checking, which a production ready
server might, and it simply ignores any line without a pair of
comma-separated values; certainly it is easy enough to add in error
handling if you want to use this code in your applications. Once we
find a line with a pair of values, the line is broken up and the
class identifier and class name are stored as an entry within the
Hashtable
. Add the import
statements for the required classes and then the new
getHandlers( )
method to the
LightweightServer
class now:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;import java.util.Hashtable;
import helma.xmlrpc.XmlRpc; import helma.xmlrpc.WebServer; .../**
* <p>
* This is a method that parses the configuration file
* (in a very simplistic manner) and reads the handler
* definitions supplied.
* </p>
*
* @return <code>Hashtable</code> - class id/class pairs.
* @throws <code>IOException</code> - when errors occur in
* reading/parsing the file.
*/
private Hashtable getHandlers( ) throws IOException {
Hashtable handlers = new Hashtable( );
BufferedReader reader =
new BufferedReader(new FileReader(configFile));
String line = null;
while ((line = reader.readLine( )) != null) {
// Syntax is "handlerName, handlerClass"
int comma;
// Skip comments
if (line.startsWith("#")) {
continue;
}
// Skip empty or useless lines
if ((comma = line.indexOf(",")) < 2) {
continue;
}
// Add the handler name and the handler class
handlers.put(line.substring(0, comma),
line.substring(comma+1));
}
return handlers;
}
...
Instead of adding code to save the result of this method, we can use
that result as input to a method that iterates through the
Hashtable
and adds each handler to the server. The code needed to accomplish
this task is not complicated; the only notable items are that the
addHandler( )
method of
WebServer
requires an instantiated class as a
parameter. This requires us to take the name of the class to register
from the Hashtable
, load that class into the JVM
with Class.forName( )
, and then instantiate that
class with newInstance( )
. This is the methodology
used in class loaders and other dynamic applications in Java, but may
be unfamiliar to you if you are new to Java or have not had to
dynamically instantiate classes from a textual name before. Once the
class is loaded in this way, it and the class identifier are passed
to the addHandler( )
method, and the iteration
continues. Once the contents of the Hashtable
are
loaded, the server is set up and ready to go. We use the
Enumeration
class to cycle through the keys in the
Hashtable
, so we must add this
import
statement to our file:
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException;import java.util.Enumeration;
import java.util.Hashtable; import helma.xmlrpc.XmlRpc; import helma.xmlrpc.WebServer; .../**
* <p>
* This will register the handlers supplied in the XML-RPC
* server (typically from <code>{@link #getHandlers( )}</code>.
* </p>
*
* @param handlers <code>Hashtable</code> of handlers to register.
*/
private void registerHandlers(Hashtable handlers) {
Enumeration handlerNames = handlers.keys( );
// Loop through the requested handlers
while (handlerNames.hasMoreElements( )) {
String handlerName = (String)handlerNames.nextElement( );
String handlerClass = (String)handlers.get(handlerName);
// Add this handler to the server
try {
server.addHandler(handlerName,
Class.forName(handlerClass).newInstance( ));
System.out.println("Registered handler " + handlerName +
" to class " + handlerClass);
} catch (Exception e) {
System.out.println("Could not register handler " +
handlerName + " with class " +
handlerClass);
}
}
}
...
This is simply a complement to our getHandlers( )
method; in fact, it takes the result of that method as input. It uses
the String
values within the
Hashtable
and registers each, and just that
simply, our server is running and will have any handlers in the
configuration file loaded and available for remote calls. Be aware
that we could have just as easily consolidated these methods into one
larger method. However, the purpose of the two methods is
significantly different; while one, getHandlers( )
, deals with parsing a file, the other,
registerHandlers( )
, deals with registering
handlers once information about the handlers is available. With this
methodology, we can change the way we parse the configuration file
(or even have it read from a database or other medium) without having
to worry about the way the handlers are registered. In fact, in the
next chapter we remove the getHandlers( )
method
in lieu of a helper class that reads this information from an XML
configuration file! In this case, a good design decision early in the
process (here) avoids a lot of change to our working code later in
the process (in the next chapter).
Once you have added these two helper methods, add their invocation to
the start( )
method of our server class:
/** * <p> * This will start up the server. * </p> * * @throws <code>IOException</code> when problems occur. */ public void start( ) throws IOException { try { // Use Apache Xerces SAX Parser XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser"); System.out.println("Starting up XML-RPC Server..."); server = new WebServer(port);// Register handlers
registerHandlers(getHandlers( ));
} catch (ClassNotFoundException e) { throw new IOException("Error loading SAX parser: " + e.getMessage( )); } }
We add a try
-catch
block around the
method invocations we have added so that we can distinguish between
exceptions that occur in the server itself (the outer block) as
opposed to exceptions that occur specifically related to the loading
of handlers. In this latter case, we report the error as being
generated by our handler methods. Compile this code, ensure you have
created the configuration file, and our server is ready for
use.
Our
client has no new concepts or techniques in it; just as our
HelloClient
class was simple, so is the
SchedulerClient
class. It needs to start up an
XML-RPC client, invoke handler methods, and print out the result of
those handlers. The complete code for the client is here. Comments
indicate what is occurring, and since this is all ground we have
covered you can simply enter the code in Example 10.9
into your editor and compile it.
Example 10-9. The SchedulerClient Class
import java.io.IOException; import java.net.MalformedURLException; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import helma.xmlrpc.XmlRpc; import helma.xmlrpc.XmlRpcClient; import helma.xmlrpc.XmlRpcException; /** * <b><code>SchedulerClient</code></b> is an XML-RPC client * that makes XML-RPC requests to <code>Scheduler</code>. * * @version 1.0 */ public class SchedulerClient { /** * <p> * Add events to the Scheduler. * </p> * * @param client <code>XmlRpcClient</code> to connect to */ public static void addEvents(XmlRpcClient client) throws XmlRpcException, IOException { System.out.println(" Adding events... "); // Parameters for events Vector params = new Vector( ); // Add an event for next month params.addElement("Proofread final draft"); Calendar cal = Calendar.getInstance( ); cal.add(Calendar.MONTH, 1); params.addElement(cal.getTime( )); // Add the event if (((Boolean)client.execute("scheduler.addEvent", params)) .booleanValue( )) { System.out.println("Event added."); } else { System.out.println("Could not add event."); } // Add an event for tomorrow params.clear( ); params.addElement("Submit final draft"); cal = Calendar.getInstance( ); cal.add(Calendar.DAY_OF_MONTH, 1); params.addElement(cal.getTime( )); // Add the event if (((Boolean)client.execute("scheduler.addEvent", params)) .booleanValue( )) { System.out.println("Event added."); } else { System.out.println("Could not add event."); } } /** * <p> * List the events currently in the Scheduler. * </p> * * @param client <code>XmlRpcClient</code> to connect to */ public static void listEvents(XmlRpcClient client) throws XmlRpcException, IOException { System.out.println(" Listing events... "); // Get the events in the scheduler Vector params = new Vector( ); Vector events = (Vector)client.execute("scheduler.getListOfEvents", params); for (int i=0; i<events.size( ); i++) { System.out.println((String)events.elementAt(i)); } } /** * <p> * Static entry point for the demo. * </p> */ public static void main(String args[]) { try { // Use the Apache Xerces SAX Parser Implementation XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser"); // Connect to server XmlRpcClient client = new XmlRpcClient("http://localhost:8585/"); // Add some events addEvents(client); // List events listEvents(client); } catch (Exception e) { System.out.println(e.getMessage( )); } } }
As you are entering this code, notice that the events are added in
reverse order of the event time. Our server should rearrange these
events with the sortEvents( )
method to facilitate
correctly ordered results when the getListOfEvents( )
method is called. We can see that our server takes care
of this sorting next.
Once you have entered in the code for the handler, server, and client, compile all of the source files. You also will need to create the configuration file that lists handlers to register with the XML-RPC server that we discussed in that section. First, start up the XML-RPC server as a separate process:
D:prod>start java com.oreilly.xml.LightweightXmlRpcServer 8585 D:prodconfxmlrpc.conf
In Unix, use:
$ java com.oreilly.xmlrpc.LightweightServer 8585 conf/xmlrpc.conf &
You should see the server indicate that the handlers in the supplied configuration file are registered to the names you provided:
Starting up XML-RPC Server... Registered handler scheduler to class Scheduler Registered handler hello to class HelloHandler
If you never stopped the previous XML-RPC server,
HelloServer
, you will get an error trying to start
another server on the same port. Be sure to stop the
HelloServer
before trying to start the
LightweightXmlRpcServer
.
Finally, execute your client and see the results:
$ java SchedulerClient Adding events... Event added. Event added. Listing events... Event "Submit final draft" scheduled for 10:13 AM 02/14/2000 Event "Proofread final draft" scheduled for 10:13 AM 03/13/2000
You should not notice a significant pause as your client adds and lists events, yet the server still sorts the events in a separate thread within the server JVM (and bubble sorting is not a quick algorithm!). You have written your first useful XML-RPC application!
3.144.30.236