[DWR Website |Web Application Index ]

Server Side Reverse Ajax Clock

Creating a clock in a web page is easy, but what about a clock controlled by the server? This demo shows how use use a separate server side thread to control a number of browsers.

Server status:

Filtering

Client side script

We turn active Reverse Ajax on: dwr.engine.setActiveReverseAjax(true); . Reverse Ajax will work without comet, by using piggyback, however this gives you very long latency, not what we need for a clock, so we turn on comet or polling (whatever the server is configured to prefer).

Next we specify our errorHandler and retry logic. DWR has a retry feature for reverse AJAX which automatically re-attempts failed calls. With the default configuration failed calls will be re-attempted twice with one second between each attempt. If both retry attempts fail the errorHandler will be called. After the errorHandler is called DWR will re-attempt the call at 10 second intervals until successfull. At this time the pollStatusHandler will be called. You can read more about this feature on our site.

Clicking the "Start / Stop" button calls the toggle() method on our Clock class which starts or stops the clock.

By default no updates are sent to the browser. The ScriptSessions are filtered on the server and only ScriptSessions with the "UPDATES_ENABLED" attribute set to true will receive updates. Selecting the "Enable Updates" button will make a call to Clock.setEnabledAttribute(true) which will set the UPDATES_ENABLED on the ScriptSession and allow this tab/window to receive updates. You may disable updates by selecting the "Disable Updates" button.

Java Code

The server code uses a ScheduledThreadPoolExecutor to call update the screen once a second. The setClockDisplay looks like this:

public void setClockDisplay(final String output)
{
    Browser.withAllSessionsFiltered(new UpdatesEnabledFilter(UPDATES_ENABLED_ATTR), new Runnable()
    {
        public void run()
        {
            Util.setValue("clockDisplay", output);
        }
    });
}

The Browser API is being used to send updates to certain ScriptSesssions. We have implemented a ScriptSessionFilter to check for an attribute on the ScriptSessions and only send updates to pages if the attribute is true:

private class UpdatesEnabledFilter implements ScriptSessionFilter 
{
    private String attrName;
	    	
	public UpdatesEnabledFilter(String attrName) 
	{
	    this.attrName = attrName;
	}
	    	
	public boolean match(ScriptSession ss) 
	{
		Object check = ss.getAttribute(attrName);
	    return (check != null && check.equals(Boolean.TRUE));
	}    	
}
In our HTML page we set this attribute when the "Enable Updates" or "Disable Updates" buttons are selected. The action of selecting one of these buttons calls the following method on Clock which will set the UPDATES_ENABLED_ATTR to true or false:
public void setEnabledAttribute(Boolean enabled) 
{
    ScriptSession scriptSession = WebContextFactory.get().getScriptSession();
    scriptSession.setAttribute(UPDATES_ENABLED_ATTR, enabled);
}

We also use the Browser API to call a function on the HTML page when the clock has been toggled. The active boolean is passed as an argument and used on the client to display the clock's status.

public void setClockStatus()
{
    Browser.withAllSessions(new Runnable()
    {
        public void run()
        {
            ScriptSessions.addFunctionCall("setClockStatus()", active);
        }
    });
}
		
	

HTML source:

<div id="demoDiv">  	
    <input type="button" value="Start / Stop" onclick="Clock.toggle();"/>
	Server status: <span id="pollStatus"></span>
	<div id="error"></div>
    <p></p>
    <h2 id="clockDisplay"></h2>
  </div>
  <div>
    <h3>Filtering</h3>
	<input type="button" value="Enable Updates" onclick="enableUpdates(true);">
	<input type="button" value="Disable Updates" onclick="enableUpdates(false);">  	
  </div>	

Javascript source:

	
window.onload=function()
{
    dwr.engine.setActiveReverseAjax(true);
    dwr.engine.setErrorHandler(errorHandler);
    dwr.engine.setPollStatusHandler(updatePollStatus);
    updatePollStatus(true);
    Tabs.init('tabList', 'tabContents');       
}
	  
function errorHandler(message, ex) 
{
    dwr.util.setValue("error", "Cannot connect to server. Initializing retry logic.", {escapeHtml:false});
	setTimeout(function() { dwr.util.setValue("error", ""); }, 5000)
}
	  
function updatePollStatus(pollStatus) 
{
    dwr.util.setValue("pollStatus", pollStatus ? "Online" : "Offline", {escapeHtml:false});
}
	  
function enableUpdates(enabled)
{
    if (!enabled) 
	{
	    dwr.util.setValue("clockDisplay", "This tab/window does not have updates enabled.");
	}  
    Clock.setEnabledAttribute(enabled);
}

function setClockStatus(clockStatus) {
    dwr.util.setValue("clockStatus", clockStatus ? "Clock started" : "Clock stopped");
}

Java source:

public class Clock implements Runnable
{
    /**
     * Create a schedule to update the clock every second.
     */
    public Clock()
    {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());
        executor.scheduleAtFixedRate(this, 1, 50, TimeUnit.MILLISECONDS);
    }

    /* (non-Javadoc)
     * @see java.lang.Runnable#run()
     */
    public void run()
    {
        if (active)
        {
            String newTimeString = new Date().toString();
            // We check this has not already been sent to avoid duplicate transmissions
            if (!newTimeString.equals(timeString))
            {
                setClockDisplay(newTimeString);
                timeString = newTimeString;
            }
        }
    }

    /**
     * Called from the client to turn the clock on/off
     */
    public synchronized void toggle()
    {
        active = !active;
        setClockStatus();
    }

    private class UpdatesEnabledFilter implements ScriptSessionFilter
	{
    	private String attrName;
    	
    	public UpdatesEnabledFilter(String attrName)
	    {
    		this.attrName = attrName;
    	}
    	
        public boolean match(ScriptSession ss)
	    {
			Object check = ss.getAttribute(attrName);
	        return (check != null && check.equals(Boolean.TRUE));
		}    	
    }
    
    /**
     * Call a function on the client for each ScriptSession.
     * passing the clock's status for display.
     * 
     * @param output The string to display.
     */
    public void setClockStatus()
	{
        Browser.withAllSessions(new Runnable()
        {
            public void run()
            {
                ScriptSessions.addFunctionCall("setClockStatus()", active);
            }
        });
    }
    
    /**
     * Send the time String to clients that have an UPDATES_ENABLED_ATTR attribute set to true 
     * on their ScriptSession.
     * 
     * @param output The string to display.
     */
    public void setClockDisplay(final String output)
    {
        Browser.withAllSessionsFiltered(new UpdatesEnabledFilter(UPDATES_ENABLED_ATTR), new Runnable()
        {
            public void run()
            {
                Util.setValue("clockDisplay", output);
            }
        });
    }

    /**
     * 
     * @param enabled
     */
    public void setEnabledAttribute(Boolean enabled) {
    	ScriptSession scriptSession = WebContextFactory.get().getScriptSession();
    	scriptSession.setAttribute(UPDATES_ENABLED_ATTR, enabled);
    }
    
    private static String UPDATES_ENABLED_ATTR = "UPDATES_ENABLED";
    
    /**
     * Are we updating the clocks on all the pages?
     */
    protected transient boolean active = false;

    /**
     * The last time string
     */
    protected String timeString = "";