Chapter 17: ServiceDiscoveryManager

The class ServiceDiscoveryManager is only available in Jini 1.1 and later. It performs client-side functions similar to that of JoinManager for services, and simplifies the tasks of finding services.

17.1. Introduction

Both clients and services need to find lookup services. Both can do this using low-level core classes, or discovery utilities such as LookupDiscoveryManager. Once a lookup service is found, a service just needs to register with it and try to keep the lease alive for as long as it wants to. A service can make use of the JoinManager class for this.

Clients are a little more complex in their requirements:

  1. A client may wish to use a service immediately or later

  2. A client may want to use multiple services

  3. A client will want to find services by their interface, but may also want to apply additional criteria, such as being a ``fast enough'' printer

  4. A client may just wish to use a service if it is available at the time of request, but alternatively may want to be informed of new services becoming available and respond to this new availability (for example, a service browser)

The class ServiceDiscoveryManager is a utility class designed to help in the various possible cases that can occur. Due to the variety of these, it is more complex than JoinManager. Its interface includes


package net.jini.lookup;

public class ServiceDiscoveryManager {
    public ServiceDiscoveryManager(DiscoveryManagement discoveryMgr,
                               LeaseRenewalManager leaseMgr)
           throws IOException;

    public ServiceDiscoveryManager(DiscoveryManagement discoveryMgr,
                               LeaseRenewalManager leaseMgr,
                               Configuration config)
           throws IOException,
                  ConfigurationException;

    LookupCache createLookupCache(ServiceTemplate tmpl,
                                  ServiceItemFilter filter,
                                  ServiceDiscoveryListener listener);

    ServiceItem[] lookup(ServiceTemplate tmpl,
                         int maxMatches, ServiceItemFilter filter);

    ServiceItem lookup(ServiceTemplate tmpl,
                       ServiceItemFilter filter);

    ServiceItem lookup(ServiceTemplate tmpl,
                       ServiceItemFilter filter, long wait);

    ServiceItem[] lookup(ServiceTemplate tmpl,
                         int minMaxMatch, int maxMatches,
                         ServiceItemFilter filter, long wait);

    void terminate();
}

17.2. ServiceItemFilter

Most methods of the client lookup manager require a ServiceItemFilter. This is a simple interface, designed to be an additional filter on the client side to help in finding services. The primary mechanisms for a client to find a service are to ask for an instance of an interface, possibly with additional entry attributes. This matching is performed on the lookup service, and only implements a form of exact pattern matching. It allows one to ask for a toaster that will handle two slices of toast exactly, but not for one which will toast two or more. To perform arbitrary boolean matching on the lookup service is a security issue, plus possibly a performance issue on the lookup service. The ServiceItemFilter allows additional boolean filtering to be performed on the client side, by client code, so these issues are local to the client only.

The interface is


package net.jini.lookup;

public interface ServiceItemFilter {
    boolean check(ServiceItem item);
}
A client filter will implement this to perform additional checking.

Client-side filtering will not answer all of the problems of locating the ``best'' service. Some questions will still need to use other services which know ``local'' information such as distances in a building

17.3. Finding a Service Immediately

The simplest scenario for a client is that it wants to find a service immediately, use it and then (perhaps) terminate. It will be prepared to wait a certain amount of time before giving up. All issues of discovery can be given to the ServiceDiscoveryManager, and finding a service can be given to a method such as lookup() with a wait parameter. The lookup() method will block until a suitable service is found or the time limit is reached. If the time limit is reached, a null object will be returned otherwise a non-null service object will be returned.



package client;

import common.FileClassifier;
import common.MIMEType;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.core.lookup.ServiceItem;
import net.jini.lease.LeaseRenewalManager;

/**
 * ImmediateClientLookup.java
 */

public class ImmediateClientLookup {

    private static final long WAITFOR = 100000L;

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

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

    public ImmediateClientLookup() {
	ServiceDiscoveryManager clientMgr = null;

	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null, // unicast locators
                                           null); // DiscoveryListener
	    clientMgr = new ServiceDiscoveryManager(mgr, 
						new LeaseRenewalManager());
	} catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
  
	Class [] classes = new Class[] {FileClassifier.class};
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       null);

	ServiceItem item = null;
	// Try to find the service, blocking till timeout if necessary
	try {
	    item = clientMgr.lookup(template, 
				    null, // no filter 
				    WAITFOR); // timeout
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	if (item == null) {
	    // couldn't find a service in time
	    System.out.println("no service");
	    System.exit(1);
	}

	// Get the service
	FileClassifier classifier = (FileClassifier) item.service;

	if (classifier == null) {
	    System.out.println("Classifier null");
	    System.exit(1);
	}

	// Now we have a suitable service, use it
	MIMEType type;
	try {
	    String fileName;
	    
	    // Try several file types: .txt, .rtf, .abc
	    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());
	}
	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());
	}
    }
} // ImmediateClientLookup

17.4. Using a Filter

An example of an earlier chapter discussed how to select a printer with a speed greater than a certain value. This type of problem is well suited to the ServiceDiscoveryManager using a ServiceItemFilter.



package client;

import common.Printer;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.core.lookup.ServiceItem;
import net.jini.lease.LeaseRenewalManager;
import net.jini.lookup.ServiceItemFilter;
/**
 * TestPrinterSpeedFilter.java
 */

public class TestPrinterSpeedFilter implements ServiceItemFilter {
    private static final long WAITFOR = 100000L;
    
    public TestPrinterSpeedFilter() {
	ServiceDiscoveryManager clientMgr = null;

	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null,  // unicast locators
                                           null); // DiscoveryListener
	    clientMgr = new ServiceDiscoveryManager(mgr, 
						new LeaseRenewalManager());
	} catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        Class[] classes = new Class[] {Printer.class};

        ServiceTemplate template = new ServiceTemplate(null, classes, 
                                                       null);
 	ServiceItem item = null;
	try {
	    item = clientMgr.lookup(template, 
				    this,     // filter 
				    WAITFOR); // timeout
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	if (item == null) {
	    // couldn't find a service in time
	    System.exit(1);
	}

	Printer printer = (Printer) item.service;
	// Now use the printer
	// ...
    }

    public boolean check(ServiceItem item) {
	// This is the filter
	Printer printer = (Printer) item.service;
	if (printer.getSpeed() > 24) {
	    return true;
	} else {
	    return false;
	}
    }

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

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

17.5. Building a Cache of Services

A client may wish to make use of a service multiple times. If it just found a suitable reference to a service, then before each use it would have to check that the reference was still valid, and if not, find another one. It may also want to use minor variants of a service, such as a fast printer one time, a slow one the next. While this management can be done easily enough for each case, the ServiceDiscoveryManager can supply a cache of services which will do it for you. This cache will look after monitoring lookup services to keep the cache as up-to-date as possible with services.

The cache is defined as an interface


package net.jini.lookup;
  
public interface LookupCache {
    public ServiceItem lookup(ServiceItemFilter filter);
    public ServiceItem[] lookup(ServiceItemFilter filter, 
                                int maxMatches);                                           
    public void addListener(ServiceDiscoveryListener l);
    public void removeListener(ServiceDiscoveryListener l); 
    public void discard(Object serviceReference);
    void terminate(); 
}
and a suitable implementation object may be created by the ServiceDiscoveryManager method

    LookupCache createLookupCache(ServiceTemplate tmpl,
                                  ServiceItemFilter filter,
                                  ServiceDiscoveryListener listener);
We shall ignore the ServiceDiscoveryListener until the next section. It can be set to null in createLookupCache().

The LookupCache created by createLookupCache() takes a template for matching against interface and entry attributes. In addition it also takes a filter to perform additional client-side boolean filtering of services. The cache will then maintain a set of references to services matching the template and passing the filter. These references are all local to the client, and consist of the service proxies and their attributes downloaded to the client. Searching for a service can then be done by local methods LookupCache.lookup(). These can take an additional filter which can be used to further refine the set of services returned to the client.

The search in the cache is done directly on the proxy services and their attributes already found by the client, and does not involve querying lookup services. Essentially, this uses a tradeoff of lookup service activity while the client is idle in order to produce fast local response when the client is active.

There are versions of the ServiceDiscoveryManager.lookup() with a time parameter, which block until a service is found or the method times out. These methods do not use polling, but instead use event notification. This is because they are trying to find services based on remote calls to lookup services. The lookup() methods of LookupCache do not implement such a blocking call, because the methods run purely locally and it is reasonable to poll the cache for a short time if need be.

A version of the file classifier client that creates and examines the cache for suitable service is



package client;

import common.FileClassifier;
import common.MIMEType;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.lookup.LookupCache;
import net.jini.core.lookup.ServiceItem;
import net.jini.lease.LeaseRenewalManager;

/**
 * CachedClientLookup.java
 */

public class CachedClientLookup {

    private static final long WAITFOR = 100000L;

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

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

    public CachedClientLookup() {
	ServiceDiscoveryManager clientMgr = null;
	LookupCache cache = null;

	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null,  // unicast locators
                                           null); // DiscoveryListener
	    clientMgr = new ServiceDiscoveryManager(mgr, 
						new LeaseRenewalManager());
	} catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
  
	Class [] classes = new Class[] {FileClassifier.class};
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       null);

	try {
	    cache = clientMgr.createLookupCache(template, 
						null,  // no filter 
						null); // no listener
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}

	// loop till we find a service
	ServiceItem item = null;
	while (item == null) {
	    System.out.println("no service yet");
	    try {
		Thread.currentThread().sleep(1000);
	    } catch(java.lang.InterruptedException e) {
		// do nothing
	    }
	    // see if a service is there now
	    item = cache.lookup(null);
	}
	FileClassifier classifier = (FileClassifier) item.service;

	if (classifier == null) {
	    System.out.println("Classifier null");
	    System.exit(1);
	}

	// Now we have a suitable service, use it
	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());
	}
	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());
	}
    }
} // CachedClientLookup

17.5.1 Running the CachedClientLookup

While it is okay to poll the local cache, the cache itself must get its contents from lookup services, and in general it is not okay to poll these since that involves possibly heavy network traffic. The cache itself gets its information by registering itself as a listener for service events from the lookup services. The lookup services will then call notify() on the cache listener. This call is a remote call from the remote lookup service to the local cache, done (probably) using an RMI stub. In fact, the Sun implementation of ServiceDiscoveryManager uses a nested class ServiceDiscoveryManager.LookupCacheImpl.LookupListener which has an RMI stub.

So under the hood, any client that uses the caching mechanism of the ServiceDiscoveryManager acts as a listener to the lookup service, and must upload a proxy to it. The code for this proxy's class is now in one of the standard Jini jar files, jsk-lib.jar. If you are running Sun's lookup service reggie then this class will already be known to the lookup service. If however you run a different lookup service (not that there are many of these yet!) you would have to make this class available from an HTTP server. For longterm robustness, the client should either include the jsk-lib.jar file or at least the ServiceDiscoveryManager.LookupCacheImpl.LookupListener_Stub.class file in its codebase although it will work okay with Sun's reggie.

17.6. Monitoring Changes to the Cache

The cache uses remote events to monitor the state of lookup services. It includes a local mechanism to pass some of these changes onto a client by means of the ServiceDiscoveryListener interface


package net.jini.lookup;
interface ServiceDiscoveryListener {
    void serviceAdded(ServiceDiscoveryEvent event);
    void serviceChanged(ServiceDiscoveryEvent event);
    void serviceRemoved(ServiceDiscoveryEvent event);
}
with event

package net.jini.lookup;

class ServiceDiscoveryEvent extends EventObject {
    ServiceItem getPostEventServiceItem();
    ServiceItem getPreEventServiceItem();
}

Clients are not likely to be interested in all events generated by lookup services, even for services in which they are interested. For example, if a new service registers itself with ten lookup services, they will all generate transition events from NO_MATCH to MATCH, but the client will usually only be interested in seeing the first of these - the other nine are just repeated information. Similarly, if a service's lease expires from one lookup service, then that doesn't matter much; but if it expires from all lookup services that the client knows of, then it does matter as the service is no longer available to it. The cache consequently prunes events so that the client gets information about the real services rather than information about the lookup services.

In the chapter on ``Events'' an example was given on monitoring changes to services from a lookup service viewpoint, reporting each change to lookup services. A client-oriented view is just to monitor changes in services themselves, which can be done easily using ServiceDiscoveryEvent's:



package client;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.lookup.ServiceDiscoveryListener;
import net.jini.lookup.ServiceDiscoveryEvent;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.lookup.ServiceItem;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lease.LeaseRenewalManager;
import net.jini.lookup.LookupCache;

/**
 * ServiceMonitor.java
 */

public class ServiceMonitor implements ServiceDiscoveryListener {

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

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

    public ServiceMonitor() {
        ServiceDiscoveryManager clientMgr = null;
        LookupCache cache = null;

	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null,  // unicast locators
                                           null); // DiscoveryListener
            clientMgr = new ServiceDiscoveryManager(mgr, 
                                                new LeaseRenewalManager());
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
  
        ServiceTemplate template = new ServiceTemplate(null, null, 
                                                       null);
        try {
            cache = clientMgr.createLookupCache(template, 
                                                null,  // no filter
                                                this); // listener
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    // methods for ServiceDiscoveryListener
    public void serviceAdded(ServiceDiscoveryEvent evt) {
	// evt.getPreEventServiceItem() == null
	ServiceItem postItem = evt.getPostEventServiceItem();
	System.out.println("Service appeared: " +
			   postItem.service.getClass().toString());
    }

    public void serviceChanged(ServiceDiscoveryEvent evt) {
	ServiceItem preItem = evt.getPostEventServiceItem();
	ServiceItem postItem = evt.getPreEventServiceItem() ;
	System.out.println("Service changed: " +
			   postItem.service.getClass().toString());
    }
    public void serviceRemoved(ServiceDiscoveryEvent evt) {
	// evt.getPostEventServiceItem() == null
	ServiceItem preItem = evt.getPreEventServiceItem();
	System.out.println("Service disappeared: " +
			   preItem.service.getClass().toString());
    }
    
} // ServiceMonitor

17.7. Summary

The client lookup manager can handle a variety of common situations that arise as clients need to find services under different situations.

17.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.newmarch.name) jan@newmarch.name.

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