Make room for JavaSpaces, Part 6

Build and use distributed data structures in your JavaSpaces programs

1 2 3 Page 2
Page 2 of 3

Requests are added to the tail end of the channel, and taken away from the head end. You can use a tail entry to keep track of the end of the channel: it holds the position number of the last request in the channel, and it gets incremented before you append a new request to the channel. You'll also need a head entry to keep track of the front of the channel: it holds the position number of the first request in the channel, and every time a request is removed from the front, the head's position number is incremented to point to the next request.

Now I'll show how each MP3Request in the channel will look.

The MP3 Request entry

The MP3Request entry is defined as follows:

public class MP3Request implements Entry {
    public String channelName;  // recipient of the request
    public Integer position;    // position # of request in channel
    public String inputName;    // file path
    public byte[] data;         // content of the file
    public String from;         // who sent the request
    public MP3Request() {       // the no-arg constructor
    }
    public MP3Request(String channelName) {
        this.channelName = channelName;
    }
    public MP3Request(String channelName, Integer position) {
        this.channelName = channelName;
        this.position = position;
    }
    public MP3Request(String channelName, Integer position,
        String inputName, byte[] data, String from) 
    {
        this.channelName = channelName;
        this.position = position;
        this.inputName = inputName;
        this.data = data;
        this.from = from;
    }
}

Each MP3Request entry contains five fields: channelName, position, inputName, data, and from. The channelName identifies the name of the channel to which the request belongs (which in this example will always be "MP3 Request"). The position field specifies the integer position of the request in the channel. The inputName holds the absolute path name to a wav file to encode (for example, C:\Windows\Desktop\groovy-music.wav). The data field is used to hold the raw byte content of that file. Finally, the from field specifies the user who submitted the request.

Tracking the channel's start and end

You'll need both head and tail entries to keep track of the front and back ends of the channel, respectively. You can base both on the Index entry defined here:

public class Index implements Entry {
    public String type;        // head or tail
    public String channel;
    public Integer position;
    public Index() {
    }
    public Index(String type, String channel) {
        this.type = type;
        this.channel = channel;
    }
    public Index(String type, String channel, Integer position) {
        this.type = type;
        this.channel = channel;
        this.position = position;
    }
    public Integer getPosition() {
        return position;
    }
    public void increment() {
        position = new Integer(position.intValue() + 1);
    }
}

The Index entry has three fields: a type field to identify the type of index (whether the entry points to the head or tail of the channel), a channel field that holds the name of the channel, and a position field that contains the position number (of the head or tail element). You'll notice that this entry has three constructors and two convenience methods: getPosition, which returns the position of the head or tail, and increment, which increments the position by one.

You'll track the start and end of the channel using Index entries as follows. Say your channel holds five MP3Request entries, at positions one through five. In that case, the head index entry will hold position number one, and the tail index entry will hold position number five. When a new request is added to the tail end of the channel, the tail position is incremented by one. When a request is removed, it is taken away from the head end of the channel, which results in the head position being incremented by one. If requests are removed at a faster clip than new ones are added, eventually there will be just one request in the channel, with both the head and tail pointing to it. If that one remaining request is removed and the head is incremented, the head's position number will be greater than the tail's -- and that will indicate an empty channel. As long as the tail position is greater than or equal to the head position, you know there are requests in the channel.

Creating the MP3 Request channel

Now I'll show how you create your (initially empty) channel. An administrator runs a ChannelCreator application, which is defined like this:

public class ChannelCreator {
    private JavaSpace space;
    public static void main(String[] args) {
        ChannelCreator creator = new ChannelCreator();
        creator.createChannel(args[0]);
    }
       
    private void createChannel(String channelName) {
        space = SpaceAccessor.getSpace();
        Index head =
                new Index("head", channelName, new Integer(1));
        Index tail =
                new Index("tail", channelName, new Integer(0));
        System.out.println("Creating new channel " + channelName);
        try {
            space.write(head, null, Lease.FOREVER);
            space.write(tail, null, Lease.FOREVER);
        } catch (Exception e) {
            System.out.println("Error creating the channel.");
            e.printStackTrace();
            return;
        }
        System.out.println("Channel " + channelName + " created.");
    }
}

Notice that ChannelCreator takes as an argument the name of the channel to be created, which in this case is "MP3 Request." When this application is run, the main method takes the channel name and passes it to the createChannel method. The job of createChannel is straightforward: it sets up two Index entries to mark the head and tail of the channel, and it writes them to the space. Since a new channel starts out empty, createChannel sets up the tail entry with an index of zero and the head entry with an index of one (recall that when the tail position is less than the head position, you know you have an empty channel).

The MP3 Requester

Once a channel exists, you are free to add requests to it. In order to add MP3 encoding requests to your MP3 Request channel, you run an MP3Requester applet, whose interface is shown in Figure 4. To request MP3 encodings, you must fill in the topmost text field with a unique name that identifies yourself (you can use your email address, for example); this name will be used to identify your requests and results. You must also fill in the text field that asks for an absolute filename (say, C:\Windows\Desktop\wavs\drpepper.wav) that you'd like to have encoded. Then you're ready to click on the Encode It! button, which causes the MP3Requester to append the request to the MP3 Request channel.

Figure 4. The MP3Requester GUI

Here's the code skeleton for the MP3Requester applet:

public class MP3Requester extends Applet
        implements ActionListener, Runnable
{
    private JavaSpace space;
    private Thread resultTaker;
    private String from;    // unique name for person requesting MP3s
    . . . variables for user interface components 
    
    public void init() {
        space = SpaceAccessor.getSpace();
        
        // spawn thread to handle collecting & displaying results
        if (resultTaker == null) {
            resultTaker = new Thread(this);
            resultTaker.start();
        }
        . . . user interface setup 
    }
    // "Encode It!" button pressed
    public void actionPerformed(ActionEvent event) {
        from = userTextField.getText();
        String inputName = fileTextField.getText();
        . . . 
        // we'll package the file's raw data in the entry 
        byte[] rawData = null;
        rawData = Utils.getRawData(inputName); // open file & read bytes 
 
        // append an MP3 request to the channel
        if (rawData != null) {
            append("MP3 Request", inputName, rawData, from);
        }
    }
    . . . append method goes here
    
    // thread that loops, taking & displaying MP3 results
    public void run() {
        . . .
    }
}

Whenever the Encode It! button is pressed, the actionPerformed method gets called to add the MP3 encoding request to the MP3 Request channel. This method first extracts the unique username and the filename that you typed into the applet's text fields. The method opens the wav file and obtains its raw byte data. Then the append method (which I'll explain shortly) is called to add the request to the channel, passing it the name of the channel ("MP3 Request"), the filename to encode, the raw data from the file, and the name of the user making the request.

Another thing to note about this applet is that it spawns a result taker thread. The run method of this thread loops continuously, removing MP3 results from the space and displaying the resulting MP3 filenames in the applet's GUI. (This functionality needs to run in its own thread, since the main thread of the applet needs to be available and responsive to GUI events such as button clicks.)

Appending a request to the channel

Let's take a closer look at the append method that adds requests to the tail end of a channel. That method obtains the position number of the tail, increments it, and stamps the new request with that number.

First, you'll define a getRequestNumber method that obtains a position number for the new request:

private Integer getRequestNumber(String channel) {
    try {
        Index template = new Index("tail", channel);            
        Index tail = (Index) space.take(template, null, Long.MAX_VALUE);
        tail.increment();
        space.write(tail, null, Lease.FOREVER);
        return tail.getPosition();
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

In this method, you first create an Index template, specifying that you're looking for a tail index in a certain channel (but leaving the position number unspecified, to serve as a wildcard). Then you call take to remove the tail entry for that channel, waiting as long as necessary. Once you've retrieved the tail entry for the channel, you increment the tail and write it back into the space. You return the new position number of the tail to the caller, to be used as the position number of the new request. Since the tail entry for a channel starts out at position zero, the first time you call this method for a channel, position one will be returned to use for the first request.

Now you'll make use of getRequestNumber to implement the append method:

private void append(String channel, String inputName, 
            byte[] rawData, String from) {
    Integer num = getRequestNumber(channel);
    MP3Request request =
        new MP3Request(channel, num, inputName, rawData, from);
                
    try {
        space.write(request, null, Lease.FOREVER);
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }
}

The append method takes the name of the channel ("MP3 Request"), the filename to encode, the raw data from the file, and the name of the user making the request. Here you first call getRequestNumber, passing it the channel name, to increment the tail and obtain a position number for the new request. Then you create a new MP3Request entry (with the given channel name, position number, filename, raw data, and user's name) and write it into the space.

Now that you've seen the details of the MP3 Requester and examined how it appends new MP3 encoding requests to your MP3 Request channel, we'll investigate what happens to those requests in the channel.

The MP3 workers

An administrator will start up one or more MP3 workers. Each worker repeatedly removes an MP3 encoding request from the channel's head, calling a third-party program to perform the wav-to-MP3 encoding, and then constructs and writes an MP3Result entry into the space (which all MP3 Requesters are monitoring). Here's the basic outline of the MP3Worker code:

public class MP3Worker {
    private JavaSpace space;
    private String channel;
    public static void main(String[] args) {
        MP3Worker worker = new MP3Worker();
        worker.startWork();
    }
    
    public void startWork() {
        space = SpaceAccessor.getSpace();
        channel = "MP3 Request";
        while(true) {
            processNextRequest();
        }        
    }
    . . . other method definitions
}

As you can see from the main and startWork methods, the worker enters a loop, in which it continually calls the processNextRequest method, which is defined as follows:

1 2 3 Page 2
Page 2 of 3