diff options
-rw-r--r-- | server/src/com/vaadin/Application.java | 57 | ||||
-rw-r--r-- | server/src/com/vaadin/ui/Root.java | 104 |
2 files changed, 152 insertions, 9 deletions
diff --git a/server/src/com/vaadin/Application.java b/server/src/com/vaadin/Application.java index b120c8455a..62052cd3b7 100644 --- a/server/src/com/vaadin/Application.java +++ b/server/src/com/vaadin/Application.java @@ -31,6 +31,7 @@ import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; +import java.util.Iterator; import java.util.LinkedList; import java.util.Locale; import java.util.Map; @@ -575,19 +576,19 @@ public class Application implements Terminal.ErrorListener, Serializable { /** * Ends the Application. - * * <p> * In effect this will cause the application stop returning any windows when - * asked. When the application is closed, its state is removed from the - * session and the browser window is redirected to the application logout - * url set with {@link #setLogoutURL(String)}. If the logout url has not - * been set, the browser window is reloaded and the application is - * restarted. - * </p> - * . + * asked. When the application is closed, close events are fired for its + * roots, its state is removed from the session, and the browser window is + * redirected to the application logout url set with + * {@link #setLogoutURL(String)}. If the logout url has not been set, the + * browser window is reloaded and the application is restarted. */ public void close() { applicationIsRunning = false; + for (Root root : getRoots()) { + root.fireCloseEvent(); + } } /** @@ -2443,4 +2444,44 @@ public class Application implements Terminal.ErrorListener, Serializable { public void modifyBootstrapResponse(BootstrapResponse response) { eventRouter.fireEvent(response); } + + /** + * Removes all those roots from the application whose last heartbeat + * occurred more than {@link #getHeartbeatTimeout()} seconds ago. Close + * events are fired for the removed roots. + * <p> + * Called by the framework at the end of every request. + * + * @see Root.CloseEvent + * @see Root.CloseListener + * @see #getHeartbeatTimeout() + * + * @since 7.0.0 + */ + public void closeInactiveRoots() { + long now = System.currentTimeMillis(); + for (Iterator<Root> i = roots.values().iterator(); i.hasNext();) { + Root root = i.next(); + if (now - root.getLastHeartbeat() > 1000 * getHeartbeatTimeout()) { + i.remove(); + retainOnRefreshRoots.values().remove(root.getRootId()); + root.fireCloseEvent(); + } + } + } + + /** + * Returns the number of seconds that must pass without a valid heartbeat or + * UIDL request being received from a root before that root is removed from + * the application. This is a lower bound; it might take longer to close an + * inactive root. + * + * @since 7.0.0 + * + * @return The heartbeat timeout in seconds. + */ + public int getHeartbeatTimeout() { + // Permit three missed heartbeats before closing the root + return (int) (configuration.getHeartbeatInterval() * (3.1)); + } } diff --git a/server/src/com/vaadin/ui/Root.java b/server/src/com/vaadin/ui/Root.java index 685296c55a..dd3f016fc9 100644 --- a/server/src/com/vaadin/ui/Root.java +++ b/server/src/com/vaadin/ui/Root.java @@ -16,11 +16,13 @@ package com.vaadin.ui; +import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EventListener; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; @@ -46,7 +48,7 @@ import com.vaadin.terminal.Resource; import com.vaadin.terminal.Vaadin6Component; import com.vaadin.terminal.WrappedRequest; import com.vaadin.terminal.WrappedRequest.BrowserDetails; -import com.vaadin.ui.Window.CloseListener; +import com.vaadin.tools.ReflectTools; /** * The topmost component in any component hierarchy. There is one root for every @@ -389,6 +391,42 @@ public abstract class Root extends AbstractComponentContainer implements } /** + * Event fired when a Root is removed from the application. + */ + public static class CloseEvent extends Event { + + private static final String CLOSE_EVENT_IDENTIFIER = "rootClose"; + + public CloseEvent(Root source) { + super(source); + } + + public Root getRoot() { + return (Root) getSource(); + } + } + + /** + * Interface for listening {@link Root.CloseEvent root close events}. + * + */ + public interface CloseListener extends EventListener { + + public static final Method closeMethod = ReflectTools.findMethod( + CloseListener.class, "click", CloseEvent.class); + + /** + * Called when a CloseListener is notified of a CloseEvent. + * {@link Root#getCurrent()} returns <code>event.getRoot()</code> within + * this method. + * + * @param event + * The close event that was fired. + */ + public void close(CloseEvent event); + } + + /** * The application to which this root belongs */ private Application application; @@ -437,6 +475,13 @@ public abstract class Root extends AbstractComponentContainer implements }; /** + * Timestamp keeping track of the last heartbeat of this Root. Updated to + * the current time whenever the application receives a heartbeat or UIDL + * request from the client for this Root. + */ + private long lastHeartbeat = System.currentTimeMillis(); + + /** * Creates a new empty root without a caption. This root will have a * {@link VerticalLayout} with margins enabled as its content. */ @@ -564,6 +609,16 @@ public abstract class Root extends AbstractComponentContainer implements fireEvent(new ClickEvent(this, mouseDetails)); } + /** + * For internal use only. + */ + public void fireCloseEvent() { + Root current = Root.getCurrent(); + Root.setCurrent(this); + fireEvent(new CloseEvent(this)); + Root.setCurrent(current); + } + @Override @SuppressWarnings("unchecked") public void changeVariables(Object source, Map<String, Object> variables) { @@ -1055,6 +1110,30 @@ public abstract class Root extends AbstractComponentContainer implements listener); } + /** + * Adds a close listener to the Root. The listener is called when the Root + * is removed from the application. + * + * @param listener + * The listener to add. + */ + public void addListener(CloseListener listener) { + addListener(CloseEvent.CLOSE_EVENT_IDENTIFIER, CloseEvent.class, + listener, CloseListener.closeMethod); + } + + /** + * Removes a close listener from the Root if it has previously been added + * with {@link #addListener(ClickListener)}. Otherwise, has no effect. + * + * @param listener + * The listener to remove. + */ + public void removeListener(CloseListener listener) { + removeListener(CloseEvent.CLOSE_EVENT_IDENTIFIER, CloseEvent.class, + listener); + } + @Override public boolean isConnectorEnabled() { // TODO How can a Root be invisible? What does it mean? @@ -1238,4 +1317,27 @@ public abstract class Root extends AbstractComponentContainer implements getPage().showNotification(notification); } + /** + * Returns the timestamp (millisecond since the epoch) of the last received + * heartbeat for this Root. + * + * @see #heartbeat() + * @see Application#closeInactiveRoots() + * + * @return The time + */ + public long getLastHeartbeat() { + return lastHeartbeat; + } + + /** + * Updates the heartbeat timestamp of this Root to the current time. Called + * by the framework whenever the application receives a valid heartbeat or + * UIDL request for this Root. + * + * @see java.lang.System#currentTimeMillis() + */ + public void heartbeat() { + this.lastHeartbeat = System.currentTimeMillis(); + } } |