7.2 GWT & Dojo Cometd Client with Spring Bayeux on Jetty

This section and the next will cover GWT & Dojo Cometd Client with Spring Bayeux on Jetty. The first example will be a GWT version of the Dojo Cometd Chat example, which is also available for comparison in the web applicaiton. The following example is a little more complex. It is Trade Monitor that has the Bayeux service on the server generate trade summary information for a few stock symbols and the client applicaiton can dynamically subscribe and unsubscribe to each symbols summary information.

Both of these examples use Spring by Example's Web Module. It has some Spring extensions of the standard Bayeux classes to make it possible to configure a Bayeux instance in Spring and then inject it into your Bayeux services. More information can be found in the Spring by Example's Web Module's section Spring Bayeux Integration for Comet on Jetty .

1. GWT Chat with Dojo Cometd & Spring Bayeux on Jetty

The Chat example shows basic Bayeux integration for subscribing and unsubscribing from a Bayeux channel. It also shows how to display incoming messages in GWT and how to publish messages from the client.

GWT Chat with Dojo Cometd & Spring Bayeux Server Configuration

Web Configuration

This is basically the same as a standard Spring MVC web configuration, but two things to note are the Spring JS ResourceServlet and the SpringContinuationCometdServlet handles Bayeux requests. The ResourceServlet is used for serving static files from jars. The Spring JS libraries contain Dojo, and are actually served from there. The Dojox JavaScript libraries are in the dojox-resources.jar. The SpringContinuationCometdServlet handles all Bayeux publish and subscribe requests and is mapped to '/cometd/*'.

/WEB-INF/web.xml
                            
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" 
         xmlns="http://java.sun.com/xml/ns/j2ee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
                             http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>chat</display-name>
    
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/web-application-context.xml
        </param-value>
    </context-param>

    <filter>
        <filter-name>encoding-filter</filter-name>
        <filter-class>
            org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encoding-filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Serves static resource content from .jar files such as spring-faces.jar -->
    <servlet>
        <servlet-name>resources</servlet-name>
        <servlet-class>org.springframework.js.resource.ResourceServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet>
        <servlet-name>cometd</servlet-name>
        <servlet-class>org.springbyexample.cometd.continuation.SpringContinuationCometdServlet</servlet-class>
        <init-param>
            <param-name>asyncDeliver</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
        
    <servlet>
        <servlet-name>chat</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value></param-value>
		</init-param>
		<load-on-startup>2</load-on-startup>
    </servlet>
    
    <!-- Map all /resources requests to the Resource Servlet for handling -->
    <servlet-mapping>
        <servlet-name>resources</servlet-name>
        <url-pattern>/resources/*</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cometd</servlet-name>
        <url-pattern>/cometd/*</url-pattern>
    </servlet-mapping>
    
    <servlet-mapping>
        <servlet-name>chat</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
    
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

</web-app>
                            
                        

Spring Bayeux Configuration

The context:component-scan will register the Bayeux Chat Service. The bayeux bean class is SpringContinuationBayeux. This is the main instance that will control of Bayeux's publish and subscribe on the server. It will be used by all Bayeux services and by the Bayeux servlet. Basic values like timeout can be set (p-namespace is being used for setter injection, see Setter Injection using the p-namespace). The Bayeux implementation also supports filtering of any messages. This example has a filter defined to not allow any markup to be sent and also has two basic regular expression filters. One matches 'Spring In-depth' (either a capital or lower case 's' for 'spring' and 'i' for 'in') and changes it to 'Spring In-depth, In Context' and the other corrects typos for 'the' and 'spring'.

/WEB-INF/bayeux-context.xml
                            
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/context 
                        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springbyexample.springindepth.web.cometd" />
    
    <bean id="bayeux" 
          class="org.springbyexample.cometd.continuation.SpringContinuationBayeux"
          p:timeout="300000"
          p:interval="0"
          p:maxInterval="10000"
          p:multiFrameInterval="2000"
          p:logLevel="0"
          p:directDeliver="true">
        <property name="filters">
            <value>
                <![CDATA[
[
  { 
    "channels": "/**",
    "filter"  : "org.mortbay.cometd.filter.NoMarkupFilter",
    "init"    : {}
  },

  { 
    "channels": "/chat/*",
    "filter"   : "org.mortbay.cometd.filter.RegexFilter",
    "init"    : [ 
                  [ "[Ss]pring [Ii]n-depth","'Spring In-depth, In Context'" ],
                  [ "[Ss]pring [Ii]n depth","'Spring In-depth, In Context'" ]
                ]
  },
  
  { 
    "channels": "/chat/**",
    "filter"   : "org.mortbay.cometd.filter.RegexFilter",
    "init"    : [ 
                  [ "teh ","the "],
                  [ "sring ","spring "] 
                ]
  }
  
  
]
                ]]>
            </value>
        </property>
    </bean>

</beans>
                            
                        

Code Example

The ChatService is basically the same as the one from the Dojo Cometd example. The key differences are that the class is annotated with @Component and the constructor is annotated with @Autowired. So the service is picked up by the context:component-scan because of the @Component annotation and the Bayeux instance configured in the XML configuration is injected into the constructor because of the @Autowired annotation. At this point the class just calls super in it's constructor to give the Bayeux instance to it's parent and working on the service is the same. So it is very easy to modify any Bayeux service example to be Spring configured and gain the full advantage of IoC and DI.

Example 7.1. ChatService

src/main/java/org/springbyexample/springindepth/web/cometd/chat/ChatService.java
                            
@Component
public class ChatService extends BayeuxService {
    
    final Logger logger = LoggerFactory.getLogger(ChatService.class);
    
    final ConcurrentMap<String, Set<String>> _members = new ConcurrentHashMap<String, Set<String>>();

    /**
     * Constructor
     */
    @Autowired
    public ChatService(Bayeux bayeux) {
        super(bayeux, "chat");
        subscribe("/chat/**", "trackMembers");
    }

    /**
     * Tracks chat clients.
     */
    public void trackMembers(Client joiner, String channel,
                             Map<String, Object> data, String id) {
        if (Boolean.TRUE.equals(data.get("join"))) {
            Set<String> m = _members.get(channel);

            if (m == null) {
                Set<String> new_list = new CopyOnWriteArraySet<String>();
                m = _members.putIfAbsent(channel, new_list);
                if (m == null) {
                    m = new_list;
                }
            }

            final Set<String> members = m;
            final String username = (String) data.get("user");

            members.add(username);

            joiner.addListener(new RemoveListener() {
                public void removed(String clientId, boolean timeout) {
                    members.remove(username);

                    logger.info("members: " + members);
                }
            });
            
            logger.info("Members: " + members);
            
            send(joiner, channel, members, id);
        }
    }
    
}
                            
                        

GWT & Dojo Cometd Configuration

This section will cover GWT & Dojo Cometd's configuration and how they interact with each other.

Join Code Example

The AppBase is the parent of App. AppBase is used as a class without an Dojo Cometd dependencies so it can be launched in GWT hosted mode for basic debugging and looking at the GUI layout.

This excerpt from AppBase.onModuleLoad() adds a ClickListenter to the join button. When the button is clicked, the user name is set from the text field, the join method is called, the join panel's visibility is set to false, and the main chat panel is added. The join method in AppBase doesn't do anything, but the child class, App, overrides the join method and calls out to a native JavaScript method which calls Dojo Cometd to subscribe to chat.

Example 7.2. Join Code Example (excerpt from AppBase.java)

Excerpt from web/chat-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/chat/client/AppBase.java

                            
joinButton.addClickListener(new ClickListener() {
    public void onClick(Widget sender) {
        userName = joinChatTextBox.getText();
        join(userName);

        joinChatPanel.setVisible(false);
        chatPanel.add(sendChatPanel, DockPanel.SOUTH);
        
        chatTextBox.setFocus(true);
    }
});
                            
                        

The join method is native and has special comments before and after the body so the GWT compiler can process the method into JavaScript. The app variable makes a reference to this for use in the anonymous inner function. The call to external JavaScript starts with $wnd which indicates to the GWT compiler that what follows $wnd. is a native JavaScript call that is calling room.join. The room.join takes the user's name and three callbacks to GWT. The first one is for displaying messages, the second is for displaying a user joining, and the last is to clear the user table.

Inside the first anonymous function, it is configured to take from and text values for displaying a chat message. The app variable created about is used to reference the App class. This is the special syntax the GWT compiler uses to make a reference from JavaScrip to GWT generated JavaScript. You'll notice that an @ symbol is put before the full class name, which is then followed by '::' and the name of the method along with it's signature. Right after that the variables passed into the function are passed into App.display(String from, String text) (method actually located in AppBase).

Example 7.3. Join Code Example (excerpt from App.java)

Excerpt from web/chat-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/chat/client/App.java

                            
protected native void join(String userName) /*-{
    // Keep reference to self for use inside closure
    var app = this;
    
    $wnd.room.join(userName, 
        function(from, text) { 
            app.@org.springbyexample.springindepth.web.gwt.chat.client.App::display(Ljava/lang/String;Ljava/lang/String;)(from, text);
        },
        function(user) { 
            app.@org.springbyexample.springindepth.web.gwt.chat.client.App::addUser(Ljava/lang/String;)(user);
        },
        function() { 
            app.@org.springbyexample.springindepth.web.gwt.chat.client.App::clearUserTable()();
        }
    );
}-*/;
                            
                        

This shows the JavaScript function being called from GWT to join the chat room ($wnd.room.join).

Example 7.4. Join Code Example (excerpt from gwt-chat.js)

Excerpt from src/main/webapp/js/gwt-chat.js

                            
join: function(userName, displayCallback, addUserCallback, clearUserTableCallback) {
	if (userName == null || userName.length == 0) {
		alert('Please enter a username!');
		return;
	}

    var hostName = window.location.hostname;
    var port = window.location.port;
    var contextPath = "\/chat";
    
    var host = "http:\/\/" + hostName;
    
    if (port) {
    	host += ":" + port;
    }
    
    host += contextPath;
    
    dojox.cometd.init(host + "\/cometd\/chat\/");  1

    room.connected = true;		
    room.userName = userName;
    room.displayCallback = displayCallback;  2
    room.addUserCallback = addUserCallback;
    room.clearUserTableCallback = clearUserTableCallback;
    
    // subscribe and join
    dojox.cometd.startBatch();  3
    dojox.cometd.subscribe("/chat/demo", room, "processMessageEvent");  4
    dojox.cometd.publish("/chat/demo", {  5    
    	user: room.userName,
    	join: true,
    	chat: room.userName + " has joined"
    });
    dojox.cometd.endBatch();  6
    
    ...
       
},
                            
                        
1 This makes the main connection to Bayeux using Dojo Cometd's init function.
2 This sets the GWT callback for displaying a chat message when this code receives a message.
3 Dojo Cometd's startBatch() is called to batch together subscribing to the chat room and publishing the current user has joined the room.
4 This method subscribes to /chat/demo and configures the callback to be the room variable's processMessageEvent function.
5 Dojo Cometd's publish function publishes that the current user has just joined the room so other chat users are notified.
6 This line ends the current batch so it can be processed.

Send Code Example

This excerpt from AppBase.onModuleLoad() adds a KeyboardListener to the text box that listens for the enter key being pressed to send a messag, and a ClickListenter to the the send button.

Example 7.5. Send Code Example (excerpt from AppBase.java)

Excerpt from web/chat-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/chat/client/AppBase.java

                            
chatTextBox.addKeyboardListener(new KeyboardListener() {
    public void onKeyDown(Widget sender, char keyCode, int modifiers) {
    }

    public void onKeyPress(Widget sender, char keyCode,
            int modifiers) {
        if (keyCode == KEY_ENTER) {
            send(chatTextBox.getText());
            chatTextBox.setText("");
        }                    
    }

    public void onKeyUp(Widget sender, char keyCode, int modifiers) {
    }                
});

Button sendButton = new Button();
sendButton.setText(sendLabel);
sendButton.addClickListener(new ClickListener() {
    public void onClick(Widget sender) {
        send(chatTextBox.getText());
        chatTextBox.setText("");
    }
});
                            
                        

This is the send method being called by the KeyboardListener and ClickListenter defined for send that calls the native JavaScript method $wnd.room.chat.

Example 7.6. Send Code Example (excerpt from App.java)

Excerpt from web/chat-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/chat/client/App.java

                            
protected native void send(String message) /*-{   
    $wnd.room.chat(message);  
}-*/;
                            
                        

Below is the JavaScript function being called from GWT to send a chat message ($wnd.room.chat).

Example 7.7. Send Code Example (excerpt from gwt-chat.js)

Excerpt from src/main/webapp/js/gwt-chat.js

                            
chat: function(text) {
    if (!text || !text.length) {
        return false;
    }

    dojox.cometd.publish("/chat/demo", {  1
        user: room.userName,
        chat: text
    });
},
                            
                        
1 Dojo Cometd's publish function to the /chat/demo channel sending user and chat message.

Leave Code Example

The leave button has a ClickListener that leaves the chat room, clears the screen and chat panel, then shows the join panel so the user can rejoin the chat.

Example 7.8. Leave Code Example (excerpt from AppBase.java)

Excerpt from web/chat-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/chat/client/AppBase.java

                            
leaveButton.addClickListener(new ClickListener() {
    public void onClick(Widget sender) {
        leave(userName);
        chatTextBox.setText("");
        chatPanel.remove(sendChatPanel);

         clearUserTable();
         clearChatTable();
         
        joinChatPanel.setVisible(true);
    }
});
                            
                        

The leave method calls the native JavaScript room's leave function.

Example 7.9. Leave Code Example (excerpt from App.java)

Excerpt from web/chat-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/chat/client/App.java

                            
protected native void leave(String userName) /*-{
    $wnd.room.leave();
}-*/;
                            
                        

The leave function doesn't do anything if the user name is null. If the Dojo Cometd meta subscription isn't null, it unsubscribes from the meta channel. Then it begins a batch that unsubscribes from the chat channel and publishes that the user has left the room before ending the batch. At the end it disconnects from Bayeux on the server.

Example 7.10. Leave Code Example (excerpt from gwt-chat.js)

Excerpt from src/main/webapp/js/gwt-chat.js

                            
leave: function(){
	if (!room.userName) {
		return;
	}
	
	if (room.meta) {
		dojo.unsubscribe(room.meta);
	}
	room.meta = null;
	
	dojox.cometd.startBatch();  1
	dojox.cometd.unsubscribe("/chat/demo", room, "processMessageEvent");  2
	dojox.cometd.publish("/chat/demo", {  3
		user: room.userName,
		leave: true,
		chat: room.userName + " has left"
	});
	dojox.cometd.endBatch();  4
	
	room.userName = null;
	dojox.cometd.disconnect();  5
},
                            
                        
1 Dojo Cometd's startBatch() is called to batch together unsubscribing to the chat room and publishing the current user has left the room.
2 This method unsubscribes from /chat/demo and configures the callback to be the room variable's processMessageEvent function.
3 Dojo Cometd's publish function publishes that the current user has just left the room.
4 This line ends the current batch so it can be processed.
5 Dojo Cometd's disconnect function is called to disconnect from Bayeux.

References

The project can be checked out from the subversion repository using this command or by using your IDE.

                        $ svn co http://svn.springbyexample.org/springindepth/trunk/web/chat-webapp/ chat-webapp
                    

[Note]Running Examples

Currently when making modifications to GWT classes, manually deleting the generated files from the GWT directory under src/main/webapp (ex: rc/main/webapp/org.springbyexample.springindepth.web.gwt.monitor.App/) in Eclipse is necessary even though the Groovy script in the Maven build does this. Then to run the changes, run mvn clean package jetty:run.