aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/src/com/vaadin/Application.java57
-rw-r--r--server/src/com/vaadin/ui/Root.java104
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();
+ }
}