Home  >  

Exploring Apache Pivot 1.1, Part 3

Author photo
AddThis Social Bookmark Button

This is the third in a series of articles that explore some of the new features in Apache Pivot 1.1. Pivot is a Java-based RIA toolkit that is currently undergoing incubation at the Apache Software Foundation. The previous article demonstrated Pivot's new support for browsing the local file system; this article explains how Pivot can be used to implement support for "push" notifications in a web application using the Jabber instant messaging API.

Pivot 1.1 includes enhanced support for interacting with the web browser DOM as well as communication between applications running in a page. These features are based on Netscape LiveConnect, an API that has been around since the early days of applets but wasn't universally supported until the recent release of Java 6 Update 10.

Pivot applications can call out to the host page using the new eval() method of the pivot.wtk.BrowserApplicationContext class. This method takes a string argument containing the script to be executed by the page. Conversely, a page can call into a Pivot application by getting a reference to the host applet and calling getApplication(), which returns a reference to the Pivot application running in the applet. Using these two methods, developers can create rich internet applications that seamlessly integrate with their surrounding content.

Server Push Applications

The previous two articles in this series provided practical examples of how some of Pivot's new features can be used to address common problems in web application development; namely, the browser's lack of native support for drag and drop and file browsing. This article continues the theme. "Server push technology" refers to a style of client/server interaction where communication is initiated by the server rather than the client. This is in contrast to the (more common) "pull" approach where the client initiates the transaction. Push technology is useful in situations where a client may want to be notified of an event as soon as it occurs. For example:

  • New email message or meeting request received
  • Stock price change
  • Alert notification, such as a server failure
  • New task assignment
  • Breaking news item

However, it can be awkward to build push-style applications using standard web development technologies. HTTP is itself a pull protocol - there is no way for a web server to "call back" into a browser without the browser making the first request. The term "Comet" has been used to describe an approach that uses long-standing HTTP requests to simulate push, but this is a misuse of the HTTP protocol, which wasn't really designed to operate this way, and is also inherently inefficient, as it monopolizes the limited number of HTTP connections available to the browser.

In contrast, instant messaging APIs such as AIM, YIM, and Jabber (also known as XMPP) are push protocols: chat clients don't need to poll for new messages - they are sent and recieved immediately. The remainder of this article describes the implementation of a simple instant messaging (IM) client that demonstrates how a chat protocol can be used to provide push services to a Pivot-enabled web application.

The IM Client

Two screen shots of the sample client are shown below; the first shows a basic login screen, and the second shows the state of the client immediately after a message has been received:

push_login.png

push_message.png

A runnable example is available in the Demos section of the Pivot Wiki. As messages are received, they are briefly displayed in the client window. The application demonstrates Pivot's new DOM support by logging each message to the containing page as it is received.

WTKX Source

The WTKX source code for the application is shown below. The root element is a border containing a card pane, which in turn contains two subcomponents: a login form and a message pane:

 
<Border styles="{padding:8}"
    xmlns:wtkx="http://incubator.apache.org/pivot/wtkx/1.1"
    xmlns="pivot.wtk">
    <content>
        <CardPane wtkx:id="cardPane" orientation="horizontal" selectedIndex="0">
            <Form wtkx:id="loginForm" styles="{fieldAlignment:'justify'}">
                <sections>
                    <Form.Section>
                        <TextInput wtkx:id="usernameTextInput" Form.name="Username" />
                        <TextInput wtkx:id="passwordTextInput" Form.name="Password" password="true" />
                        <TextInput wtkx:id="domainTextInput" Form.name="Domain"/>
                        <FlowPane>
                            <PushButton wtkx:id="loginButton" buttonData="Login"/>
                        </FlowPane>
                        <Label wtkx:id="errorMessageLabel" styles="{color:'#ff0000', wrapText:true}"/>
                    </Form.Section>
                </sections>
            </Form>

            <Border title="Messages" styles="{padding:4, color:10}">
                <content>
                    <Label wtkx:id="messageLabel" styles="{horizontalAlignment:'center', verticalAlignment:'center', wrapText:true}"/>
                </content>
            </Border>
        </CardPane>
    </content>
</Border>

The complete Java source for the application is as follows:

 
package pivot.demos.dom;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;

import pivot.collections.Dictionary;
import pivot.util.concurrent.Task;
import pivot.util.concurrent.TaskExecutionException;
import pivot.util.concurrent.TaskListener;
import pivot.wtk.Application;
import pivot.wtk.ApplicationContext;
import pivot.wtk.BrowserApplicationContext;
import pivot.wtk.Button;
import pivot.wtk.ButtonPressListener;
import pivot.wtk.CardPane;
import pivot.wtk.Component;
import pivot.wtk.ComponentKeyListener;
import pivot.wtk.Display;
import pivot.wtk.Form;
import pivot.wtk.Keyboard;
import pivot.wtk.Label;
import pivot.wtk.PushButton;
import pivot.wtk.TextInput;
import pivot.wtk.Window;
import pivot.wtk.effects.FadeTransition;
import pivot.wtk.effects.Transition;
import pivot.wtk.effects.TransitionListener;
import pivot.wtkx.WTKXSerializer;

public class IMClient implements Application {
     /**
      * Task for asynchronously logging into Jabber.
      *
      * @author gbrown
      */
     private class LoginTask extends Task<Void> {
          public Void execute() throws TaskExecutionException {
               try {
                    String domain = domainTextInput.getText();
    
                    ConnectionConfiguration connectionConfiguration = new ConnectionConfiguration(domain);
                    xmppConnection = new XMPPConnection(connectionConfiguration);
    
                    String username = usernameTextInput.getText();
                    String password = passwordTextInput.getText();
                    xmppConnection.connect();
                    xmppConnection.login(username, password);
                } catch(XMPPException exception) {
                    throw new TaskExecutionException(exception);
                }
   
               return null;
           }
      }
 
     private XMPPConnection xmppConnection = null;
 
     private Window window = null;
     private CardPane cardPane = null;
     private Form loginForm = null;
 
     private TextInput usernameTextInput;
     private TextInput passwordTextInput;
     private TextInput domainTextInput;
 
     private PushButton loginButton = null;
     private Label errorMessageLabel = null;
 
     private Label messageLabel = null;
 
     private ApplicationContext.ScheduledCallback scheduledFadeCallback = null;
 
     public void startup(Display display, Dictionary<String, String> properties)
         throws Exception {
          WTKXSerializer wtkxSerializer = new WTKXSerializer();
          window = new Window((Component)wtkxSerializer.readObject(getClass().getResource("im_client.wtkx")));
  
          cardPane = (CardPane)wtkxSerializer.getObjectByName("cardPane");
          loginForm = (Form)wtkxSerializer.getObjectByName("loginForm");
  
          loginForm.getComponentKeyListeners().add(new ComponentKeyListener() {
               public boolean keyTyped(Component component, char character) {
                    return false;
                }
   
               public boolean keyPressed(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
                    if (keyCode == Keyboard.KeyCode.ENTER) {
                         login();
                     }
    
                    return false;
                }
   
               public boolean keyReleased(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
                    return false;
                }
           });
  
          usernameTextInput = (TextInput)wtkxSerializer.getObjectByName("usernameTextInput");
          passwordTextInput = (TextInput)wtkxSerializer.getObjectByName("passwordTextInput");
          domainTextInput = (TextInput)wtkxSerializer.getObjectByName("domainTextInput");
  
          loginButton = (PushButton)wtkxSerializer.getObjectByName("loginButton");
          loginButton.getButtonPressListeners().add(new ButtonPressListener() {
               public void buttonPressed(final Button button) {
                    login();
                }
           });
  
          errorMessageLabel = (Label)wtkxSerializer.getObjectByName("errorMessageLabel");
  
          messageLabel = (Label)wtkxSerializer.getObjectByName("messageLabel");
  
          window.setMaximized(true);
          window.open(display);
      }
 
     public boolean shutdown(boolean optional) throws Exception {
          return false;
      }
 
     public void suspend() {
          // No-op
      }
 
     public void resume() {
          // No-op
      }
 
     private void login() {
          if (usernameTextInput.getText().length() == 0) {
               errorMessageLabel.setText("Username is required.");
           } else if (passwordTextInput.getText().length() == 0) {
               errorMessageLabel.setText("Password is required.");
           } else if (domainTextInput.getText().length() == 0) {
               errorMessageLabel.setText("Domain is required.");
           } else {
               LoginTask loginTask = new LoginTask();
               loginTask.execute(new TaskListener<Void>() {
                    public void taskExecuted(Task<Void> task) {
                         loginButton.setEnabled(true);
                         cardPane.setSelectedIndex(1);
                         listenForMessages();
                     }
    
                    public void executeFailed(Task<Void> task) {
                         loginButton.setEnabled(true);
                         errorMessageLabel.setText(task.getFault().getMessage());
                     }
                });
   
               errorMessageLabel.setText(null);
               loginButton.setEnabled(false);
           }
      }
 
     private void listenForMessages() {
          PacketFilter filter = new PacketTypeFilter(Message.class);
  
          PacketListener packetListener = new PacketListener() {
               public void processPacket(Packet packet) {
                    final Message message = (Message)packet;
    
                    ApplicationContext.queueCallback(new Runnable() {
                         public void run() {
                              // Show the message text
                              String body = message.getBody();
                              messageLabel.setText(body);
      
                              // Notify the page that a message was received
                              BrowserApplicationContext.eval("messageReceived(\"" + body + "\");", IMClient.this);
      
                              // Cancel any pending fade and schedule a new fade callback
                              if (scheduledFadeCallback != null) {
                                   scheduledFadeCallback.cancel();
                               }
      
                              scheduledFadeCallback = ApplicationContext.scheduleCallback(new Runnable() {
                                   public void run() {
                                        FadeTransition fadeTransition = new FadeTransition(messageLabel, 500, 30);
        
                                        fadeTransition.start(new TransitionListener() {
                                             public void transitionCompleted(Transition transition) {
                                                  messageLabel.setText(null);
                                              }
                                         });
                                    }
                               }, 2500);
                          }
                     });
                }
           };
  
          xmppConnection.addPacketListener(packetListener, filter);
      }
}

The application uses the Smack Jabber library, available from http://www.igniterealtime.org, for instant messaging support. It defines login task that is executed asynchronously when the user presses the "Login" button. This task creates a new XMPP connection and uses the given credentials to log into the service:

 
private class LoginTask extends Task<Void> {
     public Void execute() throws TaskExecutionException {
          try {
               String domain = domainTextInput.getText();
   
               ConnectionConfiguration connectionConfiguration = new ConnectionConfiguration(domain);
               xmppConnection = new XMPPConnection(connectionConfiguration);
   
               String username = usernameTextInput.getText();
               String password = passwordTextInput.getText();
               xmppConnection.connect();
               xmppConnection.login(username, password);
           } catch(XMPPException exception) {
               throw new TaskExecutionException(exception);
           }
  
          return null;
      }
}

On successful login, the application switches to the message panel and begins listening for messages:

 
private void listenForMessages() {
     PacketFilter filter = new PacketTypeFilter(Message.class);
 
     PacketListener packetListener = new PacketListener() {
          public void processPacket(Packet packet) {
               final Message message = (Message)packet;
   
               ApplicationContext.queueCallback(new Runnable() {
                    public void run() {
                         // Show the message text
                         String body = message.getBody();
                         messageLabel.setText(body);
     
                         // Notify the page that a message was received
                         BrowserApplicationContext.eval("messageReceived(\"" + body + "\");", IMClient.this);
     
                         // Cancel any pending fade and schedule a new fade callback
                         if (scheduledFadeCallback != null) {
                              scheduledFadeCallback.cancel();
                          }
     
                         scheduledFadeCallback = ApplicationContext.scheduleCallback(new Runnable() {
                              public void run() {
                                   FadeTransition fadeTransition = new FadeTransition(messageLabel, 500, 30);
       
                                   fadeTransition.start(new TransitionListener() {
                                        public void transitionCompleted(Transition transition) {
                                             messageLabel.setText(null);
                                         }
                                    });
                               }
                          }, 2500);
                     }
                });
           }
      };
 
     xmppConnection.addPacketListener(packetListener, filter);
}

When a message is recieved, the processPacket(Packet packet) method of the PacketListener interface is called. This method posts a callback to the UI thread that updates the message text and then calls the messageReceived() function defined by the containing page. This function appends the message to a <div> named "messageDiv":

 
function messageReceived(body) {
     var messageDiv = document.getElementById("messageDiv");
     var p = document.createElement("p");
     var date = new Date();
     p.appendChild(document.createTextNode(date + ": " + body));
     messageDiv.appendChild(p);
}

Finally, another callback is scheduled to fade the message after 2.5 seeconds. Any pending callback is cancelled if multiple messages are received before the fade occurs.

Conclusion

Though this is a somewhat contrived example, it highlights how easy it is to build push-style applications in Pivot using instant messaging APIs such as XMPP/Jabber. Note that XMPP is not limited to simple text-based chats - it is an extensible protocol that can be used to send and receive messages of arbitrary type.

Additionally, XMPP's support for presence detection can be used to provide additional information to users - for example, if a server fails while a user has an application open, the client can detect that the server has gone offline and notify the user. Pivot's new support for DOM interaction allows developers to extend this support to the web page itself, facilitating the development of sophisticated web-based push applications.

For more information, visit http://incubator.apache.org/pivot.

Read more from Greg Brown. Greg Brown's Atom feed

Comments

Leave a comment


Tag Cloud

Question of the Week: Dream App

If you had an unlimited budget and unlimited resources what application would you build and why would you build it?

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.