Chat Demo - Applet Client

We have created an applet version of our Chat Demo client as a demonstration of the applet support introduced in Ice 3.3.1. The applet's design and implementation has many similarities to its standalone Java counterpart, including the use of the push model for receiving chat updates. Although this applet is unsigned and therefore subject to the strictest security limitations imposed by the Java virtual machine, it shows that applets have access to the complete range of Ice capabilities.

User Interface

We used HTML and Cascading Style Sheets (CSS) for the applet's user interface because it integrates better with our existing web site style. This architecture, which we also used for the PHP and Silverlight clients, relies on JavaScript code to serve as the integration layer between the user interface and the applet.

JavaScript code can invoke any public method of an applet, therefore we have exposed the necessary application hooks in our ChatDemoApplet class. Our applet also calls back into JavaScript code, but we have to explicitly enable this feature by including the MAYSCRIPT attribute in the APPLET tag:

<APPLET id="applet" CODE="ChatDemoGUI.ChatDemoApplet.class"
        MAYSCRIPT
        NAME="ChatDemoApplet"
        HEIGHT=120
        WIDTH=240>
        <NAME="ARCHIVE" VALUE="ChatDemoAppletGUI.jar">
        <PARAM NAME="JAVA_ARGUMENTS" VALUE="-Djnlp.packEnabled=true"/>
</APPLET>

The CODE attribute defines the Java class that implements the applet and the ARCHIVE attribute specifies a list of the JAR files required by the applet. In this case there is only one JAR file, ChatDemoAppletGUI.jar, which is a standalone archive created using ProGuard that contains all of the applet's dependencies.

To minimize the time and bandwidth required to download the applet, we have enabled support for pack200 deployment by setting the option -Djnlp.packEnabled=true in the JAVA_ARGUMENTS parameter.

To demonstrate how the integration layer works, let's examine the JavaScript code that invokes the applet's login method to log the user into a Glacier2 router. In the code below, ChatDemoApplet corresponds to the NAME attribute from the APPLET tag and DefaultRouter is a global JavaScript variable that holds the host name of the Glacier2 router:

// JavaScript
function login()
{
    document.ChatDemoApplet.setDefaultRouter(DefaultRouter);
    document.ChatDemoApplet.login($('txtUserName').value, $('txtPassword').value);
}

The applet uses the JSObject facility to call back into the user interface when necessary. The next code sample shows how the applet's initialization method obtains a reference to the current browser window:

// Java
_jsObject = (JSObject) JSObject.getWindow(this);

Later, the applet uses this JSObject to invoke a JavaScript method that updates the user interface:

// Java
public void setState(final String state)
{
    _queue.enqueue(new Runnable() {
        public void run() {
            // ...

            if(_jsObject != null) {
                try {
                    _jsObject.call("setState", new String[] {state});
                }
                catch(Exception ex) {
                    //...
                }
            }
            else {
                try {
                    getAppletContext().showDocument(
                        new URL("javascript:setState('" + state + "')"));
                }
                catch(java.net.MalformedURLException ex) {
                    //...
                }
            }
        }
    });
}

With certain browsers, calling JSObject methods from the AWT thread causes a deadlock. We work around this issue by using a work queue, in which all calls to JSObject instances are queued for invocation by a dedicated thread. The code also checks whether _jsObject is null to avoid another browser issue. The work around in this case is to construct a JavaScript URL and execute it using the AppletContext.showDocument method. Unfortunately, we cannot use showDocument as our only solution because it is not supported by all browsers, therefore we only use it when the JSObject initialization fails.

Now let's review some of the application logic in our applet.

Creating a Session

One of the first tasks of a push client is establishing a session with the Glacier2 router. This process is greatly simplified by using the Glacier2 helper classes included with Ice, and our applet makes extensive use of these classes.

Before we can create a session, we need to configure the dispatcher to ensure that AMI callbacks are executed in the event loop thread:

initData.dispatcher = new Ice.Dispatcher() {
    public void dispatch(Runnable runnable, Ice.Connection connection) {
        // ...
        SwingUtilities.invokeLater(runnable);
    }
};
As you can see, the dispatcher implementation simply delegates to Swing's invokeLater function.

Next the client creates an instance of Glacier2.SessionFactoryHelper and passes it a callback object:

Glacier2.SessionCallback callback = ...;
_factory = new Glacier2.SessionFactoryHelper(initData, callback);
The callback is invoked to notify the application about significant events in the lifecycle of a Glacier2 session.

Now we can use the factory to initiate the creation of a session:

_factory.setRouterHost(...);
Glacier2.SessionHelper session = _factory.connect(userName, password);
The client invokes setRouterHost to configure the name of the host on which the router is running, and then invokes connect with the user-supplied account information. The return value is a new Glacier2.SessionHelper object that is responsible for managing the session and for invoking the client's SessionCallback object when necessary. For example, since the establishment of a session occurs asynchronously to avoid blocking the thread that calls connect, the client cannot begin using the session until its connected callback method is invoked to indicate that the request completed successfully:
public void
connected(SessionHelper session)
    throws SessionNotExistException
{
    // ...
    _chat = Chat.ChatSessionPrxHelper.uncheckedCast(_session.session());

    Chat.ChatRoomCallbackPrx callback =
        Chat.ChatRoomCallbackPrxHelper.uncheckedCast(
            _session.addWithUUID(new ChatRoomCallbackI(...)));

    _chat.begin_setCallback(callback, ...);
}
The SessionHelper method session returns a proxy for the newly-created Glacier2 session object. We down-cast this proxy to the derived interface Chat::ChatSession that we saw earlier.

Next the code creates the callback object that will receive events from the chat room. There's a lot happening in this short snippet of code. First, we instantiate our callback servant class ChatRoomCallbackI and pass it to the addWithUUID method. This method registers our servant with an object adapter intended especially for callback objects, creating that object adapter if it doesn't already exist. addWithUUID also ensures that the object identity associated with the servant includes a UUID and complies with Glacier2's conventions for callback objects. Finally, we narrow the resulting callback proxy to the type Chat::ChatRoomCallback using an unchecked cast.

The last step is to join the chat room, which we accomplish by invoking setCallback on the session. We use an asynchronous invocation here to avoid blocking.

Sending a Message

The code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:

ChatSessionPrx session = ...
String message = ...
_chat.begin_send(message, new AMI_ChatSession_sendI(_username, message));

The first argument to begin_send is the chat message and the second argument is the AMI callback object, whose constructor needs the name of the user sending the message as well as the message itself. As you can see in the following definition of the callback class, the completion of the asynchronous invocation causes the program to update the user interface:

public class AMI_ChatSession_sendI extends Chat.Callback_ChatSession_send {
    public AMI_ChatSession_sendI(String name, String message) {
        _name = name;
        _message = message;
    }

    public void response(long timestamp) {
        userSayEvent(timestamp, _name, _message);
    }

    public void exception(Ice.LocalException ex) {
        destroySession();
    }

    public void exception(Ice.UserException ex) {
        if(ex instanceof Chat.InvalidMessageException) {
            Chat.InvalidMessageException e = (Chat.InvalidMessageException)ex;
            appendMessage(" - " + e.reason);
        }
    }

    private final String _name;
    private final String _message;
}

The callback class derives from Slice-generated code and must override several methods. Ice invokes the response method when the invocation completes successfully; the parameters of this method correspond to the return value and output parameters of the Slice operation. In the example above, response receives the time stamp assigned by the server, which—along with the user name and message stored by the constructor—is all of the information the callback needs to display a new chat message.

If you would like to see the applet client in action, you can use ZeroC's chat server.

Previous: iPhone Client Next: Flash Client

Terms of Use | Privacy © 2011 ZeroC, Inc.