Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

How Can I Create a Push Java Channel?


Dr. Dobb's Journal May 1997: Java Q&A

Cliff, vice president of technology of Digital Focus, can be contacted at [email protected]. To submit questions, check out the Java Developer FAQ Web site at http://www.digitalfocus.com/faq/.


Push technology makes it possible for dynamic content to automatically arrive at your desktop, rather than your having to go and look for it. There are a variety of push technologies, most of which differ greatly. Rather than compare the different implementations, I'll demonstrate one way you can create your own pushable content as a Java application using what is very possibly the most robust and secure push technology available to Java programmers -- the Castanet protocol suite and associated tools from Marimba (http://www.marimba.com/). (Marimba is the startup company founded by Kim Polese, Arthur van Hoff, Sami Shaio, and Jonathan Payne, several of the original key players in the birth of Java.) I'll show how a Castanet channel can be customized on a per-user basis, so that the channel can deliver content that is unique and tailored for each user.

Publishing with Castanet

A Castanet "channel" is an application installed on your machine that automatically keeps itself up-to-date by regularly contacting the original source of the application and requesting updates. The protocol is efficient because only the differences are transmitted. It is robust because downloads are treated as transactions, and not actually installed unless the download succeeds. It also provides an automatic and transparent versioning capability, in which a channel that has an older version of a shared object continues to use that version until it obtains a complete update of all dependent objects, thereby ensuring consistency. The protocol provides compactness, because each downloaded object is identified by a checksum signature. Redundant downloads of the same object by different channels therefore cannot occur (this is valuable for keeping system DLLs or DLLs belonging to application suites up-to-date). And the protocol is secure, because it adheres to the Java security model, which is by far a more robust and flexible model than other popular competing models on the Internet.

Castanet does not care what kind of objects you publish -- it is as easy to publish an HTML channel as a Word document channel. To publish a channel, all you have to do is start the Castanet Transmitter and run the Marimba Publish tool, which prompts you for the directory containing the material you wish to publish. It is that simple. Castanet's more advanced features (like plug-ins) require a little more finesse.

End users just need to download and install the Castanet Tuner (freely available from Marimba's web site). The Tuner is a kind of browser that knows how to "channel surf," and that installs channels on your machine. In fact, the Castanet Tuner is built into Netscape Constellation. You will be able to channel surf and browse the web and install channels you see and like. The advantage is that it becomes resident on your machine, always just a click away, whether you are connected to the Internet or not, and you are not limited by the finite speed of a modem.

Marimba's Bongo is an application generator that lets you construct a user interface and behavior, all within the Bongo IDE. Bongo widgets automatically integrate with Castanet, so you could automatically generate much of the code in my demo. The demo would not reveal much about the architecture of the system, however, if it simply used Bongo. Besides, many developers will need to use other GUI component toolkits for other reasons. Therefore, I will not use Bongo here, even though I find it highly useful.

Inside Castanet

Castanet implements a protocol for updating remote objects. The entire update process is encapsulated within the protocol layer, so that it is largely transparent to the application, yet it is accessible, if the application wants to take advantage of advanced features. There are many variations in the exact sequence, but, basically, the Tuner polls the Transmitter associated with each channel that the user subscribes to. If there are updates available for a channel, that transmitter responds with a list of objects that need to be updated. The Tuner then requests those objects. The Transmitter can determine very quickly and efficiently what needs updating, because it maintains a cache of channel version checksums, each with an associated list of objects to transmit. Thus, based on a checksum sent by a Tuner, a Transmitter usually knows immediately what files the Tuner needs. These files are then updated by sending difference files, resulting in great bandwidth economy, even for binary files.

Castanet Plug-ins: Customizing the Server

In typical publishing applications, most content delivered to a Tuner is the same for all users. It is possible to customize content on a per-user basis, however. The Castanet Transmitter plug-in customizes the behavior of the Transmitter for a channel. The general idea is that whenever a Tuner requests an update, the Transmitter invokes the plug-in assigned to the channel (if there is one). The plug-in is Java code written by the channel designer, and can contain anything. It is not limited by a security manager, although Marimba may provide a configurable security manager in the future to give system administrators a means to safeguard networks.

Our Channel

The channel application I present here (see Listing One) publishes an image viewer, and an image, which is unique for each user. A system administration tool I have written lets the channel administrator select which image file each user sees. In a real application, this could be customized advertising copy, or a message pertaining to a particular department in an organization. For demonstration purposes, I'll just use an image.

Every Castanet channel program must implement the interface marimba.channel.Application which defines the callbacks that the Tuner needs in order to control the application. Ordinary applets can be implemented as channels, using a wrapper object that is automatically provided by the system for applets. My demo extends Marimba.channel.ApplicationFrame, which is a subclass of java.awt.Frame, and implements the methods required by the Tuner.

Just like an applet, a Castanet application has a start() and stop() method, which are called when the Tuner starts and stops the application. Once an application is stopped, it is effectively killed; restarting starts it again, as if it were reloaded. The start() method first detects whether the user has run this channel before by retrieving the channel profile. The profile is an object file that is stored for each channel on a Tuner. It can contain any information that the channel wants to put there. Channels can write persistent data to a data directory, but this is different from the profile. The profile is automatically sent to the server every time the channel is updated, and can therefore be used to send small amounts of user-specific information; for example, a profile of how the user has used the channel (hence the name "profile"). For this channel, I will store a user name in the profile. The start() method asks the user for a user name, which must match the name assigned by the administrator. Once entered, the channel sends this name to the server along with every update request, thereby identifying the user.

(Every Tuner has a unique ID which could also be used for identification. There is the complexity, however, of associating this ID with users as they are known to the system administrator, and so for simplicity, we just used an assigned user ID.)

The channel then looks for a file called "imagefile.gif" which is the image file downloaded by the Tuner from the Transmitter. The file is different for each user.

Our Plug-in

When the Transmitter receives an update request from a channel, it looks in the channel directory on the server and checks for a file called "properties.txt" in a directory called "plugin." Our properties.txt file has an entry that says main=Plugin, which causes the server to look for a class file in that directory called "Plugin.class." This class (see Listing Two) is then dynamically loaded by the Transmitter, and the method processRequest() is called. (Actually, the first time the plug-in is run, its init() method is called; but thereafter the processRequest() method is called.) This method is your hook into the Transmitter, and allows you to customize its behavior. Our plug-in, called "Plugin," gets the user profile sent by the tuner, which should contain the user name. You then look this user name up in a database, which can provide any kind of information about that user; in our case, it provides the absolute path of an image file to be sent to that user and only that user.

When a plug-in is run, it is passed an execution context called RequestContext which contains the state of that update, including the set of objects destined to be transferred to the Tuner. You can either add or delete from this set. I simply add the path of the image file, and tell the Transmitter to rename it to imagefile.gif. The Tuner then receives imagefile.gif as part of its update set. The next time the Tuner restarts the channel (this is configurable and controllable by the channel, and in our case, restarts after every update), the latest image is displayed. Each user may receive a different image, depending on the settings in the user database.

Admin

I wrote a simple administration tool that lets channel administrators assign images to users. It displays a list of users and the images assigned to them, and allows the administrator to assign new images, add users, and delete users. The images may reside anywhere in the server's file system. Once the administrator has made any desired changes to the database and quits the Admin tool (see Figure 1), the changes are saved and take effect. The administrator does not have to republish the channel. Thanks to the plug-in, the current state of the database always controls what is downloaded to Tuners, so it is dynamic and real-time. Figure 2 is the Castanet Tuner and Java console, with the image viewer running alongside it.

Publishing for Testing

When you publish a channel on a server, the channel contents are copied to a special channel directory, and are managed completely by Castanet. Your original directory from which you published the channel is not affected and is not used by the Transmitter, except when you republish. Thus, if you have a database file in that directory, that file will be copied along with the other channel contents when you publish. If you have a separate nonchannel admin or database tool that updates the database while the transmitter is running, it will update your copy, rather than the channel's copy. Thus, you should use an absolute path name in your plug-in to reference the database file, to ensure that the plug-in accessed the same copy that your other tools do.

To test your channel, you will need to publish it. You can publish it as a loopback to your own system, by specifying "localhost" as the hostname of the Transmitter. The default port of the Transmitter is port 80. I was not able to use this port on my machine, because I have a web server on my machine that has already claimed this port, so I changed the Transmitter port to 5000. I was then able to start the Transmitter and receive updates in the Tuner, all on one machine.

To generate diagnostic output from your plug-in you can use the debugPrint() plug-in method. The output of debugPrint() can be displayed in an invaluable plug-in debugger tool called Plugin Debugger. You can subscribe to Plugin Debugger, since it is a Marimba channel, and run it like any channel. Output from debugPrint(), in addition to stack traces, are routed across the network to Plugin Debugger, and displayed in its window.

After testing the application, you can then publish it in the normal way, specifying the actual Internet name of your Transmitter's host.

Conclusion

Java programmers have long wanted a way to free the applications they write from the confines of a browser. It is now possible to publish powerful, web-enabled, installable, and runnable content, with straightforward and platform-independent programming. Java programs can now run on your desktop, with all the performance of an installed application, merely at the push of a button -- and you will never have to reinstall or upgrade them.

DDJ

Listing One

/** Channel.java * A channel that displays an image, unique for each user.
 * Copyright 1997 by Digital Focus. This code may be used for non-commercial 
 * purposes. No warrantee or guarantee is implied.
 */


</p>
public class Channel extends marimba.channel.ApplicationFrame
{
    public Channel()
    {
    }
    
    /**
     * Start the application. This method is called by the tuner to 
     * notify that the application is started. Assumes that a context exists.
     */


</p>
    public void start()
    {
        String profile;
        byte[] bp = context.getProfile();
        if ((bp == null) || (bp.length == 0))
        {
            // Ask user to set up profile.
            // Our profile consists only of the user-id, given to the user by
           // the system administrator.
            
            frame = new ProfileFrame(context);
        }
        else
        {
            profile = new String(bp, 0);
            System.out.println("Your profile is " + profile);
        }
        
        try
        {
            java.awt.MediaTracker mt = new java.awt.MediaTracker(this);
            java.net.URL imageurl = null;
            imageurl = new java.net.URL(context.getCodeBase(),"imagefile.gif");
            image = context.getImage(imageurl);
            mt.addImage(image, 0);
            try { mt.waitForAll(); } catch (InterruptedException ex) {}
            imageFrame = new ImageFrame("My Image", image);
            imageFrame.resize(image.getWidth(this), image.getHeight(this));
            imageFrame.show();
        }
        catch (Exception ex) { ex.printStackTrace(); }
    }
    
    /**
     * Destroy the Channel, hide and dispose the base frame.
     */
    public void stop()
    {
        hide();
        dispose();
    }


</p>
    /**
     * Handle events.
     */


</p>
    public boolean handleEvent(java.awt.Event event)
    {
       if (event.id == marimba.channel.Application.DATA_UPDATE_EVENT)
       {
            return true;
       }
       else if (event.id == marimba.channel.Application.DATA_AVAILABLE_EVENT)
       {
            context.restart();
            return true;
       }
       else if (event.id == marimba.channel.Application.DATA_INSTALLED_EVENT)
       {
            return true;    // will not happen; here for completeness only
       }
       else if (event.id == marimba.channel.Application.DATA_NOTIFY_EVENT)
       {
            return false;   // don't start if we are not running
        }
        else if (event.id == java.awt.Event.WINDOW_DESTROY)
        {
            context.stop();
            return true;
        }


</p>
        return false;
    }
    
    private java.awt.Frame frame;
    private java.awt.Frame imageFrame;
    private java.awt.Image image;
}


</p>
class ImageFrame extends java.awt.Frame
{
    public ImageFrame(String title, java.awt.Image image)
    {
        super(title);
        this.image = image;
    }
    
    public void paint(java.awt.Graphics g)
    {
        if (image != null) g.drawImage(image, 0, 0, this);
    }
    
    java.awt.Image image;
}


</p>
class ProfileFrame extends java.awt.Frame
{
  private java.awt.Label label = new java.awt.Label("Please enter user id:");
  private java.awt.TextField tf = new java.awt.TextField(10);
  private java.awt.Button okbutton = new java.awt.Button("OK");


</p>
   public ProfileFrame(marimba.channel.ApplicationContext context)
   {
        super("Please enter your user id");
        this.context = context;


</p>
        setLayout(new java.awt.FlowLayout());
        add(label);
        add(tf);
        add(okbutton);
        resize(200, 100);
        show();
   }
    
    public boolean handleEvent(java.awt.Event event)
    {
        if (event.id == java.awt.Event.ACTION_EVENT)
       {
            String s = tf.getText();
            byte[] b = new byte[s.length()];
            s.getBytes(0, s.length(), b, 0);
            context.setProfile(b);
            context.restart();
            dispose();
            return true;
        }
        else return false;
    }
    
    private marimba.channel.ApplicationContext context;
}

Back to Article

Listing Two

/** Plugin.java * Castanet transmitter plugin, which dynamically updates a user's download 
 * set based on settings in a user database. To accomplish this, we will only 
 * publish class files, and user this plugin to add other files as needed.
 * Copyright 1997 by Digital Focus. This code may be used for non-commercial 
 * purposes. No warrantee or guarantee is implied.
 */
 
public class Plugin extends marimba.plugin.Plugin
{
    /**
     * Processes a subscribe or update request for this plugin.  This
     * is where logging data is processed and the personalization of
     * the index occurs.
     *
     * If this returns without an exception it is assumed that the
     * logging data, if any, was processed successfully.  An
     * IOException can be thrown if there's a problem processing the
     * logging data.
     */


public void processRequest(marimba.plugin.RequestContext context) throws java.io.IOException { debugPrint("a"); super.processRequest(context); // Get the user-id from the profile. debugPrint("b"); byte[] b = context.getProfileData(); if ((b == null) || (b.length == 0)) { // User does not have a profile; quit debugPrint("User has no profile"); return; } debugPrint("c"); String userid = new String(b, 0); debugPrint("userid=" + userid); // Get user data. Users.readUsers(); String imagefilename = (String)(Users.users.get(userid)); debugPrint("for " + userid + " imagefilename=" + imagefilename); debugPrint("There are " + Users.users.size() + " entries"); if (imagefilename == null) return; debugPrint("d");

// Put the image for that tuner in the download set.

java.io.File imagefile = new java.io.File(imagefilename); context.addFile("imagefile.gif", imagefile); } }

/** * A simple user database. */ class Users { public static void readUsers() { if (users == null) users = new java.util.Hashtable(); java.io.FileInputStream fstream = null; java.io.DataInputStream ostream = null; try { // Read in the hashtable java.io.File userfile = new java.io.File ("d:\\column\\may97\\channel\\plugin\\userfile"); fstream = new java.io.FileInputStream(userfile); ostream = new java.io.DataInputStream(fstream); for (;;) { String userid = ostream.readLine(); if (userid == null) break; userid = userid.trim(); String imagefilename = ostream.readLine(); imagefilename = imagefilename.trim(); users.put(userid, imagefilename); } } catch (java.io.IOException ex) { // done } finally { if (ostream != null) try { ostream.close(); } catch (java.io.IOException ex) {} else if (fstream != null) try { fstream.close(); } catch (java.io.IOException ex) {} } } public static void writeUsers() throws java.io.IOException { if (users == null) users = new java.util.Hashtable(); java.io.FileOutputStream fstream = null; java.io.PrintStream ostream = null; try { java.io.File userfile = new java.io.File("d:\\column\\may97\\channel\\plugin\\userfile"); fstream = new java.io.FileOutputStream(userfile); ostream = new java.io.PrintStream(fstream); java.util.Enumeration ek = users.keys(); for (java.util.Enumeration e = users.elements(); e.hasMoreElements();) { String userid = (String)(ek.nextElement()); String imagefilename = (String)(e.nextElement()); ostream.println(userid); ostream.println(imagefilename); } } catch (java.io.IOException ex) { throw new java.io.IOException(); } finally { if (ostream != null) ostream.close(); else if (fstream != null) try { fstream.close(); } catch (java.io.IOException ex) {} } } static java.util.Hashtable users; }

Back to Article


Copyright © 1997, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.