#

## Chapter 12

## Creating Transmitter Plug-Ins

A lot of the attention concerning Castanet channels involves the ability to do automatic updates–the ability to get the most recent data of a channel from the transmitter down to the individual tuners without a lot of effort on the part of the user. There is so much focus on updates, in fact, that one might think that all the data in Castanet flows in one direction: from the transmitter to the tuner.

Castanet also provides a mechanism for sending data back from each individual tuner–in a method somewhat analogous to submitting a form on a Web page. That channel data is sent to the transmitter as part of an update, and a special program on the transmitter site can then process that data and react dynamically–changing the results a channel gets from an update or otherwise processing the information in some useful way.

That special program on the transmitter is called a transmitter plug-in. In this chapter, you'll learn how to create and use transmitter plug-ins for your own channels.

### What Plug-Ins Are and How They Work

If you've used Adobe Photoshop or Netscape Navigator, you're already familiar with the general concept of the plug-in: a plug-in is a program that, when installed in the right place inside your main program, extends the larger program or gives you extra functionality in a manner that's integrated with that larger program. Transmitter plug-ins work in a similar way: You create a plug-in as part of your channel, and that plug-in is installed on the transmitter when you publish that channel. When your channel is updated, special channel information is sent back to the transmitter from the tuner, and the plug-in has an opportunity to process that data.

Creating a plug-in isn't all that difficult, but it does require that you understand the process of how a channel stores feedback data, how that data is sent back to the transmitter, and how the tuner and transmitter negotiate an update.

Channel Feedback Data

In Chapter 11 you learned about how to write local data files to the channel's local data directory. I made a mention in that chapter of two special kinds of data files: channel logging and profile files. The data in these two files is the data that is sent back to the transmitter (and the plug-in) when your channel is updated.

Logging information is simply that–information you may be interested in keeping track of as your channel runs. For example, this could be the choices your readers make for what they see in your channel, the URLs of documents they visit, how often they start and run your channel, or anything else that you might find interesting. Each time the user does something that interests you, you log that event using a special method. When the channel is updated, all the log entries are packaged and sent as a unit to the plug-in.

Profile data is intended to change less often than logging data. Profile data is data about the preferences of your user; for example, you can create a preferences panel for your channel that asks your user for his or her preferred language, and stores that information in the profile. Generally the user only sets the profile information once, or very infrequently. Along with the logging information, the profile file is also uploaded to the transmitter plug-in when the channel is updated.

Processing the Feedback Data

When the channel is updated, the tuner packages the log and profile files and sends them up to the transmitter. The transmitter then runs your plug-in if it exists. Now what? You can use the plug-in to take the logging file and process it, storing the statistics in a special file or in a database. That's one use of the plug-in. But the really interesting thing you can do with a plug-in in response to logging or profile information is to customize the contents of the channel update.

When the tuner negotiates an update to a channel with the transmitter, the tuner tests the checksums of the files and directories it has in the channel with the checksums of the files and directories the transmitter has for that channel. Based on the differences between those checksums, the tuner can construct a list of the files it needs in order to update that channel. From inside your plug-in, you can access that list of files and add or remove files from the list. Those files are sent back to the tuner, which installs them just as the tuner would install any other channel files.

So, for example, your channel could have a preferences window that lets the user choose her preferred language (English, French, German, and so on). Those preferences would be saved as profile data, which would be sent up to the channel as part of the update. In your plug-in, you can read that language preference and then modify the list of channel files so that the user interface that is downloaded to the tuner is the right one for the right language. It is entirely transparent to the user of that channel; the user sets her preferences and magically gets the interface in the right language. But the plug-in allows you to customize the channel on the fly so that the tuner doesn't have to download UI files for all the languages, just the files the user actually needs.

You could use logging information to customize a channel as well–for example, to track the sorts of topics that your reader shows an interest in, and to provide channel data that is focused to that specific information. Using channel plug-ins and the information the channel sends, there's no limit to the sorts of interesting customizations you can make to a single channel.

### Creating Plug-ins

Plug-ins are Java files, just like channels are. In fact, they're developed and stored as part of your channel, and require modifying your channel files as well. There are three steps involved to creating plug-ins:

- Setting up your channel directory correctly.
- Modifying your channel files to process logging and profile information correctly.
- Developing the plug-in itself.

The Channel Directory

Transmitter plug-in files are stored in the same channel directory as your channel files, but they're not downloaded to the tuner the way most of your channel data is. In order for the transmitter to be able to tell the difference between normal channel data and plug-in data, you've got to put the plug-in data in a special place. That place is a directory called `plugin` inside your channel directory. The `plugin` directory contains any files that your plug-in needs, and it also has its own `properties.txt` file.

The plug-in's `properties.txt` file is not created by Castanet Publish when you publish your channel; you have to create it yourself. It's a simple text file with only one line in it:

`main=yourchannelplugin`

The value of `yourchannelplugin`, in this case, is the name of your main plug-in class, minus the `.class` extension. The transmitter needs this file so that it knows which class to instantiate when your plug-in starts up.

Note that your plug-in's `CLASSPATH`–that is, where it locates other Java classes–includes both the `plugin` directory and the enclosing Java directory. This allows you to share utility class files between the channel and the plug-in, without having to copy them to both places.

With your channel directory set up, the next step is to start modifying the actual channel code.

Developing the Channel

To take advantage of a plug-in's features, you need to modify your class file to use special methods and features (or create your channels with these features in mind). The logging and profile information that the channel sends to the plug-in are not normal data files; they must be read and written in special ways, using methods in the application context. Also, you might want to trap the event `DATA_UPDATE_EVENT`, which is posted just before the tuner makes an update. You can use that opportunity to save the profile or do special logging just before the update occurs.

Channel Logs

To create an entry in the channel log, use the `context.appendLog()` method. The `appendLog()` method takes one argument, which can either be a `String` object or an array of bytes. Whatever you log is up to you, based on the goals of your channel and the information you're interested in tracking. When the log information is processed by your plug-in, that plug-in will get the information you logged plus a time stamp and the ID of the tuner where the information came from. So, for example, if you had a channel for a dynamic Web bookmarks list, you could keep track of the URLs your users actually visited by logging the URL something like this:

`context.appendLog("URL: " + target.getValue("URL"));`

Then the log entry for that call to `appendLog()` would look something like this:

`fx8oy6uhvlxt 21/12 20.59.56 URL:` `http://www.marimba.com`

Channel Profiles

Profile information isn't quite as easy. Because a profile file is supposed to store preferences in much the same way a local data file saves state, you'll need to read the existing preferences before writing any new preferences. You'll also need to write any user interfaces for setting those preferences yourself; all the channel gives you are two methods: `getProfile()` and `setProfile()`.

Both `getProfile()` and `setProfile()` are application context methods. The `context.getProfile()` method returns an array of bytes with the profile data in it (empty if the profile hasn't been created yet). You'll need to then somehow process that array of bytes into a form you can use in your channel.

The `context.setProfile()` method takes one argument: an array of bytes representing the profile data to be saved and sent to the plug-in on update. It returns a Boolean: true if the profile was saved, false if there was an error while writing the profile. The contents of the byte array overwrite any existing profile data, so be sure to read the old values before writing new ones.

Preparing for Updates

In Chapter 11, when I discussed channel update events, I briefly mentioned the `DATA_UPDATE_EVENT`. This event occurs just before the tuner actually makes an update back to the transmitter; it is a warning that the logging and profile data is about to be sent for processing. By trapping this event in your `handleEvent()` method, you have a chance to save the profile or log any last-minute data just before the update occurs.

There's no specific method for the `DATA_UPDATE_EVENT` defined in any of the standard channel classes because this event is not as frequently used as the `DATA_NOTIFY_EVENT` or `DATA_INSTALL_EVENT` events. To trap it, you must use `handleEvent()`, like this:

    public boolean handlEvent(Event evt) {

       switch (evt.id) {

          case Event.ACTION_EVENT: // process widget actions.

             ...

          case DATA_UPDATE_EVENT:

             // ... save logs and profiles

       }

    }

Developing the Plug-in

With everything set up in your channel to send feedback data to the plug-in, the last step is to develop the plug-in itself. Inside that plug-in you'll process the data sent back from the channel, and customize the channel contents, if necessary. In this section you'll learn about all three of these things.

The `Plugin` Class File

All plug-ins must be subclasses of the `Plugin` class, part of the `marimba.plugin` package. The basic template for a plug-in file, then, looks like this:

`import marimba.plugin.*;
public class MyPlugin extends Plugin {
...
}`

The default version of the `Plugin` class defines a number of methods, but the three most important (and the three you will most likely need to override) are these:

- `init()` is called once when the plug-in is started in response to the first channel update. Use `init()` to initialize the plug-in.
- `processRequest()` is called once each time the channel is updated and new data is available. Do the bulk of the plug-in processing from this method.
- `destroy()` is called once just before the plug-in is halted. Use this method to clean up or save local files.

The `init()` and `destroy()` method signatures look like this:

`public void init() {
...
}
public void destroy() {
...
}`

By default, `init()` has no definition, but `destroy()` closes the files that were opened at other points in the plug-in's lifetime. You should call `super.destroy()` if you override `destroy()` in your own plug-in class.

The `processRequest()` method looks like this:

`public void processReqeust(ReqeustContext context) throws IOException {
...
}`

The `RequestContext` argument is the special context object for this call to your plug-in; the request context for the plug-in is analogous to the application context for the channel in that the context provides the way to access not only the information from the update, but also information about that update itself including the tuner ID and the channel and server name. By default, `processRequest` logs the channel's log data to a central location; you should call `super.processRequest()` either at the beginning or end of your version (The default implementation of `processRequest()` simply logs the channel feedback information to the server, and that can happen at any time.)

Processing the Data

Inside the `processRequest()` method is where most of the plug-in's work takes place. This is where you'll read the logging data, write any local state files, and customize the channel.

The first step is usually to read the log or profile data. On this side of the process, the profile is easier to read. Just use the `getProfileData()` method, defined on the request context:

`byte[] theData = context.getProfileData();`

The data you get back from this method is a simple array of bytes; it's up to you to process that data into a form you can use.

Processing the log entries is more difficult, because multiple log entries can be packaged as one update request. To get the log entries, you have two choices. The first choice is the `getLoggingData()` method, defined on the request context and called like this:

`byte[] theLogs = context.getLoggingData();`

As with `getProfileData()`, the `getLoggingData()` method returns the log entries as a simple array of bytes. You have to process out the information yourself. The second choice is usually the easier way to proceed: Call the `Plugin` class's `getLoggingEntries()` method, using the context as the argument. The `getLoggingEntries()` method returns a `Vector` object:

`Vector theLogs = getLoggingEntries(context);`

The `Vector` object contains instances of the `LogEntry` class (also part of the `marimba.plugin` package), one for each log entry that was passed to the transmitter. The `LogEntry` class has two instance variables: `timestamp` and `data`. Use the `data` instance variable to get ahold of the original information that you logged in the channel in `appendLog()`.

You can use an `Enumeration` and a loop to work through the various `LogEntry` objects in the vector, like this:

    for (Enumeration e = theLogs.elements; e.hasMoreElements() ; {

       LogEntry le = (LogEntry)e.nextElement();

       String s = new String(le.data, 0);

       // .. the log is in the string s; process it as you will.

    }

Working with Local Files

Because the plug-in runs on your transmitter, it's generally assumed to be a trusted bit of code. Your plug-in has access to any part of the transmitter's file system; you can read and write local files at will as you need them to process the data you get from the logs or the profile or to store local state between requests.

However, for reasons of consistency between plug-ins, local data files are generally stored in a central location: the `data` directory, contained inside the transmitter's channel directory. Each channel has its own directory inside `data` in which it can store any files it needs.

The easy way to get ahold of this special channel directory is to use the `Plugin` method `getDataFile()` with the name of the file you want to read from or write to. This method returns a File object, which you can use to open the data file. The first time you write to it, you should make sure your channel directory exists and create it if it doesn't. Here's some simple code to do that:

    try {

        File theFile = getDataFile("theoutputfile.txt");

        if (theFile.exists() == false) {

          File dir = new File(theFile.getParent());

          if (dir.exists() == false)

              dir.mkdirs();

       }

       FastOutputStream rf = new FastOutputStream(theFile);

       //write to the stream

       rf.close();

    }

    catch (IOException e) {

       e.printStackTrace();

    }

Customizing Channel Lists

With the logs and profiles processed, the last task of the plug-in is to customize the list of files that is sent back to the tuner, if necessary. (You don't have to customize the channel if you don't want to; there's nothing wrong with, say, simply taking logging information and storing statistics about it.)

To customize the channel, you modify the channel's index. This is the list of files and checksums that the transmitter sends to the tuner as part of an update request; the tuner then uses this list to figure out which files it needs in order to bring its version of the channel up to date. By modifying the channel index as part of the plug-in's operation, you can dynamically change the files the transmitter appears to have.

There are three operations you can do to the channel index: You can delete a file, you can rename a file, or you can add a file. The first two are the easiest, so I'll cover them first.

For each of these operations, modifying the channel's index is a temporary measure for this specific request; the next time the tuner requests an update, you will have to modify that index all over again.

Deleting and Renaming Files

To delete a file from the index, use `context.deleteFile()` with a single string argument, representing the path and name of the file to delete, from the top of the channel directory. The argument for a file stored at the top level of the channel directory, then, would be just the name of the file. For a file stored in a subdirectory, the path would be the name of that directory plus the name of the file (watch out for separator characters):

`context.deleteFile("updatedata.txt");`

Deleting a file from the index means that this file will be deleted altogether from the tuner's version of the channel.

To rename a file, use `renameFile()` with two string arguments: the path to the original file, and the path to the new file:

`context.renameFile("updatedata.txt", "updatedata.old");`

Adding Files

Much of the time in your plug-in you'll want to add a new file or add data to the channel's list. To add files to the list of updates, use `context.addFile()`. Unlike `deleteFile()` and `renameFile()`, for `addFile()` you have four different versions to choose from.

The simplest version takes two arguments: a `String` path name and a `File` object. The path name is the path to the file as it should appear in the channel, minus the channel name. So, for example, if you're sticking a file called `results.txt` at the top level of the channel directory, the path would be simply `"results.txt"`.

The second argument is a `File` object pointing to the location of the file on the transmitter's local disk. This version of `addFile()` is useful when you want to take different versions of the same file and choose amongst them for one main channel file, as in this example:

`context.addFile("main.gui", (new File(getDataFile("mainfrench.gui")));`

In response to this call, the new file's checksum is calculated and that file is dynamically added to the channel's index. Note that this has an effect on optimized updates, because the checksum of the transmitter's (unmodified) channel index is now different from the tuner's checksum (which includes your new file). Each update runs through your plug-in, and you have to re-add the file each time.

The second version of `addFile()` is similar to the first; it also takes a `String` for the channel's path to the file and a `File` object for the local path, but it also takes a third argument: the checksum of that file as a `Checksum` object. (`Checksum` is part of the `marimba.util` package.) If you just add the file to the list, the checksum will be calculated for you, so you don't have to worry about it if you don't want to. However, for files that are frequently added or used repeatedly, you can calculate the checksum yourself, ahead of time, and then just submit it here to speed up the update.

You can use the `Plugin` method `calculateChecksum()` to calculate the checksum of the file; `calculateChecksum` takes either a string argument (the path to the file from the `plugin` directory) or an array of bytes, and returns a `Checksum` object:

`Checksum cs = calculateChecksum("mainfrench.gui");
context.addFile("main.gui", guiFile, cs);`

The last two versions of `addFile()` are more complex, but they provide extra functionality for handling the data your plug-in inserts into the channel index for updates and for proxies. Both of these versions of `addFile` don't actually add a physical file; instead they add an array of bytes as the contents of that file. Both also have an extra argument for the file's "disposition." This argument determines how the file data is processed in the channel index and by proxies for later channel update requests. I'll cover the dispositions in the next section.

The third version of `addFile()` takes three arguments: a `String` for the path name to the file as it will appear on the channel, an array of bytes for the data in that channel, and an integer representing the file's disposition:

`context.addFile("highscores.txt", hiscoredata, IF_NEEDED);`

The fourth and final version of `addFile()` is similar, except that it adds the checksum for the data as an argument to speed up processing that file. To get a checksum for an array of bytes, you can use the `calculateChecksum()` method with a byte array argument:

`Checksum csb = calculateChecksum(dataarray);
context.addFile("data.txt". dataarray, cs, IF_NEEDED);`

File Dispositions

If you use either of the `addFile()` methods that take a file disposition as an argument, you have four different dispositions to choose from (all are defined in the `Plugin` class):

- `IF_NEEDED`: This is the best choice for data that will be sent to tuner on a semi-regular basis. The `IF_NEEDED` disposition adds this file and its checksum to the channel's index, so the file's data is only sent to the tuner if the tuner doesn't already have that file (or if it has a different version). Because this disposition dynamically changes the overall index checksum, optimized updates will no longer work for this channel; each update will go through the plug-in and you'll have to modify the list of files each time. This is the default disposition for adding files using the `addFile()` methods without dispositions.
- `ALWAYS`: Use `ALWAYS` if the data you're sending back to the tuner changes extremely frequently. An `ALWAYS` disposition always sends the data for the file for every single update request, regardless of whether or not that data has changed. For `ALWAYS` the filename and checksum are not added to the channel's index, so optimized updates continue to work as usual. Files with `ALWAYS` dispositions are also never cached by a Castanet proxy.
- `PERSISTENT`: Use this disposition for data that changes only semi-frequently but for which there are frequent update requests. File dispositions marked `PERSISTENT` are treated as if they are a file for a small amount of time after the plug-in adds them to the index (at least five minutes); this allows them to be cached by the proxy or by the tuner such that subsequent updates soon after the first don't have to go through the plug-in each and every time.
- `ASK_ME`: The most complicated of the file dispositions, the `ASK_ME` disposition means that the plug-in itself will handle every request for this specific file–for example, to extract the file from a database or to generate it based on other data. The idea here is that the plug-in can keep a local copy of the data cached and send it back if the data hasn't changed. If you use a file disposition of `ASK_ME`, you also need to override the methods `getData()` and `getDataLength()` from the `Plugin` class.

In practical use, the `IF_NEEDED` disposition is often the most commonly used. Use `ALWAYS` or `PERSISTENT` for frequently updated or frequently requested data to optimize the speed of the update through the plug-in; use `ASK_ME` for dynamically generated data that requires more sophisticated processing than the standard index and file mechanism.

### An Example: The Survey of the Week

For the last half of this chapter, let's create a channel that relies heavily on the ability to send data back to the transmitter from the channel itself. This is a channel that gives you a chance to vote on different questions, one a week. Figure 12.1 shows the initial channel screen when it comes up (with a sample question that applies especially well to this chapter).

Figure 12.1. The initial survey screen.

Choosing an option and selecting the Submit button triggers a live update. The tuner sends your vote up to the transmitter, where a plug-in intercepts that data and stores it in a results file. The plug-in then sends that results file back to the channel, which updates the results bars at the bottom of the page to reflect all the current results. (See Figure 12.2.) This includes not only the user's current votes, but also the votes of everyone else who's running the channel.

Figure 12.2. The updated results.

The channel itself automatically updates every half an hour, and the plug-in intercepts those updates as well. For automatic updates, no data is sent to the plug-in from the channel, but the plug-in continues to send down a new version of the results file, so the channel continues to reflect the current vote count even after the user has cast her own vote.

Once a week the question changes, the results are set back to zero, and the survey starts all over again.

You know the routine by now. To create a channel like this one, you start with the Bongo presentation and add the code. I won't go into the details of the presentation itself; let's jump right into the code for the channel, which is also on the CD-ROM in the `Survey` subdirectory of the `Examples` directory.

Create the Channel

Like most channels, the Survey channel uses a Java class, `SurveyApplication.java`, to initialize the interface and to process the actions that occur with that interface. The basic framework is the same: You'll subclass the `ApplicationPlayerFrame` class and override the appropriate methods to manage three major events in the channel's lifetime:

- For initializing the channel, override the `start()` method to read and display the current question for the survey.
- For handling the vote submission, override `handleEvent()` to process the mouse click on the Submit button.
- For automatic updates, override `notifyInstall()` to update the interface when new data arrives (either new results or a new question).

Let's look at the pieces separately. I've printed the code in its entirety later on in this section so that you can see it in one place. I'll start with the `start()` method:

    public void start() {

       super.start();

       setTitle("Survey of the Week");

       loadQuestion();

    }

The `start()` method does only two things: It sets the title bar for the channel and calls a subroutine called `loadQuestion()`. The latter method is shown in Listing 12.1.

Listing 12.1. The `loadQuestion()` method.

    public void loadQuestion() {

       try {

          URL base = new URL(context.getBase(), question);

          FastInputStream fs = new FastInputStream(base.openStream());

          util.setValue("question", fs.readLine());

           fs.close();

       }

       catch (MalformedURLException e) {

          e.printStackTrace();

       }

       catch (IOException e) {

          e.printStackTrace();

       }

    }

You saw a method similar to this one for the Weather channel; this method simply opens the channel file `question.txt` and reads its contents into the question widget (the big scrolling text box at the top of the screen). Storing the question separate from the interface means that question can be changed frequently without having to change the whole interface each time. As with the Weather channel, it makes updates faster and easier for both the transmitter and tuner side of the process.

The second step for the channel (and the most important one) is to override `handleEvent()` to submit the vote when the user clicks on the Submit button. Listing 12.2 shows the `handleEvent()` method for this class.

Listing 12.2. The `handleEvents()` method.

     1: public boolean handleEvent(Event evt) {

     2:    if ((evt.id == Event.ACTION_EVENT) &&

     3:       (evt.target instanceof CommandButtonWidget)) {

     4:          if (util.getBoolean("yes")) context.appendLog("yes");

     5:          else if (util.getBoolean("no")) context.appendLog("no");

     6:          else if (util.getBoolean("noop")) context.appendLog("noop");

     7:           context.update();

     8:

     9:           return true;

    10:    }

    11:   return super.handleEvent(evt);

    12: }

This is probably the most important method in the class from the point of view of understanding plug-ins, and the heart of it are the three tests in lines 4 through 7, which test the value widgets and call the `context.appendLog()` method. The `getBoolean()` tests are standard widget calls; `getBoolean` tells you which of the choices have been selected. (They're grouped together, so only one can be selected at a time.) And `context. appendLog()` creates a log entry for that action. The log entry you create with `appendLog` can look any way you want it to; for this example, the log entry simply has a string that tells the plug-in which option the user voted for (`"yes"`, `"no"`, or `"noop"` for a Yes, No, or No Opinion vote, respectively).

The `appendLog()` method does not in itself trigger an update. Each time you call `appendLog` the tuner stores up each log entry on the local disk until the next update occurs; then it packages all the log entries and sends them as a group to the transmitter. In this case, however, we'll force the issue; in line 7, we call `context.update()`, which tells the tuner to request an update for this channel and send the log data to the transmitter.

> ####

> Note

> Be careful about calling `update()` from your own channels. The idea of a channel is that it shouldn't need to spend a lot of time contacting the server; it should be able to call updates in a more leisurely fashion–for example, late at night. Also, forcing an update inside your channel bypasses the ability for the user to turn off updates altogether in the tuner. For this example, I've used `update()` so that we get instant gratification from the plug-in.

Clicking the Submit vote triggers an update, but updates can also occur automatically. Let's move onto the methods for updating the channel, then, which allows us to process the new data from an update whether it arrives in response to a vote or automatically.

For the Weather channel, we overrode both the `notifyAvailable()` and `notifyInstall()` methods. The first of these methods is used to decide which data to install when new data arrives. In this example, we'll rely on the default definition of `notifyAvailable()`, which, if the properties for the channel have been set to "install," simply installs all the new data. This is precisely what we want to do, so there's no reason to override that method. Instead we'll move onto `notifyInstall()`, which is called after that new data has been installed:

    public void notifyInstall(String dir) {

       loadQuestion();

       loadData();

    }

Here we'll do two things to process the new data. The first is to call the `loadQuestion()` method again. If a new question arrives, we'll want to update the survey to reflect that. The second is to call a method called `loadData()`. Part of the update to the channel is a file called `results.txt`, which contains the current vote count for the question. The `loadData()` method is responsible for opening and reading that file and updating the results information. Because that method simply calculates the new values for all the widgets in the interface, I won't show it here; you can see it in Listing 12.3 if you're interested.

Note that for the most part, the only difference between this channel and the weather channel is the use of the `appendLog()` method. For channels that use plug-ins, that is usually the only change you'll need to make. Listing 12.3 shows the complete code for this channel so that you can see how it all fits together.

Listing 12.3. The code for `SurveyApplication.java.`

    import java.awt.*;

    import java.net.*;

    import java.io.*;

    import marimba.channel.*;

    import marimba.gui.*;

    import marimba.io.*;

    public class SurveyApplication extends ApplicationPlayerFrame {

    String question = "question.txt";

    String results = "results.txt";

    public void start() {

       super.start();

       setTitle("Survey of the Week");

       loadQuestion();

    }

    public void loadQuestion() {

       try {

          URL base = new URL(context.getBase(), question);

          FastInputStream fs = new FastInputStream(base.openStream());

          util.setValue("question", fs.readLine());

           fs.close();

       }

       catch (MalformedURLException e) {

          e.printStackTrace();

       }

       catch (IOException e) {

          e.printStackTrace();

       }

    }

    public void loadData() {

       try {

          if (context.channelFileExists(results)) {

             URL base = new URL(context.getBase(), results);

             FastInputStream fs = new FastInputStream(base.openStream());

             int yt = Integer.parseInt(fs.readLine());

             int nt = Integer.parseInt(fs.readLine());

             int not = Integer.parseInt(fs.readLine());

             float votes = yt + nt + not;

             yt = (int)((yt / votes) * 100);

             nt = (int)((nt / votes) * 100);

             not = (int)((not / votes) * 100);

             util.setValue("yestot", "Yes: " + yt);

             util.setValue("yesbar", String.valueOf(yt));

             util.setValue("notot", "No: " + nt);

             util.setValue("nobar", String.valueOf(nt));

             util.setValue("nooptot", "No Opinion: " + not);

             util.setValue("noopbar", String.valueOf(not));

             fs.close();

          }

       }

       catch (MalformedURLException e) {

          e.printStackTrace();

       }

       catch (IOException e) {

          e.printStackTrace();

       }

    }

    public void notifyInstall(String dir) {

       loadQuestion();

       loadData();

    }

    public boolean handleEvent(Event evt) {

       if ((evt.id == Event.ACTION_EVENT) &&

          (evt.target instanceof CommandButtonWidget)) {

             if (util.getBoolean("yes")) context.appendLog("yes");

             else if (util.getBoolean("no")) context.appendLog("no");

             else if (util.getBoolean("noop")) context.appendLog("noop");

             context.update();

             return true;

       }

       return super.handleEvent(evt);

    }

    }

Create the Plug-In

Right now you could compile that channel file and run it, and it would work just fine. The transmitter doesn't get confused when it gets log or profile data intended for a plug-in; if there isn't a plug-in available to process it, the transmitter simply logs the data it gets from the tuner and goes about its merry way. You'll find that log data in a file named after your channel in the transmitter's channel directory, inside the `logs`. Those log entries have three parts: the tuner ID (a unique identifier for each tuner), a time stamp, and the data you sent in the `appendLog()` method. More about this when we actually write the code to process those entries.

But this is a chapter about plug-ins, so let's create one here to handle the data that comes back to the logs. As you learned earlier in this chapter, plug-ins inherit from the class `marimba.plugin.Plugin`. They also must be contained in their own directory inside your channel directory, called `plugin`. Don't forget to create that directory and store your plug-in files inside that directory.

> ####

> Note

> The `marimba.plugin` package is contained in the library `transmitter.zip`, part of the transmitter distribution in the `lib` directory. You need to add this library to the `CLASSPATH` of your development environment to be able to compile your Java plug-in files.

Let's build this file from scratch, because much of it will be new. We'll start with the basic class framework–a whole lot of imports plus the basic class definition:

`import java.io.*;
import java.util.*;
import marimba.plugin.*;
import marimba.io.*;
import marimba.util.*;
public class SurveyPlugin extends Plugin {
}`

We'll need a few instance variables for this class to refer to the results file the plug-in stores and to the results data. To help with the latter, I've created a helper class called `Results`, which simply has fields for the various result values. We'll create an instance variable to hold an instance of that class here (`rdata`), along with a variable for the name of the requests file (`requests`) and for a `File` object (`rfile`) that will refer to that requests file:

`String results = "results.txt";
Results rdata;
File rFile;
` The `Results.java` class looks like this:
`public class Results {
int yestotal = 0;
int nototal = 0;
int nooptotal = 0;
}`

The fields in that `Results` object will be important later on in the class.

When you subclass the `Plugin` class, the three methods you will potentially override are `init()`, `processRequest()`, and `destroy()`, for starting up, processing data, and stopping. Keep in mind, as you decide which ones to override, when each method is called during the plug-in's life cycle. When the plug-in is first launched, `init()` is called, `processRequest()` is called for each update, and `destroy()` is called when the transmitter is brought down. Both `init()` and `destroy()` are called only once during the plug-in's lifetime; `processRequest()` is called as many times as there are updates from tuners.

For this example, we'll override `init()` and `processRequest()`. The `init()` method is used to initialize the plug-in, and we'll take this opportunity to read in the transmitter's local copy of the results so that we have it in memory to change when a tuner makes an update.

To get ahold of a local data file for the plug-in, use the `getDataFile()` method with the name of the file you want to create. This method returns a `File` object with the full path name to the file; you can then open and read or write to that file at will.

> ####

> Note

> Actually, you can read or write any files on the transmitter from your plug-in–no security restrictions–but it's considered good form to put your data files into the standard place where they belong.

The results file, if it exists, consists of three lines, representing the total Yes votes, the total No votes, and the total No Opinion votes. In our `init()` method, shown in Listing 12.4, we'll read those values from the file into an instance of the `Results` class, stored in the `rdata` instance variable:

Listing 12.4. The `init().` method for the plug-in.

    public void init() {

       super.init();

       /* plug-in's copy of the results file */

       rdata = new Results();

       rFile = getDataFile(results);

       if (rFile.exists()) {

       try {

           FastInputStream fs = new FastInputStream(rFile);

           rdata.yestotal = Integer.parseInt(fs.readLine());

           rdata.nototal = Integer.parseInt(fs.readLine());

           rdata.nooptotal = Integer.parseInt(fs.readLine());

           fs.close();

       }

       catch(IOException e) {

           e.printStackTrace();

       }

       }

    }

After `init()` is called, the plug-in is ready to start processing update requests. In order to do anything with those requests, you'll need to override the `processRequest()` method in your plug-in. (This will usually be the case.) The order of business inside `processRequest()` usually goes something like this:

- Read the log or profile entries the tuner has sent.
- Process them in whatever way you need to.
- Modify the list of files being sent back to the tuner to add or remove files based on the log or profile data.
- Clean up, saving any local files or clearing any values that need to be reset before the next request comes in.

Listing 12.5 shows the `processRequest()` method for this `Plugin` class. I'll go over it line by line so that you know what going on.

Listing 12.5. The `processRequest()`. method in the plug-in.

    1:public void processRequest(RequestContext context) throws IOException {

     2:      /* grab log entries */

     3:      Vector logs = getLogEntries(context);

     4:

     5:      /* update results */

     6:      for (Enumeration e = logs.elements() ; e.hasMoreElements() ; ) {

     7:          LogEntry le = (LogEntry)e.nextElement();

     8:          String s = new String(le.data, 0);

     9:          if (s.equals("yes")) rdata.yestotal++;

    10:          else if (s.equals("no")) rdata.nototal++;

    11:          else if (s.equals("noop")) rdata.nooptotal++;

    12:      }

    13:

    14:      /* ship results back to tuner */

    15:      String s = rdata.yestotal + "\n" + rdata.nototal + "\n" +

    16:           rdata.nooptotal + "\n";

    17:      int len = s.length();

    18:      byte[] b = new byte[len];

    19:      s.getBytes(0,len,b,0);

    20:      context.addFile(results,b,IF_NEEDED);

    21:

    22:   try {

    23:      /* write the new data back to the local file */

    24:      if (rFile.exists() == false) {

    25:         File dir = new File(rFile.getParent());

    26:         if (dir.exists() == false)

    27:             dir.mkdirs();

    28:      }

    29:      FastOutputStream rf = new FastOutputStream(rFile);

    30:      rf.println(rdata.yestotal);

    31:      rf.println(rdata.nototal);

    32:      rf.println(rdata.nooptotal);

    33:      rf.close();

    34:   }

    35:   catch (IOException e)

    36:   { e.printStackTrace();

    37:   }

    38:

    39:   super.processRequest(context);

    40:}

In this version of pr`ocessRequest()`, we haven't done anything with the channel profile; the only thing we're interested in are the log entries. The most important method in this respect is the `getLogEntries()` method in line 3, which takes one argument: the request context passed into the `processRequest()` method when it's called. The `getLogEntries` method returns a `Vector` object with all the log entries in it. You can then use an `Enumeration` object to read over each one. (Both `Vector` and `Enumeration` are standard Java classes for linked-list[nd]like entities.)

Note that for this particular instance of the plug-in, there will only be one entry because the channel calls update immediately after calling `appendLog()`. That's not necessarily true for any other plug-in; most of the time, the channel stores up log entries and sends them over as a group, and you have to process each one in turn. The code I used to process those log entries in lines 6 through 12 deals with multiple log entries in case an immediate update didn't happen; you can also use it in your own plug-ins.

Note the line 7 where you cast the log entry to an instance of the class `LogEntry`. The `LogEntry` class is part of the `marimba.plugin` package; it's a simple class that splits up the actual log entry into its time stamp and its data. (If you're interested in the tuner ID, you can get it from the `context.getTunerID()` method.) And finally, lines 9 through 11 increment the values in the `Result` object based on the votes that were in the log entries.

The second part of the `processReqeust()` method ships the updated results back to the tuner. The `addFile()` method is responsible for adding a new file to the list of files the tuner will receive, and there are several ways of using it (all of which you learned about previously in this chapter). In this case, because the data isn't already in a file, we'll package it as an array of bytes (lines 15 to 19) and call `addFile()` with the name of the file (`"results.txt"`), the byte array, and a disposition of `IF_NEEDED` (line 20).

Why `IF_NEEDED`? The `IF_NEEDED` disposition only sends the file if the checksum of the new data is different from the checksum of the results file the tuner already has. If the tuner has done a regular update (not in response to a vote) and the results haven't changed, the channel won't need the new data, so there's no reason to send it. (Note that if the results have changed since the last time the tuner updated, the checksums won't match and the file will be downloaded anyhow.) The `IF_NEEDED` file disposition is probably the most commonly used disposition.

The final section in `processRequest()` cleans up after the request is finished–basically by saving the new results out to the local `results.txt` file. The data still remains in memory for the next request; saving it out to the file is a useful backup maneuver. The contents of this part of the method should look familiar, except for the first few lines at 24 through 27. These lines are used only the very first time the plug-in saves the request file, and they simply make sure that the full path name to the file exists (including the channel directory).

To finish up, don't forget to call `super.processRequest()` (as shown in line 39). The default version of this method is what logs the data to the default channel logs (in the transmitter's channel directory, inside `logs`).

Got it all? Listing 12.6 shows the final code.

Listing 12.6. The code for `SurveyPlugin.java.`

    import java.io.*;

    import java.util.*;

    import marimba.plugin.*;

    import marimba.io.*;

    import marimba.util.*;

    public class SurveyPlugin extends Plugin {

    String results = "results.txt";

    Results rdata;

    File rFile;

    public void init() {

       super.init();

       /* plug-in's copy of the results file */

       rdata = new Results();

       rFile = getDataFile(results);

       if (rFile.exists()) {

       try {

           FastInputStream fs = new FastInputStream(rFile);

           rdata.yestotal = Integer.parseInt(fs.readLine());

           rdata.nototal = Integer.parseInt(fs.readLine());

           rdata.nooptotal = Integer.parseInt(fs.readLine());

           fs.close();

       }

       catch(IOException e) {

           e.printStackTrace();

       }

       }

    }

    public void processRequest(RequestContext context) throws IOException {

          /* grab log entries */

          Vector logs = getLogEntries(context);

          /* update results */

          for (Enumeration e = logs.elements() ; e.hasMoreElements() ; ) {

              LogEntry le = (LogEntry)e.nextElement();

              String s = new String(le.data, 0);

              if (s.equals("yes")) rdata.yestotal++;

              else if (s.equals("no")) rdata.nototal++;

              else if (s.equals("noop")) rdata.nooptotal++;

          }

          /* ship results back to tuner */

          String s = rdata.yestotal + "\n" + rdata.nototal + "\n" +

               rdata.nooptotal + "\n";

          int len = s.length();

          byte[] b = new byte[len];

          s.getBytes(0,len,b,0);

          context.addFile(results,b,IF_NEEDED);

       try {

          /* write the new data back to the local file */

          if (rFile.exists() == false) {

             File dir = new File(rFile.getParent());

             if (dir.exists() == false)

                 dir.mkdirs();

          }

          FastOutputStream rf = new FastOutputStream(rFile);

          rf.println(rdata.yestotal);

          rf.println(rdata.nototal);

          rf.println(rdata.nooptotal);

          rf.close();

       }

       catch (IOException e)

       { e.printStackTrace();

       }

       super.processRequest(context);

    }

    }

Finishing Up

The final step is simply to compile all the files, make sure the channel directory is arranged right, and publish the channel.

The final channel directory for this survey channel contains the following things:

- `survey.gui`, the Bongo presentation for the survey's interface.
- `SurveyApplication.class`, the main channel class.
- `question.txt`, a file containing the current question.
- `bongo.zip`, the bongo library that must be included with all channels that use bongo.
- `plugin`, a subdirectory, which in turn contains the `SurveyPlugin.class`, `Results.class`, and `properties.txt` files. Plug-in files must be contained in a directory called `plugin` in order to be recognized as plug-ins by the transmitter.

Don't forget to set the `properties.txt` file inside the `plugin` directory; this file always contains one line, which points to the main `Plugin` class, like this:

`main=SurveyPlugin`

The final step is, of course, to publish the channel. I'll use the general properties shown in Figure 12.3, which include the name of the channel and its bongo presentation. For the update properties, the inactive properties are set to `daily` for the frequency and `ignore` for the action. (There's no point to starting the channel for new data; when the reader is interested, he'll start it himself.) For the active properties, I've set the update to `hourly`, and the action to `"install"`, which will make sure the new data gets incorporated into the running channel.

Figure 12.3. The General properties for the Survey channel.

The result is a channel that allows the reader to vote for the current question of the week, and also keeps a running update about everyone else's vote for that channel.

There's one other point to note: What happens when you, the channel administrator, update the survey to have a new question? You can simply edit the `question.txt` file to ask a new question and then republish; the channel gets the new question on the next update. However, even though the channel gets the new question, the plug-in continues to keep the old results data and use it to update the results for each channel. So don't forget when you update the questions file to also delete the transmitter's result file (stored in the transmitter root's data directory). This resets everything so that the results of the survey are truly updated on a weekly basis.

### Debugging Plug-ins

One of the first things you'll discover when you start experimenting with plug-ins is that they're really hard to debug. Unlike presentations or channels, where you at least have the bongo to the tuner console to which you can send a call to `System.out.println()`, with plug-ins there isn't any easy place where you can send debugging information. This can make debugging transmitter plug-ins seem a lot like groping around in the dark. (For that survey example, I did a lot of groping around in the dark.)

If something goes wrong with your plug-in, the first sign you'll get is when you try to update your channel in the tuner, and you get an `"EOF in HTTP Response"` Error (and your channel won't update). The first place to look for information on what went wrong is in the transmitter error log, which is stored in your transmitter root directory in the directory logs (for example, `C:\channels\logs\errorlog`). At the end of that file is the stack trace from your plug-in; you can often get at least a hint of what's going on from that stack trace.

If the plug-in isn't crashing but isn't behaving quite right, then things are even more difficult to diagnose. The best solution in this case is to set up a debugging file in your plug-in's local data directory. You can create and open this file in much the same way that you do the local data files for your plug-in, using `getDataFile()` to build a `File` object for that directory.

Listing 12.7 shows an example of a`processRequest()` method that opens a debugging file with the same name as the tuner ID that made the request, stores some values, and then closes it at the end of the request. You can then view the information by opening that file. (Remember that data files are stored in your transmitter root, in the data directory.)

Listing 12.7. A debugging file for a plug-in.

    public void processRequest(RequestContext context) throws IOException {

       /* debugging;  no console for plugins (argh!) */

       File debuggingFile = getDataFile("debug.txt");

       try {

          if (debuggingFile.exists() == false) {

             File dir = new File(debuggingFile.getParent());

             if (dir.exists() == false)

                dir.mkdirs();

          }

          FastOutputStream ds = new FastOutputStream(debuggingFile);

          ds.println("Debugging output....");

          //  write various debugging things here

          ds.close();

       }

       catch (IOException e) {

          e.printStackTrace();

       }

       super.processRequest(context);

    }

> ####

> Note

> Just as I was finishing up this book, there was word that Marimba is working on a better way to debug transmitter plug-ins; however, there wasn't time to include that information here. Check Marimba's Web site, on the Developer's page (`http://www.marimba.com/developer/`) for more information about debugging plug-ins.

###
Summary

Most actions between a developer, a transmitter, and a tuner–from publishing channels, to the tuner downloading files and updates–involve getting data down to the user's local disk. Transmitter plug-ins are a mechanism for data to flow the other way: They allow the channel to send information about the user or his actions back to the transmitter during a channel update.

The plug-in has the ability to not only read that data and do with it what it will (log it to a file, or process it in some other way), but also to customize the files that will be returned to the tuner. In this way, transmitter plug-ins can change what occurs during an update based on the user's preferences or what they may have already done while executing the channel. The plug-in provides an enormously powerful mechanism for creating dynamic channels and for tracking what's going on with your channels.

If you've grasped the information in this chapter, congratulations! With a background in transmitter plug-ins, you've now learned everything there is to know about developing channels for Castanet. In Chapter 13, you'll work through a much more complex example (which also uses transmitter plug-ins, so if you didn't get it this time, you'll have another chance). Chapter 14 finishes up by giving a bigger overview of the marimba packages and how they can help you construct channels.