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
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.
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;
}
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;
}
}
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;
}
}
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
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();
}
}
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
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();
}
}
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
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 work is licensed under a
Creative Commons License, the replacement for the earlier Open Content License.