Chapter 18: Example: Flashing Clocks

18.1. Introduction

One of the early promises of Jini was that it would find its way into all sorts of devices which could advertise their presence. However, Jini does not run on the really small Java virtual machines such as the KVM. But if it could, how would it be used?

Most people have a number of electronic clocks in their house: alarm clocks, a clock on the oven, another on the microwave, and so on. When the electricity resumes after a power failure, they all start flashing, and you have to round one after another setting them manually. Wouldn't it be nice if you only had to reset one (or if it got a value from a time server somewhere) and all the others reset themselves from it.

In this chapter we look at this "flashing clocks" problem from a Jini viewpoint, to see what a Jini solution would look like. This example uses JoinManager and ServiceDiscoveryManager to advertise and discover services.

On my site http://jan.newmarch.name/internetdevices/upnp/upnp-more-programming.html is an alternative solution using UPnP - a middleware system that is making more grounds in the area of small devices than Jini is, probably due to lighter resource requirements and an active coordinating body.

18.2. Timer

Each clock is available as a service which we call a Ticker. A ticker has methods to get and set the time, and in addition it knows if it has a valid time or if it has an invalid time (and so should be shown flashing). A ticker can have its time set: when it does, it becomes valid, so that a display can stop flashing.

The interface for a ticker is


	/**
 * Timer service
 */
package clock.service;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Date;

public interface Timer extends Remote {

    public void setTime(Date t) throws RemoteException;

    public Date getTime() throws RemoteException;

    public boolean isValidTime() throws RemoteException;
}


      

18.3. TickerTimer

We shall give two implementations of this service: the first is the "dumb" one: when it starts it guesses at a start time and enters an invalid state. It uses a separate thread to keep imcreasing its time every second (approximately). When its time is set, it becomes valid, but will probably drift from the correct time due to its use of sleep to keep changing the time.

The dumb timer is


	package clock.service;

import java.util.Date;
import java.rmi.RemoteException;

public class TickerTimer implements Timer {
    private Date time;
    private boolean isValid;
    private Ticker ticker;

    /**
     * Constructor with no starting time has
     * invalid timer and any time
     */
    public TickerTimer() {
	time = new Date(0);
	isValid = false;
	ticker = new Ticker(time);
	ticker.start();
    }

    public TickerTimer(Date t) {
	time = t;
	isValid = true;
	ticker = new Ticker(time);
	ticker.start();
    }
 
    public void setTime(Date t) {
	System.out.println("Setting time to " + t);
	time = t;
	isValid = true;
	if (ticker != null) {
	    ticker.stopRunning();
	}
	ticker = new Ticker(time);
	ticker.start();
    }

    public Date getTime() {
	return ticker.getTime();
    }

    public boolean isValidTime() {
	if (isValid) {
	    return true;
	} else {
	    return false;
	}
    }
}

class Ticker extends Thread {
    private Date time;
    private boolean keepRunning = true;

    public Ticker(Date t) {
	time = t;
    }

    public Date getTime() {
	return time;
    }

    public void run() {
	while (keepRunning) {
	    try {
		sleep(1000);
	    } catch(InterruptedException e) {
	    }
	    time = new Date(time.getTime() + 1000);
	}
    }

    public void stopRunning() {
	keepRunning = false;
    }
}

      

18.4. ComputerTimer

This timer uses the computer's internal clock to always return the correct time on request. It is always valid.


	package clock.service;

import java.util.Date;
import net.jini.core.event.*;
import java.util.Vector;
import java.rmi.RemoteException;

public class ComputerTimer implements Timer {

    public ComputerTimer() {
    }

    public void setTime(Date t) {
	// void
    }

    public Date getTime() {
	return new Date();
    }

    public boolean isValidTime() {
	return true;
    }
}

      

18.5. ClockFrame

To make this more visual, we can put the timers into a Swing frame and watch them ticking away. The following code is based on that of Satoshi Konno for UPnP

A clock pane is


	/******************************************************************
 *	Copyright (C) Satoshi Konno 2002

      

A clock frame is


	/******************************************************************
 *	Copyright (C) Satoshi Konno 2002-2003

      

18.6. TickerTimer Driver

A driver for the ticker timer in the frame above is A clock frame is


	
package clock.clock;

import clock.device.*;
import clock.service.*;

public class TickerClock {
    
    public static void main(String args[]) 
    {
	ClockDevice clockDev = new ClockDevice();

	clockDev.setTimer(new TickerTimer());

	ClockFrame clock;
	if (args.length > 0) {
	    clock= new ClockFrame(clockDev, args[0]);
	} else {
	    clock = new ClockFrame(clockDev);
	}
	clock.start();
    }
}

      

18.7. ComputerTimer Driver

A driver for the computer timer in the frame above is A clock frame is


	
package clock.clock;

import clock.device.*;
import clock.service.*;

public class ComputerClock {
    
    public static void main(String args[]) 
    {
	ClockDevice clockDev = new ClockDevice();
	
	clockDev.setTimer(new ComputerTimer());

	ClockFrame clock;
	if (args.length > 0) {
	    clock= new ClockFrame(clockDev, args[0]);
	} else {
	    clock = new ClockFrame(clockDev);
	}
	clock.start();    }
}

      

When running, the two clocks look like

18.8. ClockDevice

The final part is to advertise each timer as a Jini service, to try to locate other timer services and to listen to events from each one. This is handled by the clock device (really it is what we have been calling a Jini server, we have just adopted the UPnP. The device has a timer installed by setTimer(), and advertises this using a JoinManager. In the meantime it uses a ServiceDiscoveryManager to find other timers. terminology here).


	

package clock.device;

import clock.service.*;

import java.io.*;
import java.util.Date;

import java.rmi.*; 
import java.rmi.server.ExportException;
import net.jini.export.*; 
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.tcp.TcpServerEndpoint;
import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceRegistrar;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceIDListener;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscoveryManager;

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;

public class ClockDevice implements ServiceIDListener, ServiceDiscoveryListener {

    private Timer timer;

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

	// Build a cache of all discovered clocks and monitor changes
	ServiceDiscoveryManager serviceMgr = null;
        LookupCache cache = null;
	Class [] classes = new Class[] {Timer.class};
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       null);	
	try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null,  // unicast locators
                                           null); // DiscoveryListener
            serviceMgr = new ServiceDiscoveryManager(mgr, 
						    new LeaseRenewalManager());
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
	
        try {
            cache = serviceMgr.createLookupCache(template, 
                                                null,  // no filter
                                                this); // listener
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void setTimer(Timer t) {
	timer = t;
	System.out.println("Our timer service is " + t);

	Exporter exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
                                     new BasicILFactory());

	// export a Timer proxy
	Remote proxy = null;
	try {
	    proxy = exporter.export(timer);
	} catch(ExportException e) {
	    System.exit(1);
	}

	// Register with all lookup services as they are discovered
	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   null,  // unicast locators
					   null); // DiscoveryListener
	    joinMgr = new JoinManager(proxy, // service proxy
				      null,  // attr sets
				      this,  // ServiceIDListener
				      mgr,   // DiscoveryManager
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    } 
    
    public void serviceIDNotify(ServiceID serviceID) {
	// called as a ServiceIDListener
	// Should save the id to permanent storage
	System.out.println("got service ID " + serviceID.toString());
    }


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

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

    private void tryClockValidation(Timer otherTimer) {
	try {
	    if (timer.isValidTime() &&  ! otherTimer.isValidTime()) {
		// other clock needs to be set by us
		otherTimer.setTime(timer.getTime());
	    } else if (! timer.isValidTime() && otherTimer.isValidTime()) {
		// we need to be set from the other clock
		timer.setTime(otherTimer.getTime());
	    }
	} catch(RemoteException e) {
	    // ignore other timer!
	}
    }

    public void setTime(Date t) throws RemoteException {
	timer.setTime(t);
    }

    public Date getTime()  throws RemoteException {
	return timer.getTime();
    }

    public boolean isValidTime()  throws RemoteException {
	return timer.isValidTime();
    }
}


      

18.9. Runtime Behaviour

If several clocks are started, they will advertise themselves and also attempt to find other clocks. When one finds another it tries to determine its state: if one is valid and the other invalid then either the valid one sets the time on the invalid one, or the invalid one gets the correct time from the valid one. Which takes place depends on whether the valid one discovers the invalid one or vice versa - it doesn't matter, since the result is the same!. Two valid clocks do nothing to each other, as do two invalid ones.

The ant file clock.clock.xml runs a ticker clock, pauses sixty seconds and then runs a computer clock. When one discovers the other, the ticker clock has its time reset.

18.10. Copyright

If you found this chapter of value, the full book is available from APress or Amazon . There is a review of the book at Java Zone . The current edition of the book does not yet deal with Jini 2.0, but the next edition will.

This file is Copyright (©) 1999, 2000, 2001, 2003, 2004 by Jan Newmarch (http://jan.netcomp.edu.au) jan@newmarch.name.

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