More Complex Examples

This chapter delves into some of the more complex things that can happen with Jini applications.

Contents

  1. Where is the Code?
    1. Problem domain
    2. NameEntry interface
    3. Implementation one
    4. What files need to be where?
    5. Implementation two
    6. What files need to be where?
    7. Implementation three
    8. What files need to be where?
  2. Running threads from discovery
    1. Server threads
    2. Join Manager threads
    3. Client threads
  3. Re-using RMI Proxies
  4. Inexact Service Matching
  5. Matching using Local Services

1. Where is the Code?

Clients, servers and service locators can use code from a variety of sources. Which source it uses can depend on the structure of a client and a server. This section looks at some of the variations that can occur.

1.1 Problem domain

A service may require information about a client before it can (or will) proceed. For example, a banking service may require a user id and a PIN number. Using the techniques already discussed, this could be done by the client collecting the information and calling suitable methods such as void setName(String name) in the service (or more likely, in the service's proxy) running in the client.


public class Client {

    String getName();

}

class Service {

    void setName(String name);

}

A service may wish to have more control over the setting of names and passwords than this. For example, it may wish to run verification routines based on the pattern of keystroke entries. More mundanely, it may wish to set time limits on the period between entering the name and the password. Or it may wish to enforce some particular user interface to collect this information. Whatever, the service proxy may wish to perform some sort of input processing on the client side before communicating with the real service. This section explores what happens when the service proxy needs to find extra classes in order to perform this processing.

A standalone application to get a user name might use a GUI interface such as



package standalone;

import java.awt.*;
import java.awt.event.*;

/**
 * NameEntry.java
 *
 *
 * Created: Sun Mar 28 23:47:02 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class NameEntry extends Frame {
    
    public NameEntry() {
	super("Name Entry");
	addWindowListener(new WindowAdapter() {
	    public void windowClosing(WindowEvent e) {System.exit(0);}
	    public void windowOpened(WindowEvent e) {}});

	Label label = new Label("Name");
	TextField name = new TextField(20);
	add(label, BorderLayout.WEST);
	add(name, BorderLayout.CENTER);
	name.addActionListener(new NameHandler());

	pack();
    }
    
    public static void main(String[] args) {
	
	NameEntry f = new NameEntry();
	f.show();
    }
} // NameEntry

class NameHandler implements ActionListener {
    public void actionPerformed(ActionEvent evt) {
	System.out.println("Name was: " + evt.getActionCommand());
    }
}

The classes used in this are
  1. A set of standard classes, Frame, Label, TextField, ActionListener, ActionEvent, BorderLayout, WindowEvent, System.
  2. A couple of new classes NameEntry, NameHandler.
At compile time and at runtime these will need to be accessible.

1.2 NameEntry interface

In moving to a Jini system, we have already seen that different components may only need access to a subset of the total set of classes. The same will apply here, but it will critically depend on how the application is changed into a Jini system.

We don't want to be overly concerned about program logic of what is done with the user name once it has been entered, as the interesting part in this section is the location of classes. All versions will need an interface definition, which we can make simply as



package common;

/**
 * NameEntry.java
 *
 *
 * Created: Mon Mar 29 11:36:25 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public interface NameEntry  {
    
    public void show();
    
} // NameEntry

Then the client can call upon an implementation to simply show() itself and collect information in whatever way it chooses. (Note: we don't want to get involved here in the ongoing discussion about the most appropriate interface definition for GUI classes!)

1.3 Implementation one

The first version of an implementation is



package complex;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;

/**
 * NameEntryImpl1.java
 *
 *
 * Created: Mon Mar 29 11:38:33 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class NameEntryImpl1 extends Frame implements common.NameEntry,
                                          ActionListener, java.io.Serializable {
    
    public NameEntryImpl1() {
	super("Name Entry");
	/*
	addWindowListener(new WindowAdapter() {
	    public void windowClosing(WindowEvent e) {System.exit(0);}
	    public void windowOpened(WindowEvent e) {}});
	*/
	setLayout(new BorderLayout());
	Label label = new Label("Name");
	add(label, BorderLayout.WEST);
	TextField name = new TextField(20);
	add(name, BorderLayout.CENTER);
	name.addActionListener(this);

	// don't do this here!
	// pack();
    }

    /**
     * method invoked on pressing <return> in the TextField
     */
    public void actionPerformed(ActionEvent evt) {
	System.out.println("Name was: " + evt.getActionCommand());
    }

    public void show() {
	pack();
	super.show();
    }
    
    
} // NameEntryImpl1

This creates the GUI elements in the constructor. When exported, this entire user interface will be serialized and exported. It isn't too big in this case (about 2,100 bytes), but that is because the example is small. A GUI with several hundred objects will be much larger. This is overhead, which could be avoided because the classes are all available on the client side, anyway.

Another problem with this code is that it firstly creates an object on the server machine that has heavy reliance on environmental factors on the server. It then removes itself from that environment and has to re-establish itself on the target client environment. On my current system, this shows by TextField complaining that it cannot find a whole bunch of fonts on my server machine. Well it doesn't matter here: it gets moved to the client machine. (As it happens, the fonts aren't available there either, so I end with two batches of complaint messages, from the server and from the client. I should only get the client complaints.) It could matter if the service died because of missing stuff on the server side, which would exist on the client.

1.4 What files need to be where?

The client needs to know the interface class NameEntry

The server needs to know the class files for

  1. NameEntry
  2. Server1
  3. NameEntryImpl1

The HTTP server needs to know the class files for

  1. NameEntryImpl1

1.5 Implementation two

The second implementation minimises the amount of serialised code that must be shipped around, by creating as much as possible on the client side. We don't even need to declare the class as a subclass of Frame as that class also exists on the client side. The client calls the interface method show(), and all the GUI creation is moved to there.



package complex;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;

/**
 * NameEntryImpl2.java
 *
 *
 * Created: Mon Mar 29 11:38:33 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class NameEntryImpl2 implements common.NameEntry,
                                       ActionListener, java.io.Serializable {
    
    public NameEntryImpl2() {
    }

    /**
     * method invoked on pressing <return> in the TextField
     */
    public void actionPerformed(ActionEvent evt) {
	System.out.println("Name was: " + evt.getActionCommand());
    }

    public void show() {
	Frame fr = new Frame("Name Entry");

	fr.addWindowListener(new WindowAdapter() {
	    public void windowClosing(WindowEvent e) {System.exit(0);}
	    public void windowOpened(WindowEvent e) {}});

	fr.setLayout(new BorderLayout());
	Label label = new Label("Name");
	fr.add(label, BorderLayout.WEST);
	TextField name = new TextField(20);
	fr.add(name, BorderLayout.CENTER);
	name.addActionListener(this);

	fr.pack();
	fr.show();
    }
    
    
} // NameEntryImpl2

There are some standard classes that cannot be serialised: one example is the Swing JTextArea class (as of Swing 1.1). This has been frequently logged as a bug against Swing. Until this is fixed, the only way one of these objects can be used by a service is to create it on the client.

1.6 What files need to be where?

The client needs to know the interface class NameEntry

The server needs to know the class files for

  1. NameEntry
  2. Server2
  3. NameEntryImpl2
  4. NameEntryImpl2$1
The last class in the list is an anonymous class, that acts as the WindowListener. The class file is produced by the compiler. In version 1, this part of the code was commented out for simplicity.

The HTTP server needs to know the class files for

  1. NameEntryImpl1
  2. NameEntryImpl1$1

1.7 Implementation three

Apart from the standard classes and a common interface, the previous implementations just used a single class that was uploaded to the lookup service and then passed on to the client. A more realistic situation might require the uploaded service to access a number of other classes that could not be expected to be on the client machine. It is simple to modify the examples to use a server-side specific class for the action listener, instead of the class itself. This looks like



package complex;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;

/**
 * NameEntryImpl3.java
 *
 *
 * Created: Mon Mar 29 11:38:33 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class NameEntryImpl3 implements common.NameEntry,
                                       java.io.Serializable {
    
    public NameEntryImpl3() {
    }

    public void show() {
	Frame fr = new Frame("Name Entry");

	fr.addWindowListener(new WindowAdapter() {
	    public void windowClosing(WindowEvent e) {System.exit(0);}
	    public void windowOpened(WindowEvent e) {}});

	fr.setLayout(new BorderLayout());
	Label label = new Label("Name");
	fr.add(label, BorderLayout.WEST);
	TextField name = new TextField(20);
	fr.add(name, BorderLayout.CENTER);
	name.addActionListener(new NameHandler());

	fr.pack();
	fr.show();
    }
    
    
} // NameEntryImpl3

class NameHandler implements ActionListener {
    /**
     * method invoked on pressing <return> in the TextField
     */
    public void actionPerformed(ActionEvent evt) {
	System.out.println("Name was: " + evt.getActionCommand());
    }
} // NameHandler

This version uses a class NameHandler that only exists on the server machine. When the client attempts to deserialise the NameEntryImpl3 instance it will fail to find this class, and be unable to complete deserialisation. How is this resolved? Well, in the same way as before, by making it available through the HTTP server.

1.8 What files need to be where?

The client needs to know the interface class NameEntry

The server needs to know the class files for

  1. NameEntry
  2. Server1
  3. NameEntryImpl1
  4. NameEntryImpl1$1
  5. NameHandler
The NameHandler class file is another one produced by the compiler.

The HTTP server needs to know the class files for

  1. NameEntryImpl1
  2. NameEntryImpl1$1
  3. NameHandler

2. Running threads from discovery

In all of the simple examples using explicit registration, a single thread was used. That is, as a service locator was discovered, the registration process commenced in the same thread. Now this registration may take some time, so it would be preferable to do this in a separate thread. This is also recommended in the specification.

2.1 Server threads

Running another thread is not a difficult procedure. Basically we have to define a new class that extends Thread and move most of the registration into its run method. This is done in the following version of the ``option 2'' file classifier server, using an inner class


package complex;

import option2.FileClassifierImpl;

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 com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;

/**
 * FileClassifierServer.java
 *
 *
 * Created: Wed Mar 17 14:23:44 1999
 *
 * @author Jan Newmarch
 * @version 1.1
 *    added LeaseRenewalManager
 *    moved sleep() from constructor to main()
 */

public class FileClassifierServer implements DiscoveryListener, 
                                             LeaseListener {
    
    protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();

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

        // keep server running forever to 
	// - allow time for locator discovery and
	// - keep re-registering the lease
	try {
            Thread.currentThread().sleep(Lease.FOREVER);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public FileClassifierServer() {

	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();

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

	    new RegisterThread(registrar).start();
	}
    }

    public void discarded(DiscoveryEvent evt) {

    }

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

    /**
     * an inner class to register the service in its own thread
     */
    class RegisterThread extends Thread {

	ServiceRegistrar registrar;

	RegisterThread(ServiceRegistrar registrar) {
	    this.registrar = registrar;
	}

	public void run() {
	    ServiceItem item = new ServiceItem(null,
					       new FileClassifierImpl(), 
					       null);
	    ServiceRegistration reg = null;
	    try {
		reg = registrar.register(item, Lease.FOREVER);
	    } catch(java.rmi.RemoteException e) {
		System.err.println("Register exception: " + e.toString());
	    }
	    System.out.println("service registered");

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

2.2 Join Manager threads

If you use a JoinManager to handle lookup and registration, then it essentially does this for you: creates a new thread to handle registration. Thus the examples in the chapter on ``JoinManager'' do not need any modification, as the JoinManager already uses the concepts of this section.

2.3 Client threads

It is probably more important to create threads in the client than in the server, since the client will actually perform some computation (which may be lengthy) based on the service it discovers. Again this is a simple matter, moving code into a new class that implements Thread. Doing this to the TestFileClassifier results in



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;


/**
 * TestFileClassifierThread.java
 *
 *
 * Created: Tue Apr 17 14:29:15 1999
 *
 * @author Jan Newmarch
 * @version 1.1
 *    simplified Class.forName to Class.class
 */

public class TestFileClassifierThread implements DiscoveryListener {

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

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

    public TestFileClassifierThread() {
	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();
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Service found");
            ServiceRegistrar registrar = registrars[n];

	    new LookupThread(registrar).start();
	}

	    // System.exit(0);
    }

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

    class LookupThread extends Thread {

	ServiceRegistrar registrar;

	LookupThread(ServiceRegistrar registrar) {
	    this.registrar = registrar;
	}


	public void run() {

	    Class[] classes = new Class[] {FileClassifier.class};
	    FileClassifier classifier = null;
	    ServiceTemplate template = new ServiceTemplate(null, classes, 
							   null);
	    
	    try {
		classifier = (FileClassifier) registrar.lookup(template);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
		System.exit(2);
	    }
	    if (classifier == null) {
		System.out.println("Classifier null");
		return;
	    }
	    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());
	    }
	}
    }

} // TestFileClassifier


3. Re-using RMI Proxies

In the option 3 example of ``Simple Examples'', a proxy of our design was created. However, when it came to communicating back to the original service, a remote method call using RMI was used, and for this the proxy had to locate an RMI stub (or RMI proxy) using Naming.lookup(). Thus we ended up with two proxies on the client!

This section investigates one of the two possibilities in reducing the number of proxies, and that is to reuse the RMI proxy as the only service proxy. To do this, perform the RMI proxy lookup on the server side instead of the client side. Then instead of exporting our own proxy, export the proxy returned from the lookup. There is then no need for our own proxy at all!

A version of the option 3 file classifier to do this is



package complex;

import option3.FileClassifierImpl;
import option3.RemoteFileClassifier;

import java.rmi.Naming;
import java.net.InetAddress;
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 com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
import java.rmi.RMISecurityManager;

/**
 * FileClassifierServer.java
 *
 *
 * Created: Wed Mar 17 14:23:44 1999
 *
 * @author Jan Newmarch
 * @version 1.1
 *    added LeaseRenewalManager
 *    moved sleep() from constructor to main()
 */

public class FileClassifierServerRMI implements DiscoveryListener, LeaseListener {

    // this is just a name - can be anything
    // impl object forces search for Stub
    static final String serviceName = "FileClassifier";
    String registeredName;

    protected FileClassifierImpl impl;
    protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
    
    public static void main(String argv[]) {
	new FileClassifierServerRMI();

        // no need to keep server alive, RMI will do that
    }

    public FileClassifierServerRMI() {
	try {
	    impl = new FileClassifierImpl();
	} catch(Exception e) {
            System.err.println("New impl: " + e.toString());
            System.exit(1);
	}

	// register this with RMI registry
	System.setSecurityManager(new RMISecurityManager());
	/*
	try {
	    Naming.rebind("rmi://localhost/" + serviceName, impl);
	} catch(java.net.MalformedURLException e) {
            System.err.println("Binding: " + e.toString());
            System.exit(1);
	} catch(java.rmi.RemoteException e) {
            System.err.println("Binding: " + e.toString());
            System.exit(1);
        }
	*/

	System.out.println("bound");

	// find where we are running
	String address = null;
	try {
	    address = InetAddress.getLocalHost().getHostName();
	} catch(java.net.UnknownHostException e) {
            System.err.println("Address: " + e.toString());
            System.exit(1);
        }

	registeredName = "//" + address + "/" + serviceName;
	
	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();
	RemoteFileClassifier service;
	/*
	try {
	    Object obj = Naming.lookup(registeredName);
	    service = (RemoteFileClassifier) obj;
	} catch(Exception e) {
	    e.printStackTrace();
	    return;
	}
	*/

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

	    // export the proxy service
	    ServiceItem item = new ServiceItem(null,
					       impl, //service,
					       null);
	    ServiceRegistration reg = null;
	    try {
		reg = registrar.register(item, Lease.FOREVER);
	    } catch(java.rmi.RemoteException e) {
		System.err.print("Register exception: ");
		e.printStackTrace();
		// System.exit(2);
		continue;
	    }
	    try {
		System.out.println("service registered at " +
				   registrar.getLocator().getHost());
	    } catch(Exception e) {
	    }
	    leaseManager.renewFor(reg.getLease(), Lease.FOREVER, this);
	}
    }

    public void discarded(DiscoveryEvent evt) {

    }

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

4. Inexact Service Matching

Suppose you have a printer service that prints at 30 pages per minute. A client wishes to find a printer that will print at least 24 pages per minute. How will this client find the service? The standard Jini pattern matching will either be for an exact match on an attribute or an ignored match on an attribute. So the only way a client can find this printer is to ignore the speed attribute and perform a later selection among all the printers that it sees.

We can define a printer interface that will allow us to access printer speed (plus other capabilities) as


package common;

import java.io.Serializable;

/**
 * Printer.java
 *
 *
 * Created: Tue Apr 20 21:16:22 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public interface Printer extends Serializable {
    
    public void print(String str);
    public int getSpeed();

} // Printer

Given this, a client can choose a suitably fast printer in a two-step process

  1. Find a service using the lookup exact/ignore match algorithm
  2. Query the service to see if it satifies other types of boolean condition
The following program illustrates how to find a printer that is ``fast enough'':


package client;

import common.Printer;

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;
import net.jini.core.lookup.ServiceMatches;

/**
 * TestPrinterSpeed.java
 *
 *
 * Created: Mon Mar 29 12:03:51 1999
 *
 * @author Jan Newmarch
 * @version 1.1
 *    simplified Class.forName to Class.class
 */

public class TestPrinterSpeed implements DiscoveryListener {
    
    public TestPrinterSpeed() {

	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[] {Printer.class};

        ServiceTemplate template = new ServiceTemplate(null, classes, 
                                                       null);
 
        for (int n = 0; n < registrars.length; n++) {
            ServiceRegistrar registrar = registrars[n];
	    ServiceMatches matches;

	    try {
		matches = registrar.lookup(template, 10);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
		continue;
	    }
	    // NB: matches.totalMatches may be greater than matches.items.length
	    for (int m = 0; m < matches.items.length; m++) {
		Printer printer = (Printer) matches.items[m].service;

		// Inexact matching is not performed by lookup()
		// we have to do it ourselves on each printer
		// we get
		int speed = printer.getSpeed();
		if (speed >= 24) {
		    // this one is okay, use its print() method
		    printer.print("fast enough printer");
		} else {
		    // we can't use this printer, so just say so
		    System.out.println("Printer too slow at " + speed);
		}
	    }

        }
    }

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

    public static void main(String[] args) {
	
	TestPrinterSpeed f = new TestPrinterSpeed();

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

5. Matching using Local Services

When a user connects their laptop into a brand-new network, they will probably know little about the environment they have joined. If they want to use services in this network, they will probably want to use general terms and have them translated into specific terms for this new environment. For example, the user may want to print a file on a nearby printer. In this situation, there is little likelihood that the new user knows how to work out the distance between themselves and the printers. However, a local service could be running which does know how to calculate physical distances between objects on the network.

Finding a ``close enough'' printer then becomes a matter of querying service locators both for printers and for a distance service. As each printer is found, the distance service can be asked to calculate the distance between itself and the laptop (or camera, or any other device that wants to print).

The complexity of the task to be done by clients is growing: a client has to find two sets of services, and when it finds one (a printer) invoke the other (the distance service). This calls for lookup processing to be handled in separate threads. In addition, as each locator is found, it may know about printers, it may know about distance services, both, or none! When the client starts up, it will be discovering these services in arbitrary order, and the code must be structured to deal with this.

Some of the cases that may arise are

  1. A printer may be discovered before any distance service has been found. In this case, the printer must be stored for later distance checking.
  2. A printer may be discovered after a distance service has been found. It can be checked immediately.
  3. A distance service is found, after some printers have been found. This saved set should be checked at this point.
In this problem, we only need to find one distance service, but possibly many printers. The client code given shortly will save printers in a Vector, and a distance service in a single variable.

In searching for printers, we only want to find those that have location information. However, we do not want to match on any particular values. The client will have to use wildcard patterns in a location object. The location information of a printer will need to be retrieved along with the printer, so it can be used. So instead of just storing printers, we need to store ServiceItem's, which carry the attribute information as well as the objects.

Of course, for this to work, the client also needs to know where it is!

A client satisfying these requirements is



package client;

import common.Printer;
import common.Distance;

import java.util.Vector;

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;
import net.jini.lookup.entry.Location;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceMatches;
import net.jini.core.entry.Entry;

/**
 * TestPrinterDistance.java
 *
 *
 * Created: Tue Apr 17 14:29:15 1999
 *
 * @author Jan Newmarch
 * @version 1.1
 *    simplified Class.forName to Class.class
 */

public class TestPrinterDistance implements DiscoveryListener {

    protected Distance distance = null;
    protected Object distanceLock = new Object();
    protected Vector printers = new Vector();

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

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

    public TestPrinterDistance() {
	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();
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Service found");
            ServiceRegistrar registrar = registrars[n];

	    new LookupThread(registrar).start();
	}

	    // System.exit(0);
    }

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

    class LookupThread extends Thread {

	ServiceRegistrar registrar;

	LookupThread(ServiceRegistrar registrar) {
	    this.registrar = registrar;
	}


	public void run() {

	    synchronized(distanceLock) {
		// only look for one distance service
		if (distance == null) {
		    lookupDistance();
		}
		if (distance != null) {
		    // found a new distance service
		    // process any previously found printers
		    synchronized(printers) {
			for (int n = 0; n < printers.size(); n++) {
			    ServiceItem item = (ServiceItem) printers.elementAt(n);
			    reportDistance(item);
			}
		    }
		}
	    }

	    ServiceMatches matches = lookupPrinters();
	    for (int n = 0; n < matches.items.length; n++) {
		if (matches.items[n] != null) {
		    synchronized(distanceLock) {
			if (distance != null) {
			    reportDistance(matches.items[n]);
			} else {
			    synchronized(printers) {
				printers.addElement(matches.items[n]);
			    }
			}
		    }
		}
	    }
	}

	/*
	 * We must be protected by the lock on distanceLock here
	 */
	void lookupDistance() {
	    // If we don't have a distance service, see if this
	    // locator knows of one
	    Class[] classes = new Class[] {Distance.class};
	    ServiceTemplate template = new ServiceTemplate(null, classes, 
							   null);
	    
	    try {
		distance = (Distance) registrar.lookup(template);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
	    }

	}

	ServiceMatches lookupPrinters() {
	    // look for printers with
	    // wildcard matching on all fields of Location
	    Entry[] entries = new Entry[] {new Location(null, null, null)};

	    Class[] classes = new Class[1];
	    try {
		classes[0] = Class.forName("common.Printer");
	    } catch(ClassNotFoundException e) {
		System.err.println("Class not found");
		System.exit(1);
	    }
	    ServiceTemplate template = new ServiceTemplate(null, classes, 
							   entries);
	    ServiceMatches matches = null;
	    try {
		matches =  registrar.lookup(template, 10);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
	    }
	    return matches;
	}

	/**
	 * report on the distance of the printer from
	 * this client
	 */
	void reportDistance(ServiceItem item) {
	    Location whereAmI = getMyLocation();
	    Location whereIsPrinter = getPrinterLocation(item);
	    if (whereIsPrinter != null) {
		int dist = distance.getDistance(whereAmI, whereIsPrinter);
		System.out.println("Found a printer at " + dist +
				   " units of length away");
	    }
	}

	Location getMyLocation() {
	    return new Location("1", "1", "Building 1");
	}

	Location getPrinterLocation(ServiceItem item) {
	    Entry[] entries = item.attributeSets;
	    for (int n = 0; n < entries.length; n++) {
		if (entries[n] instanceof Location) {
		    return (Location) entries[n];
		}
	    }
	    return null;
	}
    }

} // TestFileClassifier


A number of services will need to be running. At least one distance service will be needed, implementing the interface Distance


package common;

import net.jini.lookup.entry.Location;

/**
 * Distance.java
 *
 *
 * Created: Wed Apr 21 13:12:35 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public interface Distance extends java.io.Serializable {
    
    int getDistance(Location loc1, Location loc2);

} // Distance

An example implementation is

package complex;

import net.jini.lookup.entry.Location;

/**
 * DistanceImpl.java
 *
 *
 * Created: Wed Apr 21 15:24:15 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class DistanceImpl implements common.Distance {
    
    public DistanceImpl() {
	
    }

    /**
     * A very naive distance metric
     */
    public int getDistance(Location loc1, Location loc2) {
	int room1, room2;
	try {
	    room1 = Integer.parseInt(loc1.room);
	    room2 = Integer.parseInt(loc2.room);
	} catch(Exception e) {
	    return -1;
	}
	int value = room1 - room2;
	return (value > 0 ? value : -value);
    }
    
} // DistanceImpl

We have already covered some printers, we can just reuse them. A simple program to start up a distance service and two printers is

package complex;

import printer.Printer30;
import printer.Printer20;
import complex.DistanceImpl;

import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.lookup.entry.Location;
import net.jini.core.entry.Entry;

/**
 * PrinterServerLocation.java
 *
 *
 * Created: Wed Mar 17 14:23:44 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class PrinterServerLocation implements ServiceIDListener {
    
    public static void main(String argv[]) {
	new PrinterServerLocation();

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

    }

    public PrinterServerLocation() {

	JoinManager joinMgr = null;
	try {
	    // distance service
	    joinMgr = new JoinManager(new DistanceImpl(),
				      null,
				      this,
				      new LeaseRenewalManager());
	    joinMgr.setGroups(null);


	    // slow printer in room 20
	    joinMgr = new JoinManager(new Printer20(),
				      new Entry[] {new Location("1", "20", 
								"Building 1")},
				      this,
				      new LeaseRenewalManager());
	    joinMgr.setGroups(null);

	    // fast printer in room 30
	    joinMgr = new JoinManager(new Printer30(),
				      new Entry[] {new Location("1", "30", 
								"Building 1")},
				      this,
				      new LeaseRenewalManager());
	    joinMgr.setGroups(null);


	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    public void serviceIDNotify(ServiceID serviceID) {
	System.out.println("got service ID " + serviceID.toString());
    }
    
} // PrinterServerLocation

This file is Copyright ©Jan Newmarch (http://jan.newmarch.name) jan@newmarch.name

The copyright is the OpenContent License (http://www.opencontent.org/opl.shtml), which is the ``document'' version of the GNU OpenSource license.