summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java12
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java75
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/UI/UIConnector.java1
-rw-r--r--server/src/com/vaadin/Application.java111
-rw-r--r--server/src/com/vaadin/service/ApplicationContext.java6
-rw-r--r--server/src/com/vaadin/terminal/DeploymentConfiguration.java32
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java15
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java15
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java37
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java53
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java3
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/Constants.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java5
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java5
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java4
-rw-r--r--server/src/com/vaadin/ui/UI.java122
-rw-r--r--shared/src/com/vaadin/shared/ApplicationConstants.java2
17 files changed, 492 insertions, 15 deletions
diff --git a/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java b/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
index 6621de7f95..2eccd9bb8c 100644
--- a/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
+++ b/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
@@ -208,6 +208,7 @@ public class ApplicationConfiguration implements EntryPoint {
private ErrorMessage communicationError;
private ErrorMessage authorizationError;
private boolean useDebugIdInDom = true;
+ private int heartbeatInterval;
private HashMap<Integer, String> unknownComponents;
@@ -293,6 +294,14 @@ public class ApplicationConfiguration implements EntryPoint {
return uiId;
}
+ /**
+ * @return The interval in seconds between heartbeat requests, or a
+ * non-positive number if heartbeat is disabled.
+ */
+ public int getHeartbeatInterval() {
+ return heartbeatInterval;
+ }
+
public JavaScriptObject getVersionInfoJSObject() {
return getJsoConfiguration(id).getVersionInfoJSObject();
}
@@ -324,6 +333,9 @@ public class ApplicationConfiguration implements EntryPoint {
// null -> false
standalone = jsoConfiguration.getConfigBoolean("standalone") == Boolean.TRUE;
+ heartbeatInterval = jsoConfiguration
+ .getConfigInteger("heartbeatInterval");
+
communicationError = jsoConfiguration.getConfigError("comErrMsg");
authorizationError = jsoConfiguration.getConfigError("authErrMsg");
diff --git a/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
index fc063a1908..450972ddc6 100644
--- a/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
+++ b/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
@@ -246,6 +246,8 @@ public class ApplicationConnection {
uIConnector.init(cnf.getRootPanelId(), this);
showLoadingIndicator();
+
+ scheduleHeartbeat();
}
/**
@@ -2616,4 +2618,77 @@ public class ApplicationConnection {
LayoutManager getLayoutManager() {
return layoutManager;
}
+
+ /**
+ * Schedules a heartbeat request to occur after the configured heartbeat
+ * interval elapses if the interval is a positive number. Otherwise, does
+ * nothing.
+ *
+ * @see #sendHeartbeat()
+ * @see ApplicationConfiguration#getHeartbeatInterval()
+ */
+ protected void scheduleHeartbeat() {
+ final int interval = getConfiguration().getHeartbeatInterval();
+ if (interval > 0) {
+ VConsole.log("Scheduling heartbeat in " + interval + " seconds");
+ new Timer() {
+ @Override
+ public void run() {
+ sendHeartbeat();
+ }
+ }.schedule(interval * 1000);
+ }
+ }
+
+ /**
+ * Sends a heartbeat request to the server.
+ * <p>
+ * Heartbeat requests are used to inform the server that the client-side is
+ * still alive. If the client page is closed or the connection lost, the
+ * server will eventually close the inactive Root.
+ * <p>
+ * <b>TODO</b>: Improved error handling, like in doUidlRequest().
+ *
+ * @see #scheduleHeartbeat()
+ */
+ protected void sendHeartbeat() {
+ final String uri = addGetParameters(
+ translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX
+ + ApplicationConstants.HEARTBEAT_REQUEST_PATH),
+ UIConstants.UI_ID_PARAMETER + "="
+ + getConfiguration().getUIId());
+
+ final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
+
+ final RequestCallback callback = new RequestCallback() {
+
+ @Override
+ public void onResponseReceived(Request request, Response response) {
+ int status = response.getStatusCode();
+ if (status == Response.SC_OK) {
+ // TODO Permit retry in some error situations
+ VConsole.log("Heartbeat response OK");
+ scheduleHeartbeat();
+ } else {
+ VConsole.error("Heartbeat request failed with status code "
+ + status);
+ }
+ }
+
+ @Override
+ public void onError(Request request, Throwable exception) {
+ VConsole.error("Heartbeat request resulted in exception");
+ VConsole.error(exception);
+ }
+ };
+
+ rb.setCallback(callback);
+
+ try {
+ VConsole.log("Sending heartbeat request...");
+ rb.send();
+ } catch (RequestException re) {
+ callback.onError(null, re);
+ }
+ }
}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/UI/UIConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/UI/UIConnector.java
index f260481c3c..4e1bed1aa8 100644
--- a/client/src/com/vaadin/terminal/gwt/client/ui/UI/UIConnector.java
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/UI/UIConnector.java
@@ -453,5 +453,4 @@ public class UIConnector extends AbstractComponentContainerConnector
}
});
}
-
}
diff --git a/server/src/com/vaadin/Application.java b/server/src/com/vaadin/Application.java
index 96d38e31cf..bdad94355d 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.List;
import java.util.Locale;
@@ -579,19 +580,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
+ * UIs, 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 (UI ui : getUIs()) {
+ ui.fireCloseEvent();
+ }
}
/**
@@ -2415,4 +2416,98 @@ public class Application implements Terminal.ErrorListener, Serializable {
public void modifyBootstrapResponse(BootstrapResponse response) {
eventRouter.fireEvent(response);
}
+
+ /**
+ * Removes all those UIs from the application for which {@link #isUIAlive}
+ * returns false. Close events are fired for the removed UIs.
+ * <p>
+ * Called by the framework at the end of every request.
+ *
+ * @see UI.CloseEvent
+ * @see UI.CloseListener
+ * @see #isUIAlive(UI)
+ *
+ * @since 7.0.0
+ */
+ public void closeInactiveUIs() {
+ for (Iterator<UI> i = uIs.values().iterator(); i.hasNext();) {
+ UI ui = i.next();
+ if (!isUIAlive(ui)) {
+ i.remove();
+ retainOnRefreshUIs.values().remove(ui.getUIId());
+ ui.fireCloseEvent();
+ getLogger().info(
+ "Closed UI #" + ui.getUIId() + " due to inactivity");
+ }
+ }
+ }
+
+ /**
+ * Returns the number of seconds that must pass without a valid heartbeat or
+ * UIDL request being received from a UI before that UI is removed from the
+ * application. This is a lower bound; it might take longer to close an
+ * inactive UI. Returns a negative number if heartbeat is disabled and
+ * timeout never occurs.
+ *
+ * @see #getUidlRequestTimeout()
+ * @see #closeInactiveUIs()
+ * @see DeploymentConfiguration#getHeartbeatInterval()
+ *
+ * @since 7.0.0
+ *
+ * @return The heartbeat timeout in seconds or a negative number if timeout
+ * never occurs.
+ */
+ protected int getHeartbeatTimeout() {
+ // Permit three missed heartbeats before closing the UI
+ return (int) (configuration.getHeartbeatInterval() * (3.1));
+ }
+
+ /**
+ * Returns the number of seconds that must pass without a valid UIDL request
+ * being received from a UI before the UI is removed from the application,
+ * even though heartbeat requests are received. This is a lower bound; it
+ * might take longer to close an inactive UI. Returns a negative number if
+ * <p>
+ * This timeout only has effect if cleanup of inactive UIs is enabled;
+ * otherwise heartbeat requests are enough to extend UI lifetime
+ * indefinitely.
+ *
+ * @see DeploymentConfiguration#isIdleUICleanupEnabled()
+ * @see #getHeartbeatTimeout()
+ * @see #closeInactiveUIs()
+ *
+ * @since 7.0.0
+ *
+ * @return The UIDL request timeout in seconds, or a negative number if
+ * timeout never occurs.
+ */
+ protected int getUidlRequestTimeout() {
+ return configuration.isIdleUICleanupEnabled() ? getContext()
+ .getMaxInactiveInterval() : -1;
+ }
+
+ /**
+ * Returns whether the given UI is alive (the client-side actively
+ * communicates with the server) or whether it can be removed from the
+ * application and eventually collected.
+ *
+ * @since 7.0.0
+ *
+ * @param ui
+ * The UI whose status to check
+ * @return true if the UI is alive, false if it could be removed.
+ */
+ protected boolean isUIAlive(UI ui) {
+ long now = System.currentTimeMillis();
+ if (getHeartbeatTimeout() >= 0
+ && now - ui.getLastHeartbeatTime() > 1000 * getHeartbeatTimeout()) {
+ return false;
+ }
+ if (getUidlRequestTimeout() >= 0
+ && now - ui.getLastUidlRequestTime() > 1000 * getUidlRequestTimeout()) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/server/src/com/vaadin/service/ApplicationContext.java b/server/src/com/vaadin/service/ApplicationContext.java
index c6116d6e73..55495dcd5c 100644
--- a/server/src/com/vaadin/service/ApplicationContext.java
+++ b/server/src/com/vaadin/service/ApplicationContext.java
@@ -80,6 +80,12 @@ public interface ApplicationContext extends Serializable {
public void removeTransactionListener(TransactionListener listener);
/**
+ * Returns the time between requests, in seconds, before this context is
+ * invalidated. A negative time indicates the context should never timeout.
+ */
+ public int getMaxInactiveInterval();
+
+ /**
* Generate a URL that can be used as the relative location of e.g. an
* {@link ApplicationResource}.
*
diff --git a/server/src/com/vaadin/terminal/DeploymentConfiguration.java b/server/src/com/vaadin/terminal/DeploymentConfiguration.java
index 8da088969d..0cfbdb7544 100644
--- a/server/src/com/vaadin/terminal/DeploymentConfiguration.java
+++ b/server/src/com/vaadin/terminal/DeploymentConfiguration.java
@@ -23,6 +23,7 @@ import java.util.Properties;
import javax.portlet.PortletContext;
import javax.servlet.ServletContext;
+import com.vaadin.service.ApplicationContext;
import com.vaadin.terminal.gwt.server.AddonContext;
import com.vaadin.terminal.gwt.server.AddonContextListener;
@@ -136,6 +137,8 @@ public interface DeploymentConfiguration extends Serializable {
/**
* Returns whether Vaadin is in production mode.
*
+ * @since 7.0.0
+ *
* @return true if in production mode, false otherwise.
*/
public boolean isProductionMode();
@@ -143,6 +146,8 @@ public interface DeploymentConfiguration extends Serializable {
/**
* Returns whether cross-site request forgery protection is enabled.
*
+ * @since 7.0.0
+ *
* @return true if XSRF protection is enabled, false otherwise.
*/
public boolean isXsrfProtectionEnabled();
@@ -150,7 +155,34 @@ public interface DeploymentConfiguration extends Serializable {
/**
* Returns the time resources can be cached in the browsers, in seconds.
*
+ * @since 7.0.0
+ *
* @return The resource cache time.
*/
public int getResourceCacheTime();
+
+ /**
+ * Returns the number of seconds between heartbeat requests of a UI, or a
+ * non-positive number if heartbeat is disabled.
+ *
+ * @since 7.0.0
+ *
+ * @return The time between heartbeats.
+ */
+ public int getHeartbeatInterval();
+
+ /**
+ * Returns whether UIs that have no other activity than heartbeat requests
+ * should be closed after they have been idle the maximum inactivity time
+ * enforced by the session.
+ *
+ * @see ApplicationContext#getMaxInactiveInterval()
+ *
+ * @since 7.0.0
+ *
+ * @return True if UIs receiving only heartbeat requests are eventually
+ * closed; false if heartbeat requests extend UI lifetime
+ * indefinitely.
+ */
+ public boolean isIdleUICleanupEnabled();
}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
index a9e6028090..345f462239 100644
--- a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
@@ -340,7 +340,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
}
protected enum RequestType {
- FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE;
+ FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE, HEARTBEAT;
}
protected RequestType getRequestType(WrappedPortletRequest wrappedRequest) {
@@ -361,6 +361,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
} else if (ServletPortletHelper
.isApplicationResourceRequest(wrappedRequest)) {
return RequestType.APPLICATION_RESOURCE;
+ } else if (ServletPortletHelper.isHeartbeatRequest(wrappedRequest)) {
+ return RequestType.HEARTBEAT;
} else if (isDummyRequest(resourceRequest)) {
return RequestType.DUMMY;
} else {
@@ -431,6 +433,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
Application application = null;
boolean transactionStarted = false;
boolean requestStarted = false;
+ boolean applicationRunning = false;
try {
// TODO What about PARAM_UNLOADBURST & redirectToApplication??
@@ -459,6 +462,10 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
applicationManager.serveConnectorResource(wrappedRequest,
wrappedResponse);
return;
+ } else if (requestType == RequestType.HEARTBEAT) {
+ applicationManager.handleHeartbeatRequest(wrappedRequest,
+ wrappedResponse, application);
+ return;
}
/* Update browser information from request */
@@ -477,6 +484,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
/* Start the newly created application */
startApplication(request, application, applicationContext);
+ applicationRunning = true;
/*
* Transaction starts. Call transaction listeners. Transaction
@@ -583,6 +591,11 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
handleServiceException(wrappedRequest, wrappedResponse,
application, e);
} finally {
+
+ if (applicationRunning) {
+ application.closeInactiveUIs();
+ }
+
// Notifies transaction end
try {
if (transactionStarted) {
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
index 6cf9b76b0d..13fd869166 100644
--- a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
@@ -248,6 +248,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
Application application = null;
boolean transactionStarted = false;
boolean requestStarted = false;
+ boolean applicationRunning = false;
try {
// If a duplicate "close application" URL is received for an
@@ -287,6 +288,10 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
if (requestType == RequestType.CONNECTOR_RESOURCE) {
applicationManager.serveConnectorResource(request, response);
return;
+ } else if (requestType == RequestType.HEARTBEAT) {
+ applicationManager.handleHeartbeatRequest(request, response,
+ application);
+ return;
}
/* Update browser information from the request */
@@ -304,6 +309,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
// Start the application if it's newly created
startApplication(request, application, webApplicationContext);
+ applicationRunning = true;
/*
* Transaction starts. Call transaction listeners. Transaction end
@@ -354,6 +360,11 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
} catch (final Throwable e) {
handleServiceException(request, response, application, e);
} finally {
+
+ if (applicationRunning) {
+ application.closeInactiveUIs();
+ }
+
// Notifies transaction end
try {
if (transactionStarted) {
@@ -1121,7 +1132,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
}
protected enum RequestType {
- FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE;
+ FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE, HEARTBEAT;
}
protected RequestType getRequestType(WrappedHttpServletRequest request) {
@@ -1137,6 +1148,8 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
return RequestType.STATIC_FILE;
} else if (ServletPortletHelper.isApplicationResourceRequest(request)) {
return RequestType.APPLICATION_RESOURCE;
+ } else if (ServletPortletHelper.isHeartbeatRequest(request)) {
+ return RequestType.HEARTBEAT;
}
return RequestType.OTHER;
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
index 87eadd5df7..a0ecd01b89 100644
--- a/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
@@ -90,6 +90,7 @@ import com.vaadin.terminal.Vaadin6Component;
import com.vaadin.terminal.VariableOwner;
import com.vaadin.terminal.WrappedRequest;
import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext;
import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout;
import com.vaadin.terminal.gwt.server.RpcManager.RpcInvocationException;
@@ -105,7 +106,7 @@ import com.vaadin.ui.Window;
* This is a common base class for the server-side implementations of the
* communication system between the client code (compiled with GWT into
* JavaScript) and the server side components. Its client side counterpart is
- * {@link ApplicationConstants}.
+ * {@link ApplicationConnection}.
*
* TODO Document better!
*/
@@ -580,6 +581,9 @@ public abstract class AbstractCommunicationManager implements Serializable {
return;
}
+ // Keep the UI alive
+ uI.setLastUidlRequestTime(System.currentTimeMillis());
+
// Change all variables based on request parameters
if (!handleVariables(request, response, callback, application, uI)) {
@@ -2654,6 +2658,37 @@ public abstract class AbstractCommunicationManager implements Serializable {
}
+ /**
+ * Handles a heartbeat request. Heartbeat requests are periodically sent by
+ * the client-side to inform the server that the UI sending the heartbeat is
+ * still alive (the browser window is open, the connection is up) even when
+ * there are no UIDL requests for a prolonged period of time. UIs that do
+ * not receive either heartbeat or UIDL requests are eventually removed from
+ * the application and garbage collected.
+ *
+ * @param request
+ * @param response
+ * @param application
+ * @throws IOException
+ */
+ public void handleHeartbeatRequest(WrappedRequest request,
+ WrappedResponse response, Application application)
+ throws IOException {
+ UI ui = null;
+ try {
+ int uiId = Integer.parseInt(request
+ .getParameter(UIConstants.UI_ID_PARAMETER));
+ ui = application.getUIById(uiId);
+ } catch (NumberFormatException nfe) {
+ // null-check below handles this as well
+ }
+ if (ui != null) {
+ ui.setLastHeartbeatTime(System.currentTimeMillis());
+ } else {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "UI not found");
+ }
+ }
+
public StreamVariable getStreamVariable(String connectorId,
String variableName) {
Map<String, StreamVariable> map = pidToNameToStreamVariable
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java
index ad5acad5e9..4052f5a400 100644
--- a/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java
@@ -33,6 +33,8 @@ public abstract class AbstractDeploymentConfiguration implements
private boolean productionMode;
private boolean xsrfProtectionEnabled;
private int resourceCacheTime;
+ private int heartbeatInterval;
+ private boolean idleRootCleanupEnabled;
public AbstractDeploymentConfiguration(Class<?> systemPropertyBaseClass,
Properties applicationProperties) {
@@ -42,12 +44,13 @@ public abstract class AbstractDeploymentConfiguration implements
checkProductionMode();
checkXsrfProtection();
checkResourceCacheTime();
+ checkHeartbeatInterval();
+ checkIdleUICleanup();
}
@Override
public String getApplicationOrSystemProperty(String propertyName,
String defaultValue) {
-
String val = null;
// Try application properties
@@ -163,22 +166,52 @@ public abstract class AbstractDeploymentConfiguration implements
return addonContext;
}
+ /**
+ * {@inheritDoc}
+ *
+ * The default is false.
+ */
@Override
public boolean isProductionMode() {
return productionMode;
}
+ /**
+ * {@inheritDoc}
+ *
+ * The default is true.
+ */
@Override
public boolean isXsrfProtectionEnabled() {
return xsrfProtectionEnabled;
}
+ /**
+ * {@inheritDoc}
+ *
+ * The default interval is 3600 seconds (1 hour).
+ */
@Override
public int getResourceCacheTime() {
return resourceCacheTime;
}
/**
+ * {@inheritDoc}
+ *
+ * The default interval is 300 seconds (5 minutes).
+ */
+ @Override
+ public int getHeartbeatInterval() {
+ return heartbeatInterval;
+ }
+
+ @Override
+ public boolean isIdleUICleanupEnabled() {
+ return idleRootCleanupEnabled;
+ }
+
+ /**
* Log a warning if Vaadin is not running in production mode.
*/
private void checkProductionMode() {
@@ -218,6 +251,24 @@ public abstract class AbstractDeploymentConfiguration implements
}
}
+ private void checkHeartbeatInterval() {
+ try {
+ heartbeatInterval = Integer
+ .parseInt(getApplicationOrSystemProperty(
+ Constants.SERVLET_PARAMETER_HEARTBEAT_RATE, "300"));
+ } catch (NumberFormatException e) {
+ getLogger().warning(
+ Constants.WARNING_HEARTBEAT_INTERVAL_NOT_NUMERIC);
+ heartbeatInterval = 300;
+ }
+ }
+
+ private void checkIdleUICleanup() {
+ idleRootCleanupEnabled = getApplicationOrSystemProperty(
+ Constants.SERVLET_PARAMETER_CLOSE_IDLE_UIS, "false").equals(
+ "true");
+ }
+
private Logger getLogger() {
return Logger.getLogger(getClass().getName());
}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java
index d329159d95..02005e8d22 100644
--- a/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java
@@ -501,6 +501,9 @@ public abstract class BootstrapHandler implements RequestHandler {
defaults.put("standalone", true);
}
+ defaults.put("heartbeatInterval",
+ deploymentConfiguration.getHeartbeatInterval());
+
defaults.put("appUri", getAppUri(context));
return defaults;
diff --git a/server/src/com/vaadin/terminal/gwt/server/Constants.java b/server/src/com/vaadin/terminal/gwt/server/Constants.java
index 40386d6eb7..9640216488 100644
--- a/server/src/com/vaadin/terminal/gwt/server/Constants.java
+++ b/server/src/com/vaadin/terminal/gwt/server/Constants.java
@@ -41,6 +41,12 @@ public interface Constants {
+ "in web.xml. The default of 1h will be used.\n"
+ "===========================================================";
+ static final String WARNING_HEARTBEAT_INTERVAL_NOT_NUMERIC = "\n"
+ + "===========================================================\n"
+ + "WARNING: heartbeatInterval has been set to a non integer value "
+ + "in web.xml. The default of 5min will be used.\n"
+ + "===========================================================";
+
static final String WIDGETSET_MISMATCH_INFO = "\n"
+ "=================================================================\n"
+ "The widgetset in use does not seem to be built for the Vaadin\n"
@@ -58,6 +64,8 @@ public interface Constants {
static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode";
static final String SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION = "disable-xsrf-protection";
static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime";
+ static final String SERVLET_PARAMETER_HEARTBEAT_RATE = "heartbeatRate";
+ static final String SERVLET_PARAMETER_CLOSE_IDLE_UIS = "closeIdleUIs";
// Configurable parameter names
static final String PARAMETER_VAADIN_RESOURCES = "Resources";
@@ -88,5 +96,4 @@ public interface Constants {
static final String PORTAL_PARAMETER_VAADIN_WIDGETSET = "vaadin.widgetset";
static final String PORTAL_PARAMETER_VAADIN_RESOURCE_PATH = "vaadin.resources.path";
static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme";
-
}
diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java
index 8538d42604..3e0f8d6b99 100644
--- a/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java
+++ b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java
@@ -404,6 +404,11 @@ public class PortletApplicationContext2 extends AbstractWebApplicationContext {
}
}
+ @Override
+ public int getMaxInactiveInterval() {
+ return getPortletSession().getMaxInactiveInterval();
+ }
+
private Logger getLogger() {
return Logger.getLogger(PortletApplicationContext2.class.getName());
}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java
index 403cffb47c..1d35785a57 100644
--- a/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java
+++ b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java
@@ -129,4 +129,9 @@ class ServletPortletHelper implements Serializable {
return hasPathPrefix(request, ApplicationConstants.APP_REQUEST_PATH);
}
+ public static boolean isHeartbeatRequest(WrappedRequest request) {
+ return hasPathPrefix(request,
+ ApplicationConstants.HEARTBEAT_REQUEST_PATH);
+ }
+
}
diff --git a/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
index 4cc0ed188d..bfcc0c1038 100644
--- a/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
+++ b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
@@ -187,4 +187,8 @@ public class WebApplicationContext extends AbstractWebApplicationContext {
return mgr;
}
+ @Override
+ public int getMaxInactiveInterval() {
+ return getHttpSession().getMaxInactiveInterval();
+ }
}
diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java
index aede1af54b..17a028bcdf 100644
--- a/server/src/com/vaadin/ui/UI.java
+++ b/server/src/com/vaadin/ui/UI.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;
@@ -48,7 +50,7 @@ import com.vaadin.terminal.Vaadin6Component;
import com.vaadin.terminal.WrappedRequest;
import com.vaadin.terminal.WrappedRequest.BrowserDetails;
import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
-import com.vaadin.ui.Window.CloseListener;
+import com.vaadin.tools.ReflectTools;
/**
* The topmost component in any component hierarchy. There is one UI for every
@@ -390,6 +392,42 @@ public abstract class UI extends AbstractComponentContainer implements
}
/**
+ * Event fired when a UI is removed from the application.
+ */
+ public static class CloseEvent extends Event {
+
+ private static final String CLOSE_EVENT_IDENTIFIER = "uiClose";
+
+ public CloseEvent(UI source) {
+ super(source);
+ }
+
+ public UI getUI() {
+ return (UI) getSource();
+ }
+ }
+
+ /**
+ * Interface for listening {@link UI.CloseEvent UI 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 UI#getCurrent()} returns <code>event.getUI()</code> within
+ * this method.
+ *
+ * @param event
+ * The close event that was fired.
+ */
+ public void close(CloseEvent event);
+ }
+
+ /**
* The application to which this UI belongs
*/
private Application application;
@@ -445,6 +483,15 @@ public abstract class UI extends AbstractComponentContainer implements
};
/**
+ * Timestamp keeping track of the last heartbeat of this UI. Updated to the
+ * current time whenever the application receives a heartbeat or UIDL
+ * request from the client for this UI.
+ */
+ private long lastHeartbeat = System.currentTimeMillis();
+
+ private long lastUidlRequest = System.currentTimeMillis();
+
+ /**
* Creates a new empty UI without a caption. This UI will have a
* {@link VerticalLayout} with margins enabled as its content.
*/
@@ -572,6 +619,16 @@ public abstract class UI extends AbstractComponentContainer implements
fireEvent(new ClickEvent(this, mouseDetails));
}
+ /**
+ * For internal use only.
+ */
+ public void fireCloseEvent() {
+ UI current = UI.getCurrent();
+ UI.setCurrent(this);
+ fireEvent(new CloseEvent(this));
+ UI.setCurrent(current);
+ }
+
@Override
@SuppressWarnings("unchecked")
public void changeVariables(Object source, Map<String, Object> variables) {
@@ -1054,6 +1111,30 @@ public abstract class UI extends AbstractComponentContainer implements
listener);
}
+ /**
+ * Adds a close listener to the UI. The listener is called when the UI 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 UI 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 UI be invisible? What does it mean?
@@ -1238,4 +1319,43 @@ public abstract class UI extends AbstractComponentContainer implements
getPage().showNotification(notification);
}
+ /**
+ * Returns the timestamp (milliseconds since the epoch) of the last received
+ * heartbeat for this UI.
+ *
+ * @see #heartbeat()
+ * @see Application#closeInactiveUIs()
+ *
+ * @return The time the last heartbeat request occurred.
+ */
+ public long getLastHeartbeatTime() {
+ return lastHeartbeat;
+ }
+
+ /**
+ * Returns the timestamp (milliseconds since the epoch) of the last received
+ * UIDL request for this UI.
+ *
+ * @return
+ */
+ public long getLastUidlRequestTime() {
+ return lastUidlRequest;
+ }
+
+ /**
+ * Sets the last heartbeat request timestamp for this UI. Called by the
+ * framework whenever the application receives a valid heartbeat request for
+ * this UI.
+ */
+ public void setLastHeartbeatTime(long lastHeartbeat) {
+ this.lastHeartbeat = lastHeartbeat;
+ }
+
+ /**
+ * Sets the last UIDL request timestamp for this UI. Called by the framework
+ * whenever the application receives a valid UIDL request for this UI.
+ */
+ public void setLastUidlRequestTime(long lastUidlRequest) {
+ this.lastUidlRequest = lastUidlRequest;
+ }
}
diff --git a/shared/src/com/vaadin/shared/ApplicationConstants.java b/shared/src/com/vaadin/shared/ApplicationConstants.java
index ba35ddea50..0bacd2d256 100644
--- a/shared/src/com/vaadin/shared/ApplicationConstants.java
+++ b/shared/src/com/vaadin/shared/ApplicationConstants.java
@@ -23,6 +23,8 @@ public class ApplicationConstants {
public static final String UIDL_REQUEST_PATH = "UIDL/";
+ public static final String HEARTBEAT_REQUEST_PATH = "HEARTBEAT/";
+
public static final String CONNECTOR_RESOURCE_PREFIX = APP_REQUEST_PATH
+ "CONNECTOR";