Chapter 9: A Simple Example

This chapter looks at a simple problem, implementing it in a number of different ways

9.1. Problem Description

Applications often need to work out the type of a file, to see if it is a text file, an HTML document, an executable, etc. This can be done in two ways:

  1. By examining the file's name

  2. By examining the file's contents

Utilities such as the Unix file command use the second method, and have a complex description file (such as /etc/magic or /usr/share/magic) to aid in this. Many other applications such as Web browsers, mail readers (and even some operating systems!) use the first method and work out a file's type based on its name.

A common file classification is into MIME types such as text/plain and image/gif. There are tables of "official" MIME types (unofficial ones can be added on an adhoc basis), and there are also tables of mappings from filename endings to corresponding MIME types. These tables have entries such as


application/postscript          ai eps ps
application/rtf                 rtf
application/zip                 zip
image/gif                       gif
image/jpeg                      jpeg jpg jpe
text/html                       html htm
text/plain                      txt
and are stored in files for applications to access.

This storage of tables separate from the applications that would use them is rated as bad from the O/O point of view, since each application would need to have code to interpret the tables. The multiplicity of these tables and the ability of users to modify them makes this a maintenance problem. It would be better to encapsulate at least the filename to MIME type mapping table in an object. We define a MIME class



package common;

import java.io.Serializable;

/**
 * MIMEType.java
 */

public class MIMEType implements Serializable {

    /**
     * A MIME type is made up of 2 parts
     * contentType/subtype
     */    
    private String contentType;
    private String subType;

    public MIMEType() {
        // empty constructor required just in case
        // we want to use this as a Java Bean
    }

    public MIMEType(String type) {
        int slash = type.indexOf('/');
        contentType = type.substring(0, slash-1);
        subType = type.substring(slash+1, type.length());
    }
    
    public MIMEType(String contentType, String subType) {
        this.contentType = contentType;
        this.subType = subType;
    }

    public String toString() {
        return contentType + "/" + subType;
    }

    /** 
     * Accessors/setters
     */
    public String getContentType() {
	return contentType;
    }

    public void setContentType(String type) {
	contentType = type;
    }

    public String getSubType() {
	return subType;
    }

    public void setSubType(String type) {
	subType = type;
    }
} // MIMEType




and a mapping class

package common;

/**
 * FileClassifier.java
 */

public interface FileClassifier {
    
    public MIMEType getMIMEType(String fileName) 
	throws java.rmi.RemoteException;
    
} // FileClasssifier

This mapping class has no constructors, as it justs acts as a lookup table via its static method getMIMEType().

Applications may make use of these classes as they stand, by simply compiling them and having the class files available at runtime. This will still result in duplication throughout JVMs, possible multiple copies of the class files, and potentially severe maintenance problems if applications need to be re-compiled. It may be better to have the FileClassifier as a network service. What will be involved in this?

9.2. Service Specification

If we wish to make a version of FileClassifier available across the network, there are a number of possibilities. The client will be asking for an instance of a class, and generally will not care too much about the details of this instance. For example, it will want an instance of a DiskDrive or a Calendar. Usually it will not care which drive it gets, or which calendar. If it requires further specification it can either ask for a subclass instance (such as a SeagateDiskDrive, or use an Entry object for this additional information.

Services will have particular implementations, and will upload these to the service locators. The uploaded service will be of a quite specific class, and may have associated entries.

There are several options that the client could use in trying to locate a suitable service.

Option 1
The silly one: push the entire implementation upto the lookup service and make the client ask for it by its class. Then the client might just as well create the classifier as a local object as it has all the information needed! This doesn't lend itself to flexibility with new unknown services coming along if the client already has to know the details. So this option is not feasible.
Option 2
Let the client ask for a superclass of the service. This is better, as it allows new implementations of a service to just be implemented as new subclasses. It is not ideal, as classes have implementation code, and if ever this changes over time then there is a maintenance issue with the possibility of version "skew" over time. This can be used for Jini: it just isn't the best way.
Option 3
Separate the interface completely from the implementation. Make the interface available to the client, and upload the implementation to the lookup service. Then when the client asks for an instance object that implements the interface, it will get any object for this interface. This will reduce maintenance: if the client is coded just in terms of the interface then it will not need recompilation even if the implementation changes. Note that these words will translate straight into Java terms: the client knows about a Java interface, whereas the service provider deals in terms of a Java class that implement's the interface.

The ideal mechanism in the Jini world is to specify services by Java interfaces, and have all clients know this interface. Then each service can be an implementation of this interface. This is simple in Java terms, simple in specification terms, and simple for maintenance. This is not the complete set of choices for the service, but is enough to allow a service to be specified and get on with building the client. One possibility for service implementation is looked at later in this chapter, and the next chapter is devoted to the full range of possibilities.

Although we do not wish to get involved in discussions about "which middleware is best", we just note that consistent use of Java throughout Jini, and in particular its use for both specification and implementation avoids many of the "mismatch" problems that can occur when specification and implementation occur in different languages. For example, Web services use XML data types, and this is a very rich system distinct from the Java type system. It is not possible to represent all XML types in Java nor all Java types in XML. This either leads to compromises with a "least common denominator" approach or to services that cannot be written or specified properly.

9.3. Common Classes

The client and any implementations of a service must share some common classes. For a file classification service the common classes are the classifier itself (which can be implemented as many different services) and the return value, the MIME type. These have to change very slightly from their standalone form.

9.3.1 MIMEType

The class MIMEType is known to the client and to any file classifier service. The class files can be expected to be known to the JVMs of all clients and services. That is, these class files need to be in the classpath of every file classifier service and of every client that wants to use a file classifier service.

The method getMIMEType() will return an object from the file classifer service. There are implementation possibilities that can affect this object:

  1. If the service runs in the client JVM, then nothing special needs to be done

  2. If the service is implemented remotely and runs in a separate JVM, then the MIMEType object must be serialized for transport to the client JVM. For this to be possible, it must implement the Serializable interface. Note that while the class files are accessible to both client and service, the instance data of the MIMEType object needs to be serializable to move the object from one machine to the other

There is a difference in the object depending on possible implementations. If it implements Serializable then it can be used in both the remote and local cases, but if it doesn't then it can only be used in the local case.

Making decisions about interfaces based on future implementation concerns is traditionally rated as poor design. In particular, the philosophy behind remote procedure calls is that they hide the network as much as possible and make the calls behave as though they were local calls. With this pilosophy, there is no need to make a distinction between local and remote calls at design time. However, a document from Sun "A Note on Distributed Computing" by Jim Waldo et al argues that this is wrong, particularly in the case of distributed objects. The basis of their argument is that the network brings in a host of other factors, in particular that of partial failure. That is, part of the network itself may fail, or a component on the network may fail without all of the network, or all of the components failing. If other components do not make allowance for this possible (or maybe even, likely) behaviour then the system as a whole will not be robust and could be brought down by the failure of a single component.

According to this Note, it is important to determine if the objects may be running remotely and adjust interfaces and classes accordingly at the design stage. This is to take into account possible extra failure modes of methods, and in this case, an extra requirement on the object. The paper is reprinted in the Jini specification book from Sun and is also at http://www.sun.com/research/techrep/1994/abstract_29.html

This leads to an interface which adds the Serializable interface to the previous version, as objects of this class may be sent across the network. The objects sent are copies of the one on the server, not references to one that remains on the server.



package common;

import java.io.Serializable;

/**
 * MIMEType.java
 */

public class MIMEType implements Serializable {

    /**
     * A MIME type is made up of 2 parts
     * contentType/subtype
     */    
    private String contentType;
    private String subType;

    public MIMEType() {
        // empty constructor required just in case
        // we want to use this as a Java Bean
    }

    public MIMEType(String type) {
        int slash = type.indexOf('/');
        contentType = type.substring(0, slash-1);
        subType = type.substring(slash+1, type.length());
    }
    
    public MIMEType(String contentType, String subType) {
        this.contentType = contentType;
        this.subType = subType;
    }

    public String toString() {
        return contentType + "/" + subType;
    }

    /** 
     * Accessors/setters
     */
    public String getContentType() {
	return contentType;
    }

    public void setContentType(String type) {
	contentType = type;
    }

    public String getSubType() {
	return subType;
    }

    public void setSubType(String type) {
	subType = type;
    }
} // MIMEType




9.3.2 FileClassifier interface

Changes have to be made to the file classifier interface as well. Firstly, interfaces cannot have static methods, so we shall have to turn the method getMIMEType() into a public instance method.

In addition, all methods are defined to throw a java.rmi.RemoteException. This type of exception is used throughout Java (not just the RMI component) to mean "a network error has occurred". This could be a lost connection, a missing server, a class not downloadable, etc. There is a little subtlety here, related to the java.rmi.Remote class: the methods of Remote must all throw a RemoteException, but the converse is not true. If all the methods throw RemoteException, it does not mean the class implements/extends Remote. It only means that an implementation may be implemented as a remote (distributed) object, and this implementation might also use the RMI Remote interface.

There are some very fine points to this, which may be skipped if desired. Basically, you can't go wrong if every method throws RemoteException and the interface does not extend Remote. In fact, prior to JDK 1.2.2, making the interface extend Remote would force each implementation of the interface to actually be a remote object. But at JDK 1.2.2 the semantics of Remote was changed a little so that this requirement was relaxed. From JDK 1.2.2 onwards an interface can extend Remote without implementation consequences. At least, that is almost the case: "unusual" ways of implementing RMI such as over IIOP have not yet caught up to this. So for absolutely maximum flexibility just throw RemoteException from each method and don't extend Remote.

This gives the following interface


package common;

/**
 * FileClassifier.java
 */

public interface FileClassifier {
    
    public MIMEType getMIMEType(String fileName) 
	throws java.rmi.RemoteException;
    
} // FileClasssifier

Why does this interface throw a java.rmi.RemoteException in the getMIMEType() method? Well, an interface is supposed to be above all possible implementations, and should never change. The implementation discussed later in this chapter does not throw such an exception. However, other implementations in other sections use a Remote implementation, and this will require that the method throws an java.rmi.RemoteException. Since it is not possible to just add a new exception in a subclass or interface implementation, the possibility must be added in the interface specification.

9.3.3 Compiling the common classes

There is nothing Jini-specific about these classes. They can be compiled using any Java compiler with no special flags. For example, using the JDK compiler,


javac common/MIMEType.java common/FileClassifier.java

9.4. Client

The client is the same for all of the possible server implementations discussed throughout this book. The client does not care how the service implementation is done, just as long as it gets a service that it wants, and it specifies this by asking for a FileClassifier interface.

9.4.1 Unicast Client

If there is a known service locator which will know about the service, then there is no need to search. This doesn't mean that the location of the service is known, only of the locator. For example, there may be a (fictitious) organisation "All About Files" at www.all_about_files.com that would know about various file services, keeping track of them as they come on line, move, disappear, etc. A client would ask the service locator running on this site for the service, wherever it is. This client uses the unicast lookup techniques.



package client;

import common.FileClassifier;
import common.MIMEType;

import net.jini.core.discovery.LookupLocator;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import java.rmi.RMISecurityManager;
import net.jini.core.lookup.ServiceTemplate;

/**
 * TestUnicastFileClassifier.java
 */

public class TestUnicastFileClassifier {

    public static void main(String argv[]) {
	new TestUnicastFileClassifier();
    }

    public TestUnicastFileClassifier() {
	LookupLocator lookup = null;
	ServiceRegistrar registrar = null;
	FileClassifier classifier = null;

        try {
            // lookup = new LookupLocator("jini://www.all_about_files.com");
            lookup = new LookupLocator("jini://192.168.1.13");
        } catch(java.net.MalformedURLException e) {
            System.err.println("Lookup failed: " + e.toString());
	    System.exit(1);
        }

	System.setSecurityManager(new RMISecurityManager());

	try {
	    registrar = lookup.getRegistrar();
	} catch (java.io.IOException e) {
            System.err.println("Registrar search failed: " + e.toString());
	    System.exit(1);
	} catch (java.lang.ClassNotFoundException e) {
            System.err.println("Registrar search failed: " + e.toString());
	    System.exit(1);
	}

	Class[] classes = new Class[] {FileClassifier.class};
	ServiceTemplate template = new ServiceTemplate(null, classes, null);
	try {
	    classifier = (FileClassifier) registrar.lookup(template);
	} catch(java.rmi.RemoteException e) {
	    e.printStackTrace();
	    System.exit(1);
	}

	if (classifier == null) {
	    System.out.println("Classifier null");
	    System.exit(2);
	}
	MIMEType type;
	try {
	    type = classifier.getMIMEType("file1.txt");
	    System.out.println("Type is " + type.toString());
	} catch(java.rmi.RemoteException e) {
	    System.err.println(e.toString());
	}
	System.exit(0);
    }
} // TestUnicastFileClassifier







The client's JVM looks like figure 9.1. This shows a UML class diagram, "surrounded" by the JVM in which the objects exist.

Figure 9.1: Objects in client JVM
The client has main class TestFileClassifier. This has two objects of types LookupDiscovery and MIMEType. It also has objects that implement the interfaces ServiceRegistrar and FileClassifier, but it doesn't know - or need to know - what classes they are. These objects have come across the network as implementation objects of the two interfaces.

Figure 9.2 shows the situation when the lookup service's JVM is added in. The lookup service has an object implementing ServiceRegistrar, and this is the object exported to the client.

Figure 9.2: Objects in client and service locator JVM's
This shows that the client gets its registrar from the JVM of the service locator. This object is not specified in detail. Sun supply a service locator known as reggie. This locator implements the ServiceRegistrar using an implementation that neither clients nor services are expected to know. The classes which implement this object are contained in the file reggie-dl.jar and are downloaded to the clients and services using (typically) an HTTP server.

The source of the object in the client implementing FileClassifier is not yet shown: it will get that from a service, but we haven't yet discussed any of the possible implementations of a FileClassifier service.

9.4.2 Compiling the Unicast Client

The client uses a number of Jini classes. These classes must be in the classpath of the compiler. The classes are in the Jini lib directory in the jar files jsk-platform.jar and jsk-lib.jar. These need to be in the classpath for any compiler. For example


javac -classpath .../jsk-platform.jar:.../jsk-lib.jar client/TestUnicastFileClassifier.java

An Ant file to build this client is client.TestUnicastFileClassifier.xml


	  
	

9.4.3 Multicast Client

More likely, a client will need to search through all of the service locators till it finds one holding a service it is looking for. It would need to use a multicast search for this. If it only needs one occurrence of the service, then it can do something like exit after using the service. More complex behaviour will be illustrated in later examples. The client does not need to have long-term persistence. But it does need a user thread to remain in existence for long enough to find service locators and find a suitable service. So in main() a user thread again sleeps for a short period (ten seconds).



package client;

import common.FileClassifier;
import common.MIMEType;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;

/**
 * TestFileClassifier.java
 */

public class TestFileClassifier implements DiscoveryListener {

    public static void main(String argv[]) {
	new TestFileClassifier();

        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(100000L);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public TestFileClassifier() {
	System.setSecurityManager(new RMISecurityManager());

	LookupDiscovery discover = null;
        try {
            discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        discover.addDiscoveryListener(this);

    }
    
    public void discovered(DiscoveryEvent evt) {

        ServiceRegistrar[] registrars = evt.getRegistrars();
	Class [] classes = new Class[] {FileClassifier.class};
	FileClassifier classifier = null;
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       null);
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Lookup service found");
            ServiceRegistrar registrar = registrars[n];
	    try {
		classifier = (FileClassifier) registrar.lookup(template);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
		continue;
	    }
	    if (classifier == null) {
		System.out.println("Classifier null");
		continue;
	    }

	    // Use the service to classify a few file types
	    MIMEType type;
	    try {
		String fileName;

		fileName = "file1.txt";
		type = classifier.getMIMEType(fileName);
		printType(fileName, type);

		fileName = "file2.rtf";
		type = classifier.getMIMEType(fileName);
		printType(fileName, type);

		fileName = "file3.abc";
		type = classifier.getMIMEType(fileName);
		printType(fileName, type);
	    } catch(java.rmi.RemoteException e) {
		System.err.println(e.toString());
		continue;
	    }
	    // success
	    System.exit(0);
	}
    }

    private void printType(String fileName, MIMEType type) {
	System.out.print("Type of " + fileName + " is ");
	if (type == null) {
	    System.out.println("null");
	} else {
	    System.out.println(type.toString());
	}
    }

    public void discarded(DiscoveryEvent evt) {
	// empty
    }
} // TestFileClassifier

9.4.4 Compiling the Multicast Client

The client uses a number of Jini classes. These classes must be in the classpath of the compiler. The classes are in the Jini lib directory in the jar files jsk-platform.jar and jsk-lib.jar. These need to be in the classpath for any compiler. For example


javac -classpath .../jsk-platform.jar:.../jsk-lib.jar client/TestFileClassifier.java

An Ant file to build this client is client.TestFileClassifier.xml


	  
	

9.4.5 Exception Handling

A Jini program can generate a huge number of exceptions, often related to the network nature of Jini. This is not accidental, but lies at the heart of the Jini approach to network programming. Services can disappear, because the link to them has vanished, the server machine has crashed, or the service provider has died. Class files can disappear, for similar reasons applied to the HTTP server that delivers them. Timeouts can occur due to unpredictable network delays. Many of these exceptions have their own exception types such as LookupUnmarshalException which can occur when unmarshalling objects. Many others are simply wrapped in a RemoteException, which has a detail field for the wrapped exception.

Since many Jini calls can generate exceptions, these must be handled in some way. Many Java programs (rather, their programmers!) adopt a somewhat cavalier attitude to exceptions: catch them, maybe put out an error message and continue - Java makes it easy to handle errors! More seriously, whenever an exception occurs the question has to be asked as to whether the program can continue, or has its state been corrupted but not so badly that it cannot recover, or whether the program state has been damaged so much that the program must exit.

The multicast TestFileClassifier of the last section can throw exceptions at a number of places.

  1. The LookupDiscovery constructor can fail. This is indicative of some serious network error. The created discover object is needed to add a listener, and if this cannot be done, then the program really can't do anything. So it is appropriate to exit with an error value

  2. The ServiceRegistrar.lookup() can fail. This is indicative of some network error in the connection with a particular service locator. While this may have failed, it is possible that other network connections may succeed. The application can restore a consistent state by skipping the rest of the code in this iteration of the for() loop by a continue statement

  3. The FileClassifier.getMIMEType() can fail. This can be caused by a network error, or perhaps the service has simply gone away. Whatever, consistent state can again be restored by skipping the rest of this loop iteration

Finally, if one part of a program can exit with an abnormal (non-zero) error value, then a successful exit should signal its success with an exit value of zero. If this is not done, then the exit value becomes indeterminate, and of no value to other processes which may wish to establish if the program exited successfully or not.

9.5. Service Proxy

A service will be delivered from out of a service provider. That is, a server will be started, to act as a service provider. It will create one or more objects which between them will implement the service. Amongst these will be a distinguished object - the service object. The service provider will register the service object with service locators, and then wait for network requests to come in for the service. What the service provider will actually export as service object is usually a proxy for the service. The proxy is an object that will eventually run in a client, and will usually make calls back across the network to service backend objects. These backend objects running within the server actually complete the implementation of the service.

The proxy and the service backend objects are tightly integrated: they must communicate using a protocol known to them both, and must exchange information in an agreed manner. However, the relative size of each is up to the designer of a service and its proxy. For example, the proxy may be "fat" (or "smart"), which means it does a lot of processing on the client side. Backend object(s) within the service provider itself are then typically "thin", not doing much at all. Alternatively, the proxy may be "thin", doing little more (or nothing more) than passing requests between the client and "fat" backend objects, and most processing will be done by these backend objects running in the service provider.

As well as this choice of size, there is also a choice of communication mechanisms between the client and service provider objects. Client-server systems often have the choice of message-based or remote procedure call. These choices are also available between a Jini proxy and its service. Since they are both in Java, there is a standard RPC-like mechanism called RMI (Remote Method Invocation), and this can be used if wanted. There is no necessity to use this, but many implementations of Jini proxies will do so since it is easy. RMI does force a particular choice of thin proxy to fat service backend, though, and this may not be ideal for all situations.

This chapter will look at one possibility only, where the proxy is fat and is the whole of the service implementation (the service backend is an empty set of objects). The next chapter will look in more detail at the other possibilities.

9.6. Uploading a Complete Service

The file classifier service does not rely on any particular properties of its host - it is not hardware or operating system dependant, and does not make use of any files on the host side. In this case it is possible to upload the entire service to the client and let it run there. The proxy is the service, and no processing elements need to be left on the server.

9.6.1 FileClassifier implementation

The implementation of this is straightforward



package complete;

import common.MIMEType;
import common.FileClassifier;

/**
 * FileClassifierImpl.java
 */

public class FileClassifierImpl implements FileClassifier, java.io.Serializable {

    public MIMEType getMIMEType(String fileName) {
        if (fileName.endsWith(".gif")) {
            return new MIMEType("image", "gif");
        } else if (fileName.endsWith(".jpeg")) {
            return new MIMEType("image", "jpeg");
        } else if (fileName.endsWith(".mpg")) {
            return new MIMEType("video", "mpeg");
        } else if (fileName.endsWith(".txt")) {
            return new MIMEType("text", "plain");
        } else if (fileName.endsWith(".html")) {
            return new MIMEType("text", "html");
        } else
            // fill in lots of other types,
            // but eventually give up and
            return null;
    }


    public FileClassifierImpl() {
	// empty
    }
    
} // FileClassifierImpl

9.6.2 Compiling the FileClassifier implementation

This implementation consists of ordinary Java code and does not require any special libraries. It does need the FileClassifier and MIMEType in its classpath. The implementation can be compiled by a simple command


javac complete/FileClassifierImpl.java
Other implementations may require other packages to be included, of course.

9.6.3 Server

The service provider for this needs to create an instance of the exportable service object, register this and keep the lease alive. In the discovered() method it not only registers the service but also adds it to a LeaseRenewalManager, to keep the lease alive "forever". This manager runs its own threads to keep re-registering the leases, but these are daemon threads. So in the main() method the user thread goes to sleep for as long as we want the server to stay around. The code shown uses an "unsatisfied wait" condition that will sleep forever until interrupted. Note that if the server does terminate, then the lease will fail to be renewed and the exported service object will be discarded from lookup locators even though the server is not required for delivery of the service.

The serviceID is initially set to null. This may be the first time this service is ever run, or at least the first time it is ever run with this particular implementation. Since service ID's are issued by lookup services, it must remain null until at least the first registration. Then it can be extracted from the registration and re-used for all further lookup services. In addition, it can be saved in some permanent form so that if the server crashes and restarts then the service ID can be retrieved from permanent storage and used. The following server code saves and retrieves this value in a file FileClassifier.id Note that we get the service id from the registration not the registrar.


package complete;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceID ;
import net.jini.lease.LeaseListener;             
import net.jini.lease.LeaseRenewalEvent;         
import net.jini.lease.LeaseRenewalManager;       

import java.io.*;

/**
 * FileClassifierServer.java
 */

public class FileClassifierServer implements DiscoveryListener, 
                                             LeaseListener {
    
    protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
    protected ServiceID serviceID = null;
    protected 	FileClassifierImpl impl;

    public static void main(String argv[]) {
	FileClassifierServer s = new FileClassifierServer();
	
        // keep server running forever to 
	// - allow time for locator discovery and
	// - keep re-registering the lease
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(java.lang.InterruptedException e) {
		// do nothing
	    }
	}
    }

    public FileClassifierServer() {
	// Create the service
	impl = new FileClassifierImpl();

	// Try to load the service ID from file.
	// It isn't an error if we can't load it, because
	// maybe this is the first time this service has run
	DataInputStream din = null;
	try {
	    din = new DataInputStream(new FileInputStream("FileClassifier.id"));
	    serviceID = new ServiceID(din);
	} catch(Exception e) {
	    // ignore
	}

        System.setSecurityManager(new RMISecurityManager());

	LookupDiscovery discover = null;
        try {
            discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
        } catch(Exception e) {
            System.err.println("Discovery failed " + e.toString());
            System.exit(1);
        }

        discover.addDiscoveryListener(this);
    }
    
    public void discovered(DiscoveryEvent evt) {

        ServiceRegistrar[] registrars = evt.getRegistrars();

        for (int n = 0; n < registrars.length; n++) {
            ServiceRegistrar registrar = registrars[n];

	    ServiceItem item = new ServiceItem(serviceID,
					       impl, 
					       null);
	    ServiceRegistration reg = null;
	    try {
		reg = registrar.register(item, Lease.FOREVER);
	    } catch(java.rmi.RemoteException e) {
		System.err.println("Register exception: " + e.toString());
		continue;
	    }
	    System.out.println("Service registered with id " + reg.getServiceID());

	    // set lease renewal in place
	    leaseManager.renewUntil(reg.getLease(), Lease.FOREVER, this);

	    // set the serviceID if necessary
	    if (serviceID == null) {
		serviceID = reg.getServiceID();

		// try to save the service ID in a file
		DataOutputStream dout = null;
		try {
		    dout = new DataOutputStream(new FileOutputStream("FileClassifier.id"));
		    serviceID.writeBytes(dout);
		    dout.flush();
		} catch(Exception e) {
		    // ignore
		}

	    }
	}
    }

    public void discarded(DiscoveryEvent evt) {

    }

    public void notify(LeaseRenewalEvent evt) {
	System.out.println("Lease expired " + evt.toString());
    }   
    
} // FileClassifierServer

Figure 9.3 shows the server by itself running in its JVM.

Figure 9.3: Objects in server JVM
This receives an object implementing ServiceRegistrar from the service locator (such as reggie). Adding in the service locator and the client in their JVM's is shown in figure 9.4.
Figure 9.4: Objects in all the JVM's
The unknown FileClassifier object in the client is here supplied by the service object FileClassifierImpl (via the lookup service, where it is stored in passive form).

9.6.4 Compiling the Server

The server uses a number of Jini classes. These classes must be in the classpath of the compiler. The classes are in the Jini lib directory in the jar files jsk-platform.jar and jsk-lib.jar. These need to be in the classpath for any compiler. For example


javac -classpath .../jsk-platform.jar:.../jsk-lib.jar complete/FileClassifierServer.java

An Ant file to build this server is complete.FileClassifierServer.xml


	  
	

9.6.5 Client

The client for this service was discussed earlier. The client does not need any special information about this implementation of the service and so can remain quite generic.

9.6.6 What classes need to be where?

We have the classes

  1. common.MIMEType

  2. common.FileClassifier

  3. complete.FileClassifierImpl

  4. complete.FileClassifierServer

  5. client.TestFileClassifier

Instance objects of these classes could be running on upto four different machines
  1. The server machine for FileClassifier

  2. The machine for the lookup service

  3. The machine running the client TestFileClassifier

  4. An HTTP server will need to run somewhere to deliver the class file definition of FileClassifierImpl to clients

What classes need to be "known" to which machines? The meaning of "known" can vary: it can mean
  1. In the classpath of a JVM

  2. Loadable across the network

  3. Accessible by an HTTP server

Service provider
The server running FileClassifierServer needs to know the following classes and interfaces
  1. The common.FileClassifier interface

  2. The class common.MIMEType

  3. The class complete.FileClassifierServer

  4. The class complete.FileClassifierImpl

These classes all need to be in the CLASSPATH of the server.

HTTP server

The class complete.FileClassifierImpl will need to be accessible to an HTTP server, as discussed in the next section.

Lookup service

The lookup service does not need to know any of these classes. It just deals with them in the form of a java.rmi.MarshalledObject

Client

The client needs to know

  1. The common.FileClassifier interface

  2. The class common.MIMEType

  3. The class client.TestFileClassifier

These all need to be in the classpath of the client. In addition, it will need to "know" the class files for complete.FileClassifierImpl. However, these will come across the network as part of the discovery process, and this will be invisible to the client's programmer.

9.6.7 Running the FileClassifier

We now have a service FileClassifierServer and a client TestFleClassifier to run. There should also be at least one lookup locator already running. The CLASSPATH should be set for each to include the classes discussed in the last section, in addition to the standard ones.

A serialized instance of complete.FileClassifierImpl will be passed from the server to the locator and then to the client. Once on the client, it will need to be able to run the class file for this object, so will need to load its class file from an HTTP server. The location of this class file relative to the server's DocumentRoot will need to be specified by the service invocation. For example, if it could be stored in /DocumentRoot/classes/complete/FileClassifierImpl.class. The server will also be downloading a registrar object from the lookup service, so it will need a security policy. The service will be started by:


java -Djava.rmi.server.codebase=http://hostname/classes \
     -Djava.security.policy=policy.all \
     complete.FileClassifierServer
In this, hostname is the name of the host the server is running on. Note that this host name cannot be localhost, because the local host for the server will not be the local host for the client!

In this case we only need to put one file FileClassifierImpl.class on the HTTP server. Although the implementation relies on the MIMEType and the FileClassifier interface, the client has copies of these. In more complex situtations the implementation may consist of more classes, some which will not be known to the client. All of these class files may be put individually on the HTTP server, but it has become common practise to put them all into a jar file with a name including -dl (for download, such as FileClassifierImpl-dl.jar. We should also point out that service browsers will not know about the classes used by the implementation, so for them to be able to examine the service the jar file should include all classes that the service depends on. That is, the jar file should be created by


jar cf FileClassifierImpl-dl.jar \
       common/MIMEType.class \
       common/FileClassifier.class \
       complete/FileClassifierImpl.class
and the server would then be run by

java -Djava.rmi.server.codebase=http://hostname/classes/FileClassifierImpl-dl.jar \
     -Djava.security.policy=policy.all \
     complete.FileClassifierServer

The client will be loading a class definition across the network. It will need to allow this in a security policy file by


java -Djava.security.policy=policy.all client.TestFileClassifier
The client does not need to know anything about the implementation classes. It just needs to know the interface FileClassifier, the MIMEType class and the standard Jini classes. All other classes are downloaded as needed from the HTTP server specified by the service.

9.7. Summary

The material of the previous chapters is put together in a simple example. The requirements of class structures for a Jini system are discussed. A discussion is also given of what classes need to be available to each component of a Jini system.

9.8. Copyright

If you found this chapter of value, the full book "Foundations of Jini 2 Programming" is available from APress or Amazon .

This file is Copyright (©) 1999, 2000, 2001, 2003, 2004, 2005 by Jan Newmarch (http://jan.netcomp.monash.edu.au) [email protected].

Creative Commons License This work is licensed under a Creative Commons License, the replacement for the earlier Open Content License.