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:

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:

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):

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:

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:

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 processRequest(), 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:

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.