#

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

- When the tuner is about to make an update (either because the update interval is up, or because the channel specifically requested an update), it sends a "data update" event to the channel. This event gives the channel a chance to save any profile or log data to be sent back to the transmitter. (This case usually happens only when the channel includes a plug-in. You'll learn more about it in Chapter 12, "Creating Transmitter Plug-ins.")
- The tuner makes an update request from the transmitter. The tuner and transmitter negotiate any new files for the channel, and those files are downloaded to the local machine. The tuner stores those files in a holding area–not in the channel directory.
- The tuner sends a "data available" event to the running channel. This event says "Data is here; what should I do?" The channel can respond by restarting (which also tells the tuner to install the data) or by telling the tuner to go ahead and install the data while the channel is still running.
- The tuner installs the data and sends a "data installed" event to the channel (if it's still running). The channel can then do whatever it needs to do to update itself–change the values of widgets or reset any counters, for example.

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:

- `ignore`: Ignores the update and installs the data. The next time the channel starts it is updated and ready to go.
- `start`: Starts the channel with the new data installed and available.
- `notify`: Sends a "data notify" event to the channel. The channel can then decide whether to start based on the data. You'll learn more details about this value in the section "Updates to Non-Running 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:

- `DATA_UPDATE_EVENT`: The update is about to happen; save any profiles or log information.
- `DATA_AVAILABLE_EVENT`: The update has happened, and the data is waiting in the holder area, ready to be installed. Usually, you either call `context.restart()` or `context.installData()` in response to this event. The former stops the channel, installs the data, and restarts the channel. The latter installs the data quietly while the channel is still running. (Note that you, the programmer, don't install any of the data yourself; you just tell the tuner to install it.)
- `DATA_INSTALLED_EVENT`: You get this event when `installData()` has been successful; the channel has been updated with new data. In response to this event, you incorporate the new data into your running channel.
- `DATA_NOTIFY_EVENT`: You get this event only when the channel isn't running–or rather, the tuner starts up just enough of your channel for you to be able to handle this event. See "Updates to Non-Running Channels" for more information on this case.

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:

- The tuner receives an update, and the channel properties for inactive channels are set to `notify`. It installs the new data into the channel directory.
- The tuner instantiates the channel class, calls `setContext()`, and then posts the `DATA_NOTIFY_EVENT` event.
- At this point, enough of the channel is running for the channel to be able to react to the event. The channel can then decide what to do next. If the event handler returns false, the tuner shuts down the channel. If the handler returns true, the tuner calls `start()` to fully launch the channel with the new data installed and available.

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 interface, created in Bongo, which includes a set of images for each of the different conditions (sunny, partly cloudy, cloudy, rainy, snowing).
- A text file, called `data.txt`, which stores the raw weather data for the cities.
- A main Java class to manage the channel, including loading in the data and handling update events.
- A utility Java class called `WeatherData`.

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:

- `weather.gui`, the Bongo presentation for the user interface to the weather channel.
- `WeatherApplication.class` and `WeatherData.class`, the compiled Java files for the channel.
- `data.txt`, the weather data file.
- The images directory, which contains the five weather images (all of them GIF files).
- The `bongo.zip` library. Don't forget this one! You can't guarantee that your users won't have it. (Chances are, they will, but better safe than sorry.)

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:

- Line 2 creates a new File object (`File` is a standard java.io class) with the pathname returned from the `getDataDirectory()` method and the name of the file contained in `src`. The result is the full local pathname to the values file.
- Line 4 opens the file and attaches an output stream (an instance of `FastOutputStream`) to that file.
- Lines 6 through 15 extract the values from the important widgets in the tax calculator and write those values to the file, one per line. Note that you only write the widgets that have user input; widgets such as Total Income and Total Tax are automatically calculated and don't need to be saved.
- Line 17 closes the output stream.
- All the output code is wrapped in a `try`/`catch` block to catch any IO errors that might occur.

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.