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.

All of an applet's public methods are accessible in JavaScript, therefore we exposed the necessary application hooks in our ChatDemoApplet class. Note however that the applet must declare itself as scriptable by including the MAYSCRIPT attribute in its APPLET tag:

<APPLET id="applet" CODE="ChatDemoGUI.ChatDemoApplet.class"
        MAYSCRIPT
        NAME="ChatDemoApplet"
        HEIGHT=120
        WIDTH=240>
        <PARAM NAME="CACHE_ARCHIVE_EX" VALUE="ChatDemoAppletGUI.jar;preload;1.2.0">
        <PARAM NAME="JAVA_ARGUMENTS" VALUE="-Djnlp.packEnabled=true"/>
</APPLET>
The CODE attribute defines the Java class that implements the applet and the CACHE_ARCHIVE_EX 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 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)
{
    ...
    new Thread()
    {
        public void run()
        {
            if(_jsObject != null)
            {
                _jsObject.call("setState", new String[] {state});
            }
            else
            {
                getAppletContext().showDocument(new URL(
                    "javascript:setState('" + state + "')"));
            }
        }
    }.start();
}
This method invokes on the JSObject from a separate thread in order to work around a problem in some browsers in which the AWT dispatch thread is put to sleep during a JSObject call. The method 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

As in other push clients, our first task is to create a session with the Glacier2 router. (The code snippets shown below are excerpts from the applet client.)

The client begins by obtaining a proxy for the communicator's default router and down-casting it to a proxy for the derived interface Glacier2::Router. This is done using a checked cast, which behind the scenes sends a request to the target object to confirm that it implements the desired interface:

Ice.Communicator communicator = ...
Ice.RouterPrx defaultRouter = communicator.getDefaultRouter();
Glacier2.RouterPrx router = Glacier2.RouterPrxHelper.checkedCast(defaultRouter);

With the router's proxy in hand, the client can create a Glacier2 session. The createSession operation returns a proxy of type Glacier2::Session, and the client narrows this proxy to the derived interface Chat::ChatSession that we saw earlier. In this case, the client uses an uncheckedCast; this avoids sending a confirmation request to the target object and is useful when the client already knows that the target object implements the desired interface:

Glacier2.SessionPrx s = router.createSession("user", "password");
Chat.ChatSessionPrx session = Chat.ChatSessionPrxHelper.uncheckedCast(s);

After successfully creating the session, the client must take care of a little house-keeping before it can instantiate its callback object. The first step is to create an object adapter, which is the Ice construct responsible for dispatching incoming requests to Ice objects. Activating the object adapter allows the Ice run time to begin dispatching requests:

Ice.ObjectAdapter adapter =
    communicator.createObjectAdapterWithRouter("ChatDemo.Client", router);
adapter.activate();

Every Ice object requires a unique identity, which is a structure consisting of two strings, a name and a category. When using callbacks with Glacier2, the client must use a category supplied by Glacier2. The code below prepares the identity for the callback object:

Ice.Identity callbackId = new Ice.Identity();
callbackId.name = Ice.Util.generateUUID();
callbackId.category = router.getCategoryForClient();

Now the client can instantiate its callback object. Since the client activated the object adapter earlier, it is possible for requests to be dispatched to this object as soon as we add it to the object adapter. In turn, the object adapter returns a proxy that we can down-cast to the appropriate interface:

Ice.Object obj = new ChatRoomCallbackI();
Ice.ObjectPrx proxy = adapter.add(obj, callbackId);
Chat.ChatRoomCallbackPrx callback =
    Chat.ChatRoomCallbackPrxHelper.uncheckedCast(proxy);

The last step is to join the chat room, which we accomplish by invoking setCallback on the session:

session.setCallback(callback);

To avoid blocking Swing's event loop, the program uses asynchronous invocations when communicating with the chat server. For example, 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 = ...
session.send_async(new AMI_ChatSession_sendI(userName, message), message);

The first argument to send_async 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 prompts the program to schedule an update to the user interface:

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

    public void ice_response(long timestamp)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                String message = formatTimestamp(timestamp) + " - <" + _name +
                                 "> " + unstripHtml(_message));
                appendMessage(message);
            }
        });
    }

    ...
}

The callback class derives from Slice-generated code and must override several methods. Ice invokes the ice_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, ice_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. The implementation of ice_response calls the SwingUtilities.invokeLater method to ensure that the nested Runnable object gets executed from Swing's event loop thread.

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

Previous: iPhone Client

Terms of Use | Privacy © 2010 ZeroC, Inc.