diff options
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"; |