RMI

Contents

  1. Re-using RMI Proxies
  2. Using the Naming service
    1. Server using Naming Service
    2. Proxy using Naming Service
  3. Avoiding RMI
    1. Heart interface
    2. Heart implementation
    3. Heart server
    4. Heart client
Most applications will make some use of RMI (Remote Method Invocation). There are several ways in which this may be done. In addition, it is possible to avoid use of RMI proxies completely. This chapter looks at such RMI-related issues.

1. 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 used an RMI stub (or RMI proxy) for the implementation. 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, export the implementation only. The Java runtime will then ensure that the exported object will in fact be the RMI proxy for that service, leaving the original service behind. There is then no need for an explicit proxy at all!

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



package rmi;

import option3.FileClassifierImpl;
import option3.RemoteFileClassifier;

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 {

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

	// install suitable security manager
	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();
	RemoteFileClassifier service;

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

	    // export the proxy service
	    ServiceItem item = new ServiceItem(null,
					       impl,
					       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());
    }
        
} // FileClassifierServerRMI

2. Using the Naming service

The option 3 service and the last section exported an RMI proxy object directly. They were able to do this because the Jini protocol allows objects to be moved around to their destination, where the ``lookup'' to find the object has been done by the Jini discovery protocol. In more ``traditional' uses of RMI, a client wishing to use an RMI service has to first locate it using RMU lookup mechanisms. This is possible with Jini also, but it is a little more complex and probably not so useful.

An RMI exportable object can advertise itself in various ways, and Jini registration is only one of these. RMI comes with a simple lookup service, the Naming service. This uses methods such as Naming.bind() and Naming.rebind() to register a name for the RMI object in an RMI registry, and Naming.lookup() to retrieve it from there.

An RMI registry is started by the command rmiregistry. This registry must be run on the same machine as the same as a service trying to bind to it. So if there are many services running on many machines, then there will be many RMI registries running, one per machine. A server will create an RMI object and then register it with its local RMI naming service. It will also still need to export a service registration to the Jini lookup service. So this method will export two services: an RMI service to an RMI naming registry, and a Jini service to a Jini lookup service.

On the other side, a client wishing to locate an object using this method will still need to find the Jini service, and will follow the methods of option 3. When it gets its Jini proxy, it will then need to locate its RMI proxy to make calls on it. But this time, it has not carried the RMI proxy along, and this must be found from the RMI registry. In order to use this, it must know the internet address of the registry's machine. By the time it has reached the client it is too late to have gthis information, so the Jini proxy must be ``primed'' with the address of the registry and the name of the RMI service while it is still in the service and before it is exported to the Jini lookup service.

2.1 Server using Naming Service

The implementation of the service remains unaltered as the option3.FileClassifierImpl. The server must bind this implementation into the rmi naming registry using bind() orrebind(). It must also prime the proxy with the address of this rmi registry, and the name the implementation is bound to.



package rmi;

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;

/**
 * FileClassifierServerNaming.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 FileClassifierServerNaming implements DiscoveryListener, LeaseListener {

    // This is just a name for the service
    // It can be anything, just needs to shared by both
    // ends of the Naming service
    static final String serviceName = "FileClassifier";

    protected option3.FileClassifierImpl impl;
    protected FileClassifierNamingProxy proxy;
    protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
    
    public static void main(String argv[]) {
	new FileClassifierServerNaming();

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

    public FileClassifierServerNaming() {
	try {
	    impl = new option3.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);
        }

	String registeredName = "//" + address + "/" + serviceName;

	// make a proxy that knows the service address
	proxy = new FileClassifierNamingProxy(registeredName);

	// now continue as before
	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];

	    // export the proxy service
	    ServiceItem item = new ServiceItem(null,
					       proxy, 
					       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());
    }
        
} // FileClassifierServerNaming

2.2 Proxy using Naming Service

The proxy which uses the naming service can still continue to use the ``option 3'' implementation. The way it finds this is different, as it uses the naming service lookup() method.



package rmi;

import common.FileClassifier;
import common.MIMEType;

import option3.RemoteFileClassifier;

import java.io.Serializable;
import java.io.IOException;
import java.rmi.Naming;

/**
 * FileClassifierNamingProxy
 *
 *
 * Created: Thu Mar 18 14:32:32 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

public class FileClassifierNamingProxy implements FileClassifier, Serializable {
    
    protected String serviceLocation;
    transient RemoteFileClassifier server = null;

    public FileClassifierNamingProxy(String serviceLocation) {
	this.serviceLocation = serviceLocation;
    }

    private void readObject(java.io.ObjectInputStream stream) 
	throws java.io.IOException, ClassNotFoundException {
	stream.defaultReadObject();

	try {
	    Object obj = Naming.lookup(serviceLocation);
	    server = (RemoteFileClassifier) obj;
	} catch(Exception e) {
	    System.err.println(e.toString());
	    System.exit(1);
	}
    }

    public MIMEType getMIMEType(String fileName) 
	throws java.rmi.RemoteException {
	return server.getMIMEType(fileName);
    }
} // FileClassifierNamingProxy

3. Avoiding RMI

The Jini service (or a proxy for the service) runs locally to the client. In the examples so far given it makes use of an RMI proxy object (itself or another object) to make calls back to the server it came from. But RMI is only one of many possible protocols that can be used to communicate between the client and the server. Any other protocol can be used, depending on the problem domain. An easy alternative is to use a direct socket connection from the service component running in the client to the service. This opens up a number of possibilities, including

  1. The Jini service turns into an ordinary socket service, listening on some port and handling connections made to it. Exported Jini services could connect to it, but also any other client could connect. This turns it into a more-or-less standard client-server system. In effect, the Jini contribution to this is that it exports a service that knows that particular client-server protocol, and handles it invisibly to clients making use of it. Other clients, not having this Jini agent, would need to understand the particular client-server protocol directly.
  2. The exported component of the service need not talk back to its original service at all! Instead, it can open a socket connection to any other service it knows about. The role of the Jini service would be the same as the last case, as an agent knowing about the client-server protocol. The role of the service it started from would be reduced to just keeping the lease alive!
  3. If the exported component starts talking to other services, then these other services do not need to be written in Java - they could be in any language. As long as they act using an understood protocol on the network, then they can be accessed. This could be one way, say, of making a CORBA service or ActiveX control available to Jini clients.

We shall give an example of this last type. Recently an Australian, Pat Farmer, attempted to set a world record for jogging the longest distance. While he was running around, I became involved in a project to broadcast his heart beart live to the Web: a heart monitor was attached to him, which talked on an RS232 link to a mobile he was carrying. This did a data transfer to a program running at ... which forwarded the data to a machine at DSTC. This ran a Web server delivering an applet, and the applet talked back to a server on this DSTC machine which farmed out the data to each applet as it was received from the heart monitor.

Now that the experiment is over, the broadcast data is sitting as a file at http://www.micromed.com.au/patfarmer/v2/patfhr.ecg , and it can be viewed on the applet from http://www.micromed.com.au/patfarmer/v2/heart.html We can make it into a Jini service as follows

  1. Have a service which we can locate using the service type (``display a heart monitor trace'') and information about it such as whose heart trace it is showing
  2. The service can connect to an http address encoded into the service by its constructor (or other means), read from this and display the contents read assuming it is heart cardiograph data
  3. The information about whose trace it is can be given by a Name entry
The client shows as

The heart monitor service can be regarded in a number of ways:

  1. It is a full-blown service uploaded to the client, that just happens to use an external data source supplied from an HTTP server
  2. It is a ``fat'' proxy to the HTTP service, which acts as a client to this service by displaying the data
Many other non-RMI services can be built that act in this ``fat proxy'' style.

3.1 Heart interface

The Heart interface only has one method, and that is to show() the heart trace in some manner.



/**
 * Heart.java
 *
 *
 * Created: Thu Jul 15 16:11:56 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package heart;

public interface Heart extends java.io.Serializable {
    
    public void show();
} // Heart

3.2 Heart server

The HeartServer is similar to the ``option 2'' method, where it exports a complete service. This service, of type HeartImpl is primed with a URL of where the heart data is stored. This data will later be delivered by an HTTP server. This implementation is enough to locate the service. However, rather than just anyone's heart data, a client may wish to search for a particular persons data. This can be done by adding a Name entry as additional information to the service.


package heart;

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

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

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

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

        // 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 HeartServer() {

	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];

	    ServiceItem item = new ServiceItem(null,
					       // new HeartImpl("file:/home/jan/projects/jini/doc/heart/TECG3.ecg"),
					       new HeartImpl("http://www.micromed.com.au/patfarmer/v2/patfhr.ecg"), 
					       new Entry[] {new Name("Pat Farmer")});
	    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, this);
	}
    }

    public void discarded(DiscoveryEvent evt) {

    }

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

3.3 Heart client

The client searches for a service implementing the Heart interface, with the additional requirement that it be for a particular person. Once it has this, it just calls the method show() to display this in some manner.



package heart;

import heart.Heart;

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.entry.Entry;
import net.jini.lookup.entry.Name;

/**
 * HeartClient.java
 *
 *
 * Created: Wed Mar 17 14:29:15 1999
 *
 * @author Jan Newmarch
 * @version 1.3
 *    moved sleep() from constructor to main()
 *    moved to package client
 *    simplified Class.forName to Class.class
 */

public class HeartClient implements DiscoveryListener {

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

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

    public HeartClient() {
	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[] {Heart.class};
	Entry [] entries = new Entry[] {new Name("Pat Farmer")};
	Heart heart = null;
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       entries);
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Service found");
            ServiceRegistrar registrar = registrars[n];
	    try {
		heart = (Heart) registrar.lookup(template);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
		System.exit(2);
	    }
	    if (heart == null) {
		System.out.println("Heart null");
		continue;
	    }
	    heart.show();
	}
    }

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


3.4 Heart implementation

The HeartImpl class opens a connection to an HTTP server and requests delivery of a file. For heart data it needs to display this at a reasonable rate, so it reads, draws, sleeps, in a loop. It acts as a fat client to the HTTP server, displaying the data in a suitable format (in this case, it uses HTTP as a transport mechanism for data delivery). As a ``client-aware'' service it customises this delivery to the characteristics of the client platform, just occupying a ``reasonable'' amount of screen space and using local colors and fonts.



/**
 * HeartImpl.java
 *
 *
 * Created: Thu Jul 15 15:57:47 1999
 *
 * @author Jan Newmarch
 * @version 1.0
 */

package heart;

import java.io.*;
import java.net.*;
import java.awt.*;

public class HeartImpl implements Heart {
    
    protected String url;

    public static void main(String argv[]) {

	HeartImpl impl =
	    new HeartImpl("file:/home/jan/projects/jini/doc/heart/TECG3.ecg");

	impl.show();
    }

    public HeartImpl(String u) {
	url = u;
    }

    double[] points = null;
    Painter painter = null;

    String heartRate = "--";


    public void setHeartRate(int rate) {
	if (rate > 20 && rate <= 250) {
	    heartRate = "Heart Rate: " + rate;
	} else {
	    heartRate = "Heart Rate: --";
	}
	// ? ask for repaint? 
    }

    public void quit(Exception e, String s) {
	System.err.println(s);
	e.printStackTrace();
	System.exit(1);
    }

    public void show() {
	int SAMPLE_SIZE = 300 / Toolkit.getDefaultToolkit().
	                                            getScreenResolution();
	Dimension size = Toolkit.getDefaultToolkit().
	                         getScreenSize();
	int width = (int) size.getWidth();
	// capture points in an array, for redrawing in app if needed
	points = new double[width * SAMPLE_SIZE];
	for (int n = 0; n < width; n++) {
	    points[n] = -1;
	}


	URL dataUrl = null;
	InputStream in = null;

	try {
	    dataUrl = new URL(url);
	    in = dataUrl.openStream();
	} catch (Exception ex) {
	    quit(ex, "connecting to ECG server");
	    return;
	}

	Frame frame = new Frame("Heart monitor");
	frame.setSize((int) size.getWidth()/2, (int) size.getHeight()/2);
	try {
	    painter = new Painter(this, frame, in);
	    painter.start();
	} catch (Exception ex) {
	    quit(ex, "fetching data from ECG server");
	    return;
	}
	frame.setVisible(true);
    }

    /*    
    public void paint(Graphics g) {

	if (points == null) 
	    return;
	int SAMPLE_SIZE = 300 / Toolkit.getDefaultToolkit().
	                                            getScreenResolution();
	g.setColor(Color.red);
	int min = 127;
	int max = -128;

	for (int n = 0; n < points.length; n++) {
	    int x = n + 1; 
	    int magnitude = points[n];
	    if (magnitude == -1) {
		return;
	    }
	    
	    if (x % SAMPLE_SIZE == 0) {
		// draw only on multiples of sample size
		// Data is in the range -128 .. 127. Need to
		// draw so -128 is at the bottom, 127 at the top
		int x0 = x / SAMPLE_SIZE;
		g.drawLine(x0, min, x0, max);
		min = 127;
		max = -128;
	    } else {
		if (magnitude > max) max = magnitude;
		if (magnitude < min) min = magnitude;
	    }
	}
    }
    */    
} // HeartImpl

class Painter extends Thread {

    static final int DEFAULT_SLEEP_TIME = 25; // milliseconds
    static final int CLEAR_AHEAD = 15;
    static final int MAX = 255;
    static final int MIN = 0;
    final int READ_SIZE = 10;

    protected HeartImpl app;
    protected Frame frame;

    protected InputStream in;
    protected final int RESOLUTION = Toolkit.getDefaultToolkit().
	                                            getScreenResolution();
    protected final int UNITS_PER_INCH = 125;
    protected final int SAMPLE_SIZE = 300 / RESOLUTION;
    protected int sleepTime = DEFAULT_SLEEP_TIME;
    
    public Painter(HeartImpl app, Frame frame, InputStream in) throws Exception {
	this.app = app;
	this.frame = frame;
	this.in = in;
    }

    public void run() {

	while (!frame.isVisible()) {
	   try {
	       Thread.sleep(1000);
	   } catch(Exception e) {
	       // ignore
	   }
	}

	int height = frame.getSize().height;
	int width = frame.getSize().width;
	int x = 1; // start at 1 rather than 0 to avoid drawing initial line
	           // from -128 .. 127
	int magnitude;
	int nread;
	int max = MIN; // top bound of magnitude
	int min = MAX;  // bottom bound of magnitude
	int oldMax = MAX + 1;
	byte[] data = new byte[READ_SIZE];
	Graphics g = frame.getGraphics();
	g.setColor(Color.red);
	try {
	    Font f = new Font("Serif", Font.BOLD, 20);
	    g.setFont(f);
	} catch (Exception ex) {
	    // ....
	}



	try {
            boolean expectHR = false;   // true ==> next byte is heartrate

	    while ((nread = in.read(data)) != -1) {
		for (int n = 0; n < nread; n++) {
		    int thisByte = data[n] & 0xFF;
		    if (expectHR) {
			expectHR = false;
			app.setHeartRate(thisByte);
			continue;
		    } else if (thisByte == 255) {
			expectHR = true;
			continue;
		    }
		    
		    // we are reading bytes, from -127..128
		    // conver to unsigned
		    magnitude = thisByte;

		    // then convert to correct scale
		    magnitude -= 128;
		    // scale and convert to window coord from the top downwards
		    int y = ((128 - magnitude) * RESOLUTION) / UNITS_PER_INCH;
		    app.points[x] = y;

		    // draw only on multiples of sample size
		    if (x % SAMPLE_SIZE == 0) {
			// delay to draw at a reasonable rate
			Thread.sleep(sleepTime);

			int x0 = x / SAMPLE_SIZE;
			g.clearRect(x0, 0, CLEAR_AHEAD, height);
			if (oldMax != MAX + 1) {
			    g.drawLine(x0-1, oldMax, x0, min); 
			}
			g.drawLine(x0, min, x0, max);
			oldMax = max;
			min = 1000;
			max = -1000;
			if (app.heartRate != null) {
			    g.setColor(Color.black);
			    g.clearRect(0, 180, 200, 100);
			    g.drawString(app.heartRate, 0, 220);
			    g.setColor(Color.red);
			}
		    } else {
			if (y > max) max = y;
			if (y < min) min = y;
		    }
		    if (++x >= width * SAMPLE_SIZE) {
			x = 0;
		    }
		}
	    }
	} catch(Exception ex) {
	    if (! (ex instanceof SocketException)) {
		System.out.println("Applet quit -- got " + ex);
	    }
	} finally {
	    try {
		if (in != null) {
		    in.close();
		    in = null;
		}
	    } catch (Exception ex) {
		// hide it
	    }
	}
    }
}


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

This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v0.4 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.