Chapter 11

Managing Updates and User Information

With the basic framework in place for your Castanet channel, the next step is to add the features that make channels interesting: the capabilities to handle live updates from the transmitter and to save state on the user's local disk. The Castanet framework provides several events and methods to accomplish both of these capabilities.

This chapter is divided into two main parts: the first will teach you all about handling updates to application channels whether or not those channels are running; the second will fill you in on how to save state in both application channels and applets running as channels.

The Castanet Update Mechanism

As I've mentioned throughout this book, Castanet channels can be automatically updated at regular intervals so that the version of a channel that is running on a tuner is essentially guaranteed to be the most up to date at all times. This capability is useful for simple executable channels, in which new bug fixes and new features can be downloaded and incorporated into the new channels so that the next time you start them the new version is available. But Castanet's update mechanism also allows you to create channels with live updates–channels that rely on the frequent updating mechanism to incorporate new data and new information into the channel even as it's running. The mechanism Castanet uses to update channels–both live, running channels and inactive channels–involves a sophisticated conversation between the tuner and the channel, with the help of the channel properties, the application context, and a special set of events specifically for updates.

As a channel programmer, you have a wide variety of choices for how to manage updates, from the very simple (ignore the new data until the next time the channel is restarted, as you might for a basic executable program) to the very complex (incorporate the new data into a running channel, or, if the channel isn't running, make a decision whether to launch the channel).

How Updates Work

When an update occurs, the tuner and the channel negotiate for how to handle the new information that has arrived as part of the update. Different things happen depending on whether or not the channel is running. If your channel is indeed running, the conversation between the tuner and the channel goes something like this:

If the channel isn't running, the process works slightly differently. For inactive channels, when the tuner makes an update, the channel properties determine what to do. There are three values for the Data Available Action event for inactive channels:

Update Events and Helper Methods

Channel update events are part of the standard Application interface in the marimba.channel package. In your channel application, you need to test for and handle these events in your handleEvent() method, just as you would test for and manage any other events in your application. The four channel update events and the reactions you're expected to have to these events are as follow:

If you're using either the ApplicationFrame or ApplicationPlayerFrame classes for your channel, a default version of handleEvent()manages the DATA_AVAILABLE_EVENT and DATA_INSTALLED_EVENT events in a sensible way. (These two are typically the events you use.) To make this default implementation easier to use, ApplicationFrame and ApplicationPlayerFrame define two event handler methods: notifyAvailable() to handle DATA_AVAILABLE_EVENT events, and notifyInstall() to handle DATA_INSTALLED_EVENT events.

The notifyAvailable() Method

The nofifyAvailable() method from either ApplicationFrame or ApplicationPlayerFrame looks like this:

public void notifyAvailable(String dir) {

   if ("restart".equals(context.getParameter("update.action")))

      context.restart();

   else if ("install".equals(context.getParameter("update.action")))

      context.installData("");

}

In this default implementation, the channel checks its properties (available from the application context using the getParameter() method) to determine what to do next. You set these properties in the publish tool. For active channels, three possible values are available for the Data Available Action under Active channels: ignore, restart, or install. (See Figure 11.1.)

Figure 11.1. Update properties for active channels.

If the update property (update.action) is set to restart(), the notifyAvailable() method calls context.restart(), which tells the tuner to stop the channel (call stop()), install the data, and start the channel (call setContext() and start()).

If the properties for the Data Available Action have been set to install, then this method calls context.InstallData(). The null string argument to installData() says to install all the new data; you can also choose to install only some of the data by indicating a filename as the argument to installData().

Finally, if the update property is set to ignore, this method does nothing. The new data will not be installed until the next time the user starts the channel.

If you want different behavior for managing updates, you can override this method in your own subclass of ApplicationFrame, as long as you use the same signature:

public void notifyAvailable(String dir) {
...
}

The notifyInstall() Method

The second event handler method that ApplicationFrame and ApplicationPlayerFrame provides is notifyInstall(), in response to DATA_INSTALLED_EVENT events. This method is called in response to the context.installData() method, to let your channel know that the new data has been installed and is ready for you to process it.

The default implementation of notifyInstall() looks like this:

public void notifyInstall(String dir) {
}

Not very useful, is it? That's because generally you want to do your own thing here–call a method to load a data file or to update the values of your widgets, for example. This empty template for notifyInstall() is available in the ApplicationFrame and ApplicationPlayerFrame classes so that you can remember to override it in your own subclasses.

Finding Out Which Files Will Be Updated

Both the notifyInstall() and notifyAvailable() methods take one argument: the string argument of the event, which is always null (""). The original intent of this argument was to provide the path to the topmost directory that contained files that would change, so you could figure out which files to actually install as part of the update, if any. Unfortunately, this didn't work, as the properties.txt file that comes with the channel always changes, and the argument is always a null string ("").

Instead, you can use context.getPendingUpdates() inside the body of your notifyInstall() or notifyAvailable() methods to find out find out which files will be created, changed, or deleted as part of the updates. The getPendingUpdates() method returns an object of type Updates. Updates is a class in marimba.channel, whose methods will allow you to check for which files are going to be changed as part of the update. There are three methods to choose from: getDeletes(), getCreates(), and getUpdates(), all of which return an Enumeration object. The Enumeration, in turn, contains strings representing paths to the files which will be deleted, created, or changed as part of the update, respectively. You can use this information to pick which files to install, should you so choose.

Updates to Non-Running Channels

Previously in this chapter, I mentioned DATA_NOTIFY_EVENT, which is sent only to channels that aren't running. This may seem kind of silly–if the channel isn't running, how can it possibly handle the event? In reality, the process is more complicated than that. Managing updates to channels that aren't running is a special case that allows the channel to decide, on the fly, if an update is worth starting up the channel. You could think of this process as the tuner nudging your channel awake and saying, "I have this data. What do you want to do?" Your channel then either decides that the data isn't important and goes back to sleep, or it becomes fully awake and launches to handle that new data. Here's how it works:

So what sort of situation would the channel need in order whether to decide to start? Perhaps the channel has preferences that allow the user to decide whether to launch the channel when new data is available. In response to the DATA_NOTIFY_EVENT, the channel could check those preferences and respond in kind. Or, perhaps, the channel should start itself only during business hours while someone is potentially there to pay attention, in which case the channel could check the current time on the system. Or your channel might have some other reason for deciding whether to launch based on new information. With this mechanism in place, you have that choice.

If you do want to process events of this type, you have to add a test for the DATA_NOTIFY_ACTION event in your handleEvent() method. No helper methods are defined by default, so you don't have to override them. Return false to ignore the new data or true to start the channel, something like this:

public boolean handleEvent(Event evt) {

   switch (evt.id) {

      case DATA_NOTIFY_EVENT:

         boolean start = test_to_see_if_you_need_to_start();

         if (start)

            return true;

         else return false;

   }

}

Keep in mind also that, when you handle this event (say, in the method I've called test_to_see_if_you_need_to_start() in the example), the start() method for your channel has not yet been called. You therefore may have to initialize some values before you have enough information to work with.

After you modify your channel to take advantage of these kinds of events, don't forget to set the properties for that channel when you publish it so that the Data Available Action for the Inactive channel is notify. (See Figure 11.2.)

Figure 11.2. Set the Data Available Action to notify.


Updating Applications and Applets

If you create a channel from scratch from the ApplicationFrame or ApplicationPlayerFrame classes, templates for event management are there for you. If you convert an existing application or applet to a channel, however, you'll have to manage updates differently.

If you convert a pure Java application to a channel by implementing the Application interface, neither notifyInstall() or notifyAvailable() will be available to you. You'll have to implement those methods yourself, or handle all the update events in your own handleEvent() methods, just as you manage other events. You can use the source to ApplicationFrame and ApplicationPlayerFrame (or the examples shown in the preceding section) as a template to manage your own events.

If you're using an applet as a channel, unfortunately with the current version of Castanet, events are not passed to your applet class, so you cannot intercept or handle live update data. However, you can set up your applet such that if new data arrives as part of an update, you can tell the applet to restart itself. The tuner stops the applet and then reloads all the applet's code and data–effectively updating the running applet.

To take this step, you don't need to modify the applet code at all. Simply change the Update properties for that channel in Castanet Publish so that the value of the Data Available Action menu is restart and then republish the channel. (See Figure 11.3.)

Figure 11.3. Set the Data Available Action to restart.

Having this value for your applet channel causes the tuner (actually, the applet context in which your applet is running) to restart the applet if any data is available for that update.

An Example: The Weather Channel

Background is all well and good, but an example can really help make it all clear. For this example, I'll walk you through building a channel that relies on frequently updated data and incorporates updates to that data into the channel even if it's running.

The channel created here, called the Weather Channel, is shown in Figure 11.4. It provides basic weather information about three cities: San Francisco, New York, and London, including the conditions (sunny, cloudy, raining), the temperature, and the humidity. The data that the weather channel displays is updated on a regular basis (say, every 15 minutes), and is updated on the fly even if the channel is running. So if you run the weather channel, you can be sure of having the latest up-to-date weather information for these three cities.

Figure 11.4. The Weather channel.



Note

The source for this channel is available on the CD-ROM for this book, in the directory Examples and the subdirectory Weather.


This channel is made up of four files:


The Presentation

I created the interface for this presentation in Bongo, with essentially nine parts: three image widgets for each of the condition images (with the names SFimg, NYimg and Limg), three static text widgets for the temperature (SFtemp, Nytemp, and Ltemp), and three for the humidity (SFhum, NYhum, and Lhum).



Note

My implementation of this channel is terribly inefficient, as will become more apparent later when you see the Java code. This interface might make a lot more sense if I had written a custom Bongo widget for each of the cities. The widgets would make the channel easier to update and allow the creation of a menu of cities in which the user is interested. However, because I haven't covered creating bongo widgets in this book, explaining it this way is easier. If you're interested in learning more about custom widgets, check out The Official Marimba Guide to Bongo (Sams.net Publishing).


Five condition images are also included; they're stored in a directory called images. Figure 11.5 shows these images:

Figure 11.5. The five condition images.

The Framework

The main framework for this channel is contained in a file called WeatherChannel.java. You can start by creating the basic framework for this channel and then add the methods for loading the data and for managing the updates later.

This channel uses several classes from other packages, including input classes from marimba.io, URL classes from java.net, and the StringTokenizer class from java.util to parse the data file. The first part of the class, then, is this prodigious set of import statements:

import java.awt.*;
import java.net.*;
import java.util.*;
import java.io.IOException;
import marimba.gui.*;
import marimba.channel.*;
import marimba.persist.*;

import marimba.io.*;

Next is the definition of the channel class, called WeatherChannel, which inherits from ApplicationPlayerFrame. Inside that class is an instance variable to store the name of the data file ("data.txt"), a set of constants for the kind of weather condition (they will be important later in the class), and a basic start() method. The start() method here does two things: it sets the value of the title bar (a nice UI feature), and it calls the loadData(), method (defined in the next section), which opens, reads, and parses the data file. Here's the basic class framework:

public class WeatherChannel extends ApplicationPlayerFrame {



/* data file */

String src = "data.txt";



/* conditions */

static final int SUNNY = 0;

static final int PARTLY_CLOUDY = 1;

static final int CLOUDY = 2;

static final int RAINY = 3;

static final int SNOWY = 4;



/* override start to read in the initial data */

public void start() {

  super.start();

  setTitle("The Weather Channel");

  loadData();

}



}

Load the Data

All the actual weather data for this application channel is stored in a separate file. This way, the data can be updated independently, without having to update the entire channel each time. But what this also means is that as part of the start-up process for the channel, you have to open and read that data from the file. To do so, you can define a method called loadData(), which goes into the class file just below the start() method.

loadData() has several important features. The first is the input class it uses: the class FastInputStream. This class is part of the marimba.io package, and is an optimized buffered input stream. It's much more efficient than the standard Java IO classes, so you can use it here. (Because the Bongo classes are already included as part of this channel, there's no penalty in taking advantage of other classes as well.)

The second important feature is the use of a utility class called WeatherData, which is a simple class that collects the weather data into a single structure. The WeatherData class looks like this:

public class WeatherData {
String city;
int conditions;
String temp;
String humidity;

}

Each of the instance variables in the WeatherData class corresponds to an entry in the data.txt file. The data.txt file itself stores the weather information, one city per line, with the different bits of data separated by spaces. Comments are lines that start with a hash sign (#). A sample data.txt file might look like this:

# weather channel data file
# city conditions temperature humidity
#
SF 0 0 50%
NY 1 0 89%

L 0 0 20%

If you keep these features and the format of the data file in mind, the rest of the loadData() method should be straightforward. First, you open the FastInputStream using a URL. (You can get the base URL from the application context; you'll learn more about that in the section on saving state.) Next, use the standard Java StringTokenizer class to read each bit of the data file into an instance of the WeatherData class, and then call the fillWidgets() method. This method is used to actually update the widgets in the presentation with the data you've read from the file. And, finally, you wrap all the stuff in loadData() in a try/catch block to make sure that you catch any URL or IO errors.

Here's the complete loadData() method:

public void loadData() {

   try {

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

      String str;

      WeatherData data;

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



      while ((str = in.readLine()) != null) {

         str.trim();

         data = new WeatherData();

         if ((str.length() > 0) && !str.startsWith("#")) {

            StringTokenizer st = new StringTokenizer(str);

            data.city = st.nextToken();

            data.conditions = Integer.parseInt(st.nextToken());

            data.temp = st.nextToken();

            data.humidity = st.nextToken();



            fillWidgets(data);

         }

      }

      in.close();

   }

   catch (MalformedURLException e) {

      e.printStackTrace();

   }

   catch (IOException e) {

      e.printStackTrace();

   }

}



You can now move on to the fillWidgets() method, which simply uses the PlayerUtil object stored in util and the setValue() method to put the data into the right spot in each widget in the presentation.



Note

With this method, it should become all too apparent that my design could be much better implemented. With a better interface, this code could probably be shrunk down to only a few lines.


Because all of fillWidget() is straightforward (using the same widget methods you've learned about before), I'll simply print it here. Note that when you use the setValue() method on image objects, you're setting the path to that image (unlike other widgets that have real displayable values.) Keep in mind as well that because the interface was created in Bongo, Bongo manages updating the screen for the new images automatically; you don't have the deal with paint() or repaint() at all as you do when you use the AWT.

public void fillWidgets(WeatherData data) {

   String cityimg ="";

   String citytemp = "";

   String cityhum   = "";



   if (data.city.equals("SF")) {

      cityimg = "SFimg";

      citytemp = "SFtemp";

      cityhum = "SFhum";

   } else if (data.city.equals("NY")) {

      cityimg = "NYimg";

      citytemp = "NYtemp";

      cityhum = "NYhum";

   } else if (data.city.equals("L")) {

      cityimg = "Limg";

      citytemp = "Ltemp";

      cityhum = "Lhum";

   }



   switch (data.conditions) {

      case SUNNY:

         util.setValue(cityimg, "images/sunny.gif");

         break;

      case PARTLY_CLOUDY:

         util.setValue(cityimg, "images/pcloud.gif");

         break;

      case CLOUDY:

         util.setValue(cityimg, "images/cloudy.gif");

         break;

      case RAINY:

         util.setValue(cityimg, "images/rainy.gif");

         break;

      case SNOWY:

         util.setValue(cityimg, "images/snow.gif");

         break;

   }

   util.setValue(citytemp, data.temp);

   util.setValue(cityhum, data.humidity);

}


Add Update Management

At this point, you can compile, publish, subscribe to, and run the weather channel, and it would work just fine. But to get new weather data through updates, you would need to quit and restart the channel each time. That's no fun at all.

Instead you can add methods to deal with updating the channel as it runs. This way, no matter how frequently the weather data gets updated, the running channel always displays the latest information. To do this, you can override with the notifyAvailable() and notifyInstall() methods that the ApplicationPlayerFrame class gives you.

The default notifyAvailable() checks to see if the properties for the channel indicate whether the channel is to restart or the channel is to be notified. I know how the channel will be published, and that the properties will indicate that the data is to be installed, so my version of notifyAvailable() is much smaller. The only data I'm interested in is the data.txt file. If there's anything else to update, it can be installed the next time the channel is restarted.

My new version of nofityAvailable() does only one thing: it installs the data.txt file in the channel directory. Note that if you call the installData() method with a null argument (""),all the new data is installed. Here I've called it explicitly with the data file to speed up the process:

public void notifyAvailable(String dir) {

context.installData(src);

}

After the data has been installed, the tuner sends the DATA_INSTALL_EVENT, which is then passed to notifyInstall(). Inside notifyInstall(), you need to do whatever you need to do to process that new data file (remember, the default implementation of notifyInstall() is empty). Fortunately, because the data loading is factored into its own method, this is easy. All you need to do is to call loadData() again, and the new data is loaded and displayed in the existing presentation. Easy!

public void notifyInstall(String dir) {

   loadData();

}

And that, believe it or not, is it. These two small methods are all you need to make automatic updates to live channels. Of course, if your channel is more complicated than the simple channel created here, then your update methods could very well be much more complex than these, but that's the basic idea.

Publish the Channel

Just as a reminder, let me go over organizing your files into a channel directory and publishing the channel so that all the settings are right.

You can put all the channel files into a directory called Weather, which includes the following files:

To publish the channel, you set the General properties to refer to the WeatherApplication as the main class, weather.gui as the presentation, and bongo.zip as the classpath. Figure 11.6 shows these settings.

Figure 11.6. General properties for the Weather channel.

You also should check the update properties of the channel. Inactive channels can update only once a day, and you can ignore new data (it is installed when the user decides to start the channel). For active channels, however, set the frequency to something reasonable. (I don't think anyone needs to know about changes in the weather more than once an hour, so I set the interval to an hour.) Also, set the Data Available Action to install. (This setting triggers the update events in your channel.) Figure 11.7 shows the update properties for this channel.

Figure 11.7. Update properties for the Weather channel.



Note

If you set the Data Available Action to restart, what happens? Remember that when you overrode the original version of notifyAvailable(), you took out the check for the restart property. Therefore, context.restart() never gets called. The data is installed regardless of the update properties, but restart doesn't work unless you explicitly call context.restart() in your event handler methods.



The Complete Code

Listing 11.1 shows the full code for the WeatherApplication.java file, built in the preceding sections. You can also view this code on the CD-ROM that came with this book.

Listing 11.1. Complete code for the Weather channel.

import java.awt.*;

import java.net.*;

import java.util.*;

import java.io.IOException;

import marimba.gui.*;

import marimba.channel.*;

import marimba.persist.*;

import marimba.io.*;



public class WeatherChannel extends ApplicationPlayerFrame {



/* data file */

String src = "data.txt";



/* conditions */

static final int SUNNY = 0;

static final int PARTLY_CLOUDY = 1;

static final int CLOUDY = 2;

static final int RAINY = 3;

static final int SNOWY = 4;



/* override start to read in the initial data */

public void start() {

  super.start();

  setTitle("The Weather Channel");

  loadData();

}



/* notifyAvailable is called just before the data is

actually installed. It's like the tuner is asking

permission. installData() says OK, go ahead.

Here we'll restrict the install to the data file

because that's all we care about. All other new data

will get installed when the channel restarts */

public void notifyAvailable(String dir) {

   context.installData(src);

}



/* notifyInstall is called after the data has been installed.

Here's where we process the new stuff. */

public void notifyInstall(String dir) {

   loadData();

}



/* read in the data.txt file, which has the current weather values

we'll display. Format is

city condition temperature humidity

*/

public void loadData() {

   try {

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

      String str;

      WeatherData data;

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



      while ((str = in.readLine()) != null) {

         str.trim();

         data = new WeatherData();

         if ((str.length() > 0) && !str.startsWith("#")) {

            StringTokenizer st = new StringTokenizer(str);

            data.city = st.nextToken();

            data.conditions = Integer.parseInt(st.nextToken());

            data.temp = st.nextToken();

            data.humidity = st.nextToken();



            fillWidgets(data);

         }

      }

      in.close();

   }

   catch (MalformedURLException e) {

      e.printStackTrace();

   }

   catch (IOException e) {

      e.printStackTrace();

   }

}



/* fill in widget data. This is so not efficient (kludge!  kludge!)

This app would be much better implemented with

each city as a custom bongo widget; that'd let them get

reused here as well as dynamically built. */

public void fillWidgets(WeatherData data) {

   String cityimg ="";

   String citytemp = "";

   String cityhum   = "";



   if (data.city.equals("SF")) {

      cityimg = "SFimg";

      citytemp = "SFtemp";

      cityhum = "SFhum";

   } else if (data.city.equals("NY")) {

      cityimg = "NYimg";

      citytemp = "NYtemp";

      cityhum = "NYhum";

   } else if (data.city.equals("L")) {

      cityimg = "Limg";

      citytemp = "Ltemp";

      cityhum = "Lhum";

   }



   switch (data.conditions) {

      case SUNNY:

         util.setValue(cityimg, "images/sunny.gif");

         break;

      case PARTLY_CLOUDY:

         util.setValue(cityimg, "images/pcloud.gif");

         break;

      case CLOUDY:

         util.setValue(cityimg, "images/cloudy.gif");

         break;

      case RAINY:

         util.setValue(cityimg, "images/rainy.gif");

         break;

      case SNOWY:

         util.setValue(cityimg, "images/snow.gif");

         break;

   }

   util.setValue(citytemp, data.temp);

   util.setValue(cityhum, data.humidity);

}



}


Saving State

Now that you've grasped updates, let me cover "saving state" in your channel. Saving state is the ability to store information about the current state of the application on the users' machines–for example, their current position in a game, or files they've created using your channel. You can also save state for things like user preferences for the channel.

The ability to save state in channels on the users' machines is a significant advantage to using channels over applets. To save state in an applet, applet developers usually have to save the state back to the Web server where the applet came from. This process requires an application running on the server to process that data, file space on the server to store it, and an extra network connection to take up bandwidth and time for the applet to run. Being able to save local state is a much better idea.



Note

If saving state to the local system is that great an idea, why can't applets do it? When applets first arrived on the Web, the security concerns were so great that browser developers decided not to let the applets have any access to the local file systems whatsoever. Although this was good for security and for encouraging warm, fuzzy feelings among nervous Java users, it has restricted the kinds of applications that can be developed with Java.

Browser developers have also come to the conclusion that perhaps denying access to the file systems altogether is a bit extreme. Both Microsoft and Netscape are planning to loosen security in future versions of their browsers, configurable by the user, for saving local state in applets. But in the meantime, you can save state in channels right now.


Channels are given access to one directory on the local file system: the channel's data directory. Most other local file accesses are restricted or forbidden. I say "most" because there's one other exception: If the user allows it by setting an option in the tuner, channels can also read files from a CD-ROM.

In this section, you'll learn about the channel's data directory and the classes and methods you can use to help read and write local files from that data directory.

A Note on Accessing Channel Data

Before you dive into the meat of this section, I need to make an important distinction here between accessing files that are part of the channel itself and reading or writing files that are part of the local data for that channel.

You'll commonly want to read or process files that are part of the actual channel itself–for example, the data file in the Weather Channel example. These files are created on the transmitter side and sent to the tuner as part of a channel subscription or an update. These files are stored as part of the channel and are not to be modified by the user running that channel.

You can read any of these kinds of files in your channel code from any location in your channel directory, with one restriction: you must use a URL to access them. You can get the base URL of the channel itself using the context.getBase() method. That URL will be of the form tuner://hostname:port/channel, where the hostname and port are the hostname and port of the transmitter from which the channel came, and the channel is the local directory path to that channel. You can then append the pathname of the file you want to access to that URL (the standard java.net.URL methods let you do this) and use the openStream() method to attach an input stream to that file. Here's an example template of how to do this using the FastInputStream class from marimba.io:

URL base = new URL(context.getBase(), "myfile.txt");
FastInputStream in = new FastInputStream(base.openStream());

Accessing Local Data

Accessing channel data using getBase() is different from accessing local channel data. The former is data used by every instance of the channel, regardless of who has downloaded it. The latter is for saving files and data specific to an individual instance of a channel–for example, preferences files, scores a user has made in a game, local state files, and so on. With channels, you can read and write any number of local state files using standard file access methods–with the one restriction that those files can be contained only in a special data directory intended for just that purpose.

The data directory is located on a user's local disk, in the .marimba directory used for storing channel files (the default directories are C:\Windows\.marimba or C:\WinNT\.marimba on Windows systems, $HOME/.marimba on UNIX systems). Inside the .marimba directory is a subdirectory for each transmitter, and inside these directories are the channel and data directories. The channel directory stores the channel data for each channel; the data directory contains the data.

You don't really need to know this specific information to access the data directory for your channel; the application context provides methods for just that purpose. Two methods, context.getDataDirectory() and context.getDataBase(), give you the pathname to the local data directory. The former returns a String; the latter returns a URL object.

To open a file called preferences.txt in the local data directory, then, you can use the following two lines (which use java.io.File and marimba.io.FastInputStream classes):

File f = new File(context.getDataDirectory(), "preferences.txt");
FastInputStream fs = new FastInputStream(f);

After the input stream is open and attached to a file, you can read it using standard input methods just as you would read any other file. The same process applies to writing files; just use the getDataDirectory() method to get the pathname to that file.



Note

Earlier versions of the Castanet software used getChannelDirectory() for this same purpose. This method is obsolete and will be removed from a later version of Castanet. If you're working with code that uses this method, you should switch over to getDataDirectory().


In addition to the basic files you can read and write into the data directory for your channel, Castanet also defines two special kinds of files: channel log files and channel profiles. These files are used to generate feedback data to the transmitter and are part of the transmitter plug-in mechanism. You'll learn more about these files in Chapter 12, "Creating Transmitter Plug-ins."

Accessing the CD-ROM

The Castanet Tuner has an option in its configuration screens that allows a channel further access to the user's local disk: It allows channels to read files from a CD-ROM. You can use this feature to create a hybrid channel and CD-ROM product; you could store extremely large files on the CD, and the have the bulk of the channel as a regular updateable program.

To read files from the CD-ROM, you construct open and read the files using IO streams, just as you would with a regular Java application. (The java.awt.FileDialog class may be useful so that the user can point you to the CD-ROM.) However, because the option to allow the channel might or might not be turned on, trying to open a file from the CD may result in a security exception. If you catch that security exception in your channel code, you can tell the user to enable this option in the tuner and try again.

See Marimba's Web site (http://www.marimba.com/developer/) for examples of how to read files from a CD-ROM.

An Example: An Estimated Tax Calculator

To demonstrate the use of saving state in the channel's transmitter directory, I created a simple channel that calculates estimated income taxes based on income and deductions. If I had written this channel so that it didn't save state, the users of the channel would have to reenter the data each time they started the channel. It's much more usable if I save the data when the channel quits and then reload it when the channel starts up again.

Figure 11.8 shows the channel interface which, like most of the channels in this section, is created in Bongo.

Figure 11.8. The Tax Calculator channel.



Note

To keep things simple, this channel calculates taxes only for single people. Apologies to all those married folk; you'll need to write your own income tax calculator.


The Java code for this example is a subclass of ApplicationPlayerFrame called TaxesApplication. A single instance variable called src is included. This variable contains the name of the file to which you save the current channel state. Here's the basic framework for the channel:

import java.awt.*;
import java.net.*;
import java.io.*;
import marimba.channel.*;
import marimba.gui.*;
import marimba.io.*;

public class TaxesApplication extends ApplicationPlayerFrame {

String src = "values.txt";


}

When the channel starts up, the first step is to read any local data that might have been saved. When the channel shuts down, you have to save the local data to a file so that it can be restored when the channel starts up again. To do both these things, you can override the start() and stop() methods to call functions called readValues() and writeValues() for reading and writing the local data. The following shows both start() and stop() methods for this class:

public void start() {

   super.start();

   readValues();

}



public void stop() {

   super.stop();

   writeValues();

}

Now, working backward, start with the definition of writeValues(), which takes the values the user has entered into the application and saves that data to a file on the local machine. Listing 11.2 shows the code for the writeValues() method.

Listing 11.2. The writeValues() method for the Tax Calculator.

1: public void writeValues() {

 2:    File vfile = new File(context.getDataDirectory(), src);

 3:    try {

 4:      FastOutputStream fs = new FastOutputStream(vfile);

 5:

 6:      fs.println(util.getValue("wages"));     // wages & salary

 7:        fs.println(util.getValue("int"));        // interest & dividends

 8:        fs.println(util.getValue("business")); // business income

 9:        fs.println(util.getValue("capgains")); // capital gains

10:       fs.println(util.getValue("otherinc")); // other income

11:       fs.println(util.getValue("deduct"));   // deductions

12:       fs.println(util.getValue("exempt"));     // exemptions

13:       fs.println(util.getValue("credits"));  // credits

14:       fs.println(util.getValue("withhold")); // withholding

15:       fs.println(util.getValue("estpayment")); // estimated payments

16:

17:       fs.close();

18:    }

19:    catch (IOException e) {

20:      e.printStackTrace();

21:    }

22:}

Here's a line-by-line description of what's going on, in case you're lost:

After the data is saved, the next step is to read it back in when the application starts up again. The readValues() method, which is analogous to the writeValues() method, is shown in Listing 11.3.

Listing 11.3. The readValues method for the Tax Calculator.

 1: public void readValues() {

 2:    File vfile = new File(context.getDataDirectory(), src);

 3:    if (vfile.exists(src)) {

 4:       try {

 5:          FastInputStream fs = new FastInputStream(vfile);

 6:

 7:           util.setValue("wages", fs.readLine());     // wages & salary

 8:           util.setValue("int", fs.readLine());        // interest & dividends

 9:           util.setValue("business", fs.readLine()); // business income

10:         util.setValue("capgains", fs.readLine()); // capital gains

11:         util.setValue("otherinc", fs.readLine()); // other income

12:         util.setValue("deduct", fs.readLine());   // deductions

13:         util.setValue("exempt", fs.readLine());     // exemptions

14:         util.setValue("credits", fs.readLine());  // credits

15:         util.setValue("withhold", fs.readLine()); // withholding

16:         util.setValue("estpayment", fs.readLine()); // estimated payments

17:

18:         fs.close();

19:      }

20:      catch (IOException e) {

21:         e.printStackTrace();

22:      }

23:   }

24:}


If you figured out writeValues(), readValues() shouldn't be too much of a
stretch, although you should note one interesting line. Line 3 has a test to
make sure whether the values file actually exists. The first time the
calculator runs, it doesn't have any saved values. If you try to open the values file, it doesn't exist, and that would lead to errors. This one if statement makes sure that the file is actually there before going on.

Saving State in Applets and the Applet Context

Because applets can run as channels, you can modify an applet to take advantage of the state-saving capabilities of channels. And, if you trap your exceptions right, you can create code that works equally well if the applet is run as an applet in a Web page or as a channel.

When applets run as channels, they run inside a channel "shell" called AppletViewer (part of the marimba.channel package). Most of the time you don't need to know anything about the AppletViewer class; simply setting the properties of the applet channel causes everything to run seamlessly. However, an applet running inside an AppletViewer can gain access to an application context for the channel, and use many of the methods that application context provides, through the use of an AppletContext object.

To get ahold of the application context from inside your applet code, use the standard getAppletContext() method (from java.applet.Applet). Cast that object to an instance of AppletContext, and call getApplicationContext() on that object, like this:

ApplicationContext context = ((AppletContext)getAppletContext()).getApplicationContext();

With the channel's application context in hand, you can then use any of the common application context methods with that context, including getDataDirectory() to get the directory for saving local files. Here's a bit of sample code that saves state between invocations of an applet channel:

import marimba.channel.*;



public class MyApplet extends java.applet.Applet {

    ...



void init() {

    String dir = (((AppletContext)getAppletContext()).getApplicationContext()).

              getDataDirectory();

    try {

        FileInputStream in =

             new FileInputStream(dir + File.separator + "state.bin");

        // load the state

        ...

    } catch (IOException e) {

        // applet; can't read state from local disk

    } catch (FileNotFoundException e) {

        // there was no state

    }

}



void destroy() {

    String dir = (((AppletContext)getAppletContext()).getApplicationContext()).

             getDataDirectory();

    try {

        FileOutputStream out =

           new FileOutputStream(dir + File.separator + "state.bin");

        ...

    } catch (IOException e) {

        // applet; can't save state to local disk

    }

}

}

Note that if you do create combination applets that are intended to be run both as regular applets and channels, watch for the IOException and make sure you handle it in some way (remember, applets cannot write to local files). In addition to trapping the exceptions, you'll also have to distribute the marimba.zip library with those applets so that they can run properly.

Summary

In this chapter, you learned about updates and saving state, two of the features that make channels interesting.

Updates are handled through the use of four events, part of the standard Application interface for channels: DATA_AVAILABLE_EVENT, DATA_INSTALL_EVENT, DATA_UPDATE_EVENT, and DATA_NOTIFY_EVENT. The first two are the most common and allow you to decide on the fly whether to accept a live update and, if so, to process the new data when it arrives. DATA_NOTIFY_EVENT is particularly interesting, as it allows your channel to decide whether to start based on the new data.

Saving state in channels is essentially identical to saving state in applications, with one exception: you have to use the getDataDirectory() method to get the location of the channel's data directory, and you can read or write from only that one directory.

After this chapter, you should have all the basics for creating channels that seamlessly interact with the Castanet technology. In the next chapter, however, you'll learn how to add the features provided by transmitter plug-ins, which allow your channels to send feedback data when they're updates and which allow you to customize the data that is sent to the tuner during an update.