Chapter 0: Extended example - home audio system

This chapter considers an extended example, using Jini in a home audio situation. It uses many of the concepts of earlier chapters and shows how Jini can be used to build non-trivial systems

1. Introduction

Traditional information systems concentrated on modelling information flows, and quite explicitly avoided physical systems. The advent of object-oriented systems changed this emphasis, with increased emphasis on the behaviour of real objects and how this could form the basis of an information system. In the meantime, there has been a huge amount of work in control systems, and increasing the computational power of everyday things such as dishwashers, washing machines and so on. One area in which the convergence of computer systems and devices has become a major commercial area is that of audio/visual systems. The change from analogue to digital sytems has opened up a large area, which goes far beyond copying MP3 files from one computer to another

The home A/V area is a battleground for ideologies and commercial interests. On the one hand are the set-top vendors, owning the cable systems that pump entertainment into many homes. Their vision is to widen their pipe, while still maintaining control. The professional audio and hifi community on the other hand, see the hifi system as control centre. And of course, the computer community sees the PC as centre of any home A/V system, due to its processing power, well-developed software systems, and the ability to handle digital signals without difficulty.

I belong to the PC-centric community to some extent - for even there, there are divergences of opinion. Most current A/V systems such as the Java Media Framework and the Microsoft Media Platform treat the A/V sources and sinks as though they are on the same machine, so that all processing is done locally. Agreed, JMF allows network access using HTTP or RTP, but it tries to hide the network layer and make all components appear to be local.

The mantra from Sun for many years is "the network is the computer." This could be applied to the A/V world: "the network is the A/V system." What makes it interesting for the A/V system is what a network can do: a wireless network can support friends visiting with their own A/V systems, joining in with yours to share music; it can support music following you around the house, switching from one set of speakers to another. This chapter is my attempt at building a network wireless audio system using Jini.

2. Distributed audio

There have been many efforts to distribute A/V. Much of this is concerned with large servers, and these efforts have paid off with streaming media systems such as RealAudio. I want to look at a more local situation: my house is a medium size house, and now that I have a wireless network I can work in the lounge room, the family room, my study or even in one of the bedrooms. I like "music while you work" - either CDs, or the various community radio stations that I subscribe to, and possibly streaming audio from other stations in the world later. I don't have any children or partner there at the moment, but if I did, then they would have their own music sources and sinks and would share the house network. Friends might come and visit, with their own A/V sources and sinks and just join the house network. In a little while, guitars and microphones will have Bluetooth cards,so we will be able to have a local network band.

The wireless network density in my neighbourhood is low, but eventually I should be able to join a local community network, which should give me metropolitan access. I live in a city rich in music (Melbourne, Australia) and sometimes feel that I hardly need to go out because the local radio stations (RRR, PBS-FM) are so good, but soon I would also hope to tune into the folk concert on the other side of town through the community wireless network.

Okay: so how do we build middleware for an A/V network that is network-centric, rather than proprieter-centric? There has been one attempt that I know of to build a network-based A/V system, by Marco Lohse ("An open.middleware architecture for network-integrated multimedia"). This is CORBA-based, which gives it network objects. But a lot of their system has to be built on top of CORBA because it doesn't quite support what they want. Much of this extra structure seems to fall out quite easily under Jini.

I am approaching the rest of this chapter from a software-engineering viewpoint, trying to make a system as simple as possible for consumers (clients). If you have any comments on this, please let me know - after all, this is the system I using in my house right now, so if it can be made better, then I at least will be grateful!

3. Parameters for A/V

There are many variables that affect how A/V is sourced, moved around a network and delivered

Transport
The transport layer may be reliable (slow) TCP, unreliable (faster) UDP, HTTP (even slower), with some QOS such as RTP or some other network technology protocol such as Bluetooth or FireWire
Format
There are an enormous number of formats, from encumbered formats such as MP3 (for which you are supposed to pay license fees for encoders and decoders), unencumbered equivalents such as Ogg-Vorbis, compressed (MP3 and Ogg-Vorbis) or uncompressed (Sun AU or M/S WAV), lossy or lossless. In addition, there are many wrinkles in each format: little- or big-endian; 8, 16 or 32 bit; mono, stereo, 5-1,...; sample rate such as 44.1khz, 8khz, etc
Content description
Audio comes from many different sources: tracks off a CD, streaming audio from an FM station, speech off a telephone line. The MPEG-7 concentrates on technical aspects of an audio signal in attempts to classify it, while the CD databases (CDDB) such as freedb classify CDs by Artist/Title - which breaks down with compilation CDs and most classical CDs (who is the artist - the composer, the conductor or the orchestra?)
Push/pull
An audio stream may be "pushed", such as an FM radio stream that is always playing. Or it may be "pulled" by a client from a server, such as in fetching an MP3 file from an HTTP server

4. Source/sink interfaces

Interfaces should contain all the information about how to access services. With audio, all the information about a service can be quite complex: for example, a service might offer a CD track encoded in 16-bit stereo, big-endian, 44.1khz sampling in WAV format from an HTTP server. This information may be needed by a consumer that wants to play the file.

But in the type of A/V system I want to build there are three players:

  1. Sources of A/V data
  2. Sinks for A/V data
  3. Controller clients to link sources and sinks
From the controller viewpoint, most of this information is irrelevant: it will just want to link sources to sinks, and leave it to them to decide how and if they can communicate.

For simplicity we define two interfaces: Source and Sink. To avoid making implementation decisions about pull versus push, we have methods to tell a source about a sink, a sink about a source, to tell the source to play and the sink to record. Again, how they decide how to do this is upto the source and sink. Sometimes this won't work: an HTTP source may not be able to deliver to an RTP sink, or a WAV file may not be managed by an MP3 player. If they don't succeed in negotiating tranport and content, then an exception should be thrown. This violates the principle that a service should be usable based on its interface alone, but considerably simplifies matters for controller clients.

Notice that neither a source nor a sink have names or other descriptive information. we choose to consider all this information as "additional service information" that can be given by Entry objects.

A controller that wants to play a sequence of audio tracks to a sink will need to know when one track is finished in order to start the next. The play() and record() methods could block till finished, or return immediately and post an event on completion. The second method allows more flexibility, and so needs add/remove listener methods for the events.

Finally, there are the exceptions that can be thrown by the methods. Attempting to add a source that a sink cannot handle should throw an exception such as IncompatableSourceException. A sink that can handle only a small number of sources (for example, only one) could throw an exception if too many sources are added. A source that is already playing may not be able to satisfy a new request to play.

These considerations lead to a pair of high-level interfaces which seem to be suitable for controllers to manage sources and sinks:



/**
 * Source.java
 */

package audio.common;

import java.rmi.RemoteException;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;

/**
 * A source for A/V data
 */

public interface Source extends java.rmi.Remote {

    int STOP = 1;

    void play() throws 
                RemoteException,
	        AlreadyPlayingException;
    void stop() throws 
                RemoteException,
                NotPlayingException;
    void addSink(Sink sink) throws
                RemoteException,
                TooManySinksException,
                IncompatableSinkException;
    void removeSink(Sink sink) throws
                RemoteException,
                NoSuchSinkException;
    EventRegistration addSourceListener(RemoteEventListener listener,
					MarshalledObject handback) throws
					    RemoteException;
}// Source

and


/**
 * Sink.java
 */

package audio.common;

import java.rmi.RemoteException;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;

/**
 * A sink for audio
 */

public interface Sink extends java.rmi.Remote {

    int STOP = 1;

    void record() throws
	        RemoteException,
	        AlreadyRecordingException;
    void stop() throws 
                RemoteException,
	        NotRecordingException;
    void addSource(Source src) throws
                RemoteException,
                TooManySourcesException,
                IncompatableSourceException;
    void removeSource(Source src) throws
                RemoteException,
                NoSuchSourceException;
    EventRegistration addSinkListener(RemoteEventListener listener, 
				      MarshalledObject handback) throws
	        RemoteException;
    void removeSinkListener(RemoteEventListener listener) throws
	        RemoteException,
		NoSuchListenerException;

}// Sink

5. Content interfaces

The Java Media Framework (JMF) has methods such as getSupportedContentTypes() which returns an array of strings. Other media toolkits have similar mechanisms. This isn't type-safe: it relies on all parties having the same strings and attaching the same meaning to each. In addition to this, if a new type comes along, there isn't a reliable means of specifying this information to others. A type-safe system can at least specify this by class files.

I have chosen to use interfaces instead of strings: a WAV interface, an Ogg interface, etc. This doesn't easily allow extension to the multiplicity of content type variations (bit size, sampling rate, etc), but the current content handlers seem to be able to handle most of these variations anyway, so it seems feasible to ignore them at an application level.

The content interfaces are just place-holders:


package presentation;

public interface Ogg extends java.rmi.Remote {
}
A source that could make an audio stream available in OggVorbis format would signal this by implementing the Ogg interface. A sink that can manage OggVorbis streams would also implement this interface.

6. Transport interfaces

In a similar way, I have chosen to represent the transport mechanisms by interfaces. A transport sink will get the information from a source using some unspecified network transport mechanism. The audio stream can be made available to any other object by exposing an InputStream. This is a standard Java stream, not the special one used by JMF. Similarly, a transport source would make an output stream available for source-side objects to write data into.



/**
 * TransportSink.java
 */

package audio.transport;

import java.io.*;

public interface TransportSink {

    public InputStream getInputStream();
}// TransportSink

and


/**
 * TransportSource.java
 */

package audio.transport;

import java.io.*;

public interface TransportSource {

    public OutputStream getOutputStream();
}// TransportSource

7. Linkages

By separating the transport and content layers, we have a model that follows a part of the ISO 7-layer model: transport and presentation layers. The communication paths for a "pull" sink are

The classes involved in a "pull" sink could look like

where the choice of transport and content implementation is based on the interfaces supported by the source and the preferences of the sink.

8. An HTTP source

An HTTP source makes an audio stream available as a document from an HTTP server. All that it needs to tell a sink about is the URL for the document. There is a little hiccup in this: a Java URL can be an http URL, or a file URL, an ftp URL, etc. So I have defined a class HttpURL to enforce that it is a URL accessible by the HTTP protocol. The Java URL class is final, so we can't extend it and have to wrap around it.



/**
 * HttpURL.java
 */

package audio.transport;

import java.net.MalformedURLException;
import java.net.*;
import java.io.*;

public class HttpURL implements java.io.Serializable {
    private URL url;

    public HttpURL(URL url) throws MalformedURLException {
	this.url = url;
	if (! url.getProtocol().equals("http")) {
	    throw new MalformedURLException("Not http URL");
	}
    }

    public HttpURL(String spec) throws MalformedURLException {
	url = new URL(spec);
	if (! url.getProtocol().equals("http")) {
	    throw new MalformedURLException("Not http URL");
	}
    }

    public URLConnection openConnection()
	throws IOException {
	return url.openConnection();
    }

    public InputStream openStream()
	throws IOException {
	return url.openStream();
    }
}// HttpURL

The HttpSource interface just exposes an HttpURL:



/**
 * HttpSource.java
 */

package audio.transport;

import audio.common.*;
import java.net.URL;
import java.rmi.RemoteException;

public interface HttpSource extends Source {

    HttpURL getHttpURL() throws RemoteException;
}// HttpSource

The interface allows a sink to determine that the transport protocol it should use is the HTTP protocol, and what URL it should use to fetch the document.

An HTTP source is a "pull" source: that is, a sink will fetch the data from it. A source of this type doesn't need to worry about listeners or playing the source data. All it needs to do is store the URL. An implementation of this could be



/**
 * HttpSourceImpl.java
 */

package audio.http;

import audio.common.*;
import audio.transport.*;

import java.net.*;
import java.rmi.*;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;

/**
 * Stores an HTTP reference
 */

public class HttpSourceImpl implements HttpSource, Remote {
    private HttpURL url;

    private HttpSourceImpl() {
    }

    public HttpSourceImpl(HttpURL url) {
	this.url = url;
    }

    public HttpSourceImpl(URL url) throws MalformedURLException {
	this.url = new HttpURL(url);
    }

    public HttpURL getHttpURL() {
	return url;
    }

    public void play() {}
    public void stop() {}
    public void addSink(Sink sink) throws IncompatableSinkException { }

    public void removeSink(Sink sink) {}

    public EventRegistration addSourceListener(RemoteEventListener listener,
					       MarshalledObject handback) {
	return null;
    }
}// HttpSourceImpl

9. An HTTP OggVorbis source

If the document is an OggVorbis document, then the service signals this by implementing the Ogg interface



/**
 * HttpOggSourceImpl.java
 */

package audio.http;

import audio.presentation.Ogg;
import audio.transport.HttpURL;

import java.net.*;

/**
 * Adds Ogg interface to HttpSourceImpl for Ogg files
 */

public class HttpOggSourceImpl extends HttpSourceImpl
    implements Ogg{

    public HttpOggSourceImpl(HttpURL url) throws MalformedURLException {
	super(url);
    }

    public HttpOggSourceImpl(URL url) throws MalformedURLException {
	super(url);
    }
}// HttpOggSourceImpl

In an identical manner, a document can be a WAV or MP3C document by implementing the WAV or MP3C interface respectively.

10. An HTTP sink

An HTTP sink needs to find the URL from an HttpSource, open a connection to it and get an InputStream. The Java URL class makes this quite straightforward:



/**
 * HttpSinkImpl.java
 */

package audio.transport;

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

public class HttpSinkImpl implements TransportSink {

    protected HttpSource source;

    public HttpSinkImpl(HttpSource source) {
	this.source = source;
    }

    public InputStream getInputStream() {
	try {
	    HttpURL url = source.getHttpURL();
	    URLConnection connect = url.openConnection();

	    connect.setDoInput(true);
	    connect.setDoOutput(false);
	    InputStream in = connect.getInputStream();
	    return in;
	} catch (IOException e) {
	    System.err.println("Getting in stream " + e.toString());
	    return null;
	}
    }
}// HttpSinkImpl

11. Content sinks

A ContentSink will get an InputStream from a TransportSink. Then it can read bytes from this stream and interpret it based on the content type it understands. There are some content handlers in the Java JMF. But many are missing: MP3 files are encumbered by patent rights, and encoders and decoders should cost (someone!) money to use, so currently there is no MP3 player in JMF (well, there is an MPEG movie player which can be called with no video stream...). There is very little activity from Sun on new codecs, and there is no current OggVorbis player. There are attempts to fill the gaps: for example, there is a pure Java Ogg Vorbis decoder, JOrbis, and an MP3 decoder for JMF, JFFMPEG. But the situation has not settled down to any clarity yet.

Im the meantime, it is easier to make O/S system calls into players such as mpg123 or sox (under Unix). I have generically labelled these as playmp3, etc, and these read from a pipeline. I am being lazy here: I should have a MP3ContentSink, etc, and createSink() should act like a proper factory and return the right kind of content sink.



/**
 * ContentSink.java
 */

package audio.pull;

import java.io.*;
import audio.presentation.*;
import audio.common.*;

public class ContentSink {

    private InputStream in;
    private OutputStream out;
    private String cmd;
    private boolean stopped = false;
    private SinkImpl sink;

    public static ContentSink createSink(SinkImpl sink, 
					 InputStream in, Source source) {
	String cmd = "";

	if (source instanceof WAV) {
	    cmd = "playwav";
	} else if (source instanceof Ogg) {
	    cmd = "playogg";
	} else if (source instanceof MP3) {
	    cmd = "playmp3";
	} else {
	    cmd = "true";
	}

	ContentSink csink = new ContentSink(sink, in, cmd);
	return csink;
    }

    /**
     * There should really be a
     * WAVContentSink, OggContentSink, etc
     * I cheated since they would be so simple
     */
    private ContentSink(SinkImpl sink, InputStream in, String cmd) {
	this.sink = sink;
	this.in = in;
	this.cmd = cmd;
    }

    public void record() {
	
	Process proc = null;
	InputStream err = null;
	try {
	    proc = Runtime.getRuntime().exec(cmd);
	    out = proc.getOutputStream();
	    err = proc.getErrorStream();
	} catch(IOException e) {
	    System.err.println("Playing " + e.toString());
	    // ignore
	    return;
	}
	
	int ch;
	try {
	    while (((ch = in.read()) != -1) &&
		   (! stopped)) {
		out.write(ch);
	    }
	} catch(IOException e) {
	    // ignore
	    System.err.println("Exception writing " + e.toString());
	    int navail = 0;
	    try {
		if ((navail = err.available()) > 0 ) {
		    byte avail[] = new byte[navail];
		    int nread = err.read(avail, 0, navail);
		    System.out.println("Error channel " + 
				       new String(avail));
		}
	    } catch(IOException ee) {
		ee.printStackTrace();
	    }
	    return;
	} finally {
	    if (stopped) {
		System.out.println("Record stop called");
	    } else {
		System.out.println("Record finished naturally");
		stopped = true;
	    }
	    try {
		if (proc != null) {
		    proc.destroy();
		    try {
			// wait for soundcard to be released
			proc.waitFor();
		    } catch(InterruptedException ei) {
			System.out.println("Int " + ei);
		    }
		}
		in.close();
		out.close();
	    } catch(IOException e) {
		// ignore
		System.out.println("Finally " + e);
	    }

	    sink.contentStopped();
	}
    }

    public void stop() {
	if (stopped) {
	    return;
	}
	stopped  = true;
    }
} // ContentSink

The playogg script for my Linux system is


#!/bin/sh

if [ $# -eq 0 ]
then
    infile="-"
else
    infile="$1"
fi

play -t ogg -c 2 $infile
wait            # ensure /dev/dsp is free
sleep 3         # and give it extra time to be really free :-(
while playmp3 is

#!/bin/sh

if [ $# -eq 0 ]
then
    infile="-"
else
    infile="$1"
fi

mpg123 -s $infile | sox -t raw -r 44100 -s -w -c 2 - -t ossdsp -w -s /dev/dsp
wait            # ensure that /dev/dsp is given up
sleep 2         # and then give it more time, since "wait" isn't enough
(Some of these programs won't work properly if the artsd daemon is running in the KDE enviroment. I typically kill it off, although there must be a "better" way.)

12. Sink implementation

A sink must create the appropriate transport and content handlers, and link the two together. It needs to look after listeners, and post events to them when they occur. This sink will handle a TCP and HTTP connections, and will manage WAV, Ogg and MP3 content.



/**
 * SinkImpl.java
 */

package audio.pull;

import audio.transport.*;

import java.io.*;
import java.net.*;
import java.rmi.*;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEvent;
import java.util.Vector;
import java.util.Enumeration;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.UnknownEventException;
import java.util.Hashtable;

import audio.common.*;

public class SinkImpl implements Sink, Remote {
    private Source source;
    private boolean stopped;
    private CopyIO copyIO;
    private Hashtable listeners = new Hashtable();
    private int seqNum = 0;
    private Remote proxy;
    private MimeType contentType = null;
    private InputStream in = null;

    public SinkImpl() {

    }

    public void setProxy(Remote proxy) {
	this.proxy = proxy;
    }

    public void record() throws RemoteException, AlreadyRecordingException {

	if ((copyIO != null) && ( ! stopped)) {
	    throw new AlreadyRecordingException();
	}

	if (source == null) {
	    return;
	}

	stopped = false;

	if (in  == null) {
	    System.out.println("Couldn't get input stream");
	    stopped = true;
	    return;
	}

        // hand play over to a CopyIO object
        // This will run a ContentSink in its own thread
	copyIO = new CopyIO(this, in, source);
	copyIO.start();
	System.out.println("Play returning");
    }

    public void stop() throws RemoteException {
	stopped = true;
	if (copyIO != null) {
	    copyIO.stopRecord();
	}
    }

    public void contentStopped() {
	copyIO = null;
	fireNotify(Sink.STOP);
	System.out.println("Stopped");
    }

    public void addSource(Source source) throws
	IncompatableSourceException,
	TooManySourcesException {
	TransportSink transportSink = null;

	this.source = source;

        // which transport sink to use?
	if (source instanceof HttpSource) {
	    transportSink = new HttpSinkImpl((HttpSource) source);
	    in = transportSink.getInputStream();
	} else if (source instanceof TcpSource) {
	    System.out.println("Setting up Tcp sink");
	    transportSink = new TcpSinkImpl((TcpSource) source);
	    in = transportSink.getInputStream();
	    System.out.println("Got tcp source input stream " + in);
	} else {
	    throw new IncompatableSourceException();
	}
    }

    public void removeSource(Source source) throws
	RemoteException,
	NoSuchSourceException {
	if (this.source == source) {
	    this.source = null;
	} else {
	    throw new NoSuchSourceException();
	}
    }

    public EventRegistration addSinkListener(RemoteEventListener listener,
					      MarshalledObject handback) {
	System.out.println("Adding listener: " + listener);
	listeners.put(listener, handback);
	return new EventRegistration(0L, proxy, null, 0L);
    }

    public void removeSinkListener(RemoteEventListener listener) {
	listeners.remove(listener);
    }

    public void fireNotify(int evtType) {
	Enumeration elmts = listeners.keys();
	System.out.println("Fire notify");
	while (elmts.hasMoreElements()) {
	    RemoteEventListener listener = (RemoteEventListener) elmts.nextElement();
	    MarshalledObject handback = (MarshalledObject) listeners.get(listener);
	    RemoteEvent evt = new RemoteEvent(proxy, evtType, seqNum++, handback);
	    System.out.println("Updating listener " + listener);
	    try {
		listener.notify(evt);
	    } catch(UnknownEventException e) {
		// ??
	    } catch(RemoteException e) {
		// ?
	    }
	}
    }

    class CopyIO extends Thread {

	private SinkImpl sink;
	private ContentSink contentSink;

	CopyIO(SinkImpl sink, InputStream in, Source source) {
	    contentSink = ContentSink.createSink(sink, in, source);
	    this.sink = sink;
	}
	
	public void stopRecord() {
	    if (contentSink != null) {
		contentSink.stop();
	    }
	}

	public void run() {
	    contentSink.record();
	}
    }
}// SinkImpl

13. Servers

Each source will need a server to create, advertise it and keep it alive. So will each sink.

13.1 Sink server

A sink server is audio.pull.SinkServer


package audio.pull;

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.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;

import net.jini.config.*; 
import net.jini.export.*; 

import net.jini.id.UuidFactory;
import net.jini.id.Uuid;

import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;
import java.io.*;

/**
 * PullSinkServer.java
 */

public class SinkServer {

    // explicit proxy for Jini 2.0
    protected Remote proxy;
    protected SinkImpl impl;
    private String sinkName = "No name";
    private ServiceID serviceID;

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

        // stay around forever
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(InterruptedException e) {
		// do nothing
	    }
	}
    }

    public SinkServer(String[] argv) {
	File serviceIDFile = null;

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

	String[] configArgs = new String[] {argv[0]};

	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 
	    
	    // and use this to construct an exporter
	    Exporter exporter = (Exporter) config.getEntry( "HttpSinkServer", 
							    "exporter", 
							    Exporter.class); 
	    // export an object of this class
	    proxy = exporter.export(impl);
	    impl.setProxy(proxy);

	    sinkName = (String) config.getEntry( "HttpSinkServer", 
						   "sinkName", 
						   String.class);

	    serviceIDFile = (File) config.getEntry("HttpSinkServer", 
						   "serviceIdFile", 
						   File.class); 
	    getOrMakeServiceID(serviceIDFile);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   null,  // unicast locators
					   null); // DiscoveryListener
	    joinMgr = new JoinManager(proxy, // service proxy
				      new Entry[] {new Name(sinkName)},  // attr sets
				      serviceID,  // ServiceID
				      mgr,   // DiscoveryManager
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    private void getOrMakeServiceID(File serviceIDFile) {
	try {
	    ObjectInputStream ois = 
		new ObjectInputStream(new FileInputStream(serviceIDFile));
	    serviceID = (ServiceID) ois.readObject();
	} catch(Exception e) {
	    System.out.println("Couldn't get service IDs - generating new ones");
	    try {
		ObjectOutputStream oos = 
		    new ObjectOutputStream(new FileOutputStream(serviceIDFile));

		Uuid uuid = UuidFactory.generate();
		serviceID = new ServiceID(uuid.getMostSignificantBits(), 
				      uuid.getLeastSignificantBits());
		oos.writeObject(serviceID);
	    } catch(Exception e2) {
		System.out.println("Couldn't save ids");
		e2.printStackTrace();
	    }
	}
    }
} // SinkServer

This gets information from a configuration file, such as /http_sink_server.config

import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
import java.io.File;

HttpSinkServer {
    exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
                                     new BasicILFactory()); 
    sinkName = new String("Jan's laptop");
    serviceIdFile = new File("sinkServiceId.id");
}

It exports a proxy for a SinkImpl which will handle TCP and HTTP connections, and will manage WAV, Ogg and MP3 content.

13.2 Running the sink server

This sink server depends on the following classes

  1. audio.pull.SinkServer

  2. audio.pull.SinkImpl

  3. audio.pull.SinkImpl$CopyIO (an inner class)

  4. audio.transport.HttpSinkImpl

  5. audio.transport.TcpSinkImpl

  6. audio.pull.ContentSink

  7. audio.transport.TransportSink

  8. All the classes in the audio.common package

These can be collected into a jar file such as audio.pull.SinkServer.jar and run with a configuration such as the above, as in

java -classpath audio.pull.SinkServer.jar audio.pull.SinkServer http_sink_server.config

13.3 Source server

An individual piece of music may be a song, a movement from a classical symphony, an instrumental piece, and so on. Individual pieces of music may be collected together in many ways: a symphony is formed of movements; a pop CD is made up of individual songs; a CD may be made up from a collection of CDs; a boxed set of CDs will be made up of CDs themselves; the complete oeuvres of a composer is another classification. How do we want to represent all of these possibilities as services?

Databases of CDs such as CDDB have a simplistic solution: a CD is classified by an Artist/Title. So CD is a collection of pieces, with no other structure. This breaks down with "best of" collections and almost all classical music - who is the artist? The composer? The conductor? The orchestra? The soloist? MPEG-7 is vastly overkill from our point of view, and only a tiny part ("The Collection Structure DS") has anything to say about organising music from our perspective. MPEG7-Lite as used by the UPnP Audio/Visual framework (ContentDirectory1.0) has a simplified structure, but is quite good for representing the different possibilities just described.

But UPnP is a device-oriented system, where a device (such as a PVR) is responsible for all the individual items stored on it. Although the device may contain a complex directory structure, the individual components of this are not "first class" objects, directly visible and addressable. This would make it hard to, say, set up a "playlist" across a set of devices such as a PVR, an iPod and a home server storing copies of LPs.

The REST community criticises Web services (using SOAP) on the grounds that services have no "addressable endpoint" and that data returned from a service is an XML document that is not addressable at all. UPnP A/V directories are not addressable. In both cases this leads to a loss of flexibility in that clients and services can only work within the bounds of the supplied services and are hence restricted in what they can do - in the case of UPnP it is hard to build up cross-device playlists. So we adopt the extreme viewpoint: every piece of music is advertised as its own service. That allows any other service to build and structure service hierarchies in any way that it wants to. For example, a new service could link photos from an external web site to pieces of music, which would be hard to do with web services or UPnP.

13.4 File source server

We shall present three servers: one that will just advertise a single piece of music available from an HTTP server, one that will advertise a group of pieces (such as a CD of many pieces) and one that will advertise a collection of groups (such as all the CDs on a disk).

The file server will advertise one file as a service. Details of the file will need to be stored in a configuration file such as sting.cfg. The service itself will just be an HttpSource of some kind. Descriptive information (such as artist, name of song, etc) is not given as part of the service, but as service information by Entry objects:


import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;

import java.net.URL;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;

import java.io.File;

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

    url = new URL("http://localhost/soundfiles/pinkfloyd/audio_02.wav");
    entries = new Entry[] {new Name("Sting intro track")
	                  };
    serviceIDFile = new File("sting.id");
}

The server is quite straightforward: it gets information about exporter and service entries from a configuration file and advertises the source as a service using a JoinManager.


package audio.httpsource;

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.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import java.net.URL;
import net.jini.lookup.entry.*;
import net.jini.core.entry.Entry;
import net.jini.core.discovery.LookupLocator;

import net.jini.config.*; 
import net.jini.export.*; 
import net.jini.id.UuidFactory;
import net.jini.id.Uuid;

import java.io.*;

import audio.http.*;

/**
 * FileServer.java
 */

public class FileServer {

    // explicit proxy for Jini 2.0
    private Remote proxy;
    private HttpSourceImpl impl;
    private static String configFile;
    private Entry[] entries;
    private File serviceIDFile;
    private ServiceID serviceID;

    public static void main(String argv[]) {
	configFile = argv[0];

	FileServer serv = new FileServer(argv);

        // stay around forever
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(InterruptedException e) {
		// do nothing
	    }
	}
    }

    public FileServer(String[] argv) {
	URL url = null;
	Exporter exporter = null;

	if (argv.length != 1) {
	    System.err.println("Usage: FileServer config_file");
	    System.exit(1);
	}

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

	String[] configArgs = argv;
	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 

	    // and use this to construct an exporter
	    exporter = (Exporter) config.getEntry( "HttpFile", 
						   "exporter", 
						   Exporter.class);

	    url = (URL) config.getEntry("HttpFile",
					"url",
					URL.class);

	    serviceIDFile = (File) config.getEntry("HttpFile",
						    "serviceIDFile",
						    File.class);
	    getOrMakeServiceID(serviceIDFile);

	    
	    Class cls = Class.forName("[Lnet.jini.core.entry.Entry;");
	    System.out.println(cls.toString());
	    entries = (Entry []) config.getEntry("HttpFile",
						 "entries",
						 cls);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	// Find the right implementation for the content type
        String urlStr = url.toString();
	try {
            if (urlStr.endsWith("wav")) {
                impl = new HttpWAVSourceImpl(url);
            } else if (urlStr.endsWith("mp3")) {
                impl = new HttpMP3SourceImpl(url);
            } else if (urlStr.endsWith("ogg")) {
                impl = new HttpOggSourceImpl(url);
            } else {
                System.out.println("Can't handle presentation type: " +
                                   url);
                return;
            }
	} catch(java.net.MalformedURLException e) {
	    System.err.println(e.toString());
	    System.exit(1);
	}


	try {
	    // export an object of this class
	    proxy = exporter.export(impl);
	} catch(java.rmi.server.ExportException e) {
	    System.err.println(e.toString());
	    System.exit(1);
	}

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   new LookupLocator[] {
					       new LookupLocator("jini://jannote.jan.home/")},
					   // unicast locators
					   null); // DiscoveryListener
	    joinMgr = new JoinManager(proxy,     // service proxy
				      entries,   // attr sets
				      serviceID, // ServiceID
				      mgr,       // DiscoveryManager
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    private void getOrMakeServiceID(File serviceIDFile) {
	// try to read the service ID as
	// object from the file
	serviceID = null;
	try {
	    ObjectInputStream ois = 
		new ObjectInputStream(new FileInputStream(serviceIDFile));
	    serviceID = (ServiceID) ois.readObject();
	    System.out.println("Got dir service id " + serviceID);
	} catch(Exception e) {
	    System.out.println("Couldn't get service IDs - generating new one");
	    try {
		ObjectOutputStream oos = 
		    new ObjectOutputStream(new FileOutputStream(serviceIDFile));

		Uuid uuid = UuidFactory.generate();
		serviceID = new ServiceID(uuid.getMostSignificantBits(), 
				      uuid.getLeastSignificantBits());
		oos.writeObject(serviceID);
		oos.close();
	    } catch(Exception e2) {
		System.out.println("Couldn't save ids");
		e2.printStackTrace();
	    }
	}
    }
} // FileServer

13.5 Running the file source server

The file source server requires the following classes

  1. audio.httpsource.FileServer

  2. audio.httpsource.FileServer

  3. audio.http.HttpSourceImpl

  4. audio.http.HttpOggSourceImpl

  5. audio.http.HttpMP3SourceImpl

  6. audio.http.HttpWAVSourceImpl

  7. audio.presentation.MP3

  8. audio.presentation.WAV

  9. audio.presentation.Ogg

  10. audio.transport.HttpURL

  11. audio.transport.HttpSource

  12. All the classes in the audio.common package

These can be collected into a jar file such as audio.httpsource.FileServer.jar and run with a configuration such as the above, as in

java -classpath audio.httpsource.FileServer.jar audio.httpsource.FileServer sting.cfg

13.6 Directory source server

Much music comes on CDs, on LPs, tapes or cassettes, or in some similarly structured format (even a radio show has a structure). This structure often mirrors that of a "directory". So a CD might contains a directory of tracks, and so on. A directory can be a service in its own right, so there is an interface to define it.



/**
 * Directory.java
 */

package audio.common;

import java.rmi.Remote;
import java.rmi.RemoteException;
import net.jini.core.lookup.ServiceID;

/**
 * A one-level directory of services. If the directory is also
 */

public interface Directory extends Remote {

    ServiceID[] getServiceIDs() throws RemoteException;
}// Directory

The directory defines the minimum about each of its services: their ServiceID's. This allows a directory to contain any type of service: individual songs, other directories, even image services or other services. For a directory like this to work, each service must have a persistent service ID, but this is expected of a Jini service anyway.

While a directory can hold any type of service, we will only look at a directory that contains sound file services available from an HTTP server. The minimal description for each service is then its URL (so that a service can be constructed) and a service ID so that it can be part of the directory. The set of URLs is given in an array of trackURLs in a configuration, and the service IDs are stored as objects in a file referenced by the same configuration. Additional information such as the name of the directory can be given as an Entry property in the configuration. Similarly extra information about each piece such as its track name can also be given there. Miscellaneous information relevant to a directory derived from a CD is how to look it up on a CDDB (CD data-base) server. A typical configuration might look like clapton.cfg


import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;

import java.net.URL;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;
import com.sun.jini.config.ConfigUtil;
import java.io.File;

HttpFileSourceServer {
    exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
                                     new BasicILFactory()); 
    serviceIDFile = new File("soundfiles/clapton.ids");
    
    entries = new Entry[] {new Name("Eric Clapton / Unplugged")
	                  };

    cdIndexDiscID = "H2WKL5utOWh9nlMK6S3y537vygQ-";
    cddbDiscID = "cb0e7b0e";

    localhost = ConfigUtil.getHostName();
    urlBase = ConfigUtil.concat(new String[] {
			 		"http://",
					localhost,
					"/soundfiles/clapton/"
				    }
			       );

    trackURLs = new URL[] {
			new URL(ConfigUtil.concat(new String[] {
							urlBase,
							"audio_01.ogg"
						      }
						 )
				),
			new URL(ConfigUtil.concat(new String[] {
							urlBase,
							"audio_02.ogg"
						      }
						 )
				),
			new URL(ConfigUtil.concat(new String[] {
							urlBase,
							"audio_03.ogg"
						      }
						 )
				)
		};
	trackNames = new String[] {
			"Signe",
			"Before You Accuse Me",
			"Hey Hey"
		};
}

This server is a bit of a departure from previous ones in this book: it creates and advertises a number of services, not just one. Each service will need to respond to its own requests and not to those of others. So each one will need to have a separate dispatcher for its own requests. This is done quite simply by exporting each service by a separate exporter - in fact this is a requirement, as an exporter can only one service. However, the servers we have looked at have typically had a JoinManager, a LookupDiscoveryManager and a LeaseManager.

A single LeaseManager can manage any number of classes, so it is enough to have a single copy and then use it to manage all leases. A single LookupDiscoveryManager is required too but there is no need for more than one. The case of JoinManager is different: it can only manage a single esrvice. For a small number of services this does not matter. But if you consider that an iPod can hold 10,000 services, then we might expect in our situation to have to deal with 10,000 services. Individual JoinManager's do not scale well here, and the memory requirements are quite large. So it is better to avoid this class and handle lookup service discovery directly ourselves.



/**
 * DirectoryServerOpt.java
 */

package audio.httpsource;

import common.*;
import java.io.*;
import java.util.Arrays;
import java.util.ArrayList;

import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.lease.LeaseRenewalManager;
import net.jini.lookup.entry.*;
import net.jini.core.entry.Entry;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import net.jini.core.discovery.LookupLocator;
import net.jini.discovery.LookupDiscovery;
import java.net.URL;
import net.jini.discovery.LookupDiscoveryManager;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceIDListener;
import net.jini.core.lookup.ServiceID;
import net.jini.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import net.jini.core.lookup.*;
import net.jini.core.lease.*;

import net.jini.id.UuidFactory;
import net.jini.id.Uuid;

import net.jini.config.*; 
import net.jini.export.*; 
import com.sun.jini.config.Config;
				       
import java.util.Vector;

import audio.common.Directory;
import audio.http.*;
import audio.transport.*;

public class DirectoryServerOpt implements Directory, DiscoveryListener {
    private ServiceID serviceID = null;
    private ServiceID[] serviceIDs;
    protected Remote proxy;
    protected HttpSourceImpl impl;
    private Entry[] dirEntries;
    private String cdInfo;

    // info pulled out of a Configuration
    private URL url = null;
    private Exporter exporter = null;
    private String cdIndexDiscID;
    private String cddbDiscID;
    private URL[] trackURLs;
    private String[] trackNames;
    private Configuration config;
    private File serviceIDFile;

    // JoinManager's to keep the services alive
    // private JoinManager[] joinMgrs;
    // private JoinManager dirJoinMgr;
    private LeaseRenewalManager leaseRenewalManager;
    private LookupDiscoveryManager mgr;

    private Vector sources = new Vector();

    public DirectoryServerOpt(String[] configArgs, 
			      LeaseRenewalManager leaseRenewalManager,
			      LookupDiscoveryManager lookupDiscoveryManager) {
	this.leaseRenewalManager = leaseRenewalManager;
	this.mgr = lookupDiscoveryManager;

	System.out.println("1");
	getConfiguration(configArgs);
	System.out.println("2");
	cdInfo = getCDInfo();
	System.out.println("3");

	// joinMgrs = new JoinManager[trackURLs.length];
	System.out.println("4");
	/*
	try {
	    mgr = new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					     //new LookupLocator[] {
					     //    new LookupLocator("jini://jannote.jan.home/")},
					     null,
					     // unicast locators
					     this); // DiscoveryListener
	} catch(IOException e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	*/
	mgr.addDiscoveryListener(this);

    }

    public void discovered(DiscoveryEvent e) {
	ServiceRegistrar[] regs = e.getRegistrars();
	for (int m = 0; m < regs.length; m++) {
	    ServiceRegistrar reg = regs[m];

	    System.out.println("5");
	    for (int n = 0; n < trackURLs.length; n++) {
		makeFileService(reg, n, trackURLs[n]);
	    }

	    makeDirService(reg);
	}
    }

    public void discarded(DiscoveryEvent e) {

    }

    public ServiceID[] getServiceIDs() throws RemoteException {
	return serviceIDs;
    }

    private void getConfiguration(String[] configArgs) {
	try {
	    // get the configuration (by default a FileConfiguration)
	    // in configArgs[0]
	    config = ConfigurationProvider.getInstance(configArgs); 
	    System.out.println("Config is " + config);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	try {
	    Class cls = Entry[].class;
	    System.out.println(cls.toString());
	    dirEntries = (Entry []) config.getEntry("HttpFileSourceServer",
						 "entries",
						 cls);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    cdIndexDiscID = (String) config.getEntry("HttpFileSourceServer",
						     "cdIndexDiscID",
						     String.class);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    // This is an unsigned long - sometimes too big for Java long
	    cddbDiscID = (String) config.getEntry("HttpFileSourceServer",
						  "cddbDiscID",
						  String.class,
						  null);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}


	try {
	    Class cls = URL[].class;
	    System.out.println(cls.toString());
	    trackURLs = (URL []) config.getEntry("HttpFileSourceServer",
						 "trackURLs",
						 cls);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    Class cls = String[].class;
	    System.out.println(cls.toString());
	    trackNames = (String []) config.getEntry("HttpFileSourceServer",
						     "trackNames",
						     cls);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    serviceIDFile = (File) config.getEntry("HttpFileSourceServer",
						   "serviceIDFile",
						   File.class);
	    getOrMakeServiceIDs(serviceIDFile);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}
    }

    private void getOrMakeServiceIDs(File serviceIDFile) {
	// try to read all the service IDs as
	// objects from the file
	serviceIDs = new ServiceID[trackURLs.length];
	try {
	    ObjectInputStream ois = 
		new ObjectInputStream(new FileInputStream(serviceIDFile));
	    serviceID = (ServiceID) ois.readObject();
	    System.out.println("Got dir service id " + serviceID);
	    for (int n = 0; n < trackURLs.length; n++) {
		serviceIDs[n] =  (ServiceID) ois.readObject();
		System.out.println("Got service id " + serviceIDs[n]);
	    }
	} catch(Exception e) {
	    System.out.println("Couldn't get service IDs - generating new ones");
	    try {
		ObjectOutputStream oos = 
		    new ObjectOutputStream(new FileOutputStream(serviceIDFile));

		Uuid uuid = UuidFactory.generate();
		serviceID = new ServiceID(uuid.getMostSignificantBits(), 
				      uuid.getLeastSignificantBits());
		oos.writeObject(serviceID);

		for (int n = 0; n < serviceIDs.length; n++) {
		    Uuid uuidFile = UuidFactory.generate();
		    ServiceID id = new ServiceID(uuidFile.getMostSignificantBits(), 
						 uuidFile.getLeastSignificantBits());
		    oos.writeObject(id);
		    serviceIDs[n] = id;
		    System.out.println("Generating service id " + serviceIDs[n]);
		}
	    } catch(Exception e2) {
		System.out.println("Couldn't save ids");
		e2.printStackTrace();
	    }
	}
    }

    private void makeDirService(ServiceRegistrar registrar) {
	try {
	    exporter = (Exporter) config.getEntry( "HttpFileSourceServer", 
						   "exporter", 
						   Exporter.class);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	try {
	    System.out.println("Export dir using exporter " + exporter);
	    // export an object of this class
	    proxy = exporter.export(this);
	} catch(java.rmi.server.ExportException e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}
	ServiceRegistration sr = null;
	try {
	    sr = registrar.register(new ServiceItem(serviceID,
						    proxy,
						    dirEntries),
				    Lease.FOREVER);
	} catch(RemoteException e) {
	    e.printStackTrace();
	}

	leaseRenewalManager.renewUntil(sr.getLease(), Lease.FOREVER, null);
	// dirJoinMgr = registerService(proxy, serviceID, dirEntries);
     }

    private void makeFileService(ServiceRegistrar registrar, int index, URL url) {

	Exporter exporter = null;
	try {
	    exporter = (Exporter) config.getEntry( "HttpFileSourceServer", 
							    "exporter", 
							    Exporter.class);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	String urlStr = url.toString();
 	System.out.println("URL is " + urlStr);
	try {
	    if (urlStr.endsWith("wav")) {
		impl = new HttpWAVSourceImpl(url);
	    } else if (urlStr.endsWith("mp3")) {
		impl = new HttpMP3SourceImpl(url);
	    } else if (urlStr.endsWith("ogg")) {
		impl = new HttpOggSourceImpl(url);
	    } else {
		System.out.println("Can't handle presentation type: " + 
				   url);
		return;
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	    return;
	}
	sources.add(impl);

	try {
	    System.out.println("Export file " +  url + " using exporter " + exporter);
	    // export an object of this class
	    proxy = exporter.export(impl);
	} catch(java.rmi.server.ExportException e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	String  trackInfo = getTrackInfo(index);

	Entry[] entries = new Entry[] {new Name(trackInfo)};
	// joinMgrs[index] = registerService(proxy, serviceIDs[index], entries);

	ServiceRegistration sr = null;
	try {
	    sr = registrar.register(new ServiceItem(serviceIDs[index],
						    proxy,
						    entries),
				    Lease.FOREVER);
	} catch(RemoteException e) {
	    e.printStackTrace();
	}
	leaseRenewalManager.renewUntil(sr.getLease(), Lease.FOREVER, null);

    }

    private JoinManager registerService(Remote proxy, ServiceID serviceID, Entry[] entries) {
	JoinManager joinMgr = null;
	try {

	    System.out.println("Registering with id " + serviceID);
	    joinMgr = new JoinManager(proxy,     // service proxy
				      entries,   // attr sets
				      serviceID, // ServiceID
				      mgr,       // DiscoveryManager
				      leaseRenewalManager);
	    
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	return joinMgr;
    }

    public String getTrackInfo(int index) {
	// get from trackNames if possible
	if (trackNames != null) {
	    if ((trackNames.length > index) &&
		(trackNames[index] != null)) {
		return(cdInfo + ": " + trackNames[index]);
	    }
	}

	// failing that, get from CDDB d/b if possible
	// ... not yet implemented

	// failing that...
	String indexStr = null;
	if (index < 9) indexStr = "0" + (index + 1);
	else indexStr = "" + (index + 1);

	return(cdInfo + ": Track " + indexStr);
    }

    public String getCDInfo() {
	// get from CDDB d/b if possible
	// failing that...
	if (dirEntries == null) {
	    return "";
	}
	for (int n = 0; n  < dirEntries.length; n++) {
	    if (dirEntries[n] instanceof Name) {
		return ((Name) dirEntries[n]).name;
	    }
	}
	return "";
    }

    public static void main(String[] argv) {
	LeaseRenewalManager leaseRenewalManager = new LeaseRenewalManager();
	LookupDiscoveryManager lookupDiscoveryManager = null;
	try {
	    lookupDiscoveryManager =
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   //new LookupLocator[] {
					   //    new LookupLocator("jini://jannote.jan.home/")},
					   null,	// unicast locators
					   null);  // DiscoveryListener
	} catch(IOException e) {
	    e.printStackTrace();
	    System.exit(1);
	}

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	DirectoryServerOpt ds = new DirectoryServerOpt(argv, 
						       leaseRenewalManager,
						       lookupDiscoveryManager);

       // stay around forever
        Object keepAlive = new Object();
        synchronized(keepAlive) {
            try {
                keepAlive.wait();
            } catch(InterruptedException e) {
                // do nothing
            }
        }
    }
}// DirectoryServer

13.7 Multiple directories source server

The final source server that we shall build generalises the idea of a "set of CDs", by looking at a set of directories. If a set of configurations are each stored in files in a common directory, then by running through the directory and advertising each directory we will have advertised the entire set. This server takes a directory name as command line argument and creates a new DirectoryServer for each configuration file ending in .cfg. All of the directory servers can share the same LeaseManager.



/**
 * DirDirectoryServer.java
 */

package audio.httpsource;

import common.*;
import java.io.*;
import java.rmi.RMISecurityManager;
import java.util.Vector;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.discovery.LookupDiscovery;

/**
 * Read a (file) directory for a list of .cfg files
 */
  
public class DirDirectoryServer {
    
    private Vector dirs = new Vector();

    public DirDirectoryServer(String dirStr) {
	LeaseRenewalManager leaseRenewalManager = new LeaseRenewalManager();
	LookupDiscoveryManager lookupDiscoveryManager = null;
	try {
	    lookupDiscoveryManager =
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   //new LookupLocator[] {
					   //    new LookupLocator("jini://jannote.jan.home/")},
					   null,	// unicast locators
					   null);  // DiscoveryListener
	} catch(IOException e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	
	File dir = new File(dirStr);
	String[] files = dir.list(new java.io.FilenameFilter() {
		public boolean accept(File d, String name) {
		    System.out.println("Checking file " + name);
		    if (name.endsWith(".cfg"))
			return true;
		    else
			return false;
		}
	    });

	System.out.println("Dir length: " + files.length);
	for (int n = 0; n  < files.length; n++) {
	    dirs.add(
		     new DirectoryServerOpt(new String[] {dirStr + "/" + files[n]},
					    leaseRenewalManager,
					    lookupDiscoveryManager)
		     );
	}
    }

    public static void main(String[] argv) {

	System.setSecurityManager(new RMISecurityManager());

	DirDirectoryServer dd = new DirDirectoryServer(argv[0]);

       // stay around forever
        Object keepAlive = new Object();
        synchronized(keepAlive) {
            try {
                keepAlive.wait();
            } catch(InterruptedException e) {
                // do nothing
            }
        }
    }
}// DirDirectoryServer

14. Clients

A client will locate sources and sinks and allow a user to make selections from them. Each sink will be told about the selected sources, and each source will be told about the selected sinks. The client may register itself as a listener for events (such as STOP) from the services. Then the client will ask the sources to play() and the sinks to record().

14.1 Basic client

A really basic client will just find a sink and a source (any source, any sink), tell each other about the other and then play/record to audio stream. This can be done by


package audio.client;

import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;

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 audio.common.*;

/**
 * BasicClient.java
 */

public class BasicClient {

    private static final long WAITFOR = 100000L;
    private ServiceDiscoveryManager clientMgr = null;

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

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

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

	// find a source and sink
	Sink sink = (Sink) getService(Sink.class);
	Source source = (Source) getService(Source.class);

	// tell them about each other
	try {
	    source.addSink(sink);
	    sink.addSource(source);
	} catch(Exception e) {
	    System.err.println("Error setting source or sink " + e);
	    e.printStackTrace();
	    System.exit(1);
	}

	// play the audio
	try {
	    System.out.println("Playing...");
	    source.play();
	    sink.record();
	} catch(Exception e) {
	    System.out.println("Error in playing " + e);
	    System.exit(1);
	}
    }

    private Object getService(Class cls) {
  
	Class [] classes = new Class[] {cls};
	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 for class " + cls);
	    System.exit(1);
	}

	// Return the service
	return item.service;
    }
} // BasicClient


14.2 Running the basic client

The basic client requires the following classes

  1. audio.client.BasicClient

  2. All the classes in the audio.common package

These can be collected into a jar file such as audio.client.BasicClient.jar and run with a configuration such as the above, as in

java -classpath audio.client.BasicClient.jar audio.client.BasicClient

14.3 GUI client

A more complex client will monitor the sinks and sources (and directories) and display them in suitable panels. These panels will allow selections of sources and sink. The GUIClient looks after service management.


/**
 * GUIClient.java
 */

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;
import net.jini.core.lookup.ServiceID;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceItemFilter;
import java.rmi.Remote;

import net.jini.config.*;
import net.jini.export.*;

import common.*;

/** 
 * An A/V client that monitors sources and sinks and
 */

public class GUIClient implements ServiceDiscoveryListener {

    private ClientFrame clientFrame;
    private LookupCache cache;
    private static String CONFIG_FILE = "jeri/http_sink_server.config";

    public static void main(String argv[]) {
	ClientFrame cf = new ClientFrame();
	GUIClient client = new GUIClient(cf);
	cf.setClient(client);

	cf.setSize(600, 600);
	cf.setVisible(true);

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

    public GUIClient(ClientFrame cf) {
	clientFrame = cf;

        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);
        }
  
        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());
	if (postItem.service instanceof Directory) {
	    System.out.println("  is dir");
	    addDirectory(postItem);

	}
	if (postItem.service instanceof Sink) {
	    System.out.println("  is sink");
	    clientFrame.addSink(postItem);
	}
	if (postItem.service instanceof Source) {
	    System.out.println("  is source");
	    clientFrame.addSource(postItem);
	}
    }

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

	if (preItem.service instanceof Directory) {
	    System.out.println("  was dir");
	    clientFrame.removeDirectory(preItem);
	}
	if (preItem.service instanceof Sink) {
	    System.out.println("  was sink");
	    clientFrame.removeSink(preItem);
	}
	if (preItem.service instanceof Source) {
	    System.out.println("  was source");
	    clientFrame.removeSource(preItem);
	}

    }
    
    private void addDirectory(ServiceItem item) {
	Object node = clientFrame.addDirectory(item);
	ServiceID[] ids = null;
	try {
	    ids = ((Directory) item.service).getServiceIDs();
	} catch(RemoteException e) {
	    System.err.println(e.toString());
	    return;
	}

	for (int n = 0; n < ids.length; n++) {
	    final ServiceID id = ids[n];
	    // find a service in the cache that matches the service ID
	    // we are looking for - doesn't seem to be a way to retrieve
	    // by serviceID
	    ServiceItemFilter filter = new ServiceItemFilter() {
		    public boolean check(ServiceItem dirItem) {
			if (dirItem.serviceID.equals(id)) {
			    return true;
			} else {
			    return false;
			}
		    }
		};
	    ServiceItem dirItem = cache.lookup(filter);
	    if (dirItem != null) {
		clientFrame.addDirectoryElement(node, dirItem);
	    }
	}
    }

    public Remote export(PlayFrame cf) {
	String[] configArgs = new String[] {CONFIG_FILE};
	Remote proxy;

	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 
	    
	    // and use this to construct an exporter
	    Exporter exporter = (Exporter) config.getEntry( "HttpSinkServer", 
							    "exporter", 
							    Exporter.class); 
	    // export an object for the client listener
	    proxy = exporter.export(cf);
	    return proxy;

	} catch(Exception e) {
	    e.printStackTrace();
	    return null;
	}
    }
} // GUIClient

The ClientFrame looks after display and selection of sources/directories/sinks. It shows each of these in Swing JTabbedPane. The directory information is displayed using a JTree (this is good for playing a CD in track order).

The individual sources are shown in a JList with the services alphabetically sorted.
The individual sinks are shown in a JList.

The ClientFrame is



/**
 * ClientFrame.java
 */

package client;

import common.*;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceID;
import java.rmi.RemoteException;
import java.util.Enumeration;
import net.jini.core.event.RemoteEvent;
import java.rmi.Remote;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;
import java.io.IOException;


/**
 * The main Swing JFrame for the GUIClient
 */

public class ClientFrame extends JFrame
				 implements ActionListener, 
					    ListSelectionListener,
					    TreeSelectionListener,
					    TreeExpansionListener,
					    RemoteEventListener {

    private GUIClient client;

    private JList sources = new JList();
    private JList sinks = new JList();
    private JTree directories = new JTree();

    private JButton playFolderBtn = new JButton("Play folder");
    private JButton playSourcesBtn = new JButton("Play sources");
    private JButton stopBtn = new JButton("Stop");

    private Remote proxy;

    public ClientFrame() {
	super("Audio Router");
	makeLayout();
	addListeners();
	setupLists();
    }

    public void setClient(GUIClient client) {
	this.client = client;
    }

    private void makeLayout() {
	Container contentPane = getContentPane();
	contentPane.setLayout(new BorderLayout());

	JPanel bottom = new JPanel();
	JTabbedPane top = new JTabbedPane();
	contentPane.add(bottom, BorderLayout.SOUTH);
	contentPane.add(top, BorderLayout.CENTER);

	JPanel dirPane = new JPanel();
	JPanel sourcePane = new JPanel();
	JPanel sinkPane = new JPanel();
	top.add("Folders", dirPane);
	top.add("Sources", sourcePane);
	top.add("Sinks", sinkPane);
	
	dirPane.setLayout(new BorderLayout());
	JScrollPane js0 = new  JScrollPane();
	js0.getViewport().setView(directories);
	dirPane.add(js0,
		       BorderLayout.CENTER);

	sourcePane.setLayout(new BorderLayout());
	JScrollPane js1 = new  JScrollPane();
	js1.getViewport().setView(sources);
	sourcePane.add(js1,
		       BorderLayout.CENTER);

	sinkPane.setLayout(new BorderLayout());
	JScrollPane js2 = new  JScrollPane();
	js2.getViewport().setView(sinks);
	sinkPane.add(js2, BorderLayout.CENTER);

	bottom.setLayout(new BorderLayout());

	JPanel buttons = new JPanel();
	buttons.setLayout(new GridLayout(1, 3));
	buttons.add(playFolderBtn);
	buttons.add(playSourcesBtn);
	// buttons.add(stopBtn);

	bottom.add(buttons, BorderLayout.CENTER);
    }

    private void addListeners() {
	playFolderBtn.addActionListener(this);
	playSourcesBtn.addActionListener(this);
 	stopBtn.addActionListener(this);

	directories.addTreeSelectionListener(this);
	directories.addTreeExpansionListener(this);

	sources.addListSelectionListener(this);

	sinks.addListSelectionListener(this);
    }

    private void setupLists() {
	LabelCellRenderer labelRend = new LabelCellRenderer();
    
	directories.setCellRenderer(labelRend);
	DefaultMutableTreeNode root = new DefaultMutableTreeNode("Directories");
	directories.setModel(new DefaultTreeModel(root));

	sources.setCellRenderer(labelRend);
	sources.setModel(new DefaultListModel());

	sinks.setCellRenderer(labelRend);
	sinks.setModel(new DefaultListModel());
    }

    public void actionPerformed(ActionEvent evt) {
	Object[] sourceSels = null;
	if (evt.getSource() == playSourcesBtn) {
	    sourceSels = sources.getSelectedValues();
	} else if (evt.getSource() == playFolderBtn) {
	    TreePath[] selectionPaths = directories.getSelectionPaths();
	    if (selectionPaths == null) {
		sourceSels = null;
	    } else {
		sourceSels = new Object[selectionPaths.length];
		for (int n = 0; n < sourceSels.length; n++) {
		    DefaultMutableTreeNode node = (DefaultMutableTreeNode) 
			selectionPaths[n].
			getLastPathComponent();
		    Object lastComponent = node.getUserObject();
		    sourceSels[n] = lastComponent;
		    }
	    }
	}

	if ((sourceSels == null) || (sourceSels.length == 0)) {
	    JOptionPane.showMessageDialog(null, 
					  "No source selected", "Source is null",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	}
	
	Object[] sinkSels = sinks.getSelectedValues();
	if (sinkSels == null) {
	    JOptionPane.showMessageDialog(null, 
					  "No sink selected", "Sink is null",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	}
	
	ServiceItem[] sourceSelections = new ServiceItem[sourceSels.length];
	for (int n = 0; n < sourceSels.length; n++) {
	    sourceSelections[n] = (ServiceItem) sourceSels[n];
	}
	
	ServiceItem[] sinkSelections = new ServiceItem[sinkSels.length];
	for (int n = 0; n < sinkSels.length; n++) {
	    sinkSelections[n] = (ServiceItem) sinkSels[n];
	}
	// Sink sink = (Sink) sinkSelections[0].service;
	
	new PlayFrame(client).play(sourceSelections, sinkSelections[0]);
	// play(sourceSelections, sinkSelections[0]);
    }

    private void play(ServiceItem[] sourceSelections, ServiceItem sinkItem) {
	if ((sourceSelections == null) || (sourceSelections.length == 0)) {
	    System.out.println("Play: null sources");
	    return;
	}
	Source source = (Source) sourceSelections[0].service;
	Sink sink = (Sink) sinkItem.service;

	ServiceItem[] rest = new ServiceItem[sourceSelections.length - 1];
	for (int n = 0; n < rest.length; n++) {
	    rest[n] = sourceSelections[n + 1];
	}

	MarshalledObject handback = null;
	try {
	    handback = new MarshalledObject(new SourceSink(rest, sinkItem));
	} catch(java.io.IOException e) {
	    e.printStackTrace();
	    return;
	}

	try {
	    if (proxy == null) {
		// proxy = client.export(this);
	    }
	    // source.addSourceListener((RemoteEventListener) proxy, null);
	    System.out.println("Added source " + source + " proxy " + proxy);
	    sink.addSinkListener((RemoteEventListener) proxy, handback);
	    System.out.println("Added sink " + sink + " proxy " + proxy + " handback " + 
			       ((SourceSink) handback.get()).sources);
	} catch(RemoteException e) {
	    e.printStackTrace();
	} catch(Exception e) {
	    e.printStackTrace();
	}

	try{
	    System.out.println("Setting sink to: " + sink);
	    source.addSink(sink);
	    System.out.println("Setting source to: " + source);
	    sink.addSource(source);
	    System.out.println("Playing " + sourceSelections[0].attributeSets[0]);

	    // and then play...
	    source.play();
	    sink.record();
	} catch(AlreadyPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source already playing",
					  "Play error",
					  JOptionPane.ERROR_MESSAGE);
	} catch(AlreadyRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink already recording",
					  "Record error",
					  JOptionPane.ERROR_MESSAGE);
	} catch(Exception e) {
	    // IncompatableSink/Source
	    e.printStackTrace();
	}
    }

    private void stop(ServiceItem[] sourceSelections, ServiceItem sinkItem) {
	// Source source = (Source) sourceSelections[0].service;
	System.out.println("Stopping");
	Sink sink = (Sink) sinkItem.service;

	try{
	    // source.stop();
	    sink.stop();
	    /*
	} catch(NotPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source not playing",
					  "Play stop error",
					  JOptionPane.ERROR_MESSAGE);
	    */
	} catch(NotRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink not recording",
					  "Record stop error",
					  JOptionPane.ERROR_MESSAGE);
	} catch(Exception e) {
	    // ignore?
	    e.printStackTrace();
	}
    }

    public void notify(RemoteEvent evt) {
	Object src = evt.getSource();
	System.out.println("Updating " + src);
	if ((src instanceof Sink) && 
	    (evt.getID() == Sink.STOP)) {
	    System.out.println("Sink stopped event");
	    Sink sink = (Sink) src;

	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
	    } catch(RemoteException e) {
		// ignore
	    } catch(common.NoSuchListenerException e) {
		// ignore
	    }

	    MarshalledObject handback = evt.getRegistrationObject();
	    SourceSink ss = null;
	    try {
		ss = (SourceSink) handback.get();
	    } catch(Exception e) {
		e.printStackTrace();
		return;
	    }

	    ServiceItem[] sources = ss.sources;
	    ServiceItem sinkItem = ss.sink;
	    System.out.println("  stop -> play: sources " + sources + " sink " + sink);
	    play(sources, sinkItem);

	} else if ((src instanceof Source) && 
	    (evt.getID() == Source.STOP)) {
	    System.out.println("Source stopped event");
	}
    }

    /**********************************
     * Source tree manipulation methods
     */
    public void valueChanged(TreeSelectionEvent evt) {
	JTree tree = (JTree) evt.getSource();
	
	TreePath selectionPath = tree.getSelectionPath();
	if (selectionPath == null) {
	    return;
	}

	DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectionPath.
	    getLastPathComponent();
	Object lastComponent = node.getUserObject();
	System.out.println("Selection: " + lastComponent.toString());

	if ( ! node.isLeaf()) {
	    // select all children of this node
	    TreePath[] childPaths = new TreePath[node.getChildCount()];
	    Object[] selPathObjects = selectionPath.getPath();
	    int selPathObjectsLength = selPathObjects.length;
	    
	    int n = 0;
	    Enumeration children = node.children();
	    while (children.hasMoreElements()) {
		Object[] childPathObjects = new Object[selPathObjectsLength + 1]; 
		for (int m = 0; m < selPathObjectsLength; m++) {
		    childPathObjects[m] = selPathObjects[m];
		}
		childPathObjects[selPathObjectsLength] = children.nextElement();
		
		childPaths[n++] = new TreePath(childPathObjects);
	    }
	    tree.setSelectionPaths(childPaths);
	    
	}
    }
    
    public void treeCollapsed(TreeExpansionEvent evt) {
	System.out.println("Tree collapsed " + evt.getPath());
    }
    
    public void treeExpanded(TreeExpansionEvent evt) {
	System.out.println("Tree expanded " + evt.getPath());
    }
    
    public DefaultMutableTreeNode addDirectory(ServiceItem item) {
	DefaultTreeModel model = (DefaultTreeModel) directories.getModel();
	
	DefaultMutableTreeNode newNode =     
	    addDirectoryTreeNode(new ComparableServiceItem(item), model);
	return newNode;
    }
    
    public void addDirectoryElement(Object node, ServiceItem item) {
	final DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) node;
	final DefaultMutableTreeNode newNode =     
	    new DefaultMutableTreeNode(new ComparableServiceItem(item));
	Runnable addDirElmt = new Runnable() {
		public void run() {
		    parentNode.add(newNode);
		}
	    };
	// Swing isn't thread-safe: all model changes 
	// have to be placed in the event queue
	SwingUtilities.invokeLater(addDirElmt);
    }
    
    public void removeDirectory(ServiceItem item) {
 	DefaultTreeModel model = (DefaultTreeModel) directories.getModel();
	removeTreeItem(new ComparableServiceItem(item), model);
    }
    
    
    private DefaultMutableTreeNode addDirectoryTreeNode(final ComparableServiceItem item, 
							final DefaultTreeModel model) {
	final DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(item);
	
	Runnable insertElement = new Runnable() {
		public void run() {
		    System.out.println("Inserting dir tree node " + item);
		    DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
		    
		    Enumeration elmts = root.children();
		    
		    int n = 0;
		    while (elmts.hasMoreElements()) {
			DefaultMutableTreeNode node = (DefaultMutableTreeNode) elmts.nextElement();
			if (item.compareTo(node.getUserObject()) <= 0) {
			    break;
			}
			n++;
		    }
		    model.insertNodeInto(newNode, root, n);
		}
	    };
	try {
	    // Swing isn't thread-safe: all model changes 
	    // have to be placed in the event queue
	    SwingUtilities.invokeAndWait(insertElement);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	} catch(java.lang.reflect.InvocationTargetException e) {
	    e.printStackTrace();
	}
  	return newNode;
    }

    private DefaultMutableTreeNode removeTreeItem(final ComparableServiceItem item, 
						  final DefaultTreeModel model) {
	final DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(item);

	Runnable removeElement = new Runnable() {
		public void run() {
		    System.out.println("Removing tree node " + item);
		    DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
		    
		    Enumeration elmts = root.children();
		    
		    DefaultMutableTreeNode foundNode = null;
		    while (elmts.hasMoreElements()) {
			DefaultMutableTreeNode node = (DefaultMutableTreeNode) elmts.nextElement();
			if (item.compareTo(node.getUserObject()) == 0) {
			    System.out.println("Removing directory");
			    node.removeFromParent();
			    return;
			}
		    }
		    System.out.println("Failed to remove directory");
		}
	    };
	try {
	    // Swing isn't thread-safe: all model changes 
	    // have to be placed in the event queue
	    SwingUtilities.invokeAndWait(removeElement);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	} catch(java.lang.reflect.InvocationTargetException e) {
	    e.printStackTrace();
	}
  	return newNode;
    }



    /********************************** 
     * Source List manipulation methods
     */
    public void addSource(ServiceItem item) {
	DefaultListModel model = (DefaultListModel) sources.getModel();

	insertListItem(new ComparableServiceItem(item), model);
    }
    public void removeSource(final ServiceItem item) {
 	final DefaultListModel model = (DefaultListModel) sources.getModel();
	Runnable removeElement = new Runnable() {
		public void run() {
		    if (model.removeElement(new ComparableServiceItem(item))) {
			System.err.println("Removed source elmt " + item.service);
		    } else {
			System.err.println("Failed to remove source elmt " + item.service);
		    }
		}
	    };
	// Swing isn't thread-safe: all model changes 
	// have to be placed in the event queue
	SwingUtilities.invokeLater(removeElement);
   }



    /********************************
     * Sink list manipulation methods
     */

    public void addSink(ServiceItem item) {
	DefaultListModel model = (DefaultListModel) sinks.getModel();

	insertListItem(new ComparableServiceItem(item), model);
     }

    public void removeSink(final ServiceItem item) {
 	final DefaultListModel model = (DefaultListModel) sinks.getModel();
	Runnable removeElement = new Runnable() {
		public void run() {
		    if (model.removeElement(new ComparableServiceItem(item))) {
			System.err.println("Removed sink elmt " + item.service);
		    } else {
			System.err.println("Failed to remove sink elmt " + item.service);
		    }
		}
	    };
	// Swing isn't thread-safe: all model changes 
	// have to be placed in the event queue
	SwingUtilities.invokeLater(removeElement);
    }

    /*********************************
     * General list methods
     */
    public void valueChanged(ListSelectionEvent evt) {
	if (evt.getValueIsAdjusting()) {
	    return;
	}
    }


    private void insertListItem(final ComparableServiceItem item, 
				final DefaultListModel model) {
	Runnable insertElement = new Runnable() {
		public void run() {
		    Enumeration elmts = model.elements();
		    
		    int n = 0;
		    while (elmts.hasMoreElements()) {
			if (item.compareTo(elmts.nextElement()) <= 0) {
			    break;
			}
			n++;
		    }
		    model.add(n, item);
		}
	    };
	// there may be other insert's in the event queue, so we can't
	// just compare the current list with the item. We have to wait
	// till all the pending events have been processed, and then compare
	try {
	    SwingUtilities.invokeAndWait(insertElement);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	} catch(java.lang.reflect.InvocationTargetException e) {
	    e.printStackTrace();
	}
    }

        
 }// ClientFrame

When source(s) and a sink are selected and a "play" button is pressed, a PlayFrame is displayed. This shows the currently chosen set of sources. This frame listens for events from the sink so that it can tell when a particular track has finished. Then it will start playing the next one. It gets the information about the "next one" (and all of its successors) from the "handback" object, which has a list of the remaining tracks to play



/**
 * PlayFrame.java
 */

package client;

import common.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import net.jini.core.event.RemoteEvent;
import java.rmi.Remote;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;
import java.io.IOException;
import java.rmi.RemoteException;
import javax.swing.event.*;

import net.jini.core.entry.Entry;
import net.jini.lookup.ui.MainUI;
import net.jini.lookup.ui.factory.JDialogFactory;
import net.jini.lookup.entry.UIDescriptor;
import net.jini.lookup.ui.attribute.UIFactoryTypes;
import java.util.Set;
import java.util.Iterator;

import net.jini.core.lookup.ServiceItem;

/**
 * Play a selection of sources to a single sink
 */

public class PlayFrame extends JFrame implements ActionListener,
						 ChangeListener,
						 RemoteEventListener {
    private ServiceItem[] sources;
    private ServiceItem sinkItem;
    private Sink sink;
    private Source source;

    private JList sourceList = new JList();
    private JButton stopBtn = new JButton("Stop");
    private JButton stopAllBtn = new JButton("Stop all");
    private JLabel sinkLabel = new JLabel();
    private JSlider sourceVolumeCtl = new JSlider();
    private JSlider sinkVolumeCtl = new JSlider();

    private DefaultListModel  model;
    private Remote proxy;
    private GUIClient client;

    public PlayFrame(GUIClient client) {
	setTitle("Player");
	this.client = client;

	JPanel bottomPanel = new JPanel();
	bottomPanel.setLayout(new BorderLayout());
	Container contentPane = getContentPane();
	contentPane.setLayout(new BorderLayout());
	contentPane.add(sinkLabel, BorderLayout.NORTH);
	contentPane.add(sourceList, BorderLayout.CENTER);
	contentPane.add(bottomPanel, BorderLayout.SOUTH);

	JPanel volumeControls = new JPanel();
	volumeControls.setLayout(new GridLayout(1, 2));
	volumeControls.add(sourceVolumeCtl);
	volumeControls.add(sinkVolumeCtl);

	JPanel buttons = new JPanel();
	buttons.setLayout(new GridLayout(1,3));
	buttons.add(stopBtn);
	buttons.add(stopAllBtn);

	bottomPanel.add(volumeControls, BorderLayout.NORTH);
	bottomPanel.add(buttons, BorderLayout.SOUTH);

	stopBtn.addActionListener(this);
	stopAllBtn.addActionListener(this);

	LabelCellRenderer labelRend = new LabelCellRenderer();
    
	sourceList.setCellRenderer(labelRend);
	model = new DefaultListModel();
	sourceList.setModel(model);

	sourceVolumeCtl.setEnabled(false);
	sinkVolumeCtl.setEnabled(false);
	sourceVolumeCtl.addChangeListener(this);
	sinkVolumeCtl.addChangeListener(this);
	sourceVolumeCtl.setToolTipText("Set volume on the source");
	sinkVolumeCtl.setToolTipText("Set volume on the sink");

	setSize(400, 300);
	setVisible(true);
    }

    public void actionPerformed(ActionEvent evt) {
	if (evt.getSource() == stopBtn) {
	    try {
		sink.stop();
		sinkVolumeCtl.setEnabled(false);
		sourceVolumeCtl.setEnabled(false);
	    } catch(RemoteException e) {
		e.printStackTrace();
	    } catch(NotRecordingException e) {
		e.printStackTrace();
	    }
	} else if (evt.getSource() == stopAllBtn) {
	    // kill the rest of the playlist by removing
	    // our listener and putting in a new one with
	    // null playback
	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
		System.out.println("Removed listener " + proxy);
		sink.addSinkListener((RemoteEventListener) proxy, 
				     new MarshalledObject(
					 new SourceSink(null, null)));
		System.out.println("Added listener with null playback");
	    } catch(RemoteException e) {
		// ignore
		e.printStackTrace();
	    } catch(NoSuchListenerException e) {
		System.err.println("Can't change listener " + proxy);
	    } catch(IOException e) {
		System.err.println("Can't marshall null " + proxy);
	    }

	    // now stop the play
	    try {
		sink.stop();
		sinkVolumeCtl.setEnabled(false);
		sourceVolumeCtl.setEnabled(false);
	    } catch(RemoteException e) {
		e.printStackTrace();
	    } catch(NotRecordingException e) {
		e.printStackTrace();
	    }
	}
    }

    public void stateChanged(ChangeEvent e) {
	JSlider slider = (JSlider) e.getSource();
	int vol = slider.getValue();
	VolumeControl volCtl = null;
	if (slider == sinkVolumeCtl) {
	    volCtl = (VolumeControl) sink;
	} else if (slider == sourceVolumeCtl) {
	    volCtl = (VolumeControl) source;
	}
	try {
	    volCtl.setVolume(vol);
	} catch(RemoteException ex) {
	    // ignore
	}
    }

    public void play(ServiceItem[] sources, ServiceItem sinkItem) {
	if ((sources == null) || (sources.length == 0)) {
	    System.out.println("Play: null sources");
	    // dispose();
	    Runnable doDispose = new Runnable() {
		    public void run() {
			dispose();
		    }
		};
	    SwingUtilities.invokeLater(doDispose);

	    return;
	}

	this.sources = sources;
	this.sinkItem = sinkItem;
	sink = (Sink) sinkItem.service;
	source = (Source) sources[0].service;

	sinkLabel.setText("Playing to " + sinkItem.toString());

	model.clear();
	for (int n = 0; n < sources.length; n++) {
	    model.addElement(sources[n]);
	}

	ServiceItem[] rest = new ServiceItem[sources.length - 1];
	for (int n = 0; n < rest.length; n++) {
	    rest[n] = sources[n + 1];
	}
	MarshalledObject handback = null;
	try {
	    handback = new MarshalledObject(new SourceSink(rest, sinkItem));
	} catch(java.io.IOException e) {
	    e.printStackTrace();
	    return;
	}

	try {
	    if (proxy == null) {
		proxy = client.export(this);
	    }
	    // source.addSourceListener((RemoteEventListener) proxy, null);
	    System.out.println("Added source " + source + " proxy " + proxy);
	    sink.addSinkListener((RemoteEventListener) proxy, handback);
	    System.out.println("Added sink " + sink + " proxy " + proxy + " handback " + 
			       ((SourceSink) handback.get()).sources);
	} catch(RemoteException e) {
	    e.printStackTrace();
	} catch(Exception e) {
	    e.printStackTrace();
	}

	try{
	    System.out.println("Setting sink to: " + sink);
	    source.addSink(sink);
	    System.out.println("Setting source to: " + source);
	    sink.addSource(source);
	    System.out.println("Playing" + sources[0].attributeSets[0]);

	    // check for UI elements
	    checkUI(sinkItem);

	    source.play();
	    sink.record();
	} catch(AlreadyPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source already playing",
					  "Play error",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	} catch(AlreadyRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink already recording",
					  "Record error",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	} catch(Exception e) {
	    // IncompatableSink/Source
	    e.printStackTrace();
	    return;
	}

	System.out.println("Checking VC " + sink);

	if (source instanceof VolumeControl) {
	    System.out.println("Source is VC");
	    enableVolumeControl( (VolumeControl) source, sourceVolumeCtl);
	}
	if (sink instanceof VolumeControl) {
	    System.out.println("Sink is VC");
	    enableVolumeControl( (VolumeControl) sink, sinkVolumeCtl);
	}
    }

    public void notify(RemoteEvent evt) {
	Object src = evt.getSource();
	System.out.println("Updating " + src);
	if ((src instanceof Sink) && 
	    (evt.getID() == Sink.STOP)) {
	    System.out.println("Sink stopped event");
	    Sink sink = (Sink) src;

	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
		System.out.println("Removed listener " + proxy);
	    } catch(RemoteException e) {
		// ignore
		e.printStackTrace();
	    } catch(NoSuchListenerException e) {
		System.err.println("Can't remove listener " + proxy);
	    }

	    MarshalledObject handback = evt.getRegistrationObject();
	    SourceSink ss = null;
	    try {
		ss = (SourceSink) handback.get();
	    } catch(Exception e) {
		e.printStackTrace();
		return;
	    }
	    if (ss == null) {
		// no more playlist
		return;
	    }

	    final ServiceItem[] sources = ss.sources;
	    final ServiceItem sinkItem = ss.sink;
	    System.out.println("  stop -> play: sources " + sources + " sink " + sink);
	    Runnable doPlay = new Runnable() {
		    public void run() {
			play(sources, sinkItem);
		    }
		};
	    SwingUtilities.invokeLater(doPlay);

	} else if ((src instanceof Source) && 
	    (evt.getID() == Source.STOP)) {
	    System.out.println("Source stopped event");
	}
    }

    private void enableVolumeControl(VolumeControl vol, JSlider slider) {
	int maxVol = 0;
	int currVol = 0;
	try {
	    maxVol = vol.getMaxVolume();
	    if (maxVol <= 0) return;

	    currVol = vol.getVolume();
	    System.out.println("Current vol: " + currVol);
	    if (currVol < 0 || currVol > maxVol) return;
	} catch (RemoteException e) {
	    e.printStackTrace();
	    return;
	}
	slider.setMinimum(0);
	slider.setMaximum(maxVol);
	slider.setValue(currVol);
	slider.setEnabled(true);
    }


    /************************************
     * Check for UI entries
     */
    private void checkUI(ServiceItem item) {
        // Find and check the UIDescriptor's
        Entry[] attributes = item.attributeSets;
	System.out.println("Entries: " + attributes.length);
        for (int m = 0; m < attributes.length; m++) {
            Entry attr = attributes[m];
	    System.out.println("Checking Entry " + attr);
            if (attr instanceof UIDescriptor) {
		System.out.println("Found a UI");
                // does it deliver a Swing Dialog?
		// how do we decide if we want a Frame or Dialog?
                checkForSwingDialog(item, (UIDescriptor) attr);
            }
        }
    }
   
    private void checkForSwingDialog(ServiceItem item, UIDescriptor desc) {
        Set attributes = desc.attributes;
        Iterator iter = attributes.iterator();
        while (iter.hasNext()) {
            // search through the attributes, to find a UIFactoryTypes
            Object obj = iter.next();
            if (obj instanceof UIFactoryTypes) {
                UIFactoryTypes types = (UIFactoryTypes) obj;
                // see if it produces a Swing Dialog Factory
                if (types.isAssignableTo(JDialogFactory.class)) {
                    JDialogFactory factory = null;
                    try {
                        factory = (JDialogFactory) desc.getUIFactory(this.getClass().
                                                                  getClassLoader());
                    } catch(Exception e) {
                        e.printStackTrace();
                        continue;
                    }

                    System.out.println("calling dialog with " + item);
		    // how do we decide if it should be modal?
                    JDialog dialog = factory.getJDialog(item, this, true);
                    dialog.setVisible(true);
                } 
            }
        }
    }
}// PlayFrame

15. A file sink

A variety of different sources and sinks can be handled by this framework. This is not just different tracks from a CD and different players around the home, but can include different implementations. For example, the equivalent of a cassette recorder can be built by making a sink save its input into a file instead of sending it to the soundcard. This section looks at what is involved in doing this.

If a service is going to save audio to a file, then it will need to allow a client to specify this file. At the very least, it will need to expose a setFile method. But it could be more complex than this: the client may need to browse the service file system before choosing a name: for example, to record the "Bitches brew" jazz program off PBS-FM it may want to change to an appropriate directory and check that it isn't going to overwrite an earlier recording.

File browsing is usually handled in a GUI environment by JFileChooser. In a non-GUI environment there are methods such as File.list() to list a directory. However, these are local methods in local classes: the JFileChooser has no implicit support for browsing the filesystem of a remote Jini system. But it does have a hook to allow such remote browsing, through the FileSystemView class, which is intended to isolate O/S dependent code into a single class.

Based on the FileSystemView class, we can define an interface that captures remote file system browsing and choosing a file



/**
 * FileSink.java
 */

package audio.filesink;

import java.io.File;
import java.rmi.RemoteException;

public interface FileSink extends audio.common.Sink {

    public boolean setFile(File sinkFile) throws RemoteException;

    /**
     * methods to browse the file system
     * Based on FileSystemView from JFileChooser
     */

    public File[] getFiles(File dir, boolean useFileHiding) throws RemoteException;
    public File getHomeDirectory() throws RemoteException;
    public File getDefaultDirectory() throws RemoteException;
    public File createNewFolder(File dir) throws RemoteException, java.io.IOException;

}// FileSink

While the client sees the interface, the server needs to implement it. This can be done by keeping a FileSystemView object on the server side and passing most calls through to it



/**
 * SinkImpl.java
 */

package audio.filesink;

import audio.common.*;
import audio.transport.*;
import audio.aumix.*;
import audio.pull.*;

import java.io.*;
import java.net.*;
import java.rmi.*;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEvent;
import java.util.Vector;
import java.util.Enumeration;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.UnknownEventException;
import java.util.Hashtable;
import javax.swing.filechooser.FileSystemView;

import common.*;

public class SinkImpl implements FileSink, Remote {
    private Source source;
    private boolean stopped;
    private CopyIO copyIO;
    private Hashtable listeners = new Hashtable();
    private int seqNum = 0;
    private Remote proxy;
    private File sinkFile;
    private FileSystemView fileView;

    public SinkImpl() {
	fileView = FileSystemView.getFileSystemView();
    }

    public void setProxy(Remote proxy) {
	this.proxy = proxy;
    }

    public boolean setFile(File sinkFile) {
	this.sinkFile = sinkFile;
	System.out.println("File sink set to " + sinkFile);
	return true;
    }

    public void record() throws RemoteException, AlreadyRecordingException {
	TransportSink transportSink = null;
	InputStream in = null;

	if (source == null) {
	    return;
	}

	if (source instanceof HttpSource) {
	    transportSink = new HttpSinkImpl((HttpSource) source);
	    in = transportSink.getInputStream();
	} else if (source instanceof TcpSource) {
	    System.out.println("Setting up Tcp sink");
	    transportSink = new TcpSinkImpl((TcpSource) source);
	    in = transportSink.getInputStream();
	    System.out.println("Got tcp source input stream " + in);
	} else if (source instanceof PullSource) {
	    in = ((PullSource) source).getInputStream();
	} else {
	    return;
	}


	if ((copyIO != null) && ( ! stopped)) {
	    throw new AlreadyRecordingException();
	}

	stopped = false;

	if (in  == null) {
	    System.out.println("Couldn't get input stream");
	    stopped = true;
	    return;
	}

	copyIO = new CopyIO(this, in, source);
	copyIO.start();
	System.out.println("Play returning");
    }

    public void stop() throws RemoteException {
	stopped = true;
	if (copyIO != null) {
	    copyIO.stopRecord();
	}
    }

    public void contentStopped() {
	copyIO = null;
	fireNotify(Sink.STOP);
	System.out.println("Stopped");
    }

    public void addSource(Source source) throws
	IncompatableSourceException {
	//if (Transport.compatable(source, this)) {
	this.source = source;
	    //} else {
	    //throw new IncompatableSourceException();
	    //}
    }

    public void removeSource(Source source) throws
	RemoteException,
	NoSuchSourceException {
    }

    public EventRegistration addSinkListener(RemoteEventListener listener,
					      MarshalledObject handback) {
	System.out.println("Adding listener: " + listener);
	listeners.put(listener, handback);
	return new EventRegistration(0L, proxy, null, 0L);
    }

    public void removeSinkListener(RemoteEventListener listener) {
	listeners.remove(listener);
    }

    public void fireNotify(int evtType) {
	Enumeration elmts = listeners.keys();
	System.out.println("Fire notify");
	while (elmts.hasMoreElements()) {
	    RemoteEventListener listener = (RemoteEventListener) elmts.nextElement();
	    MarshalledObject handback = (MarshalledObject) listeners.get(listener);
	    RemoteEvent evt = new RemoteEvent(proxy, evtType, seqNum++, handback);
	    System.out.println("Updating listener " + listener);
	    try {
		listener.notify(evt);
	    } catch(UnknownEventException e) {
		// ??
	    } catch(RemoteException e) {
		// ?
	    }
	}
    }

    class CopyIO extends Thread {

	private SinkImpl sink;
	private ContentSink contentSink;

	CopyIO(SinkImpl sink, InputStream in, Source source) {
	    contentSink = ContentSink.createSink(sink, in, source, sinkFile);
	    this.sink = sink;

	}
	
	public void stopRecord() {
	    if (contentSink != null) {
		contentSink.stop();
		// stopped = true;
	    }
	}

	public void run() {
	    contentSink.record();
	}
    }

    // For remote file browsing
    public File[] getFiles(File dir, boolean useFileHiding) {
	return fileView.getFiles(dir, useFileHiding);
    }

    public File getHomeDirectory() {
	return fileView.getHomeDirectory();
    }

    public File getDefaultDirectory() {
	return fileView.getDefaultDirectory();
    }

    public File createNewFolder(File dir) throws java.io.IOException {
	return fileView.createNewFolder(dir);
    }

}// SinkImpl

This sink implementation follows the structure of the earlier ones, although it could be condensed by directly writing to the output file. In this implementation, this is done by ContentSink writing to the file instead of to a "play" program:



/**
 * ContentSink.java
 */

package audio.filesink;

import java.io.*;
import audio.presentation.*;
import audio.common.*;

public class ContentSink {

    private InputStream in;
    private OutputStream out;
    private boolean stopped = false;
    private SinkImpl sink;
    private File outFile;

    public static ContentSink createSink(SinkImpl sink, 
					 InputStream in, Source source,
					 File outFile) {

	ContentSink csink = new ContentSink(sink, in, outFile);
	return csink;
    }

    private ContentSink(SinkImpl sink, InputStream in, File outFile) {
	this.sink = sink;
	this.in = in;
	this.outFile = outFile;
	System.out.println("Content sink set to " + outFile);
    }

    public void record() {
	
	InputStream err = null;
	try {
	    out = new DataOutputStream(new FileOutputStream(outFile));
	} catch(IOException e) {
	    System.err.println("Playing " + e.toString());
	    // ignore
	    return;
	}
	
	int ch;
	try {
	    while (((ch = in.read()) != -1) &&
		   (! stopped)) {
		out.write(ch);
	    }
	} catch(IOException e) {
	    // ignore
	    System.err.println("Exception writing " + e.toString());
	    int navail = 0;
	    try {
		if ((navail = err.available()) > 0 ) {
		    byte avail[] = new byte[navail];
		    int nread = err.read(avail, 0, navail);
		    System.out.println("Error channel " + 
				       new String(avail));
		}
	    } catch(IOException ee) {
		ee.printStackTrace();
	    }
	    return;
	} finally {
	    if (stopped) {
		System.out.println("Record stop called");
	    } else {
		System.out.println("Record finished naturally");
		stopped = true;
	    }
	    try {
		in.close();
		out.close();
	    } catch(IOException e) {
		// ignore
		System.out.println("Finally " + e);
	    }

	    sink.contentStopped();
	}
    }

    public void stop() {
	if (stopped) {
	    return;
	}
	stopped  = true;
    }
}// ContentSink

The classes listed above are sufficient for a non-GUI client to interact with a file sink by making remote method calls on it. This could be suitable for an automated recording system ("record this show each week") where a client starts to record to the service at a given time each week and stops the record once the time is up.

16. A file sink with GUI

If the client is a GUI client, then it may want to allow the user to browse the remote file system using a JFileChooser. This can be done using the UI packages recently approved for Jini which are discussed in their own chapter. Here we just use them.

The JFileChooser has a FileSystemViewer object. This encapsulates many operating specific methods, such as File[] getFiles() and File createNewFolder(). The JFileChooser will run in a client, but we want it to be able to access a remote file system where it will save files. The FileSink discussed in the last section can almost be used for this, except it is a remote object and all of its methods throw RemoteException. So we have to wrap it in an object that will catch all these exceptions and return suitable values. For want of any better idea, we return null, although possibly a warning dialog should be popped up first.



/**
 * RemoteFileSystemViewWrapper.java
 */

package audio.filesink;

import javax.swing.filechooser.*;
import java.io.*;
import java.rmi.RemoteException;

public class RemoteFileSystemViewWrapper extends FileSystemView {
    private FileSink sink;

    public RemoteFileSystemViewWrapper(FileSink sink) {
	this.sink = sink; 
    }

    public File createNewFolder(File dir) throws IOException {
	try {
	    return sink.createNewFolder(dir);
	} catch(RemoteException e) {
	    e.printStackTrace();
	    return null;
	}
    }

    public File[] getFiles(File dir, boolean useFileHiding) {
	try {
	    return sink.getFiles(dir, useFileHiding);
	} catch(RemoteException e) {
	    e.printStackTrace();
	    return null;
	}
    }

    public File getDefaultDirectory() {
	try {
	    return sink.getDefaultDirectory();
	} catch(RemoteException e) {
	    e.printStackTrace();
	    return null;
	}
    }

    public File getHomeDirectory() {
	try {
	return sink.getHomeDirectory();
	} catch(RemoteException e) {
	    e.printStackTrace();
	    return null;
	}
    }
}

The exported service will be a proxy for a SinkImpl which implements the FileSink interface. The client will want to run a dialog which pops up a JFileChooser with a FileSystemViewer set to a RemoteFileSystemViewerWrapper which wraps around the FileSink service. When a selection is made, it can set the file in the service.



/**
 * FileSinkJDialog.java
 */

package audio.filesink;

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import net.jini.lookup.ui.MainUI;
import net.jini.core.lookup.ServiceItem;
import java.rmi.RemoteException;
import javax.swing.filechooser.FileSystemView;

public class FileSinkJDialog extends JDialog implements MainUI, ActionListener  {
    private JFileChooser chooser;
    private FileSink sink;

    public FileSinkJDialog(Frame owner, ServiceItem item, 
			   String name, boolean modal) {
	super(owner, name, modal);

	sink = (FileSink) item.service;

	// setup a filter based on sink.getSource() ifaces
	// ...

	RemoteFileSystemViewWrapper fileView = new RemoteFileSystemViewWrapper(sink);

	chooser = new JFileChooser((FileSystemView) fileView);
	chooser.addActionListener(this);
	getContentPane().add(chooser, BorderLayout.CENTER);
	pack();
    }

    public FileSinkJDialog(Dialog owner, ServiceItem item, 
			   String name, boolean modal) {
	super(owner, name, modal);

	sink = (FileSink) item.service;

	// setup a filter based on sink.getSource() ifaces
	// e.g. to only show .wav or .ogg files
	// ... not done yet

	RemoteFileSystemViewWrapper fileView = new RemoteFileSystemViewWrapper(sink);

	chooser = new JFileChooser((FileSystemView) fileView);
	chooser.addActionListener(this);
	getContentPane().add(chooser, BorderLayout.CENTER);
	pack();
    }

    public void actionPerformed(ActionEvent e) {
	if (e.getActionCommand().equals(JFileChooser.APPROVE_SELECTION)) {
	    // file selected
	    System.out.println("Selected file: " + chooser.getSelectedFile());
	    try {
		sink.setFile(chooser.getSelectedFile());
		setVisible(false);
	    } catch(RemoteException re) {
		System.out.println("" + re);
	    }
	} else {
	    System.out.println("Other event: " + e);
	}
    }
}// FileSinkJDialog

As discussed in the chapter on user interfaces, it is not possible to just send a FileSinkJDialog to a client, because it may not have the classes or memory to handle such an object. The first stage in hiding this is to use a factory object to produce it. This factory follows exactly the pattern for this factory, just changing the returned object to a FileSinkJDialog



/**
 * FileSinkJDialogFactory.java
 */

package audio.filesink;

import net.jini.lookup.ui.factory.JDialogFactory;
import net.jini.lookup.entry.UIDescriptor;
import javax.swing.JDialog;
import net.jini.core.entry.Entry;
import net.jini.core.lookup.ServiceItem;
import java.awt.*;

public class FileSinkJDialogFactory implements JDialogFactory {

    /**
     * Return a new FileSinkJDialog that implements the
     * MainUI role
     */
    public JDialog getJDialog(Object roleObject, Frame owner, boolean modal) {
        // we should check to see what role we have to return
        if (! (roleObject instanceof ServiceItem)) {
            // unkown role type object
            // can we return null?
            return null;
        }
        ServiceItem item = (ServiceItem) roleObject;

        // Do sanity checking that the UIDescriptor has a MainUI role
        Entry[] entries = item.attributeSets;
        for (int n = 0; n < entries.length; n++) {
            if (entries[n] instanceof UIDescriptor) {
                UIDescriptor desc = (UIDescriptor) entries[n];
                if (desc.role.equals(net.jini.lookup.ui.MainUI.ROLE)) {
                    // Ok, we are in the MainUI role, so return a UI for that
                    JDialog dialog = new FileSinkJDialog(owner, item, 
							 "File sink", modal);
                    return dialog;
                }
            }
        }
        // couldn't find a role the factory can create
        return null;
    }

    public JDialog getJDialog(Object roleObject) {
	return getJDialog(roleObject, (Frame) null, false);
    }

    public JDialog getJDialog(Object roleObject, Dialog owner) {
	return getJDialog(roleObject, owner, false);
    }

    public JDialog getJDialog(Object roleObject, Dialog owner,
			     boolean modal) {
	        // we should check to see what role we have to return
        if (! (roleObject instanceof ServiceItem)) {
            // unkown role type object
            // can we return null?
            return null;
        }
        ServiceItem item = (ServiceItem) roleObject;

        // Do sanity checking that the UIDescriptor has a MainUI role
        Entry[] entries = item.attributeSets;
        for (int n = 0; n < entries.length; n++) {
            if (entries[n] instanceof UIDescriptor) {
                UIDescriptor desc = (UIDescriptor) entries[n];
                if (desc.role.equals(net.jini.lookup.ui.MainUI.ROLE)) {
                    // Ok, we are in the MainUI role, so return a UI for that
                    JDialog dialog = new FileSinkJDialog(owner, item, 
							 "File sink", modal);
                    return dialog;
                }
            }
        }
        // couldn't find a role the factory can create
        return null;
    }

    public JDialog getJDialog(Object roleObject, Frame owner) {
	return getJDialog(roleObject, owner, false);
    }

}// FileSinkJDialogFactory

Finally from the server side, the factory has to marshalled (to hide its classes) and embedded into a UIDescriptor entry object as part of the service advertisement. This is accomplished by the getEntry() method, which is called in preparing the Entry[] array


package audio.filesink;

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.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;

import net.jini.config.*; 
import net.jini.export.*; 

import net.jini.id.UuidFactory;
import net.jini.id.Uuid;

import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;

import net.jini.lookup.ui.MainUI;
import java.rmi.MarshalledObject;
import java.util.Set;
import java.util.HashSet;
import net.jini.lookup.ui.factory.JDialogFactory;
import net.jini.lookup.ui.attribute.UIFactoryTypes;

import java.io.*;

/**
 * SinkServer.java
 */

public class SinkServer 
    implements ServiceIDListener {

    // explicit proxy for Jini 2.0
    protected Remote proxy;
    protected SinkImpl impl;
    private String sinkName = "No name";
    private ServiceID serviceID;

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

        // stay around forever
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(InterruptedException e) {
		// do nothing
	    }
	}
    }

    public SinkServer(String[] argv) {
	File serviceIDFile = null;

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

	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(argv); 
	    
	    // and use this to construct an exporter
	    Exporter exporter = (Exporter) config.getEntry( "FileSinkServer", 
							    "exporter", 
							    Exporter.class); 
	    // export an object of this class
	    proxy = exporter.export(impl);
	    impl.setProxy(proxy);

	    sinkName = (String) config.getEntry( "FileSinkServer", 
						 "sinkName", 
						 String.class);

	    serviceIDFile = (File) config.getEntry("FileSinkServer", 
						   "serviceIdFile", 
						   File.class); 
	    getOrMakeServiceID(serviceIDFile);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   null,  // unicast locators
					   null); // DiscoveryListener
	    joinMgr = new JoinManager(proxy, // service proxy
				      new Entry[] {new Name(sinkName),
				                   getUIEntry()},  // attr sets
				      serviceID,  // ServiceID
				      mgr,   // DiscoveryManager
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    private Entry getUIEntry() {
        // The typenames for the factory
        Set typeNames = new HashSet();
        typeNames.add(JDialogFactory.TYPE_NAME);

        // The attributes set
        Set attribs = new HashSet();
        attribs.add(new UIFactoryTypes(typeNames));

        // The factory
        MarshalledObject factory = null;
        try {
            factory = new MarshalledObject(new FileSinkJDialogFactory());
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(2);
        }
        UIDescriptor desc = new UIDescriptor(MainUI.ROLE,
                                             JDialogFactory.TOOLKIT,
                                             attribs,
                                             factory);

        return desc;
    }

    private void getOrMakeServiceID(File serviceIDFile) {
	try {
	    ObjectInputStream ois = 
		new ObjectInputStream(new FileInputStream(serviceIDFile));
	    serviceID = (ServiceID) ois.readObject();
	} catch(Exception e) {
	    System.out.println("Couldn't get service IDs - generating new ones");
	    try {
		ObjectOutputStream oos = 
		    new ObjectOutputStream(new FileOutputStream(serviceIDFile));

		Uuid uuid = UuidFactory.generate();
		serviceID = new ServiceID(uuid.getMostSignificantBits(), 
				      uuid.getLeastSignificantBits());
		oos.writeObject(serviceID);
	    } catch(Exception e2) {
		System.out.println("Couldn't save ids");
		e2.printStackTrace();
	    }
	}
    }

    public void serviceIDNotify(ServiceID serviceID) {
	// called as a ServiceIDListener
	// Should save the id to permanent storage
	System.out.println("got service ID " + serviceID.toString());
    }
    
} // SinkServer

Finally, the client has to be modified to look for UI objects and to use them as part of the user interaction. The client is the same as the GUI client discussed earlier except that in the PlayFrame object it checks for a UIDescriptor



/**
 * PlayFrame.java
 */

package client;

import common.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import net.jini.core.event.RemoteEvent;
import java.rmi.Remote;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;
import java.io.IOException;
import java.rmi.RemoteException;
import javax.swing.event.*;

import net.jini.core.entry.Entry;
import net.jini.lookup.ui.MainUI;
import net.jini.lookup.ui.factory.JDialogFactory;
import net.jini.lookup.entry.UIDescriptor;
import net.jini.lookup.ui.attribute.UIFactoryTypes;
import java.util.Set;
import java.util.Iterator;

import net.jini.core.lookup.ServiceItem;

/**
 * Play a selection of sources to a single sink
 */

public class PlayFrame extends JFrame implements ActionListener,
						 ChangeListener,
						 RemoteEventListener {
    private ServiceItem[] sources;
    private ServiceItem sinkItem;
    private Sink sink;
    private Source source;

    private JList sourceList = new JList();
    private JButton stopBtn = new JButton("Stop");
    private JButton stopAllBtn = new JButton("Stop all");
    private JLabel sinkLabel = new JLabel();
    private JSlider sourceVolumeCtl = new JSlider();
    private JSlider sinkVolumeCtl = new JSlider();

    private DefaultListModel  model;
    private Remote proxy;
    private GUIClient client;

    public PlayFrame(GUIClient client) {
	setTitle("Player");
	this.client = client;

	JPanel bottomPanel = new JPanel();
	bottomPanel.setLayout(new BorderLayout());
	Container contentPane = getContentPane();
	contentPane.setLayout(new BorderLayout());
	contentPane.add(sinkLabel, BorderLayout.NORTH);
	contentPane.add(sourceList, BorderLayout.CENTER);
	contentPane.add(bottomPanel, BorderLayout.SOUTH);

	JPanel volumeControls = new JPanel();
	volumeControls.setLayout(new GridLayout(1, 2));
	volumeControls.add(sourceVolumeCtl);
	volumeControls.add(sinkVolumeCtl);

	JPanel buttons = new JPanel();
	buttons.setLayout(new GridLayout(1,3));
	buttons.add(stopBtn);
	buttons.add(stopAllBtn);

	bottomPanel.add(volumeControls, BorderLayout.NORTH);
	bottomPanel.add(buttons, BorderLayout.SOUTH);

	stopBtn.addActionListener(this);
	stopAllBtn.addActionListener(this);

	LabelCellRenderer labelRend = new LabelCellRenderer();
    
	sourceList.setCellRenderer(labelRend);
	model = new DefaultListModel();
	sourceList.setModel(model);

	sourceVolumeCtl.setEnabled(false);
	sinkVolumeCtl.setEnabled(false);
	sourceVolumeCtl.addChangeListener(this);
	sinkVolumeCtl.addChangeListener(this);
	sourceVolumeCtl.setToolTipText("Set volume on the source");
	sinkVolumeCtl.setToolTipText("Set volume on the sink");

	setSize(400, 300);
	setVisible(true);
    }

    public void actionPerformed(ActionEvent evt) {
	if (evt.getSource() == stopBtn) {
	    try {
		sink.stop();
		sinkVolumeCtl.setEnabled(false);
		sourceVolumeCtl.setEnabled(false);
	    } catch(RemoteException e) {
		e.printStackTrace();
	    } catch(NotRecordingException e) {
		e.printStackTrace();
	    }
	} else if (evt.getSource() == stopAllBtn) {
	    // kill the rest of the playlist by removing
	    // our listener and putting in a new one with
	    // null playback
	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
		System.out.println("Removed listener " + proxy);
		sink.addSinkListener((RemoteEventListener) proxy, 
				     new MarshalledObject(
					 new SourceSink(null, null)));
		System.out.println("Added listener with null playback");
	    } catch(RemoteException e) {
		// ignore
		e.printStackTrace();
	    } catch(NoSuchListenerException e) {
		System.err.println("Can't change listener " + proxy);
	    } catch(IOException e) {
		System.err.println("Can't marshall null " + proxy);
	    }

	    // now stop the play
	    try {
		sink.stop();
		sinkVolumeCtl.setEnabled(false);
		sourceVolumeCtl.setEnabled(false);
	    } catch(RemoteException e) {
		e.printStackTrace();
	    } catch(NotRecordingException e) {
		e.printStackTrace();
	    }
	}
    }

    public void stateChanged(ChangeEvent e) {
	JSlider slider = (JSlider) e.getSource();
	int vol = slider.getValue();
	VolumeControl volCtl = null;
	if (slider == sinkVolumeCtl) {
	    volCtl = (VolumeControl) sink;
	} else if (slider == sourceVolumeCtl) {
	    volCtl = (VolumeControl) source;
	}
	try {
	    volCtl.setVolume(vol);
	} catch(RemoteException ex) {
	    // ignore
	}
    }

    public void play(ServiceItem[] sources, ServiceItem sinkItem) {
	if ((sources == null) || (sources.length == 0)) {
	    System.out.println("Play: null sources");
	    // dispose();
	    Runnable doDispose = new Runnable() {
		    public void run() {
			dispose();
		    }
		};
	    SwingUtilities.invokeLater(doDispose);

	    return;
	}

	this.sources = sources;
	this.sinkItem = sinkItem;
	sink = (Sink) sinkItem.service;
	source = (Source) sources[0].service;

	sinkLabel.setText("Playing to " + sinkItem.toString());

	model.clear();
	for (int n = 0; n < sources.length; n++) {
	    model.addElement(sources[n]);
	}

	ServiceItem[] rest = new ServiceItem[sources.length - 1];
	for (int n = 0; n < rest.length; n++) {
	    rest[n] = sources[n + 1];
	}
	MarshalledObject handback = null;
	try {
	    handback = new MarshalledObject(new SourceSink(rest, sinkItem));
	} catch(java.io.IOException e) {
	    e.printStackTrace();
	    return;
	}

	try {
	    if (proxy == null) {
		proxy = client.export(this);
	    }
	    // source.addSourceListener((RemoteEventListener) proxy, null);
	    System.out.println("Added source " + source + " proxy " + proxy);
	    sink.addSinkListener((RemoteEventListener) proxy, handback);
	    System.out.println("Added sink " + sink + " proxy " + proxy + " handback " + 
			       ((SourceSink) handback.get()).sources);
	} catch(RemoteException e) {
	    e.printStackTrace();
	} catch(Exception e) {
	    e.printStackTrace();
	}

	try{
	    System.out.println("Setting sink to: " + sink);
	    source.addSink(sink);
	    System.out.println("Setting source to: " + source);
	    sink.addSource(source);
	    System.out.println("Playing" + sources[0].attributeSets[0]);

	    // check for UI elements
	    checkUI(sinkItem);

	    source.play();
	    sink.record();
	} catch(AlreadyPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source already playing",
					  "Play error",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	} catch(AlreadyRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink already recording",
					  "Record error",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	} catch(Exception e) {
	    // IncompatableSink/Source
	    e.printStackTrace();
	    return;
	}

	System.out.println("Checking VC " + sink);

	if (source instanceof VolumeControl) {
	    System.out.println("Source is VC");
	    enableVolumeControl( (VolumeControl) source, sourceVolumeCtl);
	}
	if (sink instanceof VolumeControl) {
	    System.out.println("Sink is VC");
	    enableVolumeControl( (VolumeControl) sink, sinkVolumeCtl);
	}
    }

    public void notify(RemoteEvent evt) {
	Object src = evt.getSource();
	System.out.println("Updating " + src);
	if ((src instanceof Sink) && 
	    (evt.getID() == Sink.STOP)) {
	    System.out.println("Sink stopped event");
	    Sink sink = (Sink) src;

	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
		System.out.println("Removed listener " + proxy);
	    } catch(RemoteException e) {
		// ignore
		e.printStackTrace();
	    } catch(NoSuchListenerException e) {
		System.err.println("Can't remove listener " + proxy);
	    }

	    MarshalledObject handback = evt.getRegistrationObject();
	    SourceSink ss = null;
	    try {
		ss = (SourceSink) handback.get();
	    } catch(Exception e) {
		e.printStackTrace();
		return;
	    }
	    if (ss == null) {
		// no more playlist
		return;
	    }

	    final ServiceItem[] sources = ss.sources;
	    final ServiceItem sinkItem = ss.sink;
	    System.out.println("  stop -> play: sources " + sources + " sink " + sink);
	    Runnable doPlay = new Runnable() {
		    public void run() {
			play(sources, sinkItem);
		    }
		};
	    SwingUtilities.invokeLater(doPlay);

	} else if ((src instanceof Source) && 
	    (evt.getID() == Source.STOP)) {
	    System.out.println("Source stopped event");
	}
    }

    private void enableVolumeControl(VolumeControl vol, JSlider slider) {
	int maxVol = 0;
	int currVol = 0;
	try {
	    maxVol = vol.getMaxVolume();
	    if (maxVol <= 0) return;

	    currVol = vol.getVolume();
	    System.out.println("Current vol: " + currVol);
	    if (currVol < 0 || currVol > maxVol) return;
	} catch (RemoteException e) {
	    e.printStackTrace();
	    return;
	}
	slider.setMinimum(0);
	slider.setMaximum(maxVol);
	slider.setValue(currVol);
	slider.setEnabled(true);
    }


    /************************************
     * Check for UI entries
     */
    private void checkUI(ServiceItem item) {
        // Find and check the UIDescriptor's
        Entry[] attributes = item.attributeSets;
	System.out.println("Entries: " + attributes.length);
        for (int m = 0; m < attributes.length; m++) {
            Entry attr = attributes[m];
	    System.out.println("Checking Entry " + attr);
            if (attr instanceof UIDescriptor) {
		System.out.println("Found a UI");
                // does it deliver a Swing Dialog?
		// how do we decide if we want a Frame or Dialog?
                checkForSwingDialog(item, (UIDescriptor) attr);
            }
        }
    }
   
    private void checkForSwingDialog(ServiceItem item, UIDescriptor desc) {
        Set attributes = desc.attributes;
        Iterator iter = attributes.iterator();
        while (iter.hasNext()) {
            // search through the attributes, to find a UIFactoryTypes
            Object obj = iter.next();
            if (obj instanceof UIFactoryTypes) {
                UIFactoryTypes types = (UIFactoryTypes) obj;
                // see if it produces a Swing Dialog Factory
                if (types.isAssignableTo(JDialogFactory.class)) {
                    JDialogFactory factory = null;
                    try {
                        factory = (JDialogFactory) desc.getUIFactory(this.getClass().
                                                                  getClassLoader());
                    } catch(Exception e) {
                        e.printStackTrace();
                        continue;
                    }

                    System.out.println("calling dialog with " + item);
		    // how do we decide if it should be modal?
                    JDialog dialog = factory.getJDialog(item, this, true);
                    dialog.setVisible(true);
                } 
            }
        }
    }
}// PlayFrame

17. Conclusion

This chapter has discussed a framework for distributed audio. Jini makes it fairly straightforward to handle service advertisement and discovery, telling services about each other and generatikng and handling remote events. The architecture is extensible just be adding in more interfaces and implementations. For example, I am currently adding a "file sink" so that I can record audio to file. If this is linked to an FM tuner card then I will be able to record my favourite shows from a timer program.

18. 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.