diff options
author | Artur Signell <artur@vaadin.com> | 2015-09-07 10:49:38 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2015-09-07 10:49:52 +0300 |
commit | e54434a93266757616be1362fa5475fc5431c48f (patch) | |
tree | 94b306109f32ca17e18b1fa2cf7cfe9838bb1c98 | |
parent | ebceef4d44bcd61605fa92fcf7be8d3678537599 (diff) | |
parent | c8f92cd539d08962ed324ff2e7322aa8dfac2124 (diff) | |
download | vaadin-framework-e54434a93266757616be1362fa5475fc5431c48f.tar.gz vaadin-framework-e54434a93266757616be1362fa5475fc5431c48f.zip |
Merge remote-tracking branch 'origin/reconnect-dialog' (#11733)
Change-Id: Id148ac8a5b86a76ed966f96ea7732c35ad0d056d
92 files changed, 6215 insertions, 2634 deletions
diff --git a/WebContent/VAADIN/themes/base/base.scss b/WebContent/VAADIN/themes/base/base.scss index d40ac1a7bf..56b2e311c6 100644 --- a/WebContent/VAADIN/themes/base/base.scss +++ b/WebContent/VAADIN/themes/base/base.scss @@ -17,6 +17,7 @@ $v-line-height: $line-height !default; @import "caption/caption.scss"; @import "colorpicker/colorpicker.scss"; @import "common/common.scss"; +@import "common/reconnect-dialog.scss"; @import "csslayout/csslayout.scss"; @import "customcomponent/customcomponent.scss"; @import "customlayout/customlayout.scss"; @@ -89,6 +90,7 @@ $v-line-height: $line-height !default; // here for now to preserve old semantics @include base-common; + @include base-reconnect-dialog; @include base-layout; @include base-csslayout; diff --git a/WebContent/VAADIN/themes/base/common/img/reconnect-spinner.gif b/WebContent/VAADIN/themes/base/common/img/reconnect-spinner.gif Binary files differnew file mode 100644 index 0000000000..bc1a496d11 --- /dev/null +++ b/WebContent/VAADIN/themes/base/common/img/reconnect-spinner.gif diff --git a/WebContent/VAADIN/themes/base/common/reconnect-dialog.scss b/WebContent/VAADIN/themes/base/common/reconnect-dialog.scss new file mode 100644 index 0000000000..cde587cfe9 --- /dev/null +++ b/WebContent/VAADIN/themes/base/common/reconnect-dialog.scss @@ -0,0 +1,32 @@ +@mixin base-reconnect-dialog { + .v-reconnect-dialog { + color: white; + top: 12px; + right: 12px; + max-width: 100%; + border-radius: 0; + @include box-shadow(0 0 20px 0 rgba(0,0,0,0.25)); + padding: 10px; + + background-color: #444; + text-align: center; + + .text { + display: inline-block; + padding-left: 10px; + } + + .spinner { + background-image: url(img/reconnect-spinner.gif); + width: 31px; + height: 31px; + display: inline-block; + visibility: hidden; + vertical-align: middle; + } + + &.active .spinner { + visibility: visible; + } + } +}
\ No newline at end of file diff --git a/WebContent/VAADIN/themes/valo/shared/_global.scss b/WebContent/VAADIN/themes/valo/shared/_global.scss index b4e8564119..39b5a4e7d9 100644 --- a/WebContent/VAADIN/themes/valo/shared/_global.scss +++ b/WebContent/VAADIN/themes/valo/shared/_global.scss @@ -2,6 +2,7 @@ @import "contextmenu"; @import "overlay"; @import "tooltip"; +@import "reconnect-dialog"; /* @@ -374,6 +375,7 @@ $valo-shared-pathPrefix: null; @include valo-contextmenu; + @include valo-reconnect-dialog; } diff --git a/WebContent/VAADIN/themes/valo/shared/_reconnect-dialog.scss b/WebContent/VAADIN/themes/valo/shared/_reconnect-dialog.scss new file mode 100644 index 0000000000..e648795b5a --- /dev/null +++ b/WebContent/VAADIN/themes/valo/shared/_reconnect-dialog.scss @@ -0,0 +1,31 @@ +@mixin valo-reconnect-dialog { + .v-reconnect-dialog { + color: white; + top: $v-layout-spacing-vertical; + right: $v-layout-spacing-horizontal; + max-width: 100%; + border-radius: 0; + @include box-shadow(0 0 20px 0 rgba(0,0,0,0.25)); + padding: round($v-unit-size/3) round($v-unit-size/2.5); + + background-color: #444; + background-color: rgba(#444, .9); + line-height: round($v-font-size * 1.4); + + text-align: center; + .text { + display: inline-block; + padding-left: 10px; + } + + .spinner { + @include valo-spinner; + display: none; + vertical-align: middle; + } + + &.active .spinner { + display: inline-block; + } + } +}
\ No newline at end of file diff --git a/WebContent/WEB-INF/web.xml b/WebContent/WEB-INF/web.xml index a9d3db30b3..1879175109 100644 --- a/WebContent/WEB-INF/web.xml +++ b/WebContent/WEB-INF/web.xml @@ -96,6 +96,19 @@ </init-param> <async-supported>true</async-supported> </servlet> + <servlet> + <servlet-name>CommErrorEmulator</servlet-name> + <servlet-class>com.vaadin.tests.application.CommErrorEmulatorServlet</servlet-class> + <init-param> + <param-name>heartbeatInterval</param-name> + <param-value>10</param-value> + </init-param> + <init-param> + <param-name>ui</param-name> + <param-value>com.vaadin.tests.application.CommErrorEmulatorUI</param-value> + </init-param> + <async-supported>true</async-supported> + </servlet> <servlet> <!-- This servlet is a separate instance for the sole purpose of @@ -185,6 +198,11 @@ </servlet-mapping> <servlet-mapping> + <servlet-name>CommErrorEmulator</servlet-name> + <url-pattern>/commerror/*</url-pattern> + </servlet-mapping> + + <servlet-mapping> <servlet-name>VaadinStaticFiles</servlet-name> <url-pattern>/VAADIN/*</url-pattern> </servlet-mapping> diff --git a/client/src/com/vaadin/DefaultWidgetSet.gwt.xml b/client/src/com/vaadin/DefaultWidgetSet.gwt.xml index 01cbdc3222..03d0950126 100755 --- a/client/src/com/vaadin/DefaultWidgetSet.gwt.xml +++ b/client/src/com/vaadin/DefaultWidgetSet.gwt.xml @@ -26,6 +26,10 @@ class="com.vaadin.client.metadata.ConnectorBundleLoader" /> </generate-with> + <replace-with + class="com.vaadin.client.communication.DefaultReconnectDialog"> + <when-type-is class="com.vaadin.client.communication.ReconnectDialog" /> + </replace-with> <!-- Since 7.2. Compile all permutations (browser support) into one Javascript file. Speeds up compilation and does not make the Javascript significantly diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java index 0db8dc297e..d20e0568cd 100644 --- a/client/src/com/vaadin/client/ApplicationConfiguration.java +++ b/client/src/com/vaadin/client/ApplicationConfiguration.java @@ -606,7 +606,7 @@ public class ApplicationConfiguration implements EntryPoint { * * @param c */ - static void runWhenDependenciesLoaded(Command c) { + public static void runWhenDependenciesLoaded(Command c) { if (dependenciesLoading == 0) { c.execute(); } else { diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index cb780e7273..6e20908274 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -16,17 +16,8 @@ package com.vaadin.client; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.aria.client.LiveValue; @@ -35,10 +26,8 @@ import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.event.shared.EventBus; import com.google.gwt.event.shared.EventHandler; @@ -46,69 +35,38 @@ import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.event.shared.HasHandlers; import com.google.gwt.event.shared.SimpleEventBus; -import com.google.gwt.http.client.Request; -import com.google.gwt.http.client.RequestBuilder; -import com.google.gwt.http.client.RequestCallback; -import com.google.gwt.http.client.RequestException; -import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; -import com.google.gwt.regexp.shared.MatchResult; -import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.Window.ClosingEvent; -import com.google.gwt.user.client.Window.ClosingHandler; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConfiguration.ErrorMessage; +import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; import com.vaadin.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.client.ResourceLoader.ResourceLoadListener; -import com.vaadin.client.communication.HasJavaScriptConnectorHelper; +import com.vaadin.client.communication.ConnectionStateHandler; +import com.vaadin.client.communication.DefaultConnectionStateHandler; import com.vaadin.client.communication.Heartbeat; -import com.vaadin.client.communication.JavaScriptMethodInvocation; -import com.vaadin.client.communication.JsonDecoder; -import com.vaadin.client.communication.JsonEncoder; -import com.vaadin.client.communication.PushConnection; +import com.vaadin.client.communication.MessageHandler; +import com.vaadin.client.communication.MessageSender; import com.vaadin.client.communication.RpcManager; -import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.communication.ServerRpcQueue; import com.vaadin.client.componentlocator.ComponentLocator; -import com.vaadin.client.extensions.AbstractExtensionConnector; import com.vaadin.client.metadata.ConnectorBundleLoader; -import com.vaadin.client.metadata.Method; -import com.vaadin.client.metadata.NoDataException; -import com.vaadin.client.metadata.Property; -import com.vaadin.client.metadata.Type; -import com.vaadin.client.metadata.TypeData; -import com.vaadin.client.metadata.TypeDataStore; import com.vaadin.client.ui.AbstractComponentConnector; -import com.vaadin.client.ui.AbstractConnector; import com.vaadin.client.ui.FontIcon; import com.vaadin.client.ui.Icon; import com.vaadin.client.ui.ImageIcon; import com.vaadin.client.ui.VContextMenu; import com.vaadin.client.ui.VNotification; import com.vaadin.client.ui.VOverlay; -import com.vaadin.client.ui.dd.VDragAndDropManager; import com.vaadin.client.ui.ui.UIConnector; -import com.vaadin.client.ui.window.WindowConnector; -import com.vaadin.shared.ApplicationConstants; -import com.vaadin.shared.JsonConstants; import com.vaadin.shared.VaadinUriResolver; import com.vaadin.shared.Version; import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; -import com.vaadin.shared.communication.MethodInvocation; -import com.vaadin.shared.communication.SharedState; -import com.vaadin.shared.ui.ui.UIConstants; -import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; import com.vaadin.shared.util.SharedUtil; -import elemental.json.Json; -import elemental.json.JsonArray; -import elemental.json.JsonObject; -import elemental.json.JsonValue; - /** * This is the client side communication "engine", managing client-server * communication with its server side counterpart @@ -125,26 +83,6 @@ import elemental.json.JsonValue; */ public class ApplicationConnection implements HasHandlers { - /** - * Helper used to return two values when updating the connector hierarchy. - */ - private static class ConnectorHierarchyUpdateResult { - /** - * Needed at a later point when the created events are fired - */ - private JsArrayObject<ConnectorHierarchyChangeEvent> events = JavaScriptObject - .createArray().cast(); - /** - * Needed to know where captions might need to get updated - */ - private FastStringSet parentChangedIds = FastStringSet.create(); - - /** - * Connectors for which the parent has been set to null - */ - private FastStringSet detachedConnectorIds = FastStringSet.create(); - } - @Deprecated public static final String MODIFIED_CLASSNAME = StyleConstants.MODIFIED; @@ -181,106 +119,31 @@ public class ApplicationConnection implements HasHandlers { */ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; - // will hold the CSRF token once received - private String csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE; - private final HashMap<String, String> resourcesMap = new HashMap<String, String>(); - /** - * The pending method invocations that will be send to the server by - * {@link #sendPendingCommand}. The key is defined differently based on - * whether the method invocation is enqueued with lastonly. With lastonly - * enabled, the method signature ( {@link MethodInvocation#getLastOnlyTag()} - * ) is used as the key to make enable removing a previously enqueued - * invocation. Without lastonly, an incremental id based on - * {@link #lastInvocationTag} is used to get unique values. - */ - private LinkedHashMap<String, MethodInvocation> pendingInvocations = new LinkedHashMap<String, MethodInvocation>(); - - private int lastInvocationTag = 0; - private WidgetSet widgetSet; private VContextMenu contextMenu = null; private final UIConnector uIConnector; - protected boolean applicationRunning = false; - - private boolean hasActiveRequest = false; - - /** - * Webkit will ignore outgoing requests while waiting for a response to a - * navigation event (indicated by a beforeunload event). When this happens, - * we should keep trying to send the request every now and then until there - * is a response or until it throws an exception saying that it is already - * being sent. - */ - private boolean webkitMaybeIgnoringRequests = false; - protected boolean cssLoaded = false; /** Parameters for this application connection loaded from the web-page */ private ApplicationConfiguration configuration; - /** List of pending variable change bursts that must be submitted in order */ - private final ArrayList<LinkedHashMap<String, MethodInvocation>> pendingBursts = new ArrayList<LinkedHashMap<String, MethodInvocation>>(); - - /** Timer for automatic refirect to SessionExpiredURL */ - private Timer redirectTimer; - - /** redirectTimer scheduling interval in seconds */ - private int sessionExpirationInterval; - - private Date requestStartTime; - private final LayoutManager layoutManager; private final RpcManager rpcManager; - private PushConnection push; - - /** - * If responseHandlingLocks contains any objects, response handling is - * suspended until the collection is empty or a timeout has occurred. - */ - private Set<Object> responseHandlingLocks = new HashSet<Object>(); - - /** - * Data structure holding information about pending UIDL messages. - */ - private class PendingUIDLMessage { - private Date start; - private String jsonText; - private ValueMap json; - - public PendingUIDLMessage(Date start, String jsonText, ValueMap json) { - this.start = start; - this.jsonText = jsonText; - this.json = json; - } - - public Date getStart() { - return start; - } - - public String getJsonText() { - return jsonText; - } + /** Event bus for communication events */ + private EventBus eventBus = GWT.create(SimpleEventBus.class); - public ValueMap getJson() { - return json; - } + public enum ApplicationState { + INITIALIZING, RUNNING, TERMINATED; } - /** Contains all UIDL messages received while response handling is suspended */ - private List<PendingUIDLMessage> pendingUIDLMessages = new ArrayList<PendingUIDLMessage>(); - - /** The max timeout that response handling may be suspended */ - private static final int MAX_SUSPENDED_TIMEOUT = 5000; - - /** Event bus for communication events */ - private EventBus eventBus = GWT.create(SimpleEventBus.class); + private ApplicationState applicationState = ApplicationState.INITIALIZING; /** * The communication handler methods are called at certain points during @@ -349,42 +212,6 @@ public class ApplicationConnection implements HasHandlers { } - /** - * Event triggered when a XHR request has finished with the status code of - * the response. - * - * Useful for handlers observing network failures like online/off-line - * monitors. - */ - public static class ConnectionStatusEvent extends - GwtEvent<ConnectionStatusEvent.ConnectionStatusHandler> { - private int status; - - public static interface ConnectionStatusHandler extends EventHandler { - public void onConnectionStatusChange(ConnectionStatusEvent event); - } - - public ConnectionStatusEvent(int status) { - this.status = status; - } - - public int getStatus() { - return status; - } - - public final static Type<ConnectionStatusHandler> TYPE = new Type<ConnectionStatusHandler>(); - - @Override - public Type<ConnectionStatusHandler> getAssociatedType() { - return TYPE; - } - - @Override - protected void dispatch(ConnectionStatusHandler handler) { - handler.onConnectionStatusChange(this); - } - } - public static class ResponseHandlingStartedEvent extends ApplicationConnectionEvent { @@ -524,8 +351,6 @@ public class ApplicationConnection implements HasHandlers { } } - private boolean updatingState = false; - public ApplicationConnection() { // Assuming UI data is eagerly loaded ConnectorBundleLoader.get().loadBundle( @@ -533,10 +358,13 @@ public class ApplicationConnection implements HasHandlers { uIConnector = GWT.create(UIConnector.class); rpcManager = GWT.create(RpcManager.class); layoutManager = GWT.create(LayoutManager.class); - layoutManager.setConnection(this); tooltip = GWT.create(VTooltip.class); loadingIndicator = GWT.create(VLoadingIndicator.class); - loadingIndicator.setConnection(this); + serverRpcQueue = GWT.create(ServerRpcQueue.class); + connectionStateHandler = GWT + .create(DefaultConnectionStateHandler.class); + messageHandler = GWT.create(MessageHandler.class); + messageSender = GWT.create(MessageSender.class); } public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { @@ -557,6 +385,12 @@ public class ApplicationConnection implements HasHandlers { this.widgetSet = widgetSet; configuration = cnf; + layoutManager.setConnection(this); + loadingIndicator.setConnection(this); + serverRpcQueue.setConnection(this); + messageHandler.setConnection(this); + messageSender.setConnection(this); + ComponentLocator componentLocator = new ComponentLocator(this); String appRootPanelName = cnf.getRootPanelId(); @@ -569,19 +403,17 @@ public class ApplicationConnection implements HasHandlers { uIConnector.init(cnf.getRootPanelId(), this); + // Connection state handler preloads the reconnect dialog, which uses + // overlay container. This in turn depends on VUI being attached + // (done in uiConnector.init) + connectionStateHandler.setConnection(this); + tooltip.setOwner(uIConnector.getWidget()); getLoadingIndicator().show(); heartbeat.init(this); - Window.addWindowClosingHandler(new ClosingHandler() { - @Override - public void onWindowClosing(ClosingEvent event) { - webkitMaybeIgnoringRequests = true; - } - }); - // Ensure the overlay container is added to the dom and set as a live // area for assistive devices Element overlayContainer = VOverlay.getOverlayContainer(this); @@ -604,14 +436,15 @@ public class ApplicationConnection implements HasHandlers { public void start() { String jsonText = configuration.getUIDL(); if (jsonText == null) { - // inital UIDL not in DOM, request later - repaintAll(); + // initial UIDL not in DOM, request from server + getMessageSender().resynchronize(); } else { - // Update counter so TestBench knows something is still going on - hasActiveRequest = true; - // initial UIDL provided in DOM, continue as if returned by request - handleJSONText(jsonText, -1); + + // Hack to avoid logging an error in endRequest() + getMessageSender().startRequest(); + getMessageHandler().handleMessage( + MessageHandler.parseJson(jsonText)); } // Tooltip can't be created earlier because the @@ -634,7 +467,8 @@ public class ApplicationConnection implements HasHandlers { * @return true if the client has some work to be done, false otherwise */ private boolean isActive() { - return isWorkPending() || hasActiveRequest() + return !getMessageHandler().isInitialUidlHandled() || isWorkPending() + || getMessageSender().hasActiveRequest() || isExecutingDeferredCommands(); } @@ -654,12 +488,13 @@ public class ApplicationConnection implements HasHandlers { } client.getProfilingData = $entry(function() { + var smh = ap.@com.vaadin.client.ApplicationConnection::getMessageHandler(); var pd = [ - ap.@com.vaadin.client.ApplicationConnection::lastProcessingTime, - ap.@com.vaadin.client.ApplicationConnection::totalProcessingTime + smh.@com.vaadin.client.communication.MessageHandler::lastProcessingTime, + smh.@com.vaadin.client.communication.MessageHandler::totalProcessingTime ]; - pd = pd.concat(ap.@com.vaadin.client.ApplicationConnection::serverTimingInfo); - pd[pd.length] = ap.@com.vaadin.client.ApplicationConnection::bootstrapTime; + pd = pd.concat(smh.@com.vaadin.client.communication.MessageHandler::serverTimingInfo); + pd[pd.length] = smh.@com.vaadin.client.communication.MessageHandler::bootstrapTime; return pd; }); @@ -683,16 +518,6 @@ public class ApplicationConnection implements HasHandlers { $wnd.vaadin.clients[TTAppId] = client; }-*/; - private static native final int calculateBootstrapTime() - /*-{ - if ($wnd.performance && $wnd.performance.timing) { - return (new Date).getTime() - $wnd.performance.timing.responseStart; - } else { - // performance.timing not supported - return -1; - } - }-*/; - /** * Helper for tt initialization */ @@ -749,65 +574,6 @@ public class ApplicationConnection implements HasHandlers { }-*/; /** - * Runs possibly registered client side post request hooks. This is expected - * to be run after each uidl request made by Vaadin application. - * - * @param appId - */ - private static native void runPostRequestHooks(String appId) - /*-{ - if ($wnd.vaadin.postRequestHooks) { - for ( var hook in $wnd.vaadin.postRequestHooks) { - if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") { - try { - $wnd.vaadin.postRequestHooks[hook](appId); - } catch (e) { - } - } - } - } - }-*/; - - /** - * If on Liferay and logged in, ask the client side session management - * JavaScript to extend the session duration. - * - * Otherwise, Liferay client side JavaScript will explicitly expire the - * session even though the server side considers the session to be active. - * See ticket #8305 for more information. - */ - protected native void extendLiferaySession() - /*-{ - if ($wnd.Liferay && $wnd.Liferay.Session) { - $wnd.Liferay.Session.extend(); - // if the extend banner is visible, hide it - if ($wnd.Liferay.Session.banner) { - $wnd.Liferay.Session.banner.remove(); - } - } - }-*/; - - /** - * Indicates whether or not there are currently active UIDL requests. Used - * internally to sequence requests properly, seldom needed in Widgets. - * - * @return true if there are active requests - */ - public boolean hasActiveRequest() { - return hasActiveRequest; - } - - private String getRepaintAllParameters() { - String parameters = ApplicationConstants.URL_PARAMETER_REPAINT_ALL - + "=1"; - return parameters; - } - - public void repaintAll() { - makeUidlRequest(Json.createArray(), getRepaintAllParameters()); - } - - /** * Requests an analyze of layouts, to find inconsistencies. Exclusively used * for debugging during development. * @@ -832,371 +598,21 @@ public class ApplicationConnection implements HasHandlers { getUIConnector().showServerDebugInfo(serverConnector); } - /** - * Makes an UIDL request to the server. - * - * @param reqInvocations - * Data containing RPC invocations and all related information. - * @param extraParams - * Parameters that are added as GET parameters to the url. - * Contains key=value pairs joined by & characters or is empty if - * no parameters should be added. Should not start with any - * special character. - */ - protected void makeUidlRequest(final JsonArray reqInvocations, - final String extraParams) { - startRequest(); - - JsonObject payload = Json.createObject(); - if (!getCsrfToken().equals( - ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) { - payload.put(ApplicationConstants.CSRF_TOKEN, getCsrfToken()); - } - payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations); - payload.put(ApplicationConstants.SERVER_SYNC_ID, lastSeenServerSyncId); - - getLogger() - .info("Making UIDL Request with params: " + payload.toJson()); - String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX - + ApplicationConstants.UIDL_PATH + '/'); - - if (extraParams != null && extraParams.length() > 0) { - if (extraParams.equals(getRepaintAllParameters())) { - payload.put(ApplicationConstants.RESYNCHRONIZE_ID, true); - } else { - uri = SharedUtil.addGetParameters(uri, extraParams); - } - } - uri = SharedUtil.addGetParameters(uri, UIConstants.UI_ID_PARAMETER - + "=" + configuration.getUIId()); - - doUidlRequest(uri, payload); - - } - - /** - * Sends an asynchronous or synchronous UIDL request to the server using the - * given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - */ - protected void doUidlRequest(final String uri, final JsonObject payload) { - doUidlRequest(uri, payload, true); - } - - /** - * Sends an asynchronous or synchronous UIDL request to the server using the - * given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - * @param retry - * true when a status code 0 should be retried - * @since 7.3.7 - */ - protected void doUidlRequest(final String uri, final JsonObject payload, - final boolean retry) { - RequestCallback requestCallback = new RequestCallback() { - @Override - public void onError(Request request, Throwable exception) { - handleError(exception.getMessage(), -1); - } - - private void handleError(String details, int statusCode) { - handleCommunicationError(details, statusCode); - endRequest(); - - // Consider application not running any more and prevent all - // future requests - setApplicationRunning(false); - } - - @Override - public void onResponseReceived(Request request, Response response) { - getLogger().info( - "Server visit took " - + String.valueOf((new Date()).getTime() - - requestStartTime.getTime()) + "ms"); - - int statusCode = response.getStatusCode(); - // Notify network observers about response status - fireEvent(new ConnectionStatusEvent(statusCode)); - - switch (statusCode) { - case 0: - if (retry) { - /* - * There are 2 situations where the error can pop up: - * - * 1) Request was most likely canceled because the - * browser is maybe navigating away from the page. Just - * send the request again without displaying any error - * in case the navigation isn't carried through. - * - * 2) The browser failed to establish a network - * connection. This was observed with keep-alive - * requests, and under wi-fi roaming conditions. - * - * Status code 0 does indicate that there was no server - * side processing, so we can retry the request. - */ - getLogger().warning("Status code 0, retrying"); - (new Timer() { - @Override - public void run() { - doUidlRequest(uri, payload, false); - } - }).schedule(100); - } else { - handleError("Invalid status code 0 (server down?)", - statusCode); - } - return; - - case 401: - /* - * Authorization has failed. Could be that the session has - * timed out and the container is redirecting to a login - * page. - */ - showAuthenticationError(""); - endRequest(); - return; - - case 503: - /* - * We'll assume msec instead of the usual seconds. If - * there's no Retry-After header, handle the error like a - * 500, as per RFC 2616 section 10.5.4. - */ - String delay = response.getHeader("Retry-After"); - if (delay != null) { - getLogger().warning( - "503, retrying in " + delay + "msec"); - (new Timer() { - @Override - public void run() { - doUidlRequest(uri, payload); - } - }).schedule(Integer.parseInt(delay)); - return; - } - } - - if ((statusCode / 100) == 4) { - // Handle all 4xx errors the same way as (they are - // all permanent errors) - showCommunicationError( - "UIDL could not be read from server. Check servlets mappings. Error code: " - + statusCode, statusCode); - endRequest(); - return; - } else if ((statusCode / 100) == 5) { - // Something's wrong on the server, there's nothing the - // client can do except maybe try again. - handleError("Server error. Error code: " + statusCode, - statusCode); - return; - } - - String contentType = response.getHeader("Content-Type"); - if (contentType == null - || !contentType.startsWith("application/json")) { - /* - * A servlet filter or equivalent may have intercepted the - * request and served non-UIDL content (for instance, a - * login page if the session has expired.) If the response - * contains a magic substring, do a synchronous refresh. See - * #8241. - */ - MatchResult refreshToken = RegExp.compile( - UIDL_REFRESH_TOKEN + "(:\\s*(.*?))?(\\s|$)").exec( - response.getText()); - if (refreshToken != null) { - redirect(refreshToken.getGroup(2)); - return; - } - } - - // for(;;);[realjson] - final String jsonText = response.getText().substring(9, - response.getText().length() - 1); - handleJSONText(jsonText, statusCode); - } - }; - if (push != null) { - push.push(payload); - } else { - try { - doAjaxRequest(uri, payload, requestCallback); - } catch (RequestException e) { - getLogger().log(Level.SEVERE, "Error in server request", e); - endRequest(); - fireEvent(new ConnectionStatusEvent(0)); - } - } - } - - /** - * Handles received UIDL JSON text, parsing it, and passing it on to the - * appropriate handlers, while logging timing information. - * - * @param jsonText - * @param statusCode - */ - private void handleJSONText(String jsonText, int statusCode) { - final Date start = new Date(); - final ValueMap json; - try { - json = parseJSONResponse(jsonText); - } catch (final Exception e) { - endRequest(); - showCommunicationError(e.getMessage() + " - Original JSON-text:" - + jsonText, statusCode); - return; - } - - getLogger().info( - "JSON parsing took " + (new Date().getTime() - start.getTime()) - + "ms"); - if (isApplicationRunning()) { - handleReceivedJSONMessage(start, jsonText, json); - } else { - if (!cssLoaded) { - // Application is starting up for the first time - setApplicationRunning(true); - handleWhenCSSLoaded(jsonText, json); - } else { - getLogger() - .warning( - "Ignored received message because application has already been stopped"); - return; - - } - } - } - - /** - * Sends an asynchronous UIDL request to the server using the given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - * @param requestCallback - * The handler for the response - * @throws RequestException - * if the request could not be sent - */ - protected void doAjaxRequest(String uri, JsonObject payload, - RequestCallback requestCallback) throws RequestException { - RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); - // TODO enable timeout - // rb.setTimeoutMillis(timeoutMillis); - // TODO this should be configurable - rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE); - rb.setRequestData(payload.toJson()); - rb.setCallback(requestCallback); - - final Request request = rb.send(); - if (webkitMaybeIgnoringRequests && BrowserInfo.get().isWebkit()) { - final int retryTimeout = 250; - new Timer() { - @Override - public void run() { - // Use native js to access private field in Request - if (resendRequest(request) && webkitMaybeIgnoringRequests) { - // Schedule retry if still needed - schedule(retryTimeout); - } - } - }.schedule(retryTimeout); - } - } - - private static native boolean resendRequest(Request request) - /*-{ - var xhr = request.@com.google.gwt.http.client.Request::xmlHttpRequest - if (xhr.readyState != 1) { - // Progressed to some other readyState -> no longer blocked - return false; - } - try { - xhr.send(); - return true; - } catch (e) { - // send throws exception if it is running for real - return false; - } - }-*/; - int cssWaits = 0; - /** - * Holds the time spent rendering the last request - */ - protected int lastProcessingTime; - - /** - * Holds the total time spent rendering requests during the lifetime of the - * session. - */ - protected int totalProcessingTime; - - /** - * Holds the time it took to load the page and render the first view. 0 - * means that this value has not yet been calculated because the first view - * has not yet been rendered (or that your browser is very fast). -1 means - * that the browser does not support the performance.timing feature used to - * get this measurement. - */ - private int bootstrapTime; - - /** - * Holds the timing information from the server-side. How much time was - * spent servicing the last request and how much time has been spent - * servicing the session so far. These values are always one request behind, - * since they cannot be measured before the request is finished. - */ - private ValueMap serverTimingInfo; - - /** - * Holds the last seen response id given by the server. - * <p> - * The server generates a strictly increasing id for each response to each - * request from the client. This ID is then replayed back to the server on - * each request. This helps the server in knowing in what state the client - * is, and compare it to its own state. In short, it helps with concurrent - * changes between the client and server. - * <p> - * Initial value, i.e. no responses received from the server, is - * {@link #UNDEFINED_SYNC_ID} ({@value #UNDEFINED_SYNC_ID}). This happens - * between the bootstrap HTML being loaded and the first UI being rendered; - */ - private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; - - /** - * The value of an undefined sync id. - * <p> - * This must be <code>-1</code>, because of the contract in - * {@link #getLastResponseId()} - */ - private static final int UNDEFINED_SYNC_ID = -1; + protected ServerRpcQueue serverRpcQueue; + protected ConnectionStateHandler connectionStateHandler; + protected MessageHandler messageHandler; + protected MessageSender messageSender; static final int MAX_CSS_WAITS = 100; - protected void handleWhenCSSLoaded(final String jsonText, - final ValueMap json) { + public void executeWhenCSSLoaded(final Command c) { if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) { (new Timer() { @Override public void run() { - handleWhenCSSLoaded(jsonText, json); + executeWhenCSSLoaded(c); } }).schedule(50); @@ -1212,7 +628,8 @@ public class ApplicationConnection implements HasHandlers { if (cssWaits >= MAX_CSS_WAITS) { getLogger().severe("CSS files may have not loaded properly."); } - handleReceivedJSONMessage(new Date(), jsonText, json); + + c.execute(); } } @@ -1236,7 +653,7 @@ public class ApplicationConnection implements HasHandlers { * The status code returned for the request * */ - protected void showCommunicationError(String details, int statusCode) { + public void showCommunicationError(String details, int statusCode) { getLogger().severe("Communication error: " + details); showError(details, configuration.getCommunicationError()); } @@ -1247,7 +664,7 @@ public class ApplicationConnection implements HasHandlers { * @param details * Optional details. */ - protected void showAuthenticationError(String details) { + public void showAuthenticationError(String details) { getLogger().severe("Authentication error: " + details); showError(details, configuration.getAuthorizationError()); } @@ -1276,93 +693,6 @@ public class ApplicationConnection implements HasHandlers { message.getMessage(), details, message.getUrl()); } - protected void startRequest() { - if (hasActiveRequest) { - getLogger().severe( - "Trying to start a new request while another is active"); - } - hasActiveRequest = true; - requestStartTime = new Date(); - eventBus.fireEvent(new RequestStartingEvent(this)); - } - - protected void endRequest() { - if (!hasActiveRequest) { - getLogger().severe("No active request"); - } - // After checkForPendingVariableBursts() there may be a new active - // request, so we must set hasActiveRequest to false before, not after, - // the call. Active requests used to be tracked with an integer counter, - // so setting it after used to work but not with the #8505 changes. - hasActiveRequest = false; - - webkitMaybeIgnoringRequests = false; - - if (isApplicationRunning()) { - checkForPendingVariableBursts(); - runPostRequestHooks(configuration.getRootPanelId()); - } - - // deferring to avoid flickering - Scheduler.get().scheduleDeferred(new Command() { - @Override - public void execute() { - if (!isApplicationRunning() - || !(hasActiveRequest() || deferredSendPending)) { - getLoadingIndicator().hide(); - - // If on Liferay and session expiration management is in - // use, extend session duration on each request. - // Doing it here rather than before the request to improve - // responsiveness. - // Postponed until the end of the next request if other - // requests still pending. - extendLiferaySession(); - } - } - }); - eventBus.fireEvent(new ResponseHandlingEndedEvent(this)); - } - - /** - * This method is called after applying uidl change set to application. - * - * It will clean current and queued variable change sets. And send next - * change set if it exists. - */ - private void checkForPendingVariableBursts() { - cleanVariableBurst(pendingInvocations); - if (pendingBursts.size() > 0) { - for (LinkedHashMap<String, MethodInvocation> pendingBurst : pendingBursts) { - cleanVariableBurst(pendingBurst); - } - LinkedHashMap<String, MethodInvocation> nextBurst = pendingBursts - .remove(0); - buildAndSendVariableBurst(nextBurst); - } - } - - /** - * Cleans given queue of variable changes of such changes that came from - * components that do not exist anymore. - * - * @param variableBurst - */ - private void cleanVariableBurst( - LinkedHashMap<String, MethodInvocation> variableBurst) { - Iterator<MethodInvocation> iterator = variableBurst.values().iterator(); - while (iterator.hasNext()) { - String id = iterator.next().getConnectorId(); - if (!getConnectorMap().hasConnector(id) - && !getConnectorMap().isDragAndDropPaintable(id)) { - // variable owner does not exist anymore - iterator.remove(); - getLogger().info( - "Removed variable from removed component: " + id); - } - } - } - /** * Checks if the client has running or scheduled commands */ @@ -1434,1197 +764,7 @@ public class ApplicationConnection implements HasHandlers { return getLoadingIndicator().isVisible(); } - private static native ValueMap parseJSONResponse(String jsonText) - /*-{ - try { - return JSON.parse(jsonText); - } catch (ignored) { - return eval('(' + jsonText + ')'); - } - }-*/; - - private void handleReceivedJSONMessage(Date start, String jsonText, - ValueMap json) { - handleUIDLMessage(start, jsonText, json); - } - - /** - * Gets the id of the last received response. This id can be used by - * connectors to determine whether new data has been received from the - * server to avoid doing the same calculations multiple times. - * <p> - * No guarantees are made for the structure of the id other than that there - * will be a new unique value every time a new response with data from the - * server is received. - * <p> - * The initial id when no request has yet been processed is -1. - * - * @return and id identifying the response - */ - public int getLastResponseId() { - /* - * The discrepancy between field name and getter name is simply historic - * - API can't be changed, but the field was repurposed in a more - * general, yet compatible, use. "Response id" was deemed unsuitable a - * name, so it was called "server sync id" instead. - */ - return lastSeenServerSyncId; - } - - protected void handleUIDLMessage(final Date start, final String jsonText, - final ValueMap json) { - if (!responseHandlingLocks.isEmpty()) { - // Some component is doing something that can't be interrupted - // (e.g. animation that should be smooth). Enqueue the UIDL - // message for later processing. - getLogger().info("Postponing UIDL handling due to lock..."); - pendingUIDLMessages.add(new PendingUIDLMessage(start, jsonText, - json)); - if (!forceHandleMessage.isRunning()) { - forceHandleMessage.schedule(MAX_SUSPENDED_TIMEOUT); - } - return; - } - - /* - * Lock response handling to avoid a situation where something pushed - * from the server gets processed while waiting for e.g. lazily loaded - * connectors that are needed for processing the current message. - */ - final Object lock = new Object(); - suspendReponseHandling(lock); - - getLogger().info("Handling message from server"); - eventBus.fireEvent(new ResponseHandlingStartedEvent(this)); - - final int syncId; - if (json.containsKey(ApplicationConstants.SERVER_SYNC_ID)) { - syncId = json.getInt(ApplicationConstants.SERVER_SYNC_ID); - - /* - * Use sync id unless explicitly set as undefined, as is done by - * e.g. critical server-side notifications - */ - if (syncId != -1) { - if (lastSeenServerSyncId == UNDEFINED_SYNC_ID - || syncId == (lastSeenServerSyncId + 1)) { - lastSeenServerSyncId = syncId; - } else { - getLogger().warning( - "Expected sync id: " + (lastSeenServerSyncId + 1) - + ", received: " + syncId - + ". Resynchronizing from server."); - lastSeenServerSyncId = syncId; - - // Copied from below... - ValueMap meta = json.getValueMap("meta"); - if (meta == null || !meta.containsKey("async")) { - // End the request if the received message was a - // response, not sent asynchronously - endRequest(); - } - resumeResponseHandling(lock); - repaintAll(); - return; - } - } - } else { - syncId = -1; - getLogger() - .severe("Server response didn't contain a sync id. " - + "Please verify that the server is up-to-date and that the response data has not been modified in transmission."); - } - - // Handle redirect - if (json.containsKey("redirect")) { - String url = json.getValueMap("redirect").getString("url"); - getLogger().info("redirecting to " + url); - redirect(url); - return; - } - - final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); - - // Get security key - if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { - csrfToken = json - .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); - } - getLogger().info(" * Handling resources from server"); - - if (json.containsKey("resources")) { - ValueMap resources = json.getValueMap("resources"); - JsArrayString keyArray = resources.getKeyArray(); - int l = keyArray.length(); - for (int i = 0; i < l; i++) { - String key = keyArray.get(i); - resourcesMap.put(key, resources.getAsString(key)); - } - } - handleUIDLDuration.logDuration( - " * Handling resources from server completed", 10); - - getLogger().info(" * Handling type inheritance map from server"); - - if (json.containsKey("typeInheritanceMap")) { - configuration.addComponentInheritanceInfo(json - .getValueMap("typeInheritanceMap")); - } - handleUIDLDuration.logDuration( - " * Handling type inheritance map from server completed", 10); - - getLogger().info("Handling type mappings from server"); - - if (json.containsKey("typeMappings")) { - configuration.addComponentMappings( - json.getValueMap("typeMappings"), widgetSet); - - } - - getLogger().info("Handling resource dependencies"); - if (json.containsKey("scriptDependencies")) { - loadScriptDependencies(json.getJSStringArray("scriptDependencies")); - } - if (json.containsKey("styleDependencies")) { - loadStyleDependencies(json.getJSStringArray("styleDependencies")); - } - - handleUIDLDuration.logDuration( - " * Handling type mappings from server completed", 10); - /* - * Hook for e.g. TestBench to get details about server peformance - */ - if (json.containsKey("timings")) { - serverTimingInfo = json.getValueMap("timings"); - } - - Command c = new Command() { - private boolean onlyNoLayoutUpdates = true; - - @Override - public void execute() { - assert syncId == -1 || syncId == lastSeenServerSyncId; - - handleUIDLDuration.logDuration(" * Loading widgets completed", - 10); - - Profiler.enter("Handling meta information"); - ValueMap meta = null; - if (json.containsKey("meta")) { - getLogger().info(" * Handling meta information"); - meta = json.getValueMap("meta"); - if (meta.containsKey("repaintAll")) { - prepareRepaintAll(); - } - if (meta.containsKey("timedRedirect")) { - final ValueMap timedRedirect = meta - .getValueMap("timedRedirect"); - if (redirectTimer != null) { - redirectTimer.cancel(); - } - redirectTimer = new Timer() { - @Override - public void run() { - redirect(timedRedirect.getString("url")); - } - }; - sessionExpirationInterval = timedRedirect - .getInt("interval"); - } - } - Profiler.leave("Handling meta information"); - - if (redirectTimer != null) { - redirectTimer.schedule(1000 * sessionExpirationInterval); - } - - updatingState = true; - - double processUidlStart = Duration.currentTimeMillis(); - - // Ensure that all connectors that we are about to update exist - JsArrayString createdConnectorIds = createConnectorsIfNeeded(json); - - // Update states, do not fire events - JsArrayObject<StateChangeEvent> pendingStateChangeEvents = updateConnectorState( - json, createdConnectorIds); - - /* - * Doing this here so that locales are available also to the - * connectors which get a state change event before the UI. - */ - Profiler.enter("Handling locales"); - getLogger().info(" * Handling locales"); - // Store locale data - LocaleService - .addLocales(getUIConnector().getState().localeServiceState.localeData); - Profiler.leave("Handling locales"); - - // Update hierarchy, do not fire events - ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(json); - - // Fire hierarchy change events - sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); - - updateCaptions(pendingStateChangeEvents, - connectorHierarchyUpdateResult.parentChangedIds); - - delegateToWidget(pendingStateChangeEvents); - - // Fire state change events. - sendStateChangeEvents(pendingStateChangeEvents); - - // Update of legacy (UIDL) style connectors - updateVaadin6StyleConnectors(json); - - // Handle any RPC invocations done on the server side - handleRpcInvocations(json); - - if (json.containsKey("dd")) { - // response contains data for drag and drop service - VDragAndDropManager.get().handleServerResponse( - json.getValueMap("dd")); - } - - unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); - - getLogger() - .info("handleUIDLMessage: " - + (Duration.currentTimeMillis() - processUidlStart) - + " ms"); - - updatingState = false; - - if (!onlyNoLayoutUpdates) { - Profiler.enter("Layout processing"); - try { - LayoutManager layoutManager = getLayoutManager(); - layoutManager.setEverythingNeedsMeasure(); - layoutManager.layoutNow(); - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error processing layouts", e); - } - Profiler.leave("Layout processing"); - } - - if (ApplicationConfiguration.isDebugMode()) { - Profiler.enter("Dumping state changes to the console"); - getLogger().info(" * Dumping state changes to the console"); - VConsole.dirUIDL(json, ApplicationConnection.this); - Profiler.leave("Dumping state changes to the console"); - } - - if (meta != null) { - Profiler.enter("Error handling"); - if (meta.containsKey("appError")) { - ValueMap error = meta.getValueMap("appError"); - - VNotification.showError(ApplicationConnection.this, - error.getString("caption"), - error.getString("message"), - error.getString("details"), - error.getString("url")); - - setApplicationRunning(false); - } - Profiler.leave("Error handling"); - } - - // TODO build profiling for widget impl loading time - - lastProcessingTime = (int) ((new Date().getTime()) - start - .getTime()); - totalProcessingTime += lastProcessingTime; - if (bootstrapTime == 0) { - bootstrapTime = calculateBootstrapTime(); - if (Profiler.isEnabled() && bootstrapTime != -1) { - Profiler.logBootstrapTimings(); - } - } - - getLogger().info( - " Processing time was " - + String.valueOf(lastProcessingTime) - + "ms for " + jsonText.length() - + " characters of JSON"); - getLogger().info( - "Referenced paintables: " + connectorMap.size()); - - if (meta == null || !meta.containsKey("async")) { - // End the request if the received message was a response, - // not sent asynchronously - endRequest(); - } - resumeResponseHandling(lock); - - if (Profiler.isEnabled()) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - Profiler.logTimings(); - Profiler.reset(); - } - }); - } - } - - /** - * Properly clean up any old stuff to ensure everything is properly - * reinitialized. - */ - private void prepareRepaintAll() { - String uiConnectorId = uIConnector.getConnectorId(); - if (uiConnectorId == null) { - // Nothing to clear yet - return; - } - - // Create fake server response that says that the uiConnector - // has no children - JsonObject fakeHierarchy = Json.createObject(); - fakeHierarchy.put(uiConnectorId, Json.createArray()); - JsonObject fakeJson = Json.createObject(); - fakeJson.put("hierarchy", fakeHierarchy); - ValueMap fakeValueMap = ((JavaScriptObject) fakeJson.toNative()) - .cast(); - - // Update hierarchy based on the fake response - ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(fakeValueMap); - - // Send hierarchy events based on the fake update - sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); - - // Unregister all the old connectors that have now been removed - unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); - - getLayoutManager().cleanMeasuredSizes(); - } - - private void updateCaptions( - JsArrayObject<StateChangeEvent> pendingStateChangeEvents, - FastStringSet parentChangedIds) { - Profiler.enter("updateCaptions"); - - /* - * Find all components that might need a caption update based on - * pending state and hierarchy changes - */ - FastStringSet needsCaptionUpdate = FastStringSet.create(); - needsCaptionUpdate.addAll(parentChangedIds); - - // Find components with potentially changed caption state - int size = pendingStateChangeEvents.size(); - for (int i = 0; i < size; i++) { - StateChangeEvent event = pendingStateChangeEvents.get(i); - if (VCaption.mightChange(event)) { - ServerConnector connector = event.getConnector(); - needsCaptionUpdate.add(connector.getConnectorId()); - } - } - - ConnectorMap connectorMap = getConnectorMap(); - - // Update captions for all suitable candidates - JsArrayString dump = needsCaptionUpdate.dump(); - int needsUpdateLength = dump.length(); - for (int i = 0; i < needsUpdateLength; i++) { - String childId = dump.get(i); - ServerConnector child = connectorMap.getConnector(childId); - - if (child instanceof ComponentConnector - && ((ComponentConnector) child) - .delegateCaptionHandling()) { - ServerConnector parent = child.getParent(); - if (parent instanceof HasComponentsConnector) { - Profiler.enter("HasComponentsConnector.updateCaption"); - ((HasComponentsConnector) parent) - .updateCaption((ComponentConnector) child); - Profiler.leave("HasComponentsConnector.updateCaption"); - } - } - } - - Profiler.leave("updateCaptions"); - } - - private void delegateToWidget( - JsArrayObject<StateChangeEvent> pendingStateChangeEvents) { - Profiler.enter("@DelegateToWidget"); - - getLogger().info(" * Running @DelegateToWidget"); - - // Keep track of types that have no @DelegateToWidget in their - // state to optimize performance - FastStringSet noOpTypes = FastStringSet.create(); - - int size = pendingStateChangeEvents.size(); - for (int eventIndex = 0; eventIndex < size; eventIndex++) { - StateChangeEvent sce = pendingStateChangeEvents - .get(eventIndex); - ServerConnector connector = sce.getConnector(); - if (connector instanceof ComponentConnector) { - String className = connector.getClass().getName(); - if (noOpTypes.contains(className)) { - continue; - } - ComponentConnector component = (ComponentConnector) connector; - - Type stateType = AbstractConnector - .getStateType(component); - JsArrayString delegateToWidgetProperties = stateType - .getDelegateToWidgetProperties(); - if (delegateToWidgetProperties == null) { - noOpTypes.add(className); - continue; - } - - int length = delegateToWidgetProperties.length(); - for (int i = 0; i < length; i++) { - String propertyName = delegateToWidgetProperties - .get(i); - if (sce.hasPropertyChanged(propertyName)) { - Property property = stateType - .getProperty(propertyName); - String method = property - .getDelegateToWidgetMethodName(); - Profiler.enter("doDelegateToWidget"); - doDelegateToWidget(component, property, method); - Profiler.leave("doDelegateToWidget"); - } - } - - } - } - - Profiler.leave("@DelegateToWidget"); - } - - private void doDelegateToWidget(ComponentConnector component, - Property property, String methodName) { - Type type = TypeData.getType(component.getClass()); - try { - Type widgetType = type.getMethod("getWidget") - .getReturnType(); - Widget widget = component.getWidget(); - - Object propertyValue = property.getValue(component - .getState()); - - widgetType.getMethod(methodName).invoke(widget, - propertyValue); - } catch (NoDataException e) { - throw new RuntimeException( - "Missing data needed to invoke @DelegateToWidget for " - + component.getClass().getSimpleName(), e); - } - } - - /** - * Sends the state change events created while updating the state - * information. - * - * This must be called after hierarchy change listeners have been - * called. At least caption updates for the parent are strange if - * fired from state change listeners and thus calls the parent - * BEFORE the parent is aware of the child (through a - * ConnectorHierarchyChangedEvent) - * - * @param pendingStateChangeEvents - * The events to send - */ - private void sendStateChangeEvents( - JsArrayObject<StateChangeEvent> pendingStateChangeEvents) { - Profiler.enter("sendStateChangeEvents"); - getLogger().info(" * Sending state change events"); - - int size = pendingStateChangeEvents.size(); - for (int i = 0; i < size; i++) { - StateChangeEvent sce = pendingStateChangeEvents.get(i); - try { - sce.getConnector().fireEvent(sce); - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error sending state change events", e); - } - } - - Profiler.leave("sendStateChangeEvents"); - } - - private void verifyConnectorHierarchy() { - Profiler.enter("verifyConnectorHierarchy - this is only performed in debug mode"); - - JsArrayObject<ServerConnector> currentConnectors = connectorMap - .getConnectorsAsJsArray(); - int size = currentConnectors.size(); - for (int i = 0; i < size; i++) { - ServerConnector c = currentConnectors.get(i); - if (c.getParent() != null) { - if (!c.getParent().getChildren().contains(c)) { - getLogger() - .severe("ERROR: Connector " - + c.getConnectorId() - + " is connected to a parent but the parent (" - + c.getParent().getConnectorId() - + ") does not contain the connector"); - } - } else if (c == getUIConnector()) { - // UIConnector for this connection, ignore - } else if (c instanceof WindowConnector - && getUIConnector().hasSubWindow( - (WindowConnector) c)) { - // Sub window attached to this UIConnector, ignore - } else { - // The connector has been detached from the - // hierarchy but was not unregistered. - getLogger() - .severe("ERROR: Connector " - + c.getConnectorId() - + " is not attached to a parent but has not been unregistered"); - } - - } - - Profiler.leave("verifyConnectorHierarchy - this is only performed in debug mode"); - } - - private void unregisterRemovedConnectors( - FastStringSet detachedConnectors) { - Profiler.enter("unregisterRemovedConnectors"); - - JsArrayString detachedArray = detachedConnectors.dump(); - for (int i = 0; i < detachedArray.length(); i++) { - ServerConnector connector = connectorMap - .getConnector(detachedArray.get(i)); - - Profiler.enter("unregisterRemovedConnectors unregisterConnector"); - connectorMap.unregisterConnector(connector); - Profiler.leave("unregisterRemovedConnectors unregisterConnector"); - } - - if (ApplicationConfiguration.isDebugMode()) { - // Do some extra checking if we're in debug mode (i.e. debug - // window is open) - verifyConnectorHierarchy(); - } - - getLogger().info( - "* Unregistered " + detachedArray.length() - + " connectors"); - Profiler.leave("unregisterRemovedConnectors"); - } - - private JsArrayString createConnectorsIfNeeded(ValueMap json) { - getLogger().info(" * Creating connectors (if needed)"); - - JsArrayString createdConnectors = JavaScriptObject - .createArray().cast(); - if (!json.containsKey("types")) { - return createdConnectors; - } - - Profiler.enter("Creating connectors"); - - ValueMap types = json.getValueMap("types"); - JsArrayString keyArray = types.getKeyArray(); - for (int i = 0; i < keyArray.length(); i++) { - try { - String connectorId = keyArray.get(i); - ServerConnector connector = connectorMap - .getConnector(connectorId); - if (connector != null) { - continue; - } - - // Always do layouts if there's at least one new - // connector - onlyNoLayoutUpdates = false; - - int connectorType = Integer.parseInt(types - .getString(connectorId)); - - Class<? extends ServerConnector> connectorClass = configuration - .getConnectorClassByEncodedTag(connectorType); - - // Connector does not exist so we must create it - if (connectorClass != uIConnector.getClass()) { - // create, initialize and register the paintable - Profiler.enter("ApplicationConnection.getConnector"); - connector = getConnector(connectorId, connectorType); - Profiler.leave("ApplicationConnection.getConnector"); - - createdConnectors.push(connectorId); - } else { - // First UIConnector update. Before this the - // UIConnector has been created but not - // initialized as the connector id has not been - // known - connectorMap.registerConnector(connectorId, - uIConnector); - uIConnector.doInit(connectorId, - ApplicationConnection.this); - createdConnectors.push(connectorId); - } - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error handling type data", e); - } - } - - Profiler.leave("Creating connectors"); - - return createdConnectors; - } - - private void updateVaadin6StyleConnectors(ValueMap json) { - Profiler.enter("updateVaadin6StyleConnectors"); - - JsArray<ValueMap> changes = json.getJSValueMapArray("changes"); - int length = changes.length(); - - // Must always do layout if there's even a single legacy update - if (length != 0) { - onlyNoLayoutUpdates = false; - } - - getLogger() - .info(" * Passing UIDL to Vaadin 6 style connectors"); - // update paintables - for (int i = 0; i < length; i++) { - try { - final UIDL change = changes.get(i).cast(); - final UIDL uidl = change.getChildUIDL(0); - String connectorId = uidl.getId(); - - final ComponentConnector legacyConnector = (ComponentConnector) connectorMap - .getConnector(connectorId); - if (legacyConnector instanceof Paintable) { - String key = null; - if (Profiler.isEnabled()) { - key = "updateFromUIDL for " - + legacyConnector.getClass() - .getSimpleName(); - Profiler.enter(key); - } - - ((Paintable) legacyConnector).updateFromUIDL(uidl, - ApplicationConnection.this); - - if (Profiler.isEnabled()) { - Profiler.leave(key); - } - } else if (legacyConnector == null) { - getLogger() - .severe("Received update for " - + uidl.getTag() - + ", but there is no such paintable (" - + connectorId + ") rendered."); - } else { - getLogger() - .severe("Server sent Vaadin 6 style updates for " - + Util.getConnectorString(legacyConnector) - + " but this is not a Vaadin 6 Paintable"); - } - - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, "Error handling UIDL", e); - } - } - - Profiler.leave("updateVaadin6StyleConnectors"); - } - - private void sendHierarchyChangeEvents( - JsArrayObject<ConnectorHierarchyChangeEvent> events) { - int eventCount = events.size(); - if (eventCount == 0) { - return; - } - Profiler.enter("sendHierarchyChangeEvents"); - - getLogger().info(" * Sending hierarchy change events"); - for (int i = 0; i < eventCount; i++) { - ConnectorHierarchyChangeEvent event = events.get(i); - try { - logHierarchyChange(event); - event.getConnector().fireEvent(event); - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error sending hierarchy change events", e); - } - } - - Profiler.leave("sendHierarchyChangeEvents"); - } - - private void logHierarchyChange(ConnectorHierarchyChangeEvent event) { - if (true) { - // Always disabled for now. Can be enabled manually - return; - } - - getLogger() - .info("Hierarchy changed for " - + Util.getConnectorString(event.getConnector())); - String oldChildren = "* Old children: "; - for (ComponentConnector child : event.getOldChildren()) { - oldChildren += Util.getConnectorString(child) + " "; - } - getLogger().info(oldChildren); - - String newChildren = "* New children: "; - HasComponentsConnector parent = (HasComponentsConnector) event - .getConnector(); - for (ComponentConnector child : parent.getChildComponents()) { - newChildren += Util.getConnectorString(child) + " "; - } - getLogger().info(newChildren); - } - - private JsArrayObject<StateChangeEvent> updateConnectorState( - ValueMap json, JsArrayString createdConnectorIds) { - JsArrayObject<StateChangeEvent> events = JavaScriptObject - .createArray().cast(); - getLogger().info(" * Updating connector states"); - if (!json.containsKey("state")) { - return events; - } - - Profiler.enter("updateConnectorState"); - - FastStringSet remainingNewConnectors = FastStringSet.create(); - remainingNewConnectors.addAll(createdConnectorIds); - - // set states for all paintables mentioned in "state" - ValueMap states = json.getValueMap("state"); - JsArrayString keyArray = states.getKeyArray(); - for (int i = 0; i < keyArray.length(); i++) { - try { - String connectorId = keyArray.get(i); - ServerConnector connector = connectorMap - .getConnector(connectorId); - if (null != connector) { - Profiler.enter("updateConnectorState inner loop"); - if (Profiler.isEnabled()) { - Profiler.enter("Decode connector state " - + connector.getClass().getSimpleName()); - } - - JavaScriptObject jso = states - .getJavaScriptObject(connectorId); - JsonObject stateJson = Util.jso2json(jso); - - if (connector instanceof HasJavaScriptConnectorHelper) { - ((HasJavaScriptConnectorHelper) connector) - .getJavascriptConnectorHelper() - .setNativeState(jso); - } - - SharedState state = connector.getState(); - Type stateType = new Type(state.getClass() - .getName(), null); - - if (onlyNoLayoutUpdates) { - Profiler.enter("updateConnectorState @NoLayout handling"); - for (String propertyName : stateJson.keys()) { - Property property = stateType - .getProperty(propertyName); - if (!property.isNoLayout()) { - onlyNoLayoutUpdates = false; - break; - } - } - Profiler.leave("updateConnectorState @NoLayout handling"); - } - - Profiler.enter("updateConnectorState decodeValue"); - JsonDecoder.decodeValue(stateType, stateJson, - state, ApplicationConnection.this); - Profiler.leave("updateConnectorState decodeValue"); - - if (Profiler.isEnabled()) { - Profiler.leave("Decode connector state " - + connector.getClass().getSimpleName()); - } - - Profiler.enter("updateConnectorState create event"); - - boolean isNewConnector = remainingNewConnectors - .contains(connectorId); - if (isNewConnector) { - remainingNewConnectors.remove(connectorId); - } - - StateChangeEvent event = new StateChangeEvent( - connector, stateJson, isNewConnector); - events.add(event); - Profiler.leave("updateConnectorState create event"); - - Profiler.leave("updateConnectorState inner loop"); - } - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error updating connector states", e); - } - } - - Profiler.enter("updateConnectorState newWithoutState"); - // Fire events for properties using the default value for newly - // created connectors even if there were no state changes - JsArrayString dump = remainingNewConnectors.dump(); - int length = dump.length(); - for (int i = 0; i < length; i++) { - String connectorId = dump.get(i); - ServerConnector connector = connectorMap - .getConnector(connectorId); - - StateChangeEvent event = new StateChangeEvent(connector, - Json.createObject(), true); - - events.add(event); - - } - Profiler.leave("updateConnectorState newWithoutState"); - - Profiler.leave("updateConnectorState"); - - return events; - } - - /** - * Updates the connector hierarchy and returns a list of events that - * should be fired after update of the hierarchy and the state is - * done. - * - * @param json - * The JSON containing the hierarchy information - * @return A collection of events that should be fired when update - * of hierarchy and state is complete and a list of all - * connectors for which the parent has changed - */ - private ConnectorHierarchyUpdateResult updateConnectorHierarchy( - ValueMap json) { - ConnectorHierarchyUpdateResult result = new ConnectorHierarchyUpdateResult(); - - getLogger().info(" * Updating connector hierarchy"); - if (!json.containsKey("hierarchy")) { - return result; - } - - Profiler.enter("updateConnectorHierarchy"); - - FastStringSet maybeDetached = FastStringSet.create(); - - ValueMap hierarchies = json.getValueMap("hierarchy"); - JsArrayString hierarchyKeys = hierarchies.getKeyArray(); - for (int i = 0; i < hierarchyKeys.length(); i++) { - try { - Profiler.enter("updateConnectorHierarchy hierarchy entry"); - - String connectorId = hierarchyKeys.get(i); - ServerConnector parentConnector = connectorMap - .getConnector(connectorId); - JsArrayString childConnectorIds = hierarchies - .getJSStringArray(connectorId); - int childConnectorSize = childConnectorIds.length(); - - Profiler.enter("updateConnectorHierarchy find new connectors"); - - List<ServerConnector> newChildren = new ArrayList<ServerConnector>(); - List<ComponentConnector> newComponents = new ArrayList<ComponentConnector>(); - for (int connectorIndex = 0; connectorIndex < childConnectorSize; connectorIndex++) { - String childConnectorId = childConnectorIds - .get(connectorIndex); - ServerConnector childConnector = connectorMap - .getConnector(childConnectorId); - if (childConnector == null) { - getLogger() - .severe("Hierarchy claims that " - + childConnectorId - + " is a child for " - + connectorId - + " (" - + parentConnector.getClass() - .getName() - + ") but no connector with id " - + childConnectorId - + " has been registered. " - + "More information might be available in the server-side log if assertions are enabled"); - continue; - } - newChildren.add(childConnector); - if (childConnector instanceof ComponentConnector) { - newComponents - .add((ComponentConnector) childConnector); - } else if (!(childConnector instanceof AbstractExtensionConnector)) { - throw new IllegalStateException( - Util.getConnectorString(childConnector) - + " is not a ComponentConnector nor an AbstractExtensionConnector"); - } - if (childConnector.getParent() != parentConnector) { - childConnector.setParent(parentConnector); - result.parentChangedIds.add(childConnectorId); - // Not detached even if previously removed from - // parent - maybeDetached.remove(childConnectorId); - } - } - - Profiler.leave("updateConnectorHierarchy find new connectors"); - - // TODO This check should be done on the server side in - // the future so the hierarchy update is only sent when - // something actually has changed - List<ServerConnector> oldChildren = parentConnector - .getChildren(); - boolean actuallyChanged = !Util.collectionsEquals( - oldChildren, newChildren); - - if (!actuallyChanged) { - continue; - } - - Profiler.enter("updateConnectorHierarchy handle HasComponentsConnector"); - - if (parentConnector instanceof HasComponentsConnector) { - HasComponentsConnector ccc = (HasComponentsConnector) parentConnector; - List<ComponentConnector> oldComponents = ccc - .getChildComponents(); - if (!Util.collectionsEquals(oldComponents, - newComponents)) { - // Fire change event if the hierarchy has - // changed - ConnectorHierarchyChangeEvent event = GWT - .create(ConnectorHierarchyChangeEvent.class); - event.setOldChildren(oldComponents); - event.setConnector(parentConnector); - ccc.setChildComponents(newComponents); - result.events.add(event); - } - } else if (!newComponents.isEmpty()) { - getLogger() - .severe("Hierachy claims " - + Util.getConnectorString(parentConnector) - + " has component children even though it isn't a HasComponentsConnector"); - } - - Profiler.leave("updateConnectorHierarchy handle HasComponentsConnector"); - - Profiler.enter("updateConnectorHierarchy setChildren"); - parentConnector.setChildren(newChildren); - Profiler.leave("updateConnectorHierarchy setChildren"); - - Profiler.enter("updateConnectorHierarchy find removed children"); - - /* - * Find children removed from this parent and mark for - * removal unless they are already attached to some - * other parent. - */ - for (ServerConnector oldChild : oldChildren) { - if (oldChild.getParent() != parentConnector) { - // Ignore if moved to some other connector - continue; - } - - if (!newChildren.contains(oldChild)) { - /* - * Consider child detached for now, will be - * cleared if it is later on added to some other - * parent. - */ - maybeDetached.add(oldChild.getConnectorId()); - } - } - - Profiler.leave("updateConnectorHierarchy find removed children"); - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error updating connector hierarchy", e); - } finally { - Profiler.leave("updateConnectorHierarchy hierarchy entry"); - } - } - - Profiler.enter("updateConnectorHierarchy detach removed connectors"); - - /* - * Connector is in maybeDetached at this point if it has been - * removed from its parent but not added to any other parent - */ - JsArrayString maybeDetachedArray = maybeDetached.dump(); - for (int i = 0; i < maybeDetachedArray.length(); i++) { - ServerConnector removed = connectorMap - .getConnector(maybeDetachedArray.get(i)); - recursivelyDetach(removed, result.events, - result.detachedConnectorIds); - } - - Profiler.leave("updateConnectorHierarchy detach removed connectors"); - - if (result.events.size() != 0) { - onlyNoLayoutUpdates = false; - } - - Profiler.leave("updateConnectorHierarchy"); - - return result; - - } - - private void recursivelyDetach(ServerConnector connector, - JsArrayObject<ConnectorHierarchyChangeEvent> events, - FastStringSet detachedConnectors) { - detachedConnectors.add(connector.getConnectorId()); - - /* - * Reset state in an attempt to keep it consistent with the - * hierarchy. No children and no parent is the initial situation - * for the hierarchy, so changing the state to its initial value - * is the closest we can get without data from the server. - * #10151 - */ - Profiler.enter("ApplicationConnection recursivelyDetach reset state"); - try { - Profiler.enter("ApplicationConnection recursivelyDetach reset state - getStateType"); - Type stateType = AbstractConnector.getStateType(connector); - Profiler.leave("ApplicationConnection recursivelyDetach reset state - getStateType"); - - // Empty state instance to get default property values from - Profiler.enter("ApplicationConnection recursivelyDetach reset state - createInstance"); - Object defaultState = stateType.createInstance(); - Profiler.leave("ApplicationConnection recursivelyDetach reset state - createInstance"); - - if (connector instanceof AbstractConnector) { - // optimization as the loop setting properties is very - // slow, especially on IE8 - replaceState((AbstractConnector) connector, - defaultState); - } else { - SharedState state = connector.getState(); - - Profiler.enter("ApplicationConnection recursivelyDetach reset state - properties"); - JsArrayObject<Property> properties = stateType - .getPropertiesAsArray(); - int size = properties.size(); - for (int i = 0; i < size; i++) { - Property property = properties.get(i); - property.setValue(state, - property.getValue(defaultState)); - } - Profiler.leave("ApplicationConnection recursivelyDetach reset state - properties"); - } - } catch (NoDataException e) { - throw new RuntimeException("Can't reset state for " - + Util.getConnectorString(connector), e); - } finally { - Profiler.leave("ApplicationConnection recursivelyDetach reset state"); - } - - Profiler.enter("ApplicationConnection recursivelyDetach perform detach"); - /* - * Recursively detach children to make sure they get - * setParent(null) and hierarchy change events as needed. - */ - for (ServerConnector child : connector.getChildren()) { - /* - * Server doesn't send updated child data for removed - * connectors -> ignore child that still seems to be a child - * of this connector although it has been moved to some part - * of the hierarchy that is not detached. - */ - if (child.getParent() != connector) { - continue; - } - recursivelyDetach(child, events, detachedConnectors); - } - Profiler.leave("ApplicationConnection recursivelyDetach perform detach"); - - /* - * Clear child list and parent - */ - Profiler.enter("ApplicationConnection recursivelyDetach clear children and parent"); - connector - .setChildren(Collections.<ServerConnector> emptyList()); - connector.setParent(null); - Profiler.leave("ApplicationConnection recursivelyDetach clear children and parent"); - - /* - * Create an artificial hierarchy event for containers to give - * it a chance to clean up after its children if it has any - */ - Profiler.enter("ApplicationConnection recursivelyDetach create hierarchy event"); - if (connector instanceof HasComponentsConnector) { - HasComponentsConnector ccc = (HasComponentsConnector) connector; - List<ComponentConnector> oldChildren = ccc - .getChildComponents(); - if (!oldChildren.isEmpty()) { - /* - * HasComponentsConnector has a separate child component - * list that should also be cleared - */ - ccc.setChildComponents(Collections - .<ComponentConnector> emptyList()); - - // Create event and add it to the list of pending events - ConnectorHierarchyChangeEvent event = GWT - .create(ConnectorHierarchyChangeEvent.class); - event.setConnector(connector); - event.setOldChildren(oldChildren); - events.add(event); - } - } - Profiler.leave("ApplicationConnection recursivelyDetach create hierarchy event"); - } - - private native void replaceState(AbstractConnector connector, - Object defaultState) - /*-{ - connector.@com.vaadin.client.ui.AbstractConnector::state = defaultState; - }-*/; - - private void handleRpcInvocations(ValueMap json) { - if (json.containsKey("rpc")) { - Profiler.enter("handleRpcInvocations"); - - getLogger() - .info(" * Performing server to client RPC calls"); - - JsonArray rpcCalls = Util.jso2json(json - .getJavaScriptObject("rpc")); - - int rpcLength = rpcCalls.length(); - for (int i = 0; i < rpcLength; i++) { - try { - JsonArray rpcCall = rpcCalls.getArray(i); - MethodInvocation invocation = rpcManager - .parseAndApplyInvocation(rpcCall, - ApplicationConnection.this); - - if (onlyNoLayoutUpdates - && !RpcManager.getMethod(invocation) - .isNoLayout()) { - onlyNoLayoutUpdates = false; - } - - } catch (final Throwable e) { - getLogger() - .log(Level.SEVERE, - "Error performing server to client RPC calls", - e); - } - } - - Profiler.leave("handleRpcInvocations"); - } - } - - }; - ApplicationConfiguration.runWhenDependenciesLoaded(c); - } - - private void loadStyleDependencies(JsArrayString dependencies) { + public void loadStyleDependencies(JsArrayString dependencies) { // Assuming no reason to interpret in a defined order ResourceLoadListener resourceLoadListener = new ResourceLoadListener() { @Override @@ -2649,7 +789,7 @@ public class ApplicationConnection implements HasHandlers { } } - private void loadScriptDependencies(final JsArrayString dependencies) { + public void loadScriptDependencies(final JsArrayString dependencies) { if (dependencies.length() == 0) { return; } @@ -2698,221 +838,23 @@ public class ApplicationConnection implements HasHandlers { } } - // Redirect browser, null reloads current page - public static native void redirect(String url) - /*-{ - if (url) { - $wnd.location = url; - } else { - $wnd.location.reload(false); - } - }-*/; - private void addVariableToQueue(String connectorId, String variableName, Object value, boolean immediate) { boolean lastOnly = !immediate; // note that type is now deduced from value - addMethodInvocationToQueue(new LegacyChangeVariablesInvocation( - connectorId, variableName, value), lastOnly, lastOnly); - } - - /** - * Adds an explicit RPC method invocation to the send queue. - * - * @since 7.0 - * - * @param invocation - * RPC method invocation - * @param delayed - * <code>false</code> to trigger sending within a short time - * window (possibly combining subsequent calls to a single - * request), <code>true</code> to let the framework delay sending - * of RPC calls and variable changes until the next non-delayed - * change - * @param lastOnly - * <code>true</code> to remove all previously delayed invocations - * of the same method that were also enqueued with lastonly set - * to <code>true</code>. <code>false</code> to add invocation to - * the end of the queue without touching previously enqueued - * invocations. - */ - public void addMethodInvocationToQueue(MethodInvocation invocation, - boolean delayed, boolean lastOnly) { - if (!isApplicationRunning()) { - getLogger() - .warning( - "Trying to invoke method on not yet started or stopped application"); - return; - } - String tag; - if (lastOnly) { - tag = invocation.getLastOnlyTag(); - assert !tag.matches("\\d+") : "getLastOnlyTag value must have at least one non-digit character"; - pendingInvocations.remove(tag); - } else { - tag = Integer.toString(lastInvocationTag++); - } - pendingInvocations.put(tag, invocation); - if (!delayed) { - sendPendingVariableChanges(); - } - } - - /** - * Removes any pending invocation of the given method from the queue - * - * @param invocation - * The invocation to remove - */ - public void removePendingInvocations(MethodInvocation invocation) { - Iterator<MethodInvocation> iter = pendingInvocations.values() - .iterator(); - while (iter.hasNext()) { - MethodInvocation mi = iter.next(); - if (mi.equals(invocation)) { - iter.remove(); - } + serverRpcQueue.add(new LegacyChangeVariablesInvocation(connectorId, + variableName, value), lastOnly); + if (immediate) { + serverRpcQueue.flush(); } } /** - * This method sends currently queued variable changes to server. It is - * called when immediate variable update must happen. - * - * To ensure correct order for variable changes (due servers multithreading - * or network), we always wait for active request to be handler before - * sending a new one. If there is an active request, we will put varible - * "burst" to queue that will be purged after current request is handled. - * + * @deprecated as of 7.6, use {@link ServerRpcQueue#flush()} */ + @Deprecated public void sendPendingVariableChanges() { - if (!deferredSendPending) { - deferredSendPending = true; - Scheduler.get().scheduleFinally(sendPendingCommand); - } - } - - private final ScheduledCommand sendPendingCommand = new ScheduledCommand() { - @Override - public void execute() { - deferredSendPending = false; - doSendPendingVariableChanges(); - } - }; - private boolean deferredSendPending = false; - - private void doSendPendingVariableChanges() { - if (isApplicationRunning()) { - if (hasActiveRequest() || (push != null && !push.isActive())) { - // skip empty queues if there are pending bursts to be sent - if (pendingInvocations.size() > 0 || pendingBursts.size() == 0) { - pendingBursts.add(pendingInvocations); - pendingInvocations = new LinkedHashMap<String, MethodInvocation>(); - // Keep tag string short - lastInvocationTag = 0; - } - } else { - buildAndSendVariableBurst(pendingInvocations); - } - } else { - getLogger() - .warning( - "Trying to send variable changes from not yet started or stopped application"); - return; - } - } - - /** - * Build the variable burst and send it to server. - * - * When sync is forced, we also force sending of all pending variable-bursts - * at the same time. This is ok as we can assume that DOM will never be - * updated after this. - * - * @param pendingInvocations - * List of RPC method invocations to send - */ - private void buildAndSendVariableBurst( - LinkedHashMap<String, MethodInvocation> pendingInvocations) { - boolean showLoadingIndicator = false; - JsonArray reqJson = Json.createArray(); - if (!pendingInvocations.isEmpty()) { - if (ApplicationConfiguration.isDebugMode()) { - Util.logVariableBurst(this, pendingInvocations.values()); - } - - for (MethodInvocation invocation : pendingInvocations.values()) { - JsonArray invocationJson = Json.createArray(); - invocationJson.set(0, invocation.getConnectorId()); - invocationJson.set(1, invocation.getInterfaceName()); - invocationJson.set(2, invocation.getMethodName()); - JsonArray paramJson = Json.createArray(); - - Type[] parameterTypes = null; - if (!isLegacyVariableChange(invocation) - && !isJavascriptRpc(invocation)) { - try { - Type type = new Type(invocation.getInterfaceName(), - null); - Method method = type.getMethod(invocation - .getMethodName()); - parameterTypes = method.getParameterTypes(); - - showLoadingIndicator |= !TypeDataStore - .isNoLoadingIndicator(method); - } catch (NoDataException e) { - throw new RuntimeException("No type data for " - + invocation.toString(), e); - } - } else { - // Always show loading indicator for legacy requests - showLoadingIndicator = true; - } - - for (int i = 0; i < invocation.getParameters().length; ++i) { - // TODO non-static encoder? - Type type = null; - if (parameterTypes != null) { - type = parameterTypes[i]; - } - Object value = invocation.getParameters()[i]; - JsonValue jsonValue = JsonEncoder.encode(value, type, this); - paramJson.set(i, jsonValue); - } - invocationJson.set(3, paramJson); - reqJson.set(reqJson.length(), invocationJson); - } - - pendingInvocations.clear(); - // Keep tag string short - lastInvocationTag = 0; - } - - String extraParams = ""; - if (!getConfiguration().isWidgetsetVersionSent()) { - if (!extraParams.isEmpty()) { - extraParams += "&"; - } - String widgetsetVersion = Version.getFullVersion(); - extraParams += "v-wsver=" + widgetsetVersion; - - getConfiguration().setWidgetsetVersionSent(); - } - if (showLoadingIndicator) { - getLoadingIndicator().trigger(); - } - makeUidlRequest(reqJson, extraParams); - } - - private boolean isJavascriptRpc(MethodInvocation invocation) { - return invocation instanceof JavaScriptMethodInvocation; - } - - private boolean isLegacyVariableChange(MethodInvocation invocation) { - return ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation - .getInterfaceName()) - && ApplicationConstants.UPDATE_VARIABLE_METHOD - .equals(invocation.getMethodName()); + serverRpcQueue.flush(); } /** @@ -3248,7 +1190,7 @@ public class ApplicationConnection implements HasHandlers { } /** - * Gets a recource that has been pre-loaded via UIDL, such as custom + * Gets a resource that has been pre-loaded via UIDL, such as custom * layouts. * * @param name @@ -3260,6 +1202,19 @@ public class ApplicationConnection implements HasHandlers { } /** + * Sets a resource that has been pre-loaded via UIDL, such as custom + * layouts. + * + * @param name + * identifier of the resource to Set + * @param resource + * the resource + */ + public void setResource(String name, String resource) { + resourcesMap.put(name, resource); + } + + /** * Singleton method to get instance of app's context menu. * * @return VContextMenu object @@ -3324,20 +1279,6 @@ public class ApplicationConnection implements HasHandlers { private ConnectorMap connectorMap = GWT.create(ConnectorMap.class); - protected String getUidlSecurityKey() { - return getCsrfToken(); - } - - /** - * Gets the token (aka double submit cookie) that the server uses to protect - * against Cross Site Request Forgery attacks. - * - * @return the CSRF token string - */ - public String getCsrfToken() { - return csrfToken; - } - /** * Use to notify that the given component's caption has changed; layouts may * have to be recalculated. @@ -3512,71 +1453,7 @@ public class ApplicationConnection implements HasHandlers { heartbeat.send(); } - /** - * Timer used to make sure that no misbehaving components can delay response - * handling forever. - */ - Timer forceHandleMessage = new Timer() { - @Override - public void run() { - getLogger() - .warning( - "WARNING: reponse handling was never resumed, forcibly removing locks..."); - responseHandlingLocks.clear(); - handlePendingMessages(); - } - }; - - /** - * This method can be used to postpone rendering of a response for a short - * period of time (e.g. to avoid the rendering process during animation). - * - * @param lock - */ - public void suspendReponseHandling(Object lock) { - responseHandlingLocks.add(lock); - } - - /** - * Resumes the rendering process once all locks have been removed. - * - * @param lock - */ - public void resumeResponseHandling(Object lock) { - responseHandlingLocks.remove(lock); - if (responseHandlingLocks.isEmpty()) { - // Cancel timer that breaks the lock - forceHandleMessage.cancel(); - - if (!pendingUIDLMessages.isEmpty()) { - getLogger() - .info("No more response handling locks, handling pending requests."); - handlePendingMessages(); - } - } - } - - /** - * Handles all pending UIDL messages queued while response handling was - * suspended. - */ - private void handlePendingMessages() { - if (!pendingUIDLMessages.isEmpty()) { - /* - * Clear the list before processing enqueued messages to support - * reentrancy - */ - List<PendingUIDLMessage> pendingMessages = pendingUIDLMessages; - pendingUIDLMessages = new ArrayList<PendingUIDLMessage>(); - - for (PendingUIDLMessage pending : pendingMessages) { - handleReceivedJSONMessage(pending.getStart(), - pending.getJsonText(), pending.getJson()); - } - } - } - - private void handleCommunicationError(String details, int statusCode) { + public void handleCommunicationError(String details, int statusCode) { boolean handled = false; if (communicationErrorDelegate != null) { handled = communicationErrorDelegate.onError(details, statusCode); @@ -3599,15 +1476,46 @@ public class ApplicationConnection implements HasHandlers { communicationErrorDelegate = delegate; } - public void setApplicationRunning(boolean running) { - if (applicationRunning && !running) { - eventBus.fireEvent(new ApplicationStoppedEvent()); + public void setApplicationRunning(boolean applicationRunning) { + if (getApplicationState() == ApplicationState.TERMINATED) { + if (applicationRunning) { + getLogger() + .severe("Tried to restart a terminated application. This is not supported"); + } else { + getLogger() + .warning( + "Tried to stop a terminated application. This should not be done"); + } + return; + } else if (getApplicationState() == ApplicationState.INITIALIZING) { + if (applicationRunning) { + applicationState = ApplicationState.RUNNING; + } else { + getLogger() + .warning( + "Tried to stop the application before it has started. This should not be done"); + } + } else if (getApplicationState() == ApplicationState.RUNNING) { + if (!applicationRunning) { + applicationState = ApplicationState.TERMINATED; + eventBus.fireEvent(new ApplicationStoppedEvent()); + } else { + getLogger() + .warning( + "Tried to start an already running application. This should not be done"); + } } - applicationRunning = running; } + /** + * Checks if the application is in the {@link ApplicationState#RUNNING} + * state. + * + * @since + * @return true if the application is in the running state, false otherwise + */ public boolean isApplicationRunning() { - return applicationRunning; + return applicationState == ApplicationState.RUNNING; } public <H extends EventHandler> HandlerRegistration addHandler( @@ -3646,93 +1554,84 @@ public class ApplicationConnection implements HasHandlers { focusedElement); } + private static Logger getLogger() { + return Logger.getLogger(ApplicationConnection.class.getName()); + } + /** - * Sets the status for the push connection. - * - * @param enabled - * <code>true</code> to enable the push connection; - * <code>false</code> to disable the push connection. + * Returns the hearbeat instance. */ - public void setPushEnabled(boolean enabled) { - final PushConfigurationState pushState = uIConnector.getState().pushConfiguration; + public Heartbeat getHeartbeat() { + return heartbeat; + } - if (enabled && push == null) { - push = GWT.create(PushConnection.class); - push.init(this, pushState, new CommunicationErrorHandler() { - @Override - public boolean onError(String details, int statusCode) { - handleCommunicationError(details, statusCode); - return true; - } - }); - } else if (!enabled && push != null && push.isActive()) { - push.disconnect(new Command() { - @Override - public void execute() { - push = null; - /* - * If push has been enabled again while we were waiting for - * the old connection to disconnect, now is the right time - * to open a new connection - */ - if (pushState.mode.isEnabled()) { - setPushEnabled(true); - } - - /* - * Send anything that was enqueued while we waited for the - * connection to close - */ - if (pendingInvocations.size() > 0) { - sendPendingVariableChanges(); - } - } - }); - } + /** + * Returns the state of this application. An application state goes from + * "initializing" to "running" to "stopped". There is no way for an + * application to go back to a previous state, i.e. a stopped application + * can never be re-started + * + * @since 7.6 + * @return the current state of this application + */ + public ApplicationState getApplicationState() { + return applicationState; } - public void handlePushMessage(String message) { - handleJSONText(message, 200); + /** + * Gets the server RPC queue for this application + * + * @return the server RPC queue + */ + public ServerRpcQueue getServerRpcQueue() { + return serverRpcQueue; } /** - * Returns a human readable string representation of the method used to - * communicate with the server. + * Gets the communication error handler for this application * - * @since 7.1 - * @return A string representation of the current transport type + * @return the server RPC queue */ - public String getCommunicationMethodName() { - if (push != null) { - return "Push (" + push.getTransportType() + ")"; - } else { - return "XHR"; - } + public ConnectionStateHandler getConnectionStateHandler() { + return connectionStateHandler; } - private static Logger getLogger() { - return Logger.getLogger(ApplicationConnection.class.getName()); + /** + * Gets the (server to client) message handler for this application + * + * @return the message handler + */ + public MessageHandler getMessageHandler() { + return messageHandler; } /** - * Returns the hearbeat instance. + * Gets the server rpc manager for this application + * + * @return the server rpc manager */ - public Heartbeat getHeartbeat() { - return heartbeat; + public RpcManager getRpcManager() { + return rpcManager; } /** - * Checks whether state changes are currently being processed. Certain - * operations are not allowed when the internal state of the application - * might be in an inconsistent state because some state changes have been - * applied but others not. This includes running layotus. + * Gets the (client to server) message sender for this application * - * @since 7.4 - * @return <code>true</code> if the internal state might be inconsistent - * because changes are being processed; <code>false</code> if the - * state should be consistent + * @return the message sender */ - public boolean isUpdatingState() { - return updatingState; + public MessageSender getMessageSender() { + return messageSender; } + + /** + * @return the widget set + */ + public WidgetSet getWidgetSet() { + return widgetSet; + } + + public int getLastSeenServerSyncId() { + return getMessageHandler().getLastSeenServerSyncId(); + } + } diff --git a/client/src/com/vaadin/client/JavaScriptConnectorHelper.java b/client/src/com/vaadin/client/JavaScriptConnectorHelper.java index a473073712..1833b370e5 100644 --- a/client/src/com/vaadin/client/JavaScriptConnectorHelper.java +++ b/client/src/com/vaadin/client/JavaScriptConnectorHelper.java @@ -28,6 +28,7 @@ import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.dom.client.Element; import com.vaadin.client.communication.JavaScriptMethodInvocation; +import com.vaadin.client.communication.ServerRpcQueue; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler; import com.vaadin.client.ui.layout.ElementResizeEvent; @@ -63,7 +64,7 @@ public class JavaScriptConnectorHelper { /** * The id of the previous response for which state changes have been * processed. If this is the same as the - * {@link ApplicationConnection#getLastResponseId()}, it means that the + * {@link ApplicationConnection#getLastSeenServerSyncId()}, it means that the * state change has already been handled and should not be done again. */ private int processedResponseId = -1; @@ -92,7 +93,7 @@ public class JavaScriptConnectorHelper { } private void processStateChanges() { - int lastResponseId = connector.getConnection().getLastResponseId(); + int lastResponseId = connector.getConnection().getLastSeenServerSyncId(); if (processedResponseId == lastResponseId) { return; } @@ -357,9 +358,10 @@ public class JavaScriptConnectorHelper { for (int i = 0; i < parameters.length; i++) { parameters[i] = argumentsArray.get(i); } - connector.getConnection().addMethodInvocationToQueue( - new JavaScriptMethodInvocation(connector.getConnectorId(), - iface, method, parameters), false, false); + ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection()); + rpcQueue.add(new JavaScriptMethodInvocation(connector.getConnectorId(), + iface, method, parameters), false); + rpcQueue.flush(); } private String findWildcardInterface(String method) { @@ -390,8 +392,9 @@ public class JavaScriptConnectorHelper { connector.getConnectorId(), "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call", new Object[] { name, arguments }); - connector.getConnection().addMethodInvocationToQueue(invocation, false, - false); + ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection()); + rpcQueue.add(invocation, false); + rpcQueue.flush(); } public void setNativeState(JavaScriptObject state) { diff --git a/client/src/com/vaadin/client/LayoutManager.java b/client/src/com/vaadin/client/LayoutManager.java index 102e618f5e..c6c172e9c3 100644 --- a/client/src/com/vaadin/client/LayoutManager.java +++ b/client/src/com/vaadin/client/LayoutManager.java @@ -70,6 +70,13 @@ public class LayoutManager { }; private boolean everythingNeedsMeasure = false; + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ public void setConnection(ApplicationConnection connection) { if (this.connection != null) { throw new RuntimeException( @@ -252,7 +259,7 @@ public class LayoutManager { "Can't start a new layout phase before the previous layout phase ends."); } - if (connection.isUpdatingState()) { + if (connection.getMessageHandler().isUpdatingState()) { // If assertions are enabled, throw an exception assert false : STATE_CHANGE_MESSAGE; @@ -1793,7 +1800,7 @@ public class LayoutManager { /** * Clean measured sizes which are no longer needed. Only for IE8. */ - protected void cleanMeasuredSizes() { + public void cleanMeasuredSizes() { } private static Logger getLogger() { diff --git a/client/src/com/vaadin/client/LayoutManagerIE8.java b/client/src/com/vaadin/client/LayoutManagerIE8.java index 9fb6819e83..4464c3bee8 100644 --- a/client/src/com/vaadin/client/LayoutManagerIE8.java +++ b/client/src/com/vaadin/client/LayoutManagerIE8.java @@ -67,7 +67,7 @@ public class LayoutManagerIE8 extends LayoutManager { } @Override - protected void cleanMeasuredSizes() { + public void cleanMeasuredSizes() { Profiler.enter("LayoutManager.cleanMeasuredSizes"); // #12688: IE8 was leaking memory when adding&removing components. diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java index ccafd874a2..43963e14c2 100644 --- a/client/src/com/vaadin/client/Util.java +++ b/client/src/com/vaadin/client/Util.java @@ -747,36 +747,46 @@ public class Util { + id); } for (MethodInvocation invocation : invocations) { - Object[] parameters = invocation.getParameters(); - String formattedParams = null; - if (ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation - .getMethodName()) && parameters.length == 2) { - // name, value - Object value = parameters[1]; - // TODO paintables inside lists/maps get rendered as - // components in the debug console - String formattedValue = value instanceof ServerConnector ? ((ServerConnector) value) - .getConnectorId() : String.valueOf(value); - formattedParams = parameters[0] + " : " + formattedValue; - } - if (null == formattedParams) { - formattedParams = (null != parameters) ? Arrays - .toString(parameters) : null; - } - getLogger().info( - "\t\t" + invocation.getInterfaceName() + "." - + invocation.getMethodName() + "(" - + formattedParams + ")"); + getLogger().info("\t\t" + getInvocationDebugString(invocation)); + } + } + + /** + * Produces a string representation of a method invocation, suitable for + * debug output + * + * @since 7.5 + * @param invocation + * @return + */ + private static String getInvocationDebugString(MethodInvocation invocation) { + Object[] parameters = invocation.getParameters(); + String formattedParams = null; + if (ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation + .getMethodName()) && parameters.length == 2) { + // name, value + Object value = parameters[1]; + // TODO paintables inside lists/maps get rendered as + // components in the debug console + String formattedValue = value instanceof ServerConnector ? ((ServerConnector) value) + .getConnectorId() : String.valueOf(value); + formattedParams = parameters[0] + " : " + formattedValue; + } + if (null == formattedParams) { + formattedParams = (null != parameters) ? Arrays + .toString(parameters) : null; } + return invocation.getInterfaceName() + "." + invocation.getMethodName() + + "(" + formattedParams + ")"; } - static void logVariableBurst(ApplicationConnection c, - Collection<MethodInvocation> loggedBurst) { + public static void logMethodInvocations(ApplicationConnection c, + Collection<MethodInvocation> methodInvocations) { try { - getLogger().info("Variable burst to be sent to server:"); + getLogger().info("RPC invocations to be sent to the server:"); String curId = null; ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); - for (MethodInvocation methodInvocation : loggedBurst) { + for (MethodInvocation methodInvocation : methodInvocations) { String id = methodInvocation.getConnectorId(); if (curId == null) { @@ -792,7 +802,8 @@ public class Util { printConnectorInvocations(invocations, curId, c); } } catch (Exception e) { - getLogger().log(Level.SEVERE, "Error sending variable burst", e); + getLogger() + .log(Level.SEVERE, "Error logging method invocations", e); } } diff --git a/client/src/com/vaadin/client/ValueMap.java b/client/src/com/vaadin/client/ValueMap.java index 172fd84a84..460b6491d0 100644 --- a/client/src/com/vaadin/client/ValueMap.java +++ b/client/src/com/vaadin/client/ValueMap.java @@ -108,12 +108,12 @@ public final class ValueMap extends JavaScriptObject { return this[name]; }-*/; - native String getAsString(String name) + public native String getAsString(String name) /*-{ return '' + this[name]; }-*/; - native JavaScriptObject getJavaScriptObject(String name) + public native JavaScriptObject getJavaScriptObject(String name) /*-{ return this[name]; }-*/; diff --git a/client/src/com/vaadin/client/WidgetUtil.java b/client/src/com/vaadin/client/WidgetUtil.java index 4906197b29..9f7fdbdb6b 100644 --- a/client/src/com/vaadin/client/WidgetUtil.java +++ b/client/src/com/vaadin/client/WidgetUtil.java @@ -65,6 +65,23 @@ public class WidgetUtil { }-*/; /** + * Redirects the browser to the given url or refreshes the page if url is + * null + * + * @since + * @param url + * The url to redirect to or null to refresh + */ + public static native void redirect(String url) + /*-{ + if (url) { + $wnd.location = url; + } else { + $wnd.location.reload(false); + } + }-*/; + + /** * Helper method for a bug fix #14041. For mozilla getKeyCode return 0 for * space bar (because space is considered as char). If return 0 use * getCharCode. diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java index a81ab616cf..8276bf68bb 100644 --- a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java @@ -16,7 +16,6 @@ package com.vaadin.client.communication; -import java.util.ArrayList; import java.util.logging.Logger; import com.google.gwt.core.client.JavaScriptObject; @@ -27,10 +26,10 @@ import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; import com.vaadin.client.ApplicationConnection.ApplicationStoppedHandler; -import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler; import com.vaadin.client.ResourceLoader; import com.vaadin.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.client.ResourceLoader.ResourceLoadListener; +import com.vaadin.client.ValueMap; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.Version; import com.vaadin.shared.communication.PushConstants; @@ -117,8 +116,6 @@ public class AtmospherePushConnection implements PushConnection { private JavaScriptObject socket; - private ArrayList<JsonObject> messageQueue = new ArrayList<JsonObject>(); - private State state = State.CONNECT_PENDING; private AtmosphereConfiguration config; @@ -127,8 +124,6 @@ public class AtmospherePushConnection implements PushConnection { private String transport; - private CommunicationErrorHandler errorHandler; - /** * Keeps track of the disconnect confirmation command for cases where * pending messages should be pushed before actually disconnecting. @@ -147,10 +142,8 @@ public class AtmospherePushConnection implements PushConnection { */ @Override public void init(final ApplicationConnection connection, - final PushConfigurationState pushConfiguration, - CommunicationErrorHandler errorHandler) { + final PushConfigurationState pushConfiguration) { this.connection = connection; - this.errorHandler = errorHandler; connection.addHandler(ApplicationStoppedEvent.TYPE, new ApplicationStoppedHandler() { @@ -201,10 +194,10 @@ public class AtmospherePushConnection implements PushConnection { String extraParams = UIConstants.UI_ID_PARAMETER + "=" + connection.getConfiguration().getUIId(); - if (!connection.getCsrfToken().equals( - ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) { + String csrfToken = connection.getMessageHandler().getCsrfToken(); + if (!csrfToken.equals(ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) { extraParams += "&" + ApplicationConstants.CSRF_TOKEN_PARAMETER - + "=" + connection.getCsrfToken(); + + "=" + csrfToken; } // uri is needed to identify the right connection when closing @@ -226,16 +219,43 @@ public class AtmospherePushConnection implements PushConnection { } @Override + public boolean isBidirectional() { + if (transport == null) { + return false; + } + + if (!transport.equals("websocket")) { + // If we are not using websockets, we want to send XHRs + return false; + } + if (getPushConfigurationState().alwaysUseXhrForServerRequests) { + // If user has forced us to use XHR, let's abide + return false; + } + if (state == State.CONNECT_PENDING) { + // Not sure yet, let's go for using websockets still as still will + // delay the message until a connection is established. When the + // connection is established, bi-directionality will be checked + // again to be sure + } + return true; + + }; + + private PushConfigurationState getPushConfigurationState() { + return connection.getUIConnector().getState().pushConfiguration; + } + + @Override public void push(JsonObject message) { - switch (state) { - case CONNECT_PENDING: - assert isActive(); - getLogger().info("Queuing push message: " + message.toJson()); - messageQueue.add(message); - break; - case CONNECTED: - assert isActive(); - getLogger().info("Sending push message: " + message.toJson()); + if (!isBidirectional()) { + throw new IllegalStateException( + "This server to client push connection should not be used to send client to server messages"); + } + if (state == State.CONNECTED) { + getLogger().info( + "Sending push (" + transport + ") message to server: " + + message.toJson()); if (transport.equals("websocket")) { FragmentedMessage fragmented = new FragmentedMessage( @@ -246,11 +266,15 @@ public class AtmospherePushConnection implements PushConnection { } else { doPush(socket, message.toJson()); } - break; - case DISCONNECT_PENDING: - case DISCONNECTED: - throw new IllegalStateException("Can not push after disconnecting"); + return; + } + + if (state == State.CONNECT_PENDING) { + getConnectionStateHandler().pushNotConnected(message); + return; } + + throw new IllegalStateException("Can not push after disconnecting"); } protected AtmosphereConfiguration getConfig() { @@ -280,14 +304,10 @@ public class AtmospherePushConnection implements PushConnection { */ protected void onConnect(AtmosphereResponse response) { transport = response.getTransport(); - switch (state) { case CONNECT_PENDING: state = State.CONNECTED; - for (JsonObject message : messageQueue) { - push(message); - } - messageQueue.clear(); + getConnectionStateHandler().pushOk(this); break; case DISCONNECT_PENDING: // Set state to connected to make disconnect close the connection @@ -335,11 +355,16 @@ public class AtmospherePushConnection implements PushConnection { protected void onMessage(AtmosphereResponse response) { String message = response.getResponseBody(); - if (message.startsWith("for(;;);")) { - getLogger().info("Received push message: " + message); - // "for(;;);[{json}]" -> "{json}" - message = message.substring(9, message.length() - 1); - connection.handlePushMessage(message); + ValueMap json = MessageHandler.parseWrappedJson(message); + if (json == null) { + // Invalid string (not wrapped as expected) + getConnectionStateHandler().pushInvalidContent(this, message); + return; + } else { + getLogger().info( + "Received push (" + getTransportType() + ") message: " + + message); + connection.getMessageHandler().handleMessage(json); } } @@ -361,32 +386,25 @@ public class AtmospherePushConnection implements PushConnection { */ protected void onError(AtmosphereResponse response) { state = State.DISCONNECTED; - errorHandler.onError("Push connection using " - + getConfig().getTransport() + " failed!", - response.getStatusCode()); + getConnectionStateHandler().pushError(this, response); } protected void onClose(AtmosphereResponse response) { - getLogger().info("Push connection closed"); state = State.CONNECT_PENDING; + getConnectionStateHandler().pushClosed(this, response); } protected void onClientTimeout(AtmosphereResponse response) { state = State.DISCONNECTED; - errorHandler - .onError( - "Client unexpectedly disconnected. Ensure client timeout is disabled.", - -1); + getConnectionStateHandler().pushClientTimeout(this, response); } protected void onReconnect(JavaScriptObject request, final AtmosphereResponse response) { if (state == State.CONNECTED) { - getLogger() - .fine("No onClose was received before reconnect. Forcing state to closed."); state = State.CONNECT_PENDING; } - getLogger().info("Reopening push connection"); + getConnectionStateHandler().pushReconnectPending(this); } public static abstract class AbstractJSO extends JavaScriptObject { @@ -557,10 +575,8 @@ public class AtmospherePushConnection implements PushConnection { @Override public void onError(ResourceLoadEvent event) { - errorHandler.onError( - event.getResourceUrl() - + " could not be loaded. Push will not work.", - 0); + getConnectionStateHandler().pushScriptLoadError( + event.getResourceUrl()); } }); } @@ -578,11 +594,6 @@ public class AtmospherePushConnection implements PushConnection { return pushJs; } - /* - * (non-Javadoc) - * - * @see com.vaadin.client.communication.PushConnection#getTransportType() - */ @Override public String getTransportType() { return transport; @@ -591,4 +602,9 @@ public class AtmospherePushConnection implements PushConnection { private static Logger getLogger() { return Logger.getLogger(AtmospherePushConnection.class.getName()); } + + private ConnectionStateHandler getConnectionStateHandler() { + return connection.getConnectionStateHandler(); + } + } diff --git a/client/src/com/vaadin/client/communication/ConnectionStateHandler.java b/client/src/com/vaadin/client/communication/ConnectionStateHandler.java new file mode 100644 index 0000000000..cc33dd0086 --- /dev/null +++ b/client/src/com/vaadin/client/communication/ConnectionStateHandler.java @@ -0,0 +1,202 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; +import com.vaadin.client.ApplicationConnection; + +import elemental.json.JsonObject; + +/** + * Interface for handling problems and other events which occur during + * communication with the server. + * + * The handler is responsible for handling any problem in XHR, heartbeat and + * push connections in a way it sees fit. The default implementation is + * {@link DefaultConnectionStateHandler}. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public interface ConnectionStateHandler { + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + void setConnection(ApplicationConnection connection); + + /** + * Called when an exception occurs during a {@link Heartbeat} request + * + * @param request + * The heartbeat request + * @param exception + * The exception which occurred + */ + void heartbeatException(Request request, Throwable exception); + + /** + * Called when a heartbeat request returns a status code other than OK (200) + * + * @param request + * The heartbeat request + * @param response + * The heartbeat response + */ + void heartbeatInvalidStatusCode(Request request, Response response); + + /** + * Called when a {@link Heartbeat} request succeeds + */ + void heartbeatOk(); + + /** + * Called when the push connection to the server is closed. This might + * result in the push connection trying a fallback connection method, trying + * to reconnect to the server or might just be an indication that the + * connection was intentionally closed ("unsubscribe"), + * + * @param pushConnection + * The push connection which was closed + * @param response + * An object containing response data + */ + void pushClosed(PushConnection pushConnection, + JavaScriptObject responseObject); + + /** + * Called when a client side timeout occurs before a push connection to the + * server completes. + * + * The client side timeout causes a disconnection of the push connection and + * no reconnect will be attempted after this method is called, + * + * @param pushConnection + * The push connection which timed out + * @param response + * An object containing response data + */ + void pushClientTimeout(PushConnection pushConnection, + JavaScriptObject response); + + /** + * Called when a fatal error fatal error occurs in the push connection. + * + * The push connection will not try to recover from this situation itself + * and typically the problem handler should not try to do automatic recovery + * either. The cause can be e.g. maximum number of reconnection attempts + * have been reached, neither the selected transport nor the fallback + * transport can be used or similar. + * + * @param pushConnection + * The push connection where the error occurred + * @param response + * An object containing response data + */ + void pushError(PushConnection pushConnection, JavaScriptObject response); + + /** + * Called when the push connection has lost the connection to the server and + * will proceed to try to re-establish the connection + * + * @param pushConnection + * The push connection which will be reconnected + */ + void pushReconnectPending(PushConnection pushConnection); + + /** + * Called when the push connection to the server has been established. + * + * @param pushConnection + * The push connection which was established + */ + void pushOk(PushConnection pushConnection); + + /** + * Called when the required push script could not be loaded + * + * @param resourceUrl + * The URL which was used for loading the script + */ + void pushScriptLoadError(String resourceUrl); + + /** + * Called when an exception occurs during an XmlHttpRequest request to the + * server. + * + * @param xhrConnectionError + * An event containing what was being sent to the server and what + * exception occurred + */ + void xhrException(XhrConnectionError xhrConnectionError); + + /** + * Called when invalid content (not JSON) was returned from the server as + * the result of an XmlHttpRequest request + * + * @param communicationProblemEvent + * An event containing what was being sent to the server and what + * was returned + */ + void xhrInvalidContent(XhrConnectionError xhrConnectionError); + + /** + * Called when invalid status code (not 200) was returned by the server as + * the result of an XmlHttpRequest. + * + * @param communicationProblemEvent + * An event containing what was being sent to the server and what + * was returned + */ + void xhrInvalidStatusCode(XhrConnectionError xhrConnectionError); + + /** + * Called whenever a XmlHttpRequest to the server completes successfully + */ + void xhrOk(); + + /** + * Called when a message is to be sent to the server through the push + * channel but the push channel is not connected + * + * @param payload + * The payload to send to the server + */ + void pushNotConnected(JsonObject payload); + + /** + * Called when invalid content (not JSON) was pushed from the server through + * the push connection + * + * @param communicationProblemEvent + * An event containing what was being sent to the server and what + * was returned + */ + void pushInvalidContent(PushConnection pushConnection, String message); + + /** + * Called when some part of the reconnect dialog configuration has been + * changed. + */ + void configurationUpdated(); + +} diff --git a/client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java b/client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java new file mode 100644 index 0000000000..03fa436a57 --- /dev/null +++ b/client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java @@ -0,0 +1,597 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.logging.Logger; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.Timer; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; +import com.vaadin.client.ApplicationConnection.ApplicationStoppedHandler; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.communication.AtmospherePushConnection.AtmosphereResponse; +import com.vaadin.shared.ui.ui.UIState.ReconnectDialogConfigurationState; + +import elemental.json.JsonObject; + +/** + * Default implementation of the connection state handler. + * <p> + * Handles temporary errors by showing a reconnect dialog to the user while + * trying to re-establish the connection to the server and re-send the pending + * message. + * <p> + * Handles permanent errors by showing a critical system notification to the + * user + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class DefaultConnectionStateHandler implements ConnectionStateHandler { + + private ApplicationConnection connection; + private ReconnectDialog reconnectDialog = GWT.create(ReconnectDialog.class); + private int reconnectAttempt = 0; + private Type reconnectionCause = null; + + private Timer scheduledReconnect; + private Timer dialogShowTimer = new Timer() { + + @Override + public void run() { + showDialog(); + } + + }; + + protected enum Type { + HEARTBEAT(0), PUSH(1), XHR(2); + + private int priority; + + private Type(int priority) { + this.priority = priority; + } + + public boolean isMessage() { + return this == PUSH || this == XHR; + } + + /** + * Checks if this type is of higher priority than the given type + * + * @param type + * the type to compare to + * @return true if this type has higher priority than the given type, + * false otherwise + */ + public boolean isHigherPriorityThan(Type type) { + return priority > type.priority; + } + } + + @Override + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + + connection.addHandler(ApplicationStoppedEvent.TYPE, + new ApplicationStoppedHandler() { + @Override + public void onApplicationStopped( + ApplicationStoppedEvent event) { + if (isReconnecting()) { + giveUp(); + } + if (scheduledReconnect != null + && scheduledReconnect.isRunning()) { + scheduledReconnect.cancel(); + } + } + + }); + + // Allow dialog to cache needed resources to make them available when we + // are offline + reconnectDialog.preload(connection); + }; + + /** + * Checks if we are currently trying to reconnect + * + * @return true if we have noted a problem and are trying to re-establish + * server connection, false otherwise + */ + private boolean isReconnecting() { + return reconnectionCause != null; + } + + private static Logger getLogger() { + return Logger.getLogger(DefaultConnectionStateHandler.class.getName()); + } + + /** + * Returns the connection this handler is connected to + * + * @return the connection for this handler + */ + protected ApplicationConnection getConnection() { + return connection; + } + + @Override + public void xhrException(XhrConnectionError xhrConnectionError) { + debug("xhrException"); + handleRecoverableError(Type.XHR, xhrConnectionError.getPayload()); + } + + @Override + public void heartbeatException(Request request, Throwable exception) { + getLogger().severe("Heartbeat exception: " + exception.getMessage()); + handleRecoverableError(Type.HEARTBEAT, null); + } + + @Override + public void heartbeatInvalidStatusCode(Request request, Response response) { + int statusCode = response.getStatusCode(); + getLogger().warning("Heartbeat request returned " + statusCode); + + if (response.getStatusCode() == Response.SC_GONE) { + // Session expired + getConnection().showSessionExpiredError(null); + stopApplication(); + } else if (response.getStatusCode() == Response.SC_NOT_FOUND) { + // UI closed, do nothing as the UI will react to this + // Should not trigger reconnect dialog as this will prevent user + // input + } else { + handleRecoverableError(Type.HEARTBEAT, null); + } + } + + @Override + public void heartbeatOk() { + debug("heartbeatOk"); + if (isReconnecting()) { + resolveTemporaryError(Type.HEARTBEAT); + } + } + + private void debug(String msg) { + if (false) { + getLogger().warning(msg); + } + } + + /** + * Called whenever an error occurs in communication which should be handled + * by showing the reconnect dialog and retrying communication until + * successful again + * + * @param type + * The type of failure detected + * @param payload + * The message which did not reach the server, or null if no + * message was involved (heartbeat or push connection failed) + */ + protected void handleRecoverableError(Type type, final JsonObject payload) { + debug("handleTemporaryError(" + type + ")"); + if (!connection.isApplicationRunning()) { + return; + } + + if (!isReconnecting()) { + // First problem encounter + reconnectionCause = type; + getLogger().warning("Reconnecting because of " + type + " failure"); + // Precaution only as there should never be a dialog at this point + // and no timer running + stopDialogTimer(); + if (isDialogVisible()) { + hideDialog(); + } + + // Show dialog after grace period, still continue to try to + // reconnect even before it is shown + dialogShowTimer.schedule(getConfiguration().dialogGracePeriod); + } else { + // We are currently trying to reconnect + // Priority is HEARTBEAT -> PUSH -> XHR + // If a higher priority issues is resolved, we can assume the lower + // one will be also + if (type.isHigherPriorityThan(reconnectionCause)) { + getLogger().warning( + "Now reconnecting because of " + type + " failure"); + reconnectionCause = type; + } + } + + if (reconnectionCause != type) { + return; + } + + reconnectAttempt++; + getLogger().info( + "Reconnect attempt " + reconnectAttempt + " for " + type); + + if (reconnectAttempt >= getConfiguration().reconnectAttempts) { + // Max attempts reached, stop trying + giveUp(); + } else { + updateDialog(); + scheduleReconnect(payload); + } + } + + /** + * Called after a problem occurred. + * + * This method is responsible for re-sending the payload to the server (if + * not null) or re-send a heartbeat request at some point + * + * @param payload + * the payload that did not reach the server, null if the problem + * was detected by a heartbeat + */ + protected void scheduleReconnect(final JsonObject payload) { + // Here and not in timer to avoid TB for getting in between + + // The request is still open at this point to avoid interference, so we + // do not need to start a new one + if (reconnectAttempt == 1) { + // Try once immediately + doReconnect(payload); + } else { + scheduledReconnect = new Timer() { + @Override + public void run() { + scheduledReconnect = null; + doReconnect(payload); + } + }; + scheduledReconnect.schedule(getConfiguration().reconnectInterval); + } + } + + /** + * Re-sends the payload to the server (if not null) or re-sends a heartbeat + * request immediately + * + * @param payload + * the payload that did not reach the server, null if the problem + * was detected by a heartbeat + */ + protected void doReconnect(JsonObject payload) { + if (!connection.isApplicationRunning()) { + // This should not happen as nobody should call this if the + // application has been stopped + getLogger() + .warning( + "Trying to reconnect after application has been stopped. Giving up"); + return; + } + if (payload != null) { + getLogger().info("Re-sending last message to the server..."); + getConnection().getMessageSender().send(payload); + } else { + // Use heartbeat + getLogger().info("Trying to re-establish server connection..."); + getConnection().getHeartbeat().send(); + } + } + + /** + * Called whenever a reconnect attempt fails to allow updating of dialog + * contents + */ + protected void updateDialog() { + reconnectDialog.setText(getDialogText(reconnectAttempt)); + } + + /** + * Called when we should give up trying to reconnect and let the user decide + * how to continue + * + */ + protected void giveUp() { + reconnectionCause = null; + endRequest(); + + stopDialogTimer(); + if (!isDialogVisible()) { + // It SHOULD always be visible at this point, unless you have a + // really strange configuration (grace time longer than total + // reconnect time) + showDialog(); + } + reconnectDialog.setText(getDialogTextGaveUp(reconnectAttempt)); + reconnectDialog.setReconnecting(false); + + // Stopping the application stops heartbeats and push + connection.setApplicationRunning(false); + } + + /** + * Ensures the reconnect dialog does not popup some time from now + */ + private void stopDialogTimer() { + if (dialogShowTimer.isRunning()) { + dialogShowTimer.cancel(); + } + } + + /** + * Checks if the reconnect dialog is visible to the user + * + * @return true if the user can see the dialog, false otherwise + */ + protected boolean isDialogVisible() { + return reconnectDialog.isVisible(); + } + + /** + * Called when the reconnect dialog should be shown. This is typically when + * N seconds has passed since a problem with the connection has been + * detected + */ + protected void showDialog() { + reconnectDialog.setReconnecting(true); + reconnectDialog.show(connection); + + // We never want to show loading indicator and reconnect dialog at the + // same time + connection.getLoadingIndicator().hide(); + } + + /** + * Called when the reconnect dialog should be hidden. + */ + protected void hideDialog() { + reconnectDialog.hide(); + } + + /** + * Gets the text to show in the reconnect dialog after giving up (reconnect + * limit reached) + * + * @param reconnectAttempt + * The number of the current reconnection attempt + * @return The text to show in the reconnect dialog after giving up + */ + protected String getDialogTextGaveUp(int reconnectAttempt) { + return getConfiguration().dialogTextGaveUp.replace("{0}", + reconnectAttempt + ""); + } + + /** + * Gets the text to show in the reconnect dialog + * + * @param reconnectAttempt + * The number of the current reconnection attempt + * @return The text to show in the reconnect dialog + */ + protected String getDialogText(int reconnectAttempt) { + return getConfiguration().dialogText.replace("{0}", reconnectAttempt + + ""); + } + + @Override + public void configurationUpdated() { + // All other properties are fetched directly from the state when needed + reconnectDialog.setModal(getConfiguration().dialogModal); + } + + private ReconnectDialogConfigurationState getConfiguration() { + return connection.getUIConnector().getState().reconnectDialogConfiguration; + } + + @Override + public void xhrInvalidContent(XhrConnectionError xhrConnectionError) { + debug("xhrInvalidContent"); + endRequest(); + + String responseText = xhrConnectionError.getResponse().getText(); + /* + * A servlet filter or equivalent may have intercepted the request and + * served non-UIDL content (for instance, a login page if the session + * has expired.) If the response contains a magic substring, do a + * synchronous refresh. See #8241. + */ + MatchResult refreshToken = RegExp.compile( + ApplicationConnection.UIDL_REFRESH_TOKEN + + "(:\\s*(.*?))?(\\s|$)").exec(responseText); + if (refreshToken != null) { + WidgetUtil.redirect(refreshToken.getGroup(2)); + } else { + handleUnrecoverableCommunicationError( + "Invalid JSON response from server: " + responseText, + xhrConnectionError); + } + + } + + @Override + public void pushInvalidContent(PushConnection pushConnection, String message) { + debug("pushInvalidContent"); + if (pushConnection.isBidirectional()) { + // We can't be sure that what was pushed was actually a response but + // at this point it should not really matter, as something is + // seriously broken. + endRequest(); + } + + // Do nothing special for now. Should likely do the same as + // xhrInvalidContent + handleUnrecoverableCommunicationError("Invalid JSON from server: " + + message, null); + + } + + @Override + public void xhrInvalidStatusCode(XhrConnectionError xhrConnectionError) { + debug("xhrInvalidStatusCode"); + + Response response = xhrConnectionError.getResponse(); + int statusCode = response.getStatusCode(); + getLogger().warning("Server returned " + statusCode + " for xhr"); + + if (statusCode == 401) { + // Authentication/authorization failed, no need to re-try + endRequest(); + handleUnauthorized(xhrConnectionError); + return; + } else { + // 404, 408 and other 4xx codes CAN be temporary when you have a + // proxy between the client and the server and e.g. restart the + // server + // 5xx codes may or may not be temporary + handleRecoverableError(Type.XHR, xhrConnectionError.getPayload()); + } + } + + /** + * @since + */ + private void endRequest() { + getConnection().getMessageSender().endRequest(); + } + + protected void handleUnauthorized(XhrConnectionError xhrConnectionError) { + /* + * Authorization has failed (401). Could be that the session has timed + * out. + */ + connection.showAuthenticationError(""); + stopApplication(); + } + + private void stopApplication() { + // Consider application not running any more and prevent all + // future requests + connection.setApplicationRunning(false); + } + + private void handleUnrecoverableCommunicationError(String details, + XhrConnectionError xhrConnectionError) { + int statusCode = -1; + if (xhrConnectionError != null) { + Response response = xhrConnectionError.getResponse(); + if (response != null) { + statusCode = response.getStatusCode(); + } + } + connection.handleCommunicationError(details, statusCode); + + stopApplication(); + + } + + @Override + public void xhrOk() { + debug("xhrOk"); + if (isReconnecting()) { + resolveTemporaryError(Type.XHR); + } + } + + private void resolveTemporaryError(Type type) { + debug("resolveTemporaryError(" + type + ")"); + + if (reconnectionCause != type) { + // Waiting for some other problem to be resolved + return; + } + + reconnectionCause = null; + reconnectAttempt = 0; + // IF reconnect happens during grace period, make sure the dialog is not + // shown and does not popup later + stopDialogTimer(); + hideDialog(); + + getLogger().info("Re-established connection to server"); + } + + @Override + public void pushOk(PushConnection pushConnection) { + debug("pushOk()"); + if (isReconnecting()) { + resolveTemporaryError(Type.PUSH); + } + } + + @Override + public void pushScriptLoadError(String resourceUrl) { + connection.handleCommunicationError(resourceUrl + + " could not be loaded. Push will not work.", 0); + } + + @Override + public void pushNotConnected(JsonObject payload) { + debug("pushNotConnected()"); + handleRecoverableError(Type.PUSH, payload); + } + + @Override + public void pushReconnectPending(PushConnection pushConnection) { + debug("pushReconnectPending(" + pushConnection.getTransportType() + ")"); + getLogger().info("Reopening push connection"); + if (pushConnection.isBidirectional()) { + // Lost connection for a connection which will tell us when the + // connection is available again + handleRecoverableError(Type.PUSH, null); + } else { + // Lost connection for a connection we do not necessarily know when + // it is available again (long polling behind proxy). Do nothing and + // show reconnect dialog if the user does something and the XHR + // fails + } + } + + @Override + public void pushError(PushConnection pushConnection, + JavaScriptObject response) { + debug("pushError()"); + connection.handleCommunicationError("Push connection using " + + ((AtmosphereResponse) response).getTransport() + " failed!", + -1); + } + + @Override + public void pushClientTimeout(PushConnection pushConnection, + JavaScriptObject response) { + debug("pushClientTimeout()"); + // TODO Reconnect, allowing client timeout to be set + // https://dev.vaadin.com/ticket/18429 + connection + .handleCommunicationError( + "Client unexpectedly disconnected. Ensure client timeout is disabled.", + -1); + } + + @Override + public void pushClosed(PushConnection pushConnection, + JavaScriptObject response) { + debug("pushClosed()"); + getLogger().info("Push connection closed"); + } + +} diff --git a/client/src/com/vaadin/client/communication/DefaultReconnectDialog.java b/client/src/com/vaadin/client/communication/DefaultReconnectDialog.java new file mode 100644 index 0000000000..340f32b25e --- /dev/null +++ b/client/src/com/vaadin/client/communication/DefaultReconnectDialog.java @@ -0,0 +1,117 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.VOverlay; + +/** + * The default implementation of the reconnect dialog + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class DefaultReconnectDialog extends VOverlay implements ReconnectDialog { + + private static final String STYLE_RECONNECTING = "active"; + + public Label label; + + private HandlerRegistration clickHandler = null; + + public DefaultReconnectDialog() { + super(false, true); + addStyleName("v-reconnect-dialog"); + + FlowPanel root = new FlowPanel("div"); + HTML spinner = new HTML(); + spinner.addStyleName("spinner"); + + label = GWT.create(Label.class); + label.addStyleName("text"); + + root.add(spinner); + root.add(label); + + setWidget(root); + } + + @Override + public void setText(String text) { + label.setText(text); + } + + @Override + public void setReconnecting(boolean reconnecting) { + setStyleName(STYLE_RECONNECTING, reconnecting); + + // Click to refresh after giving up + if (!reconnecting) { + clickHandler = addDomHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + // refresh + WidgetUtil.redirect(null); + } + }, ClickEvent.getType()); + } else { + if (clickHandler != null) { + clickHandler.removeHandler(); + } + } + } + + @Override + public void show(ApplicationConnection connection) { + ac = connection; + show(); + } + + @Override + public void setPopupPosition(int left, int top) { + // Don't set inline styles for position, handle it in the theme + } + + @Override + public void preload(ApplicationConnection connection) { + setModal(false); // Don't interfere with application use + show(connection); + getElement().getStyle().setVisibility(Visibility.HIDDEN); + setStyleName(STYLE_RECONNECTING, true); + + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + getElement().getStyle().setVisibility(Visibility.VISIBLE); + setStyleName(STYLE_RECONNECTING, false); + hide(); + + } + }); + } +} diff --git a/client/src/com/vaadin/client/communication/Heartbeat.java b/client/src/com/vaadin/client/communication/Heartbeat.java index 5d15e5585f..f38fbca5c8 100644 --- a/client/src/com/vaadin/client/communication/Heartbeat.java +++ b/client/src/com/vaadin/client/communication/Heartbeat.java @@ -25,7 +25,6 @@ import com.google.gwt.http.client.Response; import com.google.gwt.user.client.Timer; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; -import com.vaadin.client.ApplicationConnection.ConnectionStatusEvent; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.util.SharedUtil; @@ -96,41 +95,24 @@ public class Heartbeat { public void onResponseReceived(Request request, Response response) { int status = response.getStatusCode(); - // Notify network observers about response status - connection.fireEvent(new ConnectionStatusEvent(status)); - if (status == Response.SC_OK) { - getLogger().fine("Heartbeat response OK"); - } else if (status == 0) { - getLogger().warning( - "Failed sending heartbeat, server is unreachable, retrying in " - + interval + "secs."); - } else if (status >= 500) { - getLogger().warning( - "Failed sending heartbeat, see server logs, retrying in " - + interval + "secs."); - } else if (status == Response.SC_GONE) { - connection.showSessionExpiredError(null); - // If session is expired break the loop - return; + connection.getConnectionStateHandler().heartbeatOk(); } else { - getLogger().warning( - "Failed sending heartbeat to server. Error code: " - + status); + // Handler should stop the application if heartbeat should + // no longer be sent + connection.getConnectionStateHandler() + .heartbeatInvalidStatusCode(request, response); } - // Don't break the loop schedule(); } @Override public void onError(Request request, Throwable exception) { - getLogger().severe( - "Exception sending heartbeat: " - + exception.getMessage()); - // Notify network observers about response status - connection.fireEvent(new ConnectionStatusEvent(0)); - // Don't break the loop + // Handler should stop the application if heartbeat should no + // longer be sent + connection.getConnectionStateHandler().heartbeatException( + request, exception); schedule(); } }; diff --git a/client/src/com/vaadin/client/communication/MessageHandler.java b/client/src/com/vaadin/client/communication/MessageHandler.java new file mode 100644 index 0000000000..c5b2251310 --- /dev/null +++ b/client/src/com/vaadin/client/communication/MessageHandler.java @@ -0,0 +1,1747 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.ApplicationState; +import com.vaadin.client.ApplicationConnection.MultiStepDuration; +import com.vaadin.client.ApplicationConnection.ResponseHandlingStartedEvent; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.FastStringSet; +import com.vaadin.client.HasComponentsConnector; +import com.vaadin.client.JsArrayObject; +import com.vaadin.client.LayoutManager; +import com.vaadin.client.LocaleService; +import com.vaadin.client.Paintable; +import com.vaadin.client.Profiler; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.UIDL; +import com.vaadin.client.Util; +import com.vaadin.client.VCaption; +import com.vaadin.client.VConsole; +import com.vaadin.client.ValueMap; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.metadata.Type; +import com.vaadin.client.metadata.TypeData; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.client.ui.VNotification; +import com.vaadin.client.ui.dd.VDragAndDropManager; +import com.vaadin.client.ui.ui.UIConnector; +import com.vaadin.client.ui.window.WindowConnector; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.SharedState; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +/** + * A MessageHandler is responsible for handling all incoming messages (JSON) + * from the server (state changes, RPCs and other updates) and ensuring that the + * connectors are updated accordingly. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class MessageHandler { + + public static final String JSON_COMMUNICATION_PREFIX = "for(;;);["; + public static final String JSON_COMMUNICATION_SUFFIX = "]"; + + /** + * Helper used to return two values when updating the connector hierarchy. + */ + private static class ConnectorHierarchyUpdateResult { + /** + * Needed at a later point when the created events are fired + */ + private JsArrayObject<ConnectorHierarchyChangeEvent> events = JavaScriptObject + .createArray().cast(); + /** + * Needed to know where captions might need to get updated + */ + private FastStringSet parentChangedIds = FastStringSet.create(); + + /** + * Connectors for which the parent has been set to null + */ + private FastStringSet detachedConnectorIds = FastStringSet.create(); + } + + /** The max timeout that response handling may be suspended */ + private static final int MAX_SUSPENDED_TIMEOUT = 5000; + + /** + * The value of an undefined sync id. + * <p> + * This must be <code>-1</code>, because of the contract in + * {@link #getLastSeenServerSyncId()} + */ + private static final int UNDEFINED_SYNC_ID = -1; + + /** + * If responseHandlingLocks contains any objects, response handling is + * suspended until the collection is empty or a timeout has occurred. + */ + private Set<Object> responseHandlingLocks = new HashSet<Object>(); + + /** Contains all UIDL messages received while response handling is suspended */ + private List<PendingUIDLMessage> pendingUIDLMessages = new ArrayList<PendingUIDLMessage>(); + + // will hold the CSRF token once received + private String csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE; + + /** Timer for automatic redirect to SessionExpiredURL */ + private Timer redirectTimer; + + /** redirectTimer scheduling interval in seconds */ + private int sessionExpirationInterval; + + /** + * Holds the time spent rendering the last request + */ + protected int lastProcessingTime; + + /** + * Holds the total time spent rendering requests during the lifetime of the + * session. + */ + protected int totalProcessingTime; + + /** + * Holds the time it took to load the page and render the first view. -2 + * means that this value has not yet been calculated because the first view + * has not yet been rendered (or that your browser is very fast). -1 means + * that the browser does not support the performance.timing feature used to + * get this measurement. + * + * Note: also used for tracking whether the first UIDL has been handled + */ + private int bootstrapTime = 0; + + /** + * true if state updates are currently being done + */ + private boolean updatingState = false; + + /** + * Holds the timing information from the server-side. How much time was + * spent servicing the last request and how much time has been spent + * servicing the session so far. These values are always one request behind, + * since they cannot be measured before the request is finished. + */ + private ValueMap serverTimingInfo; + + /** + * Holds the last seen response id given by the server. + * <p> + * The server generates a strictly increasing id for each response to each + * request from the client. This ID is then replayed back to the server on + * each request. This helps the server in knowing in what state the client + * is, and compare it to its own state. In short, it helps with concurrent + * changes between the client and server. + * <p> + * Initial value, i.e. no responses received from the server, is + * {@link #UNDEFINED_SYNC_ID} ({@value #UNDEFINED_SYNC_ID}). This happens + * between the bootstrap HTML being loaded and the first UI being rendered; + */ + private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; + + private ApplicationConnection connection; + + /** + * Data structure holding information about pending UIDL messages. + */ + private static class PendingUIDLMessage { + private ValueMap json; + + public PendingUIDLMessage(ValueMap json) { + this.json = json; + } + + public ValueMap getJson() { + return json; + } + } + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + } + + private static Logger getLogger() { + return Logger.getLogger(MessageHandler.class.getName()); + } + + /** + * Handles a received UIDL JSON text, parsing it, and passing it on to the + * appropriate handlers, while logging timing information. + * + * @param jsonText + * The JSON to handle + */ + public void handleMessage(final ValueMap json) { + if (json == null) { + throw new IllegalArgumentException( + "The json to handle cannot be null"); + } + if (getServerId(json) == -1) { + getLogger() + .severe("Response didn't contain a server id. " + + "Please verify that the server is up-to-date and that the response data has not been modified in transmission."); + } + + if (connection.getApplicationState() == ApplicationState.RUNNING) { + handleJSON(json); + } else if (connection.getApplicationState() == ApplicationState.INITIALIZING) { + // Application is starting up for the first time + connection.setApplicationRunning(true); + connection.executeWhenCSSLoaded(new Command() { + @Override + public void execute() { + handleJSON(json); + } + }); + } else { + getLogger() + .warning( + "Ignored received message because application has already been stopped"); + return; + } + } + + protected void handleJSON(final ValueMap json) { + final int serverId = getServerId(json); + + if (isResynchronize(json) && !isNextExpectedMessage(serverId)) { + // Resynchronize request. We must remove any old pending + // messages and ensure this is handled next. Otherwise we + // would keep waiting for an older message forever (if this + // is triggered by forceHandleMessage) + getLogger().info( + "Received resync message with id " + serverId + + " while waiting for " + getExpectedServerId()); + lastSeenServerSyncId = serverId - 1; + removeOldPendingMessages(); + } + + boolean locked = !responseHandlingLocks.isEmpty(); + + if (locked || !isNextExpectedMessage(serverId)) { + // Cannot or should not handle this message right now, either + // because of locks or because it's an out-of-order message + + if (locked) { + // Some component is doing something that can't be interrupted + // (e.g. animation that should be smooth). Enqueue the UIDL + // message for later processing. + getLogger().info("Postponing UIDL handling due to lock..."); + } else { + // Unexpected server id + if (serverId <= lastSeenServerSyncId) { + // Why is the server re-sending an old package? Ignore it + getLogger().warning( + "Received message with server id " + serverId + + " but have already seen " + + lastSeenServerSyncId + ". Ignoring it"); + endRequestIfResponse(json); + return; + } + + // We are waiting for an earlier message... + getLogger() + .info("Received message with server id " + + serverId + + " but expected " + + getExpectedServerId() + + ". Postponing handling until the missing message(s) have been received"); + } + pendingUIDLMessages.add(new PendingUIDLMessage(json)); + if (!forceHandleMessage.isRunning()) { + forceHandleMessage.schedule(MAX_SUSPENDED_TIMEOUT); + } + return; + } + + final Date start = new Date(); + /* + * Lock response handling to avoid a situation where something pushed + * from the server gets processed while waiting for e.g. lazily loaded + * connectors that are needed for processing the current message. + */ + final Object lock = new Object(); + suspendReponseHandling(lock); + + getLogger().info("Handling message from server"); + connection.fireEvent(new ResponseHandlingStartedEvent(connection)); + + // Client id must be updated before server id, as server id update can + // cause a resync (which must use the updated id) + if (json.containsKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) { + int serverNextExpected = json + .getInt(ApplicationConstants.CLIENT_TO_SERVER_ID); + getMessageSender().setClientToServerMessageId(serverNextExpected, + isResynchronize(json)); + } + + if (serverId != -1) { + /* + * Use sync id unless explicitly set as undefined, as is done by + * e.g. critical server-side notifications + */ + lastSeenServerSyncId = serverId; + } + + // Handle redirect + if (json.containsKey("redirect")) { + String url = json.getValueMap("redirect").getString("url"); + getLogger().info("redirecting to " + url); + WidgetUtil.redirect(url); + return; + } + + final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); + + // Get security key + if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { + csrfToken = json + .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); + } + getLogger().info(" * Handling resources from server"); + + if (json.containsKey("resources")) { + ValueMap resources = json.getValueMap("resources"); + JsArrayString keyArray = resources.getKeyArray(); + int l = keyArray.length(); + for (int i = 0; i < l; i++) { + String key = keyArray.get(i); + connection.setResource(key, resources.getAsString(key)); + } + } + handleUIDLDuration.logDuration( + " * Handling resources from server completed", 10); + + getLogger().info(" * Handling type inheritance map from server"); + + if (json.containsKey("typeInheritanceMap")) { + connection.getConfiguration().addComponentInheritanceInfo( + json.getValueMap("typeInheritanceMap")); + } + handleUIDLDuration.logDuration( + " * Handling type inheritance map from server completed", 10); + + getLogger().info("Handling type mappings from server"); + + if (json.containsKey("typeMappings")) { + connection.getConfiguration() + .addComponentMappings(json.getValueMap("typeMappings"), + connection.getWidgetSet()); + + } + + getLogger().info("Handling resource dependencies"); + if (json.containsKey("scriptDependencies")) { + connection.loadScriptDependencies(json + .getJSStringArray("scriptDependencies")); + } + if (json.containsKey("styleDependencies")) { + connection.loadStyleDependencies(json + .getJSStringArray("styleDependencies")); + } + + handleUIDLDuration.logDuration( + " * Handling type mappings from server completed", 10); + /* + * Hook for e.g. TestBench to get details about server peformance + */ + if (json.containsKey("timings")) { + serverTimingInfo = json.getValueMap("timings"); + } + + Command c = new Command() { + private boolean onlyNoLayoutUpdates = true; + + @Override + public void execute() { + assert serverId == -1 || serverId == lastSeenServerSyncId; + + handleUIDLDuration.logDuration(" * Loading widgets completed", + 10); + + Profiler.enter("Handling meta information"); + ValueMap meta = null; + if (json.containsKey("meta")) { + getLogger().info(" * Handling meta information"); + meta = json.getValueMap("meta"); + if (meta.containsKey("repaintAll")) { + prepareRepaintAll(); + } + if (meta.containsKey("timedRedirect")) { + final ValueMap timedRedirect = meta + .getValueMap("timedRedirect"); + if (redirectTimer != null) { + redirectTimer.cancel(); + } + redirectTimer = new Timer() { + @Override + public void run() { + WidgetUtil.redirect(timedRedirect + .getString("url")); + } + }; + sessionExpirationInterval = timedRedirect + .getInt("interval"); + } + } + Profiler.leave("Handling meta information"); + + if (redirectTimer != null) { + redirectTimer.schedule(1000 * sessionExpirationInterval); + } + + updatingState = true; + + double processUidlStart = Duration.currentTimeMillis(); + + // Ensure that all connectors that we are about to update exist + JsArrayString createdConnectorIds = createConnectorsIfNeeded(json); + + // Update states, do not fire events + JsArrayObject<StateChangeEvent> pendingStateChangeEvents = updateConnectorState( + json, createdConnectorIds); + + /* + * Doing this here so that locales are available also to the + * connectors which get a state change event before the UI. + */ + Profiler.enter("Handling locales"); + getLogger().info(" * Handling locales"); + // Store locale data + LocaleService + .addLocales(getUIConnector().getState().localeServiceState.localeData); + Profiler.leave("Handling locales"); + + // Update hierarchy, do not fire events + ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(json); + + // Fire hierarchy change events + sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); + + updateCaptions(pendingStateChangeEvents, + connectorHierarchyUpdateResult.parentChangedIds); + + delegateToWidget(pendingStateChangeEvents); + + // Fire state change events. + sendStateChangeEvents(pendingStateChangeEvents); + + // Update of legacy (UIDL) style connectors + updateVaadin6StyleConnectors(json); + + // Handle any RPC invocations done on the server side + handleRpcInvocations(json); + + if (json.containsKey("dd")) { + // response contains data for drag and drop service + VDragAndDropManager.get().handleServerResponse( + json.getValueMap("dd")); + } + + unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); + + getLogger() + .info("handleUIDLMessage: " + + (Duration.currentTimeMillis() - processUidlStart) + + " ms"); + + updatingState = false; + + if (!onlyNoLayoutUpdates) { + Profiler.enter("Layout processing"); + try { + LayoutManager layoutManager = getLayoutManager(); + layoutManager.setEverythingNeedsMeasure(); + layoutManager.layoutNow(); + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error processing layouts", e); + } + Profiler.leave("Layout processing"); + } + + if (ApplicationConfiguration.isDebugMode()) { + Profiler.enter("Dumping state changes to the console"); + getLogger().info(" * Dumping state changes to the console"); + VConsole.dirUIDL(json, connection); + Profiler.leave("Dumping state changes to the console"); + } + + if (meta != null) { + Profiler.enter("Error handling"); + if (meta.containsKey("appError")) { + ValueMap error = meta.getValueMap("appError"); + + VNotification.showError(connection, + error.getString("caption"), + error.getString("message"), + error.getString("details"), + error.getString("url")); + + connection.setApplicationRunning(false); + } + Profiler.leave("Error handling"); + } + + // TODO build profiling for widget impl loading time + + lastProcessingTime = (int) ((new Date().getTime()) - start + .getTime()); + totalProcessingTime += lastProcessingTime; + if (bootstrapTime == 0) { + bootstrapTime = calculateBootstrapTime(); + if (Profiler.isEnabled() && bootstrapTime != -1) { + Profiler.logBootstrapTimings(); + } + } + + getLogger().info( + " Processing time was " + + String.valueOf(lastProcessingTime) + "ms"); + getLogger().info( + "Referenced paintables: " + getConnectorMap().size()); + + endRequestIfResponse(json); + resumeResponseHandling(lock); + + if (Profiler.isEnabled()) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + Profiler.logTimings(); + Profiler.reset(); + } + }); + } + } + + /** + * Properly clean up any old stuff to ensure everything is properly + * reinitialized. + */ + private void prepareRepaintAll() { + String uiConnectorId = getUIConnector().getConnectorId(); + if (uiConnectorId == null) { + // Nothing to clear yet + return; + } + + // Create fake server response that says that the uiConnector + // has no children + JsonObject fakeHierarchy = Json.createObject(); + fakeHierarchy.put(uiConnectorId, Json.createArray()); + JsonObject fakeJson = Json.createObject(); + fakeJson.put("hierarchy", fakeHierarchy); + ValueMap fakeValueMap = ((JavaScriptObject) fakeJson.toNative()) + .cast(); + + // Update hierarchy based on the fake response + ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(fakeValueMap); + + // Send hierarchy events based on the fake update + sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); + + // Unregister all the old connectors that have now been removed + unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); + + getLayoutManager().cleanMeasuredSizes(); + } + + private void updateCaptions( + JsArrayObject<StateChangeEvent> pendingStateChangeEvents, + FastStringSet parentChangedIds) { + Profiler.enter("updateCaptions"); + + /* + * Find all components that might need a caption update based on + * pending state and hierarchy changes + */ + FastStringSet needsCaptionUpdate = FastStringSet.create(); + needsCaptionUpdate.addAll(parentChangedIds); + + // Find components with potentially changed caption state + int size = pendingStateChangeEvents.size(); + for (int i = 0; i < size; i++) { + StateChangeEvent event = pendingStateChangeEvents.get(i); + if (VCaption.mightChange(event)) { + ServerConnector connector = event.getConnector(); + needsCaptionUpdate.add(connector.getConnectorId()); + } + } + + ConnectorMap connectorMap = getConnectorMap(); + + // Update captions for all suitable candidates + JsArrayString dump = needsCaptionUpdate.dump(); + int needsUpdateLength = dump.length(); + for (int i = 0; i < needsUpdateLength; i++) { + String childId = dump.get(i); + ServerConnector child = connectorMap.getConnector(childId); + + if (child instanceof ComponentConnector + && ((ComponentConnector) child) + .delegateCaptionHandling()) { + ServerConnector parent = child.getParent(); + if (parent instanceof HasComponentsConnector) { + Profiler.enter("HasComponentsConnector.updateCaption"); + ((HasComponentsConnector) parent) + .updateCaption((ComponentConnector) child); + Profiler.leave("HasComponentsConnector.updateCaption"); + } + } + } + + Profiler.leave("updateCaptions"); + } + + private void delegateToWidget( + JsArrayObject<StateChangeEvent> pendingStateChangeEvents) { + Profiler.enter("@DelegateToWidget"); + + getLogger().info(" * Running @DelegateToWidget"); + + // Keep track of types that have no @DelegateToWidget in their + // state to optimize performance + FastStringSet noOpTypes = FastStringSet.create(); + + int size = pendingStateChangeEvents.size(); + for (int eventIndex = 0; eventIndex < size; eventIndex++) { + StateChangeEvent sce = pendingStateChangeEvents + .get(eventIndex); + ServerConnector connector = sce.getConnector(); + if (connector instanceof ComponentConnector) { + String className = connector.getClass().getName(); + if (noOpTypes.contains(className)) { + continue; + } + ComponentConnector component = (ComponentConnector) connector; + + Type stateType = AbstractConnector + .getStateType(component); + JsArrayString delegateToWidgetProperties = stateType + .getDelegateToWidgetProperties(); + if (delegateToWidgetProperties == null) { + noOpTypes.add(className); + continue; + } + + int length = delegateToWidgetProperties.length(); + for (int i = 0; i < length; i++) { + String propertyName = delegateToWidgetProperties + .get(i); + if (sce.hasPropertyChanged(propertyName)) { + Property property = stateType + .getProperty(propertyName); + String method = property + .getDelegateToWidgetMethodName(); + Profiler.enter("doDelegateToWidget"); + doDelegateToWidget(component, property, method); + Profiler.leave("doDelegateToWidget"); + } + } + + } + } + + Profiler.leave("@DelegateToWidget"); + } + + private void doDelegateToWidget(ComponentConnector component, + Property property, String methodName) { + Type type = TypeData.getType(component.getClass()); + try { + Type widgetType = type.getMethod("getWidget") + .getReturnType(); + Widget widget = component.getWidget(); + + Object propertyValue = property.getValue(component + .getState()); + + widgetType.getMethod(methodName).invoke(widget, + propertyValue); + } catch (NoDataException e) { + throw new RuntimeException( + "Missing data needed to invoke @DelegateToWidget for " + + component.getClass().getSimpleName(), e); + } + } + + /** + * Sends the state change events created while updating the state + * information. + * + * This must be called after hierarchy change listeners have been + * called. At least caption updates for the parent are strange if + * fired from state change listeners and thus calls the parent + * BEFORE the parent is aware of the child (through a + * ConnectorHierarchyChangedEvent) + * + * @param pendingStateChangeEvents + * The events to send + */ + private void sendStateChangeEvents( + JsArrayObject<StateChangeEvent> pendingStateChangeEvents) { + Profiler.enter("sendStateChangeEvents"); + getLogger().info(" * Sending state change events"); + + int size = pendingStateChangeEvents.size(); + for (int i = 0; i < size; i++) { + StateChangeEvent sce = pendingStateChangeEvents.get(i); + try { + sce.getConnector().fireEvent(sce); + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error sending state change events", e); + } + } + + Profiler.leave("sendStateChangeEvents"); + } + + private void verifyConnectorHierarchy() { + Profiler.enter("verifyConnectorHierarchy - this is only performed in debug mode"); + + JsArrayObject<ServerConnector> currentConnectors = getConnectorMap() + .getConnectorsAsJsArray(); + int size = currentConnectors.size(); + for (int i = 0; i < size; i++) { + ServerConnector c = currentConnectors.get(i); + if (c.getParent() != null) { + if (!c.getParent().getChildren().contains(c)) { + getLogger() + .severe("ERROR: Connector " + + c.getConnectorId() + + " is connected to a parent but the parent (" + + c.getParent().getConnectorId() + + ") does not contain the connector"); + } + } else if (c == getUIConnector()) { + // UIConnector for this connection, ignore + } else if (c instanceof WindowConnector + && getUIConnector().hasSubWindow( + (WindowConnector) c)) { + // Sub window attached to this UIConnector, ignore + } else { + // The connector has been detached from the + // hierarchy but was not unregistered. + getLogger() + .severe("ERROR: Connector " + + c.getConnectorId() + + " is not attached to a parent but has not been unregistered"); + } + + } + + Profiler.leave("verifyConnectorHierarchy - this is only performed in debug mode"); + } + + private void unregisterRemovedConnectors( + FastStringSet detachedConnectors) { + Profiler.enter("unregisterRemovedConnectors"); + + JsArrayString detachedArray = detachedConnectors.dump(); + for (int i = 0; i < detachedArray.length(); i++) { + ServerConnector connector = getConnectorMap().getConnector( + detachedArray.get(i)); + + Profiler.enter("unregisterRemovedConnectors unregisterConnector"); + getConnectorMap().unregisterConnector(connector); + Profiler.leave("unregisterRemovedConnectors unregisterConnector"); + } + + if (ApplicationConfiguration.isDebugMode()) { + // Do some extra checking if we're in debug mode (i.e. debug + // window is open) + verifyConnectorHierarchy(); + } + + getLogger().info( + "* Unregistered " + detachedArray.length() + + " connectors"); + Profiler.leave("unregisterRemovedConnectors"); + } + + private JsArrayString createConnectorsIfNeeded(ValueMap json) { + getLogger().info(" * Creating connectors (if needed)"); + + JsArrayString createdConnectors = JavaScriptObject + .createArray().cast(); + if (!json.containsKey("types")) { + return createdConnectors; + } + + Profiler.enter("Creating connectors"); + + ValueMap types = json.getValueMap("types"); + JsArrayString keyArray = types.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + try { + String connectorId = keyArray.get(i); + ServerConnector connector = getConnectorMap() + .getConnector(connectorId); + if (connector != null) { + continue; + } + + // Always do layouts if there's at least one new + // connector + onlyNoLayoutUpdates = false; + + int connectorType = Integer.parseInt(types + .getString(connectorId)); + + Class<? extends ServerConnector> connectorClass = connection + .getConfiguration() + .getConnectorClassByEncodedTag(connectorType); + + // Connector does not exist so we must create it + if (connectorClass != getUIConnector().getClass()) { + // create, initialize and register the paintable + Profiler.enter("ApplicationConnection.getConnector"); + connector = connection.getConnector(connectorId, + connectorType); + Profiler.leave("ApplicationConnection.getConnector"); + + createdConnectors.push(connectorId); + } else { + // First UIConnector update. Before this the + // UIConnector has been created but not + // initialized as the connector id has not been + // known + getConnectorMap().registerConnector(connectorId, + getUIConnector()); + getUIConnector().doInit(connectorId, connection); + createdConnectors.push(connectorId); + } + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error handling type data", e); + } + } + + Profiler.leave("Creating connectors"); + + return createdConnectors; + } + + private void updateVaadin6StyleConnectors(ValueMap json) { + Profiler.enter("updateVaadin6StyleConnectors"); + + JsArray<ValueMap> changes = json.getJSValueMapArray("changes"); + int length = changes.length(); + + // Must always do layout if there's even a single legacy update + if (length != 0) { + onlyNoLayoutUpdates = false; + } + + getLogger() + .info(" * Passing UIDL to Vaadin 6 style connectors"); + // update paintables + for (int i = 0; i < length; i++) { + try { + final UIDL change = changes.get(i).cast(); + final UIDL uidl = change.getChildUIDL(0); + String connectorId = uidl.getId(); + + final ComponentConnector legacyConnector = (ComponentConnector) getConnectorMap() + .getConnector(connectorId); + if (legacyConnector instanceof Paintable) { + String key = null; + if (Profiler.isEnabled()) { + key = "updateFromUIDL for " + + legacyConnector.getClass() + .getSimpleName(); + Profiler.enter(key); + } + + ((Paintable) legacyConnector).updateFromUIDL(uidl, + connection); + + if (Profiler.isEnabled()) { + Profiler.leave(key); + } + } else if (legacyConnector == null) { + getLogger() + .severe("Received update for " + + uidl.getTag() + + ", but there is no such paintable (" + + connectorId + ") rendered."); + } else { + getLogger() + .severe("Server sent Vaadin 6 style updates for " + + Util.getConnectorString(legacyConnector) + + " but this is not a Vaadin 6 Paintable"); + } + + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, "Error handling UIDL", e); + } + } + + Profiler.leave("updateVaadin6StyleConnectors"); + } + + private void sendHierarchyChangeEvents( + JsArrayObject<ConnectorHierarchyChangeEvent> events) { + int eventCount = events.size(); + if (eventCount == 0) { + return; + } + Profiler.enter("sendHierarchyChangeEvents"); + + getLogger().info(" * Sending hierarchy change events"); + for (int i = 0; i < eventCount; i++) { + ConnectorHierarchyChangeEvent event = events.get(i); + try { + logHierarchyChange(event); + event.getConnector().fireEvent(event); + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error sending hierarchy change events", e); + } + } + + Profiler.leave("sendHierarchyChangeEvents"); + } + + private void logHierarchyChange(ConnectorHierarchyChangeEvent event) { + if (true) { + // Always disabled for now. Can be enabled manually + return; + } + + getLogger() + .info("Hierarchy changed for " + + Util.getConnectorString(event.getConnector())); + String oldChildren = "* Old children: "; + for (ComponentConnector child : event.getOldChildren()) { + oldChildren += Util.getConnectorString(child) + " "; + } + getLogger().info(oldChildren); + + String newChildren = "* New children: "; + HasComponentsConnector parent = (HasComponentsConnector) event + .getConnector(); + for (ComponentConnector child : parent.getChildComponents()) { + newChildren += Util.getConnectorString(child) + " "; + } + getLogger().info(newChildren); + } + + private JsArrayObject<StateChangeEvent> updateConnectorState( + ValueMap json, JsArrayString createdConnectorIds) { + JsArrayObject<StateChangeEvent> events = JavaScriptObject + .createArray().cast(); + getLogger().info(" * Updating connector states"); + if (!json.containsKey("state")) { + return events; + } + + Profiler.enter("updateConnectorState"); + + FastStringSet remainingNewConnectors = FastStringSet.create(); + remainingNewConnectors.addAll(createdConnectorIds); + + // set states for all paintables mentioned in "state" + ValueMap states = json.getValueMap("state"); + JsArrayString keyArray = states.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + try { + String connectorId = keyArray.get(i); + ServerConnector connector = getConnectorMap() + .getConnector(connectorId); + if (null != connector) { + Profiler.enter("updateConnectorState inner loop"); + if (Profiler.isEnabled()) { + Profiler.enter("Decode connector state " + + connector.getClass().getSimpleName()); + } + + JavaScriptObject jso = states + .getJavaScriptObject(connectorId); + JsonObject stateJson = Util.jso2json(jso); + + if (connector instanceof HasJavaScriptConnectorHelper) { + ((HasJavaScriptConnectorHelper) connector) + .getJavascriptConnectorHelper() + .setNativeState(jso); + } + + SharedState state = connector.getState(); + Type stateType = new Type(state.getClass() + .getName(), null); + + if (onlyNoLayoutUpdates) { + Profiler.enter("updateConnectorState @NoLayout handling"); + for (String propertyName : stateJson.keys()) { + Property property = stateType + .getProperty(propertyName); + if (!property.isNoLayout()) { + onlyNoLayoutUpdates = false; + break; + } + } + Profiler.leave("updateConnectorState @NoLayout handling"); + } + + Profiler.enter("updateConnectorState decodeValue"); + JsonDecoder.decodeValue(stateType, stateJson, + state, connection); + Profiler.leave("updateConnectorState decodeValue"); + + if (Profiler.isEnabled()) { + Profiler.leave("Decode connector state " + + connector.getClass().getSimpleName()); + } + + Profiler.enter("updateConnectorState create event"); + + boolean isNewConnector = remainingNewConnectors + .contains(connectorId); + if (isNewConnector) { + remainingNewConnectors.remove(connectorId); + } + + StateChangeEvent event = new StateChangeEvent( + connector, stateJson, isNewConnector); + events.add(event); + Profiler.leave("updateConnectorState create event"); + + Profiler.leave("updateConnectorState inner loop"); + } + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error updating connector states", e); + } + } + + Profiler.enter("updateConnectorState newWithoutState"); + // Fire events for properties using the default value for newly + // created connectors even if there were no state changes + JsArrayString dump = remainingNewConnectors.dump(); + int length = dump.length(); + for (int i = 0; i < length; i++) { + String connectorId = dump.get(i); + ServerConnector connector = getConnectorMap().getConnector( + connectorId); + + StateChangeEvent event = new StateChangeEvent(connector, + Json.createObject(), true); + + events.add(event); + + } + Profiler.leave("updateConnectorState newWithoutState"); + + Profiler.leave("updateConnectorState"); + + return events; + } + + /** + * Updates the connector hierarchy and returns a list of events that + * should be fired after update of the hierarchy and the state is + * done. + * + * @param json + * The JSON containing the hierarchy information + * @return A collection of events that should be fired when update + * of hierarchy and state is complete and a list of all + * connectors for which the parent has changed + */ + private ConnectorHierarchyUpdateResult updateConnectorHierarchy( + ValueMap json) { + ConnectorHierarchyUpdateResult result = new ConnectorHierarchyUpdateResult(); + + getLogger().info(" * Updating connector hierarchy"); + if (!json.containsKey("hierarchy")) { + return result; + } + + Profiler.enter("updateConnectorHierarchy"); + + FastStringSet maybeDetached = FastStringSet.create(); + + ValueMap hierarchies = json.getValueMap("hierarchy"); + JsArrayString hierarchyKeys = hierarchies.getKeyArray(); + for (int i = 0; i < hierarchyKeys.length(); i++) { + try { + Profiler.enter("updateConnectorHierarchy hierarchy entry"); + + String connectorId = hierarchyKeys.get(i); + ServerConnector parentConnector = getConnectorMap() + .getConnector(connectorId); + JsArrayString childConnectorIds = hierarchies + .getJSStringArray(connectorId); + int childConnectorSize = childConnectorIds.length(); + + Profiler.enter("updateConnectorHierarchy find new connectors"); + + List<ServerConnector> newChildren = new ArrayList<ServerConnector>(); + List<ComponentConnector> newComponents = new ArrayList<ComponentConnector>(); + for (int connectorIndex = 0; connectorIndex < childConnectorSize; connectorIndex++) { + String childConnectorId = childConnectorIds + .get(connectorIndex); + ServerConnector childConnector = getConnectorMap() + .getConnector(childConnectorId); + if (childConnector == null) { + getLogger() + .severe("Hierarchy claims that " + + childConnectorId + + " is a child for " + + connectorId + + " (" + + parentConnector.getClass() + .getName() + + ") but no connector with id " + + childConnectorId + + " has been registered. " + + "More information might be available in the server-side log if assertions are enabled"); + continue; + } + newChildren.add(childConnector); + if (childConnector instanceof ComponentConnector) { + newComponents + .add((ComponentConnector) childConnector); + } else if (!(childConnector instanceof AbstractExtensionConnector)) { + throw new IllegalStateException( + Util.getConnectorString(childConnector) + + " is not a ComponentConnector nor an AbstractExtensionConnector"); + } + if (childConnector.getParent() != parentConnector) { + childConnector.setParent(parentConnector); + result.parentChangedIds.add(childConnectorId); + // Not detached even if previously removed from + // parent + maybeDetached.remove(childConnectorId); + } + } + + Profiler.leave("updateConnectorHierarchy find new connectors"); + + // TODO This check should be done on the server side in + // the future so the hierarchy update is only sent when + // something actually has changed + List<ServerConnector> oldChildren = parentConnector + .getChildren(); + boolean actuallyChanged = !Util.collectionsEquals( + oldChildren, newChildren); + + if (!actuallyChanged) { + continue; + } + + Profiler.enter("updateConnectorHierarchy handle HasComponentsConnector"); + + if (parentConnector instanceof HasComponentsConnector) { + HasComponentsConnector ccc = (HasComponentsConnector) parentConnector; + List<ComponentConnector> oldComponents = ccc + .getChildComponents(); + if (!Util.collectionsEquals(oldComponents, + newComponents)) { + // Fire change event if the hierarchy has + // changed + ConnectorHierarchyChangeEvent event = GWT + .create(ConnectorHierarchyChangeEvent.class); + event.setOldChildren(oldComponents); + event.setConnector(parentConnector); + ccc.setChildComponents(newComponents); + result.events.add(event); + } + } else if (!newComponents.isEmpty()) { + getLogger() + .severe("Hierachy claims " + + Util.getConnectorString(parentConnector) + + " has component children even though it isn't a HasComponentsConnector"); + } + + Profiler.leave("updateConnectorHierarchy handle HasComponentsConnector"); + + Profiler.enter("updateConnectorHierarchy setChildren"); + parentConnector.setChildren(newChildren); + Profiler.leave("updateConnectorHierarchy setChildren"); + + Profiler.enter("updateConnectorHierarchy find removed children"); + + /* + * Find children removed from this parent and mark for + * removal unless they are already attached to some + * other parent. + */ + for (ServerConnector oldChild : oldChildren) { + if (oldChild.getParent() != parentConnector) { + // Ignore if moved to some other connector + continue; + } + + if (!newChildren.contains(oldChild)) { + /* + * Consider child detached for now, will be + * cleared if it is later on added to some other + * parent. + */ + maybeDetached.add(oldChild.getConnectorId()); + } + } + + Profiler.leave("updateConnectorHierarchy find removed children"); + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error updating connector hierarchy", e); + } finally { + Profiler.leave("updateConnectorHierarchy hierarchy entry"); + } + } + + Profiler.enter("updateConnectorHierarchy detach removed connectors"); + + /* + * Connector is in maybeDetached at this point if it has been + * removed from its parent but not added to any other parent + */ + JsArrayString maybeDetachedArray = maybeDetached.dump(); + for (int i = 0; i < maybeDetachedArray.length(); i++) { + ServerConnector removed = getConnectorMap().getConnector( + maybeDetachedArray.get(i)); + recursivelyDetach(removed, result.events, + result.detachedConnectorIds); + } + + Profiler.leave("updateConnectorHierarchy detach removed connectors"); + + if (result.events.size() != 0) { + onlyNoLayoutUpdates = false; + } + + Profiler.leave("updateConnectorHierarchy"); + + return result; + + } + + private void recursivelyDetach(ServerConnector connector, + JsArrayObject<ConnectorHierarchyChangeEvent> events, + FastStringSet detachedConnectors) { + detachedConnectors.add(connector.getConnectorId()); + + /* + * Reset state in an attempt to keep it consistent with the + * hierarchy. No children and no parent is the initial situation + * for the hierarchy, so changing the state to its initial value + * is the closest we can get without data from the server. + * #10151 + */ + String prefix = getClass().getSimpleName() + " "; + Profiler.enter(prefix + "recursivelyDetach reset state"); + try { + Profiler.enter(prefix + + "recursivelyDetach reset state - getStateType"); + Type stateType = AbstractConnector.getStateType(connector); + Profiler.leave(prefix + + "recursivelyDetach reset state - getStateType"); + + // Empty state instance to get default property values from + Profiler.enter(prefix + + "recursivelyDetach reset state - createInstance"); + Object defaultState = stateType.createInstance(); + Profiler.leave(prefix + + "recursivelyDetach reset state - createInstance"); + + if (connector instanceof AbstractConnector) { + // optimization as the loop setting properties is very + // slow, especially on IE8 + replaceState((AbstractConnector) connector, + defaultState); + } else { + SharedState state = connector.getState(); + + Profiler.enter(prefix + + "recursivelyDetach reset state - properties"); + JsArrayObject<Property> properties = stateType + .getPropertiesAsArray(); + int size = properties.size(); + for (int i = 0; i < size; i++) { + Property property = properties.get(i); + property.setValue(state, + property.getValue(defaultState)); + } + Profiler.leave(prefix + + "recursivelyDetach reset state - properties"); + } + } catch (NoDataException e) { + throw new RuntimeException("Can't reset state for " + + Util.getConnectorString(connector), e); + } finally { + Profiler.leave(prefix + "recursivelyDetach reset state"); + } + + Profiler.enter(prefix + "recursivelyDetach perform detach"); + /* + * Recursively detach children to make sure they get + * setParent(null) and hierarchy change events as needed. + */ + for (ServerConnector child : connector.getChildren()) { + /* + * Server doesn't send updated child data for removed + * connectors -> ignore child that still seems to be a child + * of this connector although it has been moved to some part + * of the hierarchy that is not detached. + */ + if (child.getParent() != connector) { + continue; + } + recursivelyDetach(child, events, detachedConnectors); + } + Profiler.leave(prefix + "recursivelyDetach perform detach"); + + /* + * Clear child list and parent + */ + Profiler.enter(prefix + + "recursivelyDetach clear children and parent"); + connector + .setChildren(Collections.<ServerConnector> emptyList()); + connector.setParent(null); + Profiler.leave(prefix + + "recursivelyDetach clear children and parent"); + + /* + * Create an artificial hierarchy event for containers to give + * it a chance to clean up after its children if it has any + */ + Profiler.enter(prefix + + "recursivelyDetach create hierarchy event"); + if (connector instanceof HasComponentsConnector) { + HasComponentsConnector ccc = (HasComponentsConnector) connector; + List<ComponentConnector> oldChildren = ccc + .getChildComponents(); + if (!oldChildren.isEmpty()) { + /* + * HasComponentsConnector has a separate child component + * list that should also be cleared + */ + ccc.setChildComponents(Collections + .<ComponentConnector> emptyList()); + + // Create event and add it to the list of pending events + ConnectorHierarchyChangeEvent event = GWT + .create(ConnectorHierarchyChangeEvent.class); + event.setConnector(connector); + event.setOldChildren(oldChildren); + events.add(event); + } + } + Profiler.leave(prefix + + "recursivelyDetach create hierarchy event"); + } + + private native void replaceState(AbstractConnector connector, + Object defaultState) + /*-{ + connector.@com.vaadin.client.ui.AbstractConnector::state = defaultState; + }-*/; + + private void handleRpcInvocations(ValueMap json) { + if (json.containsKey("rpc")) { + Profiler.enter("handleRpcInvocations"); + + getLogger() + .info(" * Performing server to client RPC calls"); + + JsonArray rpcCalls = Util.jso2json(json + .getJavaScriptObject("rpc")); + + int rpcLength = rpcCalls.length(); + for (int i = 0; i < rpcLength; i++) { + try { + JsonArray rpcCall = rpcCalls.getArray(i); + MethodInvocation invocation = getRpcManager() + .parseAndApplyInvocation(rpcCall, + connection); + + if (onlyNoLayoutUpdates + && !RpcManager.getMethod(invocation) + .isNoLayout()) { + onlyNoLayoutUpdates = false; + } + + } catch (final Throwable e) { + getLogger() + .log(Level.SEVERE, + "Error performing server to client RPC calls", + e); + } + } + + Profiler.leave("handleRpcInvocations"); + } + } + + }; + ApplicationConfiguration.runWhenDependenciesLoaded(c); + } + + private void endRequestIfResponse(ValueMap json) { + if (isResponse(json)) { + // End the request if the received message was a + // response, not sent asynchronously + getMessageSender().endRequest(); + } + } + + private boolean isResynchronize(ValueMap json) { + return json.containsKey(ApplicationConstants.RESYNCHRONIZE_ID); + } + + private boolean isResponse(ValueMap json) { + ValueMap meta = json.getValueMap("meta"); + if (meta == null || !meta.containsKey("async")) { + return true; + } + return false; + } + + /** + * Checks if the given serverId is the one we are currently waiting for from + * the server + */ + private boolean isNextExpectedMessage(int serverId) { + if (serverId == -1) { + return true; + } + if (serverId == getExpectedServerId()) { + return true; + } + if (lastSeenServerSyncId == UNDEFINED_SYNC_ID) { + // First message is always ok + return true; + } + return false; + + } + + private int getServerId(ValueMap json) { + if (json.containsKey(ApplicationConstants.SERVER_SYNC_ID)) { + return json.getInt(ApplicationConstants.SERVER_SYNC_ID); + } else { + return -1; + } + } + + private int getExpectedServerId() { + return lastSeenServerSyncId + 1; + } + + /** + * Timer used to make sure that no misbehaving components can delay response + * handling forever. + */ + Timer forceHandleMessage = new Timer() { + @Override + public void run() { + if (!responseHandlingLocks.isEmpty()) { + // Lock which was never release -> bug in locker or things just + // too slow + getLogger() + .warning( + "WARNING: reponse handling was never resumed, forcibly removing locks..."); + responseHandlingLocks.clear(); + } else { + // Waited for out-of-order message which never arrived + // Do one final check and resynchronize if the message is not + // there. The final check is only a precaution as this timer + // should have been cancelled if the message has arrived + getLogger().warning( + "Gave up waiting for message " + getExpectedServerId() + + " from the server"); + + } + if (!handlePendingMessages() && !pendingUIDLMessages.isEmpty()) { + // There are messages but the next id was not found, likely it + // has been lost + // Drop pending messages and resynchronize + pendingUIDLMessages.clear(); + getMessageSender().resynchronize(); + } + } + }; + + /** + * This method can be used to postpone rendering of a response for a short + * period of time (e.g. to avoid the rendering process during animation). + * + * @param lock + */ + public void suspendReponseHandling(Object lock) { + responseHandlingLocks.add(lock); + } + + /** + * Resumes the rendering process once all locks have been removed. + * + * @param lock + */ + public void resumeResponseHandling(Object lock) { + responseHandlingLocks.remove(lock); + if (responseHandlingLocks.isEmpty()) { + // Cancel timer that breaks the lock + forceHandleMessage.cancel(); + + if (!pendingUIDLMessages.isEmpty()) { + getLogger() + .info("No more response handling locks, handling pending requests."); + handlePendingMessages(); + } + } + } + + private static native final int calculateBootstrapTime() + /*-{ + if ($wnd.performance && $wnd.performance.timing) { + return (new Date).getTime() - $wnd.performance.timing.responseStart; + } else { + // performance.timing not supported + return -1; + } + }-*/; + + /** + * Finds the next pending UIDL message and handles it (next pending is + * decided based on the server id) + * + * @return true if a message was handled, false otherwise + */ + private boolean handlePendingMessages() { + if (pendingUIDLMessages.isEmpty()) { + return false; + } + + // Try to find the next expected message + PendingUIDLMessage toHandle = null; + for (PendingUIDLMessage message : pendingUIDLMessages) { + if (isNextExpectedMessage(getServerId(message.json))) { + toHandle = message; + break; + } + } + + if (toHandle != null) { + pendingUIDLMessages.remove(toHandle); + handleJSON(toHandle.getJson()); + // Any remaining messages will be handled when this is called + // again at the end of handleJSON + return true; + } else { + return false; + } + + } + + private void removeOldPendingMessages() { + Iterator<PendingUIDLMessage> i = pendingUIDLMessages.iterator(); + while (i.hasNext()) { + PendingUIDLMessage m = i.next(); + int serverId = getServerId(m.json); + if (serverId != -1 && serverId < getExpectedServerId()) { + getLogger().info("Removing old message with id " + serverId); + i.remove(); + } + } + } + + /** + * Gets the server id included in the last received response. + * <p> + * This id can be used by connectors to determine whether new data has been + * received from the server to avoid doing the same calculations multiple + * times. + * <p> + * No guarantees are made for the structure of the id other than that there + * will be a new unique value every time a new response with data from the + * server is received. + * <p> + * The initial id when no request has yet been processed is -1. + * + * @return an id identifying the response + */ + public int getLastSeenServerSyncId() { + return lastSeenServerSyncId; + } + + /** + * Gets the token (aka double submit cookie) that the server uses to protect + * against Cross Site Request Forgery attacks. + * + * @return the CSRF token string + */ + public String getCsrfToken() { + return csrfToken; + } + + /** + * Checks whether state changes are currently being processed. Certain + * operations are not allowed when the internal state of the application + * might be in an inconsistent state because some state changes have been + * applied but others not. This includes running layotus. + * + * @return <code>true</code> if the internal state might be inconsistent + * because changes are being processed; <code>false</code> if the + * state should be consistent + */ + public boolean isUpdatingState() { + return updatingState; + } + + /** + * Checks if the first UIDL has been handled + * + * @return true if the initial UIDL has already been processed, false + * otherwise + */ + public boolean isInitialUidlHandled() { + return bootstrapTime != 0; + } + + private LayoutManager getLayoutManager() { + return LayoutManager.get(connection); + } + + private ConnectorMap getConnectorMap() { + return ConnectorMap.get(connection); + } + + private UIConnector getUIConnector() { + return connection.getUIConnector(); + } + + private RpcManager getRpcManager() { + return connection.getRpcManager(); + } + + private MessageSender getMessageSender() { + return connection.getMessageSender(); + } + + /** + * Strips the JSON wrapping from the given json string with wrapping. + * + * If the given string is not wrapped as expected, returns null + * + * @since + * @param jsonWithWrapping + * the JSON received from the server + * @return an unwrapped JSON string or null if the given string was not + * wrapped + */ + public static String stripJSONWrapping(String jsonWithWrapping) { + if (jsonWithWrapping == null) { + return null; + } + + if (!jsonWithWrapping.startsWith(JSON_COMMUNICATION_PREFIX) + || !jsonWithWrapping.endsWith(JSON_COMMUNICATION_SUFFIX)) { + return null; + } + return jsonWithWrapping.substring(JSON_COMMUNICATION_PREFIX.length(), + jsonWithWrapping.length() - JSON_COMMUNICATION_SUFFIX.length()); + } + + /** + * Unwraps and parses the given JSON, originating from the server + * + * @param jsonText + * the json from the server + * @return A parsed ValueMap or null if the input could not be parsed (or + * was null) + */ + public static ValueMap parseJson(String jsonText) { + if (jsonText == null) { + return null; + } + final Date start = new Date(); + try { + ValueMap json = parseJSONResponse(jsonText); + getLogger().info( + "JSON parsing took " + + (new Date().getTime() - start.getTime()) + "ms"); + return json; + } catch (final Exception e) { + getLogger().severe("Unable to parse JSON: " + jsonText); + return null; + } + } + + private static native ValueMap parseJSONResponse(String jsonText) + /*-{ + return JSON.parse(jsonText); + }-*/; + + /** + * Parse the given wrapped JSON, received from the server, to a ValueMap + * + * @param wrappedJsonText + * the json, wrapped as done by the server + * @return a ValueMap, or null if the wrapping was incorrect or json could + * not be parsed + */ + public static ValueMap parseWrappedJson(String wrappedJsonText) { + return parseJson(stripJSONWrapping(wrappedJsonText)); + } + +} diff --git a/client/src/com/vaadin/client/communication/MessageSender.java b/client/src/com/vaadin/client/communication/MessageSender.java new file mode 100644 index 0000000000..cde8be48ac --- /dev/null +++ b/client/src/com/vaadin/client/communication/MessageSender.java @@ -0,0 +1,410 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.logging.Logger; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.RequestStartingEvent; +import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent; +import com.vaadin.client.Util; +import com.vaadin.client.VLoadingIndicator; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.Version; +import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +/** + * MessageSender is responsible for sending messages to the server. + * <p> + * Internally uses {@link XhrConnection} and/or {@link PushConnection} for + * delivering messages, depending on the application configuration. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class MessageSender { + + private ApplicationConnection connection; + private boolean hasActiveRequest = false; + + /** + * Counter for the messages send to the server. First sent message has id 0. + */ + private int clientToServerMessageId = 0; + private XhrConnection xhrConnection; + private PushConnection push; + + public MessageSender() { + xhrConnection = GWT.create(XhrConnection.class); + } + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + xhrConnection.setConnection(connection); + } + + private static Logger getLogger() { + return Logger.getLogger(MessageSender.class.getName()); + } + + public void sendInvocationsToServer() { + if (!connection.isApplicationRunning()) { + getLogger() + .warning( + "Trying to send RPC from not yet started or stopped application"); + return; + } + + if (hasActiveRequest() || (push != null && !push.isActive())) { + // There is an active request or push is enabled but not active + // -> send when current request completes or push becomes active + } else { + doSendInvocationsToServer(); + } + } + + /** + * Sends all pending method invocations (server RPC and legacy variable + * changes) to the server. + * + */ + private void doSendInvocationsToServer() { + + ServerRpcQueue serverRpcQueue = getServerRpcQueue(); + if (serverRpcQueue.isEmpty()) { + return; + } + + if (ApplicationConfiguration.isDebugMode()) { + Util.logMethodInvocations(connection, serverRpcQueue.getAll()); + } + + boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator(); + JsonArray reqJson = serverRpcQueue.toJson(); + serverRpcQueue.clear(); + + if (reqJson.length() == 0) { + // Nothing to send, all invocations were filtered out (for + // non-existing connectors) + getLogger() + .warning( + "All RPCs filtered out, not sending anything to the server"); + return; + } + + JsonObject extraJson = Json.createObject(); + if (!connection.getConfiguration().isWidgetsetVersionSent()) { + extraJson.put(ApplicationConstants.WIDGETSET_VERSION_ID, + Version.getFullVersion()); + connection.getConfiguration().setWidgetsetVersionSent(); + } + if (showLoadingIndicator) { + connection.getLoadingIndicator().trigger(); + } + send(reqJson, extraJson); + } + + private ServerRpcQueue getServerRpcQueue() { + return connection.getServerRpcQueue(); + } + + /** + * Makes an UIDL request to the server. + * + * @param reqInvocations + * Data containing RPC invocations and all related information. + * @param extraParams + * Parameters that are added to the payload + */ + protected void send(final JsonArray reqInvocations, + final JsonObject extraJson) { + startRequest(); + + JsonObject payload = Json.createObject(); + String csrfToken = getMessageHandler().getCsrfToken(); + if (!csrfToken.equals(ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) { + payload.put(ApplicationConstants.CSRF_TOKEN, csrfToken); + } + payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations); + payload.put(ApplicationConstants.SERVER_SYNC_ID, getMessageHandler() + .getLastSeenServerSyncId()); + payload.put(ApplicationConstants.CLIENT_TO_SERVER_ID, + clientToServerMessageId++); + + if (extraJson != null) { + for (String key : extraJson.keys()) { + payload.put(key, extraJson.get(key)); + } + } + + send(payload); + + } + + /** + * Sends an asynchronous or synchronous UIDL request to the server using the + * given URI. + * + * @param uri + * The URI to use for the request. May includes GET parameters + * @param payload + * The contents of the request to send + */ + public void send(final JsonObject payload) { + if (push != null && push.isBidirectional()) { + push.push(payload); + } else { + xhrConnection.send(payload); + } + } + + /** + * Sets the status for the push connection. + * + * @param enabled + * <code>true</code> to enable the push connection; + * <code>false</code> to disable the push connection. + */ + public void setPushEnabled(boolean enabled) { + final PushConfigurationState pushState = connection.getUIConnector() + .getState().pushConfiguration; + + if (enabled && push == null) { + push = GWT.create(PushConnection.class); + push.init(connection, pushState); + } else if (!enabled && push != null && push.isActive()) { + push.disconnect(new Command() { + @Override + public void execute() { + push = null; + /* + * If push has been enabled again while we were waiting for + * the old connection to disconnect, now is the right time + * to open a new connection + */ + if (pushState.mode.isEnabled()) { + setPushEnabled(true); + } + + /* + * Send anything that was enqueued while we waited for the + * connection to close + */ + if (getServerRpcQueue().isFlushPending()) { + getServerRpcQueue().flush(); + } + } + }); + } + } + + public void startRequest() { + if (hasActiveRequest) { + getLogger().severe( + "Trying to start a new request while another is active"); + } + hasActiveRequest = true; + connection.fireEvent(new RequestStartingEvent(connection)); + } + + public void endRequest() { + if (!hasActiveRequest) { + getLogger().severe("No active request"); + } + // After sendInvocationsToServer() there may be a new active + // request, so we must set hasActiveRequest to false before, not after, + // the call. + hasActiveRequest = false; + + if (connection.isApplicationRunning()) { + if (getServerRpcQueue().isFlushPending()) { + sendInvocationsToServer(); + } + runPostRequestHooks(connection.getConfiguration().getRootPanelId()); + } + + // deferring to avoid flickering + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (!connection.isApplicationRunning() + || !(hasActiveRequest() || getServerRpcQueue() + .isFlushPending())) { + getLoadingIndicator().hide(); + + // If on Liferay and session expiration management is in + // use, extend session duration on each request. + // Doing it here rather than before the request to improve + // responsiveness. + // Postponed until the end of the next request if other + // requests still pending. + extendLiferaySession(); + } + } + }); + connection.fireEvent(new ResponseHandlingEndedEvent(connection)); + } + + /** + * Runs possibly registered client side post request hooks. This is expected + * to be run after each uidl request made by Vaadin application. + * + * @param appId + */ + public static native void runPostRequestHooks(String appId) + /*-{ + if ($wnd.vaadin.postRequestHooks) { + for ( var hook in $wnd.vaadin.postRequestHooks) { + if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") { + try { + $wnd.vaadin.postRequestHooks[hook](appId); + } catch (e) { + } + } + } + } + }-*/; + + /** + * If on Liferay and logged in, ask the client side session management + * JavaScript to extend the session duration. + * + * Otherwise, Liferay client side JavaScript will explicitly expire the + * session even though the server side considers the session to be active. + * See ticket #8305 for more information. + */ + public static native void extendLiferaySession() + /*-{ + if ($wnd.Liferay && $wnd.Liferay.Session) { + $wnd.Liferay.Session.extend(); + // if the extend banner is visible, hide it + if ($wnd.Liferay.Session.banner) { + $wnd.Liferay.Session.banner.remove(); + } + } + }-*/; + + /** + * Indicates whether or not there are currently active UIDL requests. Used + * internally to sequence requests properly, seldom needed in Widgets. + * + * @return true if there are active requests + */ + public boolean hasActiveRequest() { + return hasActiveRequest; + } + + /** + * Returns a human readable string representation of the method used to + * communicate with the server. + * + * @return A string representation of the current transport type + */ + public String getCommunicationMethodName() { + String clientToServer = "XHR"; + String serverToClient = "-"; + if (push != null) { + serverToClient = push.getTransportType(); + if (push.isBidirectional()) { + clientToServer = serverToClient; + } + } + + return "Client to server: " + clientToServer + ", " + + "server to client: " + serverToClient; + } + + private ConnectionStateHandler getConnectionStateHandler() { + return connection.getConnectionStateHandler(); + } + + private MessageHandler getMessageHandler() { + return connection.getMessageHandler(); + } + + private VLoadingIndicator getLoadingIndicator() { + return connection.getLoadingIndicator(); + } + + /** + * Resynchronize the client side, i.e. reload all component hierarchy and + * state from the server + */ + public void resynchronize() { + getLogger().info("Resynchronizing from server"); + JsonObject resyncParam = Json.createObject(); + resyncParam.put(ApplicationConstants.RESYNCHRONIZE_ID, true); + send(Json.createArray(), resyncParam); + } + + /** + * Used internally to update what the server expects + * + * @param clientToServerMessageId + * the new client id to set + * @param force + * true if the id must be updated, false otherwise + */ + public void setClientToServerMessageId(int nextExpectedId, boolean force) { + if (nextExpectedId == clientToServerMessageId) { + // No op as everything matches they way it should + return; + } + if (force) { + getLogger().info( + "Forced update of clientId to " + clientToServerMessageId); + clientToServerMessageId = nextExpectedId; + return; + } + + if (nextExpectedId > clientToServerMessageId) { + if (clientToServerMessageId == 0) { + // We have never sent a message to the server, so likely the + // server knows better (typical case is that we refreshed a + // @PreserveOnRefresh UI) + getLogger().info( + "Updating client-to-server id to " + nextExpectedId + + " based on server"); + } else { + getLogger().warning( + "Server expects next client-to-server id to be " + + nextExpectedId + " but we were going to use " + + clientToServerMessageId + ". Will use " + + nextExpectedId + "."); + } + clientToServerMessageId = nextExpectedId; + } else { + // Server has not yet seen all our messages + // Do nothing as they will arrive eventually + } + } + +} diff --git a/client/src/com/vaadin/client/communication/PushConnection.java b/client/src/com/vaadin/client/communication/PushConnection.java index 8066746dc6..489d2c39a4 100644 --- a/client/src/com/vaadin/client/communication/PushConnection.java +++ b/client/src/com/vaadin/client/communication/PushConnection.java @@ -18,8 +18,8 @@ package com.vaadin.client.communication; import com.google.gwt.user.client.Command; import com.vaadin.client.ApplicationConnection; -import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler; import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; + import elemental.json.JsonObject; /** @@ -41,18 +41,19 @@ public interface PushConnection { * The ApplicationConnection */ public void init(ApplicationConnection connection, - PushConfigurationState pushConfigurationState, - CommunicationErrorHandler errorHandler); + PushConfigurationState pushConfigurationState); /** * Pushes a message to the server. Will throw an exception if the connection * is not active (see {@link #isActive()}). * <p> - * Implementation detail: The implementation is responsible for queuing - * messages that are pushed after {@link #init(ApplicationConnection)} has - * been called but before the connection has internally been set up and then - * replay those messages in the original order when the connection has been - * established. + * Implementation detail: If the push connection is not connected and the + * message can thus not be sent, the implementation must call + * {@link ConnectionStateHandler#pushNotConnected(JsonObject)}, which + * will retry the send later. + * <p> + * This method must not be called if the push connection is not + * bidirectional (if {@link #isBidirectional()} returns false) * * @param payload * the payload to push @@ -102,4 +103,19 @@ public interface PushConnection { */ public String getTransportType(); + /** + * Checks whether this push connection should be used for communication in + * both directions or if an XHR should be used for client to server + * communication. + * + * A bidirectional push connection must be able to reliably inform about its + * connection state. + * + * @since 7.6 + * @return true if the push connection should be used for messages in both + * directions, false if it should only be used for server to client + * messages + */ + public boolean isBidirectional(); + } diff --git a/client/src/com/vaadin/client/communication/ReconnectDialog.java b/client/src/com/vaadin/client/communication/ReconnectDialog.java new file mode 100644 index 0000000000..1e77eb63b8 --- /dev/null +++ b/client/src/com/vaadin/client/communication/ReconnectDialog.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.vaadin.client.ApplicationConnection; + +/** + * Interface which must be implemented by the reconnect dialog + * + * @since 7.6 + * @author Vaadin Ltd + */ +public interface ReconnectDialog { + + /** + * Sets the main text shown in the dialog + * + * @param text + * the text to show + */ + void setText(String text); + + /** + * Sets the reconnecting state, which is true if we are trying to + * re-establish a connection with the server. + * + * @param reconnecting + * true if we are trying to re-establish the server connection, + * false if we have given up + */ + void setReconnecting(boolean reconnecting); + + /** + * Checks if the reconnect dialog is visible to the user + * + * @return true if the user can see the dialog, false otherwise + */ + boolean isVisible(); + + /** + * Shows the dialog to the user + * + * @param connection + * the application connection this is related to + */ + void show(ApplicationConnection connection); + + /** + * Hides the dialog from the user + */ + void hide(); + + /** + * Sets the modality of the dialog. If the dialog is set to modal, it will + * prevent the usage of the application while the dialog is being shown. If + * not modal, the user can continue to use the application as normally and + * all server events will be queued until connection has been + * re-established. + * + * @param modal + * true to make the dialog modal, false to allow usage while + * dialog is shown + */ + void setModal(boolean modal); + + /** + * Checks the modality of the dialog. + * + * @see #setModal(boolean) + * @return true if the dialog is modal, false otherwise + */ + boolean isModal(); + + /** + * Called once after initialization to allow the reconnect dialog to preload + * required resources, which might not be available when the server + * connection is gone + */ + void preload(ApplicationConnection connection); +} diff --git a/client/src/com/vaadin/client/communication/RpcProxy.java b/client/src/com/vaadin/client/communication/RpcProxy.java index 31b5c92707..b757590f77 100644 --- a/client/src/com/vaadin/client/communication/RpcProxy.java +++ b/client/src/com/vaadin/client/communication/RpcProxy.java @@ -58,8 +58,12 @@ public class RpcProxy { MethodInvocation invocation = new MethodInvocation( connector.getConnectorId(), rpcInterface.getName(), method.getName(), params); - connector.getConnection().addMethodInvocationToQueue(invocation, - method.isDelayed(), method.isLastOnly()); + ServerRpcQueue serverRpcQueue = ServerRpcQueue.get(connector + .getConnection()); + serverRpcQueue.add(invocation, method.isLastOnly()); + if (!method.isDelayed()) { + serverRpcQueue.flush(); + } // No RPC iface should have a return value return null; } diff --git a/client/src/com/vaadin/client/communication/ServerRpcQueue.java b/client/src/com/vaadin/client/communication/ServerRpcQueue.java new file mode 100644 index 0000000000..2a9a8d4204 --- /dev/null +++ b/client/src/com/vaadin/client/communication/ServerRpcQueue.java @@ -0,0 +1,342 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.logging.Logger; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.metadata.Method; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Type; +import com.vaadin.client.metadata.TypeDataStore; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.communication.MethodInvocation; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonValue; + +/** + * Manages the queue of server invocations (RPC) which are waiting to be sent to + * the server. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class ServerRpcQueue { + + /** + * The pending method invocations that will be send to the server by + * {@link #sendPendingCommand}. The key is defined differently based on + * whether the method invocation is enqueued with lastonly. With lastonly + * enabled, the method signature ( {@link MethodInvocation#getLastOnlyTag()} + * ) is used as the key to make enable removing a previously enqueued + * invocation. Without lastonly, an incremental id based on + * {@link #lastInvocationTag} is used to get unique values. + */ + private LinkedHashMap<String, MethodInvocation> pendingInvocations = new LinkedHashMap<String, MethodInvocation>(); + + private int lastInvocationTag = 0; + + protected ApplicationConnection connection; + private boolean flushPending = false; + + private boolean flushScheduled = false; + + public ServerRpcQueue() { + + } + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + } + + private static Logger getLogger() { + return Logger.getLogger(ServerRpcQueue.class.getName()); + } + + /** + * Removes any pending invocation of the given method from the queue + * + * @param invocation + * The invocation to remove + */ + public void removeMatching(MethodInvocation invocation) { + Iterator<MethodInvocation> iter = pendingInvocations.values() + .iterator(); + while (iter.hasNext()) { + MethodInvocation mi = iter.next(); + if (mi.equals(invocation)) { + iter.remove(); + } + } + } + + /** + * Adds an explicit RPC method invocation to the send queue. + * + * @param invocation + * RPC method invocation + * @param delayed + * <code>false</code> to trigger sending within a short time + * window (possibly combining subsequent calls to a single + * request), <code>true</code> to let the framework delay sending + * of RPC calls and variable changes until the next non-delayed + * change + * @param lastOnly + * <code>true</code> to remove all previously delayed invocations + * of the same method that were also enqueued with lastonly set + * to <code>true</code>. <code>false</code> to add invocation to + * the end of the queue without touching previously enqueued + * invocations. + */ + public void add(MethodInvocation invocation, boolean lastOnly) { + if (!connection.isApplicationRunning()) { + getLogger() + .warning( + "Trying to invoke method on not yet started or stopped application"); + return; + } + String tag; + if (lastOnly) { + tag = invocation.getLastOnlyTag(); + assert !tag.matches("\\d+") : "getLastOnlyTag value must have at least one non-digit character"; + pendingInvocations.remove(tag); + } else { + tag = Integer.toString(lastInvocationTag++); + } + pendingInvocations.put(tag, invocation); + } + + /** + * Returns a collection of all queued method invocations + * <p> + * The returned collection must not be modified in any way + * + * @return a collection of all queued method invocations + */ + public Collection<MethodInvocation> getAll() { + return pendingInvocations.values(); + } + + /** + * Clears the queue + */ + public void clear() { + pendingInvocations.clear(); + // Keep tag string short + lastInvocationTag = 0; + flushPending = false; + } + + /** + * Returns the current size of the queue + * + * @return the number of invocations in the queue + */ + public int size() { + return pendingInvocations.size(); + } + + /** + * Returns the server RPC queue for the given application + * + * @param connection + * the application connection which owns the queue + * @return the server rpc queue for the given application + */ + public static ServerRpcQueue get(ApplicationConnection connection) { + return connection.getServerRpcQueue(); + } + + /** + * Checks if the queue is empty + * + * @return true if the queue is empty, false otherwise + */ + public boolean isEmpty() { + return size() == 0; + } + + /** + * Triggers a send of server RPC and legacy variable changes to the server. + */ + public void flush() { + if (flushScheduled) { + return; + } + flushPending = true; + flushScheduled = true; + Scheduler.get().scheduleFinally(scheduledFlushCommand); + } + + private final ScheduledCommand scheduledFlushCommand = new ScheduledCommand() { + @Override + public void execute() { + flushScheduled = false; + if (!isFlushPending()) { + // Somebody else cleared the queue before we had the chance + return; + } + connection.getMessageSender().sendInvocationsToServer(); + } + }; + + /** + * Checks if a flush operation is pending + * + * @return true if a flush is pending, false otherwise + */ + public boolean isFlushPending() { + return flushPending; + } + + /** + * Checks if a loading indicator should be shown when the RPCs have been + * sent to the server and we are waiting for a response + * + * @return true if a loading indicator should be shown, false otherwise + */ + public boolean showLoadingIndicator() { + for (MethodInvocation invocation : getAll()) { + if (isLegacyVariableChange(invocation)) { + // Always show loading indicator for legacy requests + return true; + } else if (!isJavascriptRpc(invocation)) { + Type type = new Type(invocation.getInterfaceName(), null); + Method method = type.getMethod(invocation.getMethodName()); + if (!TypeDataStore.isNoLoadingIndicator(method)) { + return true; + } + } + } + return false; + } + + /** + * Returns the current invocations as JSON + * + * @return the current invocations in a JSON format ready to be sent to the + * server + */ + public JsonArray toJson() { + JsonArray json = Json.createArray(); + if (isEmpty()) { + return json; + } + + for (MethodInvocation invocation : getAll()) { + String connectorId = invocation.getConnectorId(); + if (!connectorExists(connectorId)) { + getLogger().info( + "Ignoring RPC for removed connector: " + connectorId + + ": " + invocation.toString()); + continue; + } + + JsonArray invocationJson = Json.createArray(); + invocationJson.set(0, connectorId); + invocationJson.set(1, invocation.getInterfaceName()); + invocationJson.set(2, invocation.getMethodName()); + JsonArray paramJson = Json.createArray(); + + Type[] parameterTypes = null; + if (!isLegacyVariableChange(invocation) + && !isJavascriptRpc(invocation)) { + try { + Type type = new Type(invocation.getInterfaceName(), null); + Method method = type.getMethod(invocation.getMethodName()); + parameterTypes = method.getParameterTypes(); + } catch (NoDataException e) { + throw new RuntimeException("No type data for " + + invocation.toString(), e); + } + } + + for (int i = 0; i < invocation.getParameters().length; ++i) { + // TODO non-static encoder? + Type type = null; + if (parameterTypes != null) { + type = parameterTypes[i]; + } + Object value = invocation.getParameters()[i]; + JsonValue jsonValue = JsonEncoder.encode(value, type, + connection); + paramJson.set(i, jsonValue); + } + invocationJson.set(3, paramJson); + json.set(json.length(), invocationJson); + } + + return json; + } + + /** + * Checks if the connector with the given id is still ok to use (has not + * been removed) + * + * @param connectorId + * the connector id to check + * @return true if the connector exists, false otherwise + */ + private boolean connectorExists(String connectorId) { + ConnectorMap connectorMap = ConnectorMap.get(connection); + return connectorMap.hasConnector(connectorId) + || connectorMap.isDragAndDropPaintable(connectorId); + } + + /** + * Checks if the given method invocation originates from Javascript + * + * @param invocation + * the invocation to check + * @return true if the method invocation originates from javascript, false + * otherwise + */ + public static boolean isJavascriptRpc(MethodInvocation invocation) { + return invocation instanceof JavaScriptMethodInvocation; + } + + /** + * Checks if the given method invocation represents a Vaadin 6 variable + * change + * + * @param invocation + * the invocation to check + * @return true if the method invocation is a legacy variable change, false + * otherwise + */ + public static boolean isLegacyVariableChange(MethodInvocation invocation) { + return ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation + .getInterfaceName()) + && ApplicationConstants.UPDATE_VARIABLE_METHOD + .equals(invocation.getMethodName()); + } + +} diff --git a/client/src/com/vaadin/client/communication/TranslatedURLReference.java b/client/src/com/vaadin/client/communication/TranslatedURLReference.java index b99f4c6e32..9296662234 100644 --- a/client/src/com/vaadin/client/communication/TranslatedURLReference.java +++ b/client/src/com/vaadin/client/communication/TranslatedURLReference.java @@ -30,8 +30,11 @@ public class TranslatedURLReference extends URLReference { private ApplicationConnection connection; /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * * @param connection - * the connection to set + * the application connection this instance is connected to */ public void setConnection(ApplicationConnection connection) { this.connection = connection; diff --git a/client/src/com/vaadin/client/communication/XhrConnection.java b/client/src/com/vaadin/client/communication/XhrConnection.java new file mode 100644 index 0000000000..aefdafec87 --- /dev/null +++ b/client/src/com/vaadin/client/communication/XhrConnection.java @@ -0,0 +1,276 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.Date; +import java.util.logging.Logger; + +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.Response; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.Window.ClosingEvent; +import com.google.gwt.user.client.Window.ClosingHandler; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.CommunicationHandler; +import com.vaadin.client.ApplicationConnection.RequestStartingEvent; +import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent; +import com.vaadin.client.ApplicationConnection.ResponseHandlingStartedEvent; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ValueMap; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.JsonConstants; +import com.vaadin.shared.ui.ui.UIConstants; +import com.vaadin.shared.util.SharedUtil; + +import elemental.json.JsonObject; + +/** + * Provides a connection to the /UIDL url on the server and knows how to send + * messages to that end point + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class XhrConnection { + + private ApplicationConnection connection; + + /** + * Webkit will ignore outgoing requests while waiting for a response to a + * navigation event (indicated by a beforeunload event). When this happens, + * we should keep trying to send the request every now and then until there + * is a response or until it throws an exception saying that it is already + * being sent. + */ + private boolean webkitMaybeIgnoringRequests = false; + + public XhrConnection() { + Window.addWindowClosingHandler(new ClosingHandler() { + @Override + public void onWindowClosing(ClosingEvent event) { + webkitMaybeIgnoringRequests = true; + } + }); + } + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + + connection.addHandler(ResponseHandlingEndedEvent.TYPE, + new CommunicationHandler() { + @Override + public void onRequestStarting(RequestStartingEvent e) { + } + + @Override + public void onResponseHandlingStarted( + ResponseHandlingStartedEvent e) { + } + + @Override + public void onResponseHandlingEnded( + ResponseHandlingEndedEvent e) { + webkitMaybeIgnoringRequests = false; + } + }); + + } + + private static Logger getLogger() { + return Logger.getLogger(XhrConnection.class.getName()); + } + + protected XhrResponseHandler createResponseHandler() { + return new XhrResponseHandler(); + } + + public class XhrResponseHandler implements RequestCallback { + + private JsonObject payload; + private Date requestStartTime; + + public XhrResponseHandler() { + } + + /** + * Sets the payload which was sent to the server + * + * @param payload + * the payload which was sent to the server + */ + public void setPayload(JsonObject payload) { + this.payload = payload; + } + + @Override + public void onError(Request request, Throwable exception) { + getConnectionStateHandler().xhrException( + new XhrConnectionError(request, payload, exception)); + } + + @Override + public void onResponseReceived(Request request, Response response) { + int statusCode = response.getStatusCode(); + + if (statusCode != 200) { + // There was a problem + XhrConnectionError problemEvent = new XhrConnectionError( + request, payload, response); + + getConnectionStateHandler().xhrInvalidStatusCode(problemEvent); + return; + } + + getLogger().info( + "Server visit took " + + String.valueOf((new Date()).getTime() + - requestStartTime.getTime()) + "ms"); + + String contentType = response.getHeader("Content-Type"); + if (contentType == null + || !contentType.startsWith("application/json")) { + getConnectionStateHandler().xhrInvalidContent( + new XhrConnectionError(request, payload, response)); + return; + } + + // for(;;);["+ realJson +"]" + String responseText = response.getText(); + + ValueMap json = MessageHandler.parseWrappedJson(responseText); + if (json == null) { + // Invalid string (not wrapped as expected or can't parse) + getConnectionStateHandler().xhrInvalidContent( + new XhrConnectionError(request, payload, response)); + return; + } + + getConnectionStateHandler().xhrOk(); + getLogger().info("Received xhr message: " + responseText); + getMessageHandler().handleMessage(json); + } + + /** + * Sets the time when the request was sent + * + * @param requestStartTime + * the time when the request was sent + */ + public void setRequestStartTime(Date requestStartTime) { + this.requestStartTime = requestStartTime; + + } + }; + + /** + * Sends an asynchronous UIDL request to the server using the given URI. + * + * @param payload + * The URI to use for the request. May includes GET parameters + * @throws RequestException + * if the request could not be sent + */ + public void send(JsonObject payload) { + RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, getUri()); + // TODO enable timeout + // rb.setTimeoutMillis(timeoutMillis); + // TODO this should be configurable + rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE); + rb.setRequestData(payload.toJson()); + + XhrResponseHandler responseHandler = createResponseHandler(); + responseHandler.setPayload(payload); + responseHandler.setRequestStartTime(new Date()); + + rb.setCallback(responseHandler); + + getLogger().info("Sending xhr message to server: " + payload.toJson()); + try { + final Request request = rb.send(); + + if (webkitMaybeIgnoringRequests && BrowserInfo.get().isWebkit()) { + final int retryTimeout = 250; + new Timer() { + @Override + public void run() { + // Use native js to access private field in Request + if (resendRequest(request) + && webkitMaybeIgnoringRequests) { + // Schedule retry if still needed + schedule(retryTimeout); + } + } + }.schedule(retryTimeout); + } + } catch (RequestException e) { + getConnectionStateHandler().xhrException( + new XhrConnectionError(null, payload, e)); + } + } + + /** + * Retrieves the URI to use when sending RPCs to the server + * + * @return The URI to use for server messages. + */ + protected String getUri() { + String uri = connection + .translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.UIDL_PATH + '/'); + + uri = SharedUtil.addGetParameters(uri, UIConstants.UI_ID_PARAMETER + + "=" + connection.getConfiguration().getUIId()); + + return uri; + + } + + private ConnectionStateHandler getConnectionStateHandler() { + return connection.getConnectionStateHandler(); + } + + private MessageHandler getMessageHandler() { + return connection.getMessageHandler(); + } + + private static native boolean resendRequest(Request request) + /*-{ + var xhr = request.@com.google.gwt.http.client.Request::xmlHttpRequest + if (xhr.readyState != 1) { + // Progressed to some other readyState -> no longer blocked + return false; + } + try { + xhr.send(); + return true; + } catch (e) { + // send throws exception if it is running for real + return false; + } + }-*/; + +} diff --git a/client/src/com/vaadin/client/communication/XhrConnectionError.java b/client/src/com/vaadin/client/communication/XhrConnectionError.java new file mode 100644 index 0000000000..025f1d70e7 --- /dev/null +++ b/client/src/com/vaadin/client/communication/XhrConnectionError.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; + +import elemental.json.JsonObject; + +/** + * XhrConnectionError provides detail about an error which occured during an XHR + * request to the server + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class XhrConnectionError { + + private Throwable exception; + private Request request; + private Response response; + private JsonObject payload; + + /** + * Constructs an event from the given request, payload and exception + * + * @param request + * the request which failed + * @param payload + * the payload which was going to the server + * @param exception + * the exception describing the problem + */ + public XhrConnectionError(Request request, JsonObject payload, + Throwable exception) { + this.request = request; + this.exception = exception; + this.payload = payload; + } + + /** + * Constructs an event from the given request, response and payload + * + * @param request + * the request which failed + * @param payload + * the payload which was going to the server + * @param response + * the response for the request + */ + public XhrConnectionError(Request request, JsonObject payload, + Response response) { + this.request = request; + this.response = response; + this.payload = payload; + } + + /** + * Returns the exception which caused the problem, if available + * + * @return the exception which caused the problem, or null if not available + */ + public Throwable getException() { + return exception; + } + + /** + * Returns the request for which the problem occurred + * + * @return the request where the problem occurred + */ + public Request getRequest() { + return request; + } + + /** + * Returns the received response, if available + * + * @return the received response, or null if not available + */ + public Response getResponse() { + return response; + } + + /** + * Returns the payload which was sent to the server + * + * @return the payload which was sent, never null + */ + public JsonObject getPayload() { + return payload; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/InfoSection.java b/client/src/com/vaadin/client/debug/internal/InfoSection.java index dfb31cdd18..39ff345cb1 100644 --- a/client/src/com/vaadin/client/debug/internal/InfoSection.java +++ b/client/src/com/vaadin/client/debug/internal/InfoSection.java @@ -166,7 +166,7 @@ public class InfoSection implements Section { addRow("Theme", connection.getUIConnector().getActiveTheme()); String communicationMethodInfo = connection - .getCommunicationMethodName(); + .getMessageSender().getCommunicationMethodName(); int pollInterval = connection.getUIConnector().getState().pollInterval; if (pollInterval > 0) { communicationMethodInfo += " (poll interval " + pollInterval diff --git a/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java b/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java index d48571452e..364b948573 100644 --- a/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java +++ b/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java @@ -24,6 +24,7 @@ import com.google.gwt.core.client.JsArray; import com.vaadin.client.ServerConnector; import com.vaadin.client.Util; import com.vaadin.client.communication.JavaScriptMethodInvocation; +import com.vaadin.client.communication.ServerRpcQueue; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.extensions.AbstractExtensionConnector; import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc; @@ -122,10 +123,11 @@ public class JavaScriptManagerConnector extends AbstractExtensionConnector { * Must invoke manually as the RPC interface can't be used in GWT * because of the JSONArray parameter */ - getConnection().addMethodInvocationToQueue( - new JavaScriptMethodInvocation(getConnectorId(), - "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", - "call", parameters), false, false); + ServerRpcQueue rpcQueue = ServerRpcQueue.get(getConnection()); + rpcQueue.add(new JavaScriptMethodInvocation(getConnectorId(), + "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call", + parameters), false); + rpcQueue.flush(); } @Override diff --git a/client/src/com/vaadin/client/ui/VNotification.java b/client/src/com/vaadin/client/ui/VNotification.java index 2f68976471..d5387b761a 100644 --- a/client/src/com/vaadin/client/ui/VNotification.java +++ b/client/src/com/vaadin/client/ui/VNotification.java @@ -655,7 +655,7 @@ public class VNotification extends VOverlay { n.show(html.toString(), VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM); } else { - ApplicationConnection.redirect(url); + WidgetUtil.redirect(url); } } @@ -674,7 +674,7 @@ public class VNotification extends VOverlay { @Override public void notificationHidden(HideEvent event) { - ApplicationConnection.redirect(url); + WidgetUtil.redirect(url); } } diff --git a/client/src/com/vaadin/client/ui/VScrollTable.java b/client/src/com/vaadin/client/ui/VScrollTable.java index 4e030b8e49..6bb8f063a6 100644 --- a/client/src/com/vaadin/client/ui/VScrollTable.java +++ b/client/src/com/vaadin/client/ui/VScrollTable.java @@ -2620,7 +2620,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets, @Override public void run() { - if (client.hasActiveRequest() || navKeyDown) { + if (client.getMessageSender().hasActiveRequest() + || navKeyDown) { // if client connection is busy, don't bother loading it more VConsole.log("Postponed rowfetch"); schedule(250); diff --git a/client/src/com/vaadin/client/ui/VUpload.java b/client/src/com/vaadin/client/ui/VUpload.java index dff45a6951..2800acccf9 100644 --- a/client/src/com/vaadin/client/ui/VUpload.java +++ b/client/src/com/vaadin/client/ui/VUpload.java @@ -36,9 +36,12 @@ import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.SimplePanel; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ConnectorMap; import com.vaadin.client.StyleConstants; import com.vaadin.client.VConsole; +import com.vaadin.client.ui.upload.UploadConnector; import com.vaadin.client.ui.upload.UploadIFrameOnloadStrategy; +import com.vaadin.shared.ui.upload.UploadServerRpc; /** * @@ -246,7 +249,9 @@ public class VUpload extends SimplePanel { t.cancel(); } VConsole.log("VUpload:Submit complete"); - client.sendPendingVariableChanges(); + ((UploadConnector) ConnectorMap.get(client) + .getConnector(VUpload.this)).getRpcProxy( + UploadServerRpc.class).poll(); } rebuildPanel(); diff --git a/client/src/com/vaadin/client/ui/datefield/DateFieldConnector.java b/client/src/com/vaadin/client/ui/datefield/DateFieldConnector.java index 042b8db8e2..c6b410fa74 100644 --- a/client/src/com/vaadin/client/ui/datefield/DateFieldConnector.java +++ b/client/src/com/vaadin/client/ui/datefield/DateFieldConnector.java @@ -32,7 +32,6 @@ import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.datefield.PopupDateFieldState; import com.vaadin.shared.ui.datefield.Resolution; import com.vaadin.ui.DateField; -import com.vaadin.ui.PopupDateField; @Connect(DateField.class) public class DateFieldConnector extends TextualDateConnector { @@ -60,7 +59,7 @@ public class DateFieldConnector extends TextualDateConnector { * communicated to the server. */ if (getWidget().isImmediate()) { - getConnection().sendPendingVariableChanges(); + getConnection().getServerRpcQueue().flush(); } } }); diff --git a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java index efca46b522..109b69f0c4 100644 --- a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java +++ b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java @@ -488,7 +488,8 @@ public class VDragAndDropManager { Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { @Override public boolean execute() { - if (!client.hasActiveRequest()) { + if (!client.getMessageSender() + .hasActiveRequest()) { removeActiveDragSourceStyleName(dragSource); return false; } diff --git a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java index 8fa885c2b9..dbd530dde1 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java @@ -226,7 +226,7 @@ public abstract class AbstractOrderedLayoutConnector extends /** * The id of the previous response for which state changes have been * processed. If this is the same as the - * {@link ApplicationConnection#getLastResponseId()}, it means that we can + * {@link ApplicationConnection#getLastSeenServerSyncId()}, it means that we can * skip some quite expensive calculations because we know that the state * hasn't changed since the last time the values were calculated. */ @@ -422,7 +422,7 @@ public abstract class AbstractOrderedLayoutConnector extends */ private void updateInternalState() { // Avoid updating again for the same data - int lastResponseId = getConnection().getLastResponseId(); + int lastResponseId = getConnection().getLastSeenServerSyncId(); if (processedResponseId == lastResponseId) { return; } diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index e0a4608505..f5656dfdc4 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -175,7 +175,7 @@ public class UIConnector extends AbstractSingleComponentContainerConnector event.getWidth(), Window.getClientWidth(), Window.getClientHeight()); if (getState().immediate || getPageState().hasResizeListeners) { - getConnection().sendPendingVariableChanges(); + getConnection().getServerRpcQueue().flush(); } } }); @@ -770,9 +770,12 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } if (stateChangeEvent.hasPropertyChanged("pushConfiguration")) { - getConnection().setPushEnabled( + getConnection().getMessageSender().setPushEnabled( getState().pushConfiguration.mode.isEnabled()); } + if (stateChangeEvent.hasPropertyChanged("reconnectDialogConfiguration")) { + getConnection().getConnectionStateHandler().configurationUpdated(); + } if (stateChangeEvent.hasPropertyChanged("overlayContainerLabel")) { VOverlay.setOverlayContainerLabel(getConnection(), @@ -797,13 +800,13 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } getRpcProxy(UIServerRpc.class).poll(); // Send changes even though poll is @Delayed - getConnection().sendPendingVariableChanges(); + getConnection().getServerRpcQueue().flush(); } }; pollTimer.scheduleRepeating(getState().pollInterval); } else { // Ensure no more polls are sent as polling has been disabled - getConnection().removePendingInvocations( + getConnection().getServerRpcQueue().removeMatching( new MethodInvocation(getConnectorId(), UIServerRpc.class .getName(), "poll")); } @@ -1042,7 +1045,7 @@ public class UIConnector extends AbstractSingleComponentContainerConnector // Request a full resynchronization from the server to deal with legacy // components - getConnection().repaintAll(); + getConnection().getMessageSender().resynchronize(); // Immediately update state and do layout while waiting for the resync forceStateChangeRecursively(UIConnector.this); diff --git a/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java b/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java new file mode 100644 index 0000000000..c2752f1953 --- /dev/null +++ b/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import org.junit.Assert; +import org.junit.Test; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class ServerMessageHandlerTest { + + @Test + public void unwrapValidJson() { + String payload = "{'foo': 'bar'}"; + Assert.assertEquals(payload, + MessageHandler.stripJSONWrapping("for(;;);[" + payload + "]")); + + } + + @Test + public void unwrapUnwrappedJson() { + String payload = "{'foo': 'bar'}"; + Assert.assertNull(MessageHandler.stripJSONWrapping(payload)); + + } + + @Test + public void unwrapNull() { + Assert.assertNull(MessageHandler.stripJSONWrapping(null)); + + } + + @Test + public void unwrapEmpty() { + Assert.assertNull(MessageHandler.stripJSONWrapping("")); + + } +} diff --git a/ivysettings.xml b/ivysettings.xml index c97b6a3bfb..5208b6bd54 100644 --- a/ivysettings.xml +++ b/ivysettings.xml @@ -27,6 +27,8 @@ <modules> <module organisation="com.vaadin" name="vaadin-testbench" resolver="vaadin-addons" /> + <module organisation="com.vaadin" name="vaadin-testbench-parent" + resolver="vaadin-addons" /> <module organisation="com.vaadin" name="vaadin-testbench-core" resolver="vaadin-addons" /> <module organisation="com.vaadin" name="vaadin-testbench-api" diff --git a/server/src/com/vaadin/server/LegacyCommunicationManager.java b/server/src/com/vaadin/server/LegacyCommunicationManager.java index e982cdf10a..87b484a65f 100644 --- a/server/src/com/vaadin/server/LegacyCommunicationManager.java +++ b/server/src/com/vaadin/server/LegacyCommunicationManager.java @@ -353,6 +353,10 @@ public class LegacyCommunicationManager implements Serializable { res.clear(); } + public boolean isEmpty() { + return res.isEmpty(); + } + } /** diff --git a/server/src/com/vaadin/server/LocaleService.java b/server/src/com/vaadin/server/LocaleService.java index 0274d227ab..ccdc49f690 100644 --- a/server/src/com/vaadin/server/LocaleService.java +++ b/server/src/com/vaadin/server/LocaleService.java @@ -23,6 +23,7 @@ import java.io.Serializable; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Locale; import java.util.logging.Logger; @@ -120,9 +121,24 @@ public class LocaleService implements Serializable { LocaleData localeData = new LocaleData(); localeData.name = locale.toString(); + Calendar c = Calendar.getInstance(locale); + c.set(2015, 0, 1); + SimpleDateFormat shortMonthFormat = new SimpleDateFormat("MMM", locale); + SimpleDateFormat longMonthFormat = new SimpleDateFormat("MMMM", locale); + + int monthsInYear = c.getMaximum(Calendar.MONTH) + 1; + localeData.shortMonthNames = new String[monthsInYear]; + localeData.monthNames = new String[monthsInYear]; + for (int month = 0; month < monthsInYear; month++) { + c.set(Calendar.MONTH, month); + String shortMonth = shortMonthFormat.format(c.getTime()); + String longMonth = longMonthFormat.format(c.getTime()); + localeData.shortMonthNames[month] = shortMonth; + localeData.monthNames[month] = longMonth; + } + final DateFormatSymbols dfs = new DateFormatSymbols(locale); - localeData.shortMonthNames = dfs.getShortMonths(); - localeData.monthNames = dfs.getMonths(); + // Client expects 0 based indexing, DateFormatSymbols use 1 based localeData.shortDayNames = new String[7]; localeData.dayNames = new String[7]; diff --git a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java index 05e4f6479f..5c0d2e14d4 100644 --- a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java +++ b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java @@ -165,7 +165,7 @@ public class AtmospherePushConnection implements PushConnection { } else { try { Writer writer = new StringWriter(); - new UidlWriter().write(getUI(), writer, false, async); + new UidlWriter().write(getUI(), writer, async); sendMessage("for(;;);[{" + writer.toString() + "}]"); } catch (Exception e) { throw new RuntimeException("Push failed", e); diff --git a/server/src/com/vaadin/server/communication/ServerRpcHandler.java b/server/src/com/vaadin/server/communication/ServerRpcHandler.java index 65fb144810..f3d4b163ff 100644 --- a/server/src/com/vaadin/server/communication/ServerRpcHandler.java +++ b/server/src/com/vaadin/server/communication/ServerRpcHandler.java @@ -29,6 +29,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.server.ClientConnector; +import com.vaadin.server.Constants; import com.vaadin.server.JsonCodec; import com.vaadin.server.LegacyCommunicationManager; import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; @@ -40,6 +41,7 @@ import com.vaadin.server.VaadinService; import com.vaadin.server.VariableOwner; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.Connector; +import com.vaadin.shared.Version; import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; import com.vaadin.shared.communication.MethodInvocation; import com.vaadin.shared.communication.ServerRpc; @@ -77,6 +79,8 @@ public class ServerRpcHandler implements Serializable { private final int syncId; private final JsonObject json; private final boolean resynchronize; + private final int clientToServerMessageId; + private String widgetsetVersion = null; public RpcRequest(String jsonString, VaadinRequest request) { json = JsonUtil.parse(jsonString); @@ -106,7 +110,19 @@ public class ServerRpcHandler implements Serializable { } else { resynchronize = false; } + if (json.hasKey(ApplicationConstants.WIDGETSET_VERSION_ID)) { + widgetsetVersion = json + .getString(ApplicationConstants.WIDGETSET_VERSION_ID); + } + if (json.hasKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) { + clientToServerMessageId = (int) json + .getNumber(ApplicationConstants.CLIENT_TO_SERVER_ID); + } else { + getLogger() + .warning("Server message without client id received"); + clientToServerMessageId = -1; + } invocations = json.getArray(ApplicationConstants.RPC_INVOCATIONS); } @@ -149,6 +165,15 @@ public class ServerRpcHandler implements Serializable { } /** + * Gets the id of the client to server message + * + * @return the server message id + */ + public int getClientToServerId() { + return clientToServerMessageId; + } + + /** * Gets the entire request in JSON format, as it was received from the * client. * <p> @@ -161,6 +186,17 @@ public class ServerRpcHandler implements Serializable { public JsonObject getRawJson() { return json; } + + /** + * Gets the widget set version reported by the client + * + * @since 7.6 + * @return The widget set version reported by the client or null if the + * message did not contain a widget set version + */ + public String getWidgetsetVersion() { + return widgetsetVersion; + } } private static final int MAX_BUFFER_SIZE = 64 * 1024; @@ -199,8 +235,43 @@ public class ServerRpcHandler implements Serializable { rpcRequest.getCsrfToken())) { throw new InvalidUIDLSecurityKeyException(""); } - handleInvocations(ui, rpcRequest.getSyncId(), - rpcRequest.getRpcInvocationsData()); + + checkWidgetsetVersion(rpcRequest.getWidgetsetVersion()); + + int expectedId = ui.getLastProcessedClientToServerId() + 1; + if (rpcRequest.getClientToServerId() != -1 + && rpcRequest.getClientToServerId() != expectedId) { + // Invalid message id, skip RPC processing but force a full + // re-synchronization of the client as it might have not received + // the previous response (e.g. due to a bad connection) + + // Must resync also for duplicate messages because the server might + // have generated a response for the first message but the response + // did not reach the client. When the client re-sends the message, + // it would only get an empty response (because the dirty flags have + // been cleared on the server) and would be out of sync + ui.getSession().getCommunicationManager().repaintAll(ui); + + if (rpcRequest.getClientToServerId() < expectedId) { + // Just a duplicate message due to a bad connection or similar + // It has already been handled by the server so it is safe to + // ignore + getLogger().fine( + "Ignoring old message from the client. Expected: " + + expectedId + ", got: " + + rpcRequest.getClientToServerId()); + } else { + getLogger().warning( + "Unexpected message id from the client. Expected: " + + expectedId + ", got: " + + rpcRequest.getClientToServerId()); + } + } else { + // Message id ok, process RPCs + ui.setLastProcessedClientToServerId(expectedId); + handleInvocations(ui, rpcRequest.getSyncId(), + rpcRequest.getRpcInvocationsData()); + } ui.getConnectorTracker().cleanConcurrentlyRemovedConnectorIds( rpcRequest.getSyncId()); @@ -208,6 +279,29 @@ public class ServerRpcHandler implements Serializable { if (rpcRequest.isResynchronize()) { ui.getSession().getCommunicationManager().repaintAll(ui); } + + } + + /** + * Checks that the version reported by the client (widgetset) matches that + * of the server. + * + * @param widgetsetVersion + * the widget set version reported by the client or null + */ + private void checkWidgetsetVersion(String widgetsetVersion) { + if (widgetsetVersion == null) { + // Only check when the widgetset version is reported. It is reported + // in the first UIDL request (not the initial request as it is a + // plain GET /) + return; + } + + if (!Version.getFullVersion().equals(widgetsetVersion)) { + getLogger().warning( + String.format(Constants.WIDGETSET_MISMATCH_INFO, + Version.getFullVersion(), widgetsetVersion)); + } } /** diff --git a/server/src/com/vaadin/server/communication/UIInitHandler.java b/server/src/com/vaadin/server/communication/UIInitHandler.java index 3a6dc1e55f..f380a6df6e 100644 --- a/server/src/com/vaadin/server/communication/UIInitHandler.java +++ b/server/src/com/vaadin/server/communication/UIInitHandler.java @@ -282,7 +282,7 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { if (session.getConfiguration().isXsrfProtectionEnabled()) { writer.write(getSecurityKeyUIDL(session)); } - new UidlWriter().write(uI, writer, true, false); + new UidlWriter().write(uI, writer, false); writer.write("}"); String initialUIDL = writer.toString(); diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java index 33a3669b7f..dda3d81453 100644 --- a/server/src/com/vaadin/server/communication/UidlRequestHandler.java +++ b/server/src/com/vaadin/server/communication/UidlRequestHandler.java @@ -22,7 +22,6 @@ import java.io.Writer; import java.util.logging.Level; import java.util.logging.Logger; -import com.vaadin.server.Constants; import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; import com.vaadin.server.ServletPortletHelper; import com.vaadin.server.SessionExpiredHandler; @@ -32,9 +31,7 @@ import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinSession; -import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.JsonConstants; -import com.vaadin.shared.Version; import com.vaadin.ui.UI; import elemental.json.JsonException; @@ -76,29 +73,12 @@ public class UidlRequestHandler extends SynchronizedRequestHandler implements return true; } - checkWidgetsetVersion(request); - // repaint requested or session has timed out and new one is created - boolean repaintAll; - - // TODO PUSH analyzeLayouts should be - // part of the message payload to make the functionality transport - // agnostic - - // Resynchronize is sent in the payload but will still support the - // parameter also for compatibility reasons - repaintAll = (request - .getParameter(ApplicationConstants.URL_PARAMETER_REPAINT_ALL) != null); - StringWriter stringWriter = new StringWriter(); try { rpcHandler.handleRpc(uI, request.getReader(), request); - if (repaintAll) { - session.getCommunicationManager().repaintAll(uI); - } - - writeUidl(request, response, uI, stringWriter, repaintAll); + writeUidl(request, response, uI, stringWriter); } catch (JsonException e) { getLogger().log(Level.SEVERE, "Error writing JSON to response", e); // Refresh on client side @@ -119,28 +99,6 @@ public class UidlRequestHandler extends SynchronizedRequestHandler implements stringWriter.toString()); } - /** - * Checks that the version reported by the client (widgetset) matches that - * of the server. - * - * @param request - */ - private void checkWidgetsetVersion(VaadinRequest request) { - String widgetsetVersion = request.getParameter("v-wsver"); - if (widgetsetVersion == null) { - // Only check when the widgetset version is reported. It is reported - // in the first UIDL request (not the initial request as it is a - // plain GET /) - return; - } - - if (!Version.getFullVersion().equals(widgetsetVersion)) { - getLogger().warning( - String.format(Constants.WIDGETSET_MISMATCH_INFO, - Version.getFullVersion(), widgetsetVersion)); - } - } - private void writeRefresh(VaadinRequest request, VaadinResponse response) throws IOException { String json = VaadinService.createCriticalNotificationJSON(null, null, @@ -149,10 +107,10 @@ public class UidlRequestHandler extends SynchronizedRequestHandler implements } private void writeUidl(VaadinRequest request, VaadinResponse response, - UI ui, Writer writer, boolean repaintAll) throws IOException { + UI ui, Writer writer) throws IOException { openJsonMessage(writer, response); - new UidlWriter().write(ui, writer, repaintAll, false); + new UidlWriter().write(ui, writer, false); closeJsonMessage(writer); } diff --git a/server/src/com/vaadin/server/communication/UidlWriter.java b/server/src/com/vaadin/server/communication/UidlWriter.java index a4797e49aa..25b1bdaaf9 100644 --- a/server/src/com/vaadin/server/communication/UidlWriter.java +++ b/server/src/com/vaadin/server/communication/UidlWriter.java @@ -63,8 +63,6 @@ public class UidlWriter implements Serializable { * The {@link UI} whose changes to write * @param writer * The writer to use - * @param repaintAll - * Whether the client should re-render the whole UI. * @param analyzeLayouts * Whether detected layout problems should be logged. * @param async @@ -74,8 +72,7 @@ public class UidlWriter implements Serializable { * @throws IOException * If the writing fails. */ - public void write(UI ui, Writer writer, boolean repaintAll, boolean async) - throws IOException { + public void write(UI ui, Writer writer, boolean async) throws IOException { VaadinSession session = ui.getSession(); VaadinService service = session.getService(); @@ -86,6 +83,8 @@ public class UidlWriter implements Serializable { Set<ClientConnector> processedConnectors = new HashSet<ClientConnector>(); LegacyCommunicationManager manager = session.getCommunicationManager(); + ClientCache clientCache = manager.getClientCache(ui); + boolean repaintAll = clientCache.isEmpty(); // Paints components ConnectorTracker uiConnectorTracker = ui.getConnectorTracker(); getLogger().log(Level.FINE, "* Creating response to client"); @@ -130,7 +129,14 @@ public class UidlWriter implements Serializable { .getCurrentSyncId() : -1; writer.write("\"" + ApplicationConstants.SERVER_SYNC_ID + "\": " + syncId + ", "); - + if (repaintAll) { + writer.write("\"" + ApplicationConstants.RESYNCHRONIZE_ID + + "\": true, "); + } + int nextClientToServerMessageId = ui + .getLastProcessedClientToServerId() + 1; + writer.write("\"" + ApplicationConstants.CLIENT_TO_SERVER_ID + + "\": " + nextClientToServerMessageId + ", "); writer.write("\"changes\" : "); JsonPaintTarget paintTarget = new JsonPaintTarget(manager, writer, @@ -202,7 +208,6 @@ public class UidlWriter implements Serializable { Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget .getUsedClientConnectors(); boolean typeMappingsOpen = false; - ClientCache clientCache = manager.getClientCache(ui); List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>(); diff --git a/server/src/com/vaadin/ui/PushConfiguration.java b/server/src/com/vaadin/ui/PushConfiguration.java index 90ad28542c..6eaf683d99 100644 --- a/server/src/com/vaadin/ui/PushConfiguration.java +++ b/server/src/com/vaadin/ui/PushConfiguration.java @@ -207,8 +207,14 @@ class PushConfigurationImpl implements PushConfiguration { @Override public Transport getTransport() { try { - return Transport + Transport tr = Transport .getByIdentifier(getParameter(PushConfigurationState.TRANSPORT_PARAM)); + if (tr == Transport.WEBSOCKET + && getState(false).alwaysUseXhrForServerRequests) { + return Transport.WEBSOCKET_XHR; + } else { + return tr; + } } catch (IllegalArgumentException e) { return null; } @@ -223,8 +229,16 @@ class PushConfigurationImpl implements PushConfiguration { */ @Override public void setTransport(Transport transport) { - setParameter(PushConfigurationState.TRANSPORT_PARAM, - transport.getIdentifier()); + if (transport == Transport.WEBSOCKET_XHR) { + getState().alwaysUseXhrForServerRequests = true; + // Atmosphere knows only about "websocket" + setParameter(PushConfigurationState.TRANSPORT_PARAM, + Transport.WEBSOCKET.getIdentifier()); + } else { + getState().alwaysUseXhrForServerRequests = false; + setParameter(PushConfigurationState.TRANSPORT_PARAM, + transport.getIdentifier()); + } } /* @@ -251,6 +265,10 @@ class PushConfigurationImpl implements PushConfiguration { */ @Override public void setFallbackTransport(Transport fallbackTransport) { + if (fallbackTransport == Transport.WEBSOCKET_XHR) { + throw new IllegalArgumentException( + "WEBSOCKET_XHR can only be used as primary transport"); + } setParameter(PushConfigurationState.FALLBACK_TRANSPORT_PARAM, fallbackTransport.getIdentifier()); } diff --git a/server/src/com/vaadin/ui/ReconnectDialogConfiguration.java b/server/src/com/vaadin/ui/ReconnectDialogConfiguration.java new file mode 100644 index 0000000000..92eb1e785f --- /dev/null +++ b/server/src/com/vaadin/ui/ReconnectDialogConfiguration.java @@ -0,0 +1,201 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +/** + * Provides method for configuring the reconnect dialog. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public interface ReconnectDialogConfiguration extends Serializable { + /** + * Gets the text to show in the reconnect dialog when trying to re-establish + * the server connection + * + * @return the text to show in the reconnect dialog + */ + public String getDialogText(); + + /** + * Sets the text to show in the reconnect dialog when trying to re-establish + * the server connection + * + * @param dialogText + * the text to show in the reconnect dialog + */ + public void setDialogText(String dialogText); + + /** + * Gets the text to show in the reconnect dialog after giving up trying to + * reconnect ({@link #getReconnectAttempts()} reached) + * + * @return the text to show in the reconnect dialog after giving up + */ + public String getDialogTextGaveUp(); + + /** + * Sets the text to show in the reconnect dialog after giving up trying to + * reconnect ({@link #getReconnectAttempts()} reached) + * + * @param dialogText + * the text to show in the reconnect dialog after giving up + */ + public void setDialogTextGaveUp(String dialogTextGaveUp); + + /** + * Gets the number of times to try to reconnect to the server before giving + * up + * + * @return the number of times to try to reconnect + */ + public int getReconnectAttempts(); + + /** + * Sets the number of times to try to reconnect to the server before giving + * up + * + * @param reconnectAttempts + * the number of times to try to reconnect + */ + public void setReconnectAttempts(int reconnectAttempts); + + /** + * Gets the interval (in milliseconds) between reconnect attempts + * + * @return the interval (in ms) between reconnect attempts + */ + public int getReconnectInterval(); + + /** + * Sets the interval (in milliseconds) between reconnect attempts + * + * @param reconnectInterval + * the interval (in ms) between reconnect attempts + */ + public void setReconnectInterval(int reconnectInterval); + + /** + * Gets the timeout (in milliseconds) between noticing a loss of connection + * and showing the dialog. + * + * @return the time to wait before showing a dialog + */ + public int getDialogGracePeriod(); + + /** + * Sets the timeout (in milliseconds) between noticing a loss of connection + * and showing the dialog. + * + * @param dialogGracePeriod + * the time to wait before showing a dialog + */ + public void setDialogGracePeriod(int dialogGracePeriod); + + /** + * Sets the modality of the dialog. + * <p> + * If the dialog is set to modal, it will prevent the usage of the + * application while the dialog is being shown. If not modal, the user can + * continue to use the application as normally and all server events will be + * queued until connection has been re-established. + * + * @param dialogModal + * true to make the dialog modal, false otherwise + */ + public void setDialogModal(boolean dialogModal); + + /** + * Checks the modality of the dialog. + * <p> + * + * @see #setDialogModal(boolean) + * @return true if the dialog is modal, false otherwise + */ + public boolean isDialogModal(); +} + +class ReconnectDialogConfigurationImpl implements ReconnectDialogConfiguration { + private UI ui; + + public ReconnectDialogConfigurationImpl(UI ui) { + this.ui = ui; + } + + @Override + public String getDialogText() { + return ui.getState(false).reconnectDialogConfiguration.dialogText; + } + + @Override + public void setDialogText(String dialogText) { + ui.getState().reconnectDialogConfiguration.dialogText = dialogText; + } + + @Override + public String getDialogTextGaveUp() { + return ui.getState(false).reconnectDialogConfiguration.dialogTextGaveUp; + } + + @Override + public void setDialogTextGaveUp(String dialogTextGaveUp) { + ui.getState().reconnectDialogConfiguration.dialogTextGaveUp = dialogTextGaveUp; + } + + @Override + public int getReconnectAttempts() { + return ui.getState(false).reconnectDialogConfiguration.reconnectAttempts; + } + + @Override + public void setReconnectAttempts(int reconnectAttempts) { + ui.getState().reconnectDialogConfiguration.reconnectAttempts = reconnectAttempts; + } + + @Override + public int getReconnectInterval() { + return ui.getState(false).reconnectDialogConfiguration.reconnectInterval; + } + + @Override + public void setReconnectInterval(int reconnectInterval) { + ui.getState().reconnectDialogConfiguration.reconnectInterval = reconnectInterval; + } + + @Override + public int getDialogGracePeriod() { + return ui.getState(false).reconnectDialogConfiguration.dialogGracePeriod; + } + + @Override + public void setDialogGracePeriod(int dialogGracePeriod) { + ui.getState().reconnectDialogConfiguration.dialogGracePeriod = dialogGracePeriod; + } + + @Override + public boolean isDialogModal() { + return ui.getState(false).reconnectDialogConfiguration.dialogModal; + } + + @Override + public void setDialogModal(boolean dialogModal) { + ui.getState().reconnectDialogConfiguration.dialogModal = dialogModal; + } + +} diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index 2129db614b..90ae713746 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -255,11 +255,19 @@ public abstract class UI extends AbstractSingleComponentContainer implements this); private PushConfiguration pushConfiguration = new PushConfigurationImpl( this); + private ReconnectDialogConfiguration reconnectDialogConfiguration = new ReconnectDialogConfigurationImpl( + this); private NotificationConfiguration notificationConfiguration = new NotificationConfigurationImpl( this); /** + * Tracks which message from the client should come next. First message from + * the client has id 0. + */ + private int lastProcessedClientToServerId = -1; + + /** * Creates a new empty UI without a caption. The content of the UI must be * set by calling {@link #setContent(Component)} before using the UI. */ @@ -1640,6 +1648,16 @@ public abstract class UI extends AbstractSingleComponentContainer implements } /** + * Retrieves the object used for configuring the reconnect dialog. + * + * @since 7.6 + * @return The instance used for reconnect dialog configuration + */ + public ReconnectDialogConfiguration getReconnectDialogConfiguration() { + return reconnectDialogConfiguration; + } + + /** * Get the label that is added to the container element, where tooltip, * notification and dialogs are added to. * @@ -1691,4 +1709,31 @@ public abstract class UI extends AbstractSingleComponentContainer implements public String getEmbedId() { return embedId; } + + /** + * Gets the last processed server message id. + * + * Used internally for communication tracking. + * + * @return lastProcessedServerMessageId the id of the last processed server + * message + * @since 7.6 + */ + public int getLastProcessedClientToServerId() { + return lastProcessedClientToServerId; + } + + /** + * Sets the last processed server message id. + * + * Used internally for communication tracking. + * + * @param lastProcessedServerMessageId + * the id of the last processed server message + * @since 7.6 + */ + public void setLastProcessedClientToServerId( + int lastProcessedClientToServerId) { + this.lastProcessedClientToServerId = lastProcessedClientToServerId; + } } diff --git a/server/src/com/vaadin/ui/Upload.java b/server/src/com/vaadin/ui/Upload.java index 693bd74dbf..2da7db53b5 100644 --- a/server/src/com/vaadin/ui/Upload.java +++ b/server/src/com/vaadin/ui/Upload.java @@ -121,6 +121,11 @@ public class Upload extends AbstractComponent implements Component.Focusable, public void change(String filename) { fireEvent(new ChangeEvent(Upload.this, filename)); } + + @Override + public void poll() { + // Nothing to do, called only to visit the server + } }); } diff --git a/server/tests/src/com/vaadin/ui/PushConfigurationTransportTest.java b/server/tests/src/com/vaadin/ui/PushConfigurationTransportTest.java index 305b2e06cd..80e7dd9261 100644 --- a/server/tests/src/com/vaadin/ui/PushConfigurationTransportTest.java +++ b/server/tests/src/com/vaadin/ui/PushConfigurationTransportTest.java @@ -40,6 +40,12 @@ public class PushConfigurationTransportTest { ui.getPushConfiguration().setTransport(transport); Assert.assertEquals(ui.getPushConfiguration().getTransport(), transport); + + if (transport == Transport.WEBSOCKET_XHR) { + Assert.assertTrue(ui.getState().pushConfiguration.alwaysUseXhrForServerRequests); + } else { + Assert.assertFalse(ui.getState().pushConfiguration.alwaysUseXhrForServerRequests); + } } } diff --git a/shared/src/com/vaadin/shared/ApplicationConstants.java b/shared/src/com/vaadin/shared/ApplicationConstants.java index 3431387b77..55d2dae946 100644 --- a/shared/src/com/vaadin/shared/ApplicationConstants.java +++ b/shared/src/com/vaadin/shared/ApplicationConstants.java @@ -133,6 +133,12 @@ public class ApplicationConstants implements Serializable { public static final String SERVER_SYNC_ID = "syncId"; /** + * The name of the parameter used to transmit the id of the client to server + * messages. + */ + public static final String CLIENT_TO_SERVER_ID = "clientId"; + + /** * Default value to use in case the security protection is disabled. */ public static final String CSRF_TOKEN_DEFAULT_VALUE = "init"; @@ -142,4 +148,10 @@ public class ApplicationConstants implements Serializable { */ public static final String RESYNCHRONIZE_ID = "resynchronize"; + /** + * The name of the parameter used for sending the widget set version to the + * server + */ + public static final String WIDGETSET_VERSION_ID = "wsver"; + } diff --git a/shared/src/com/vaadin/shared/ui/ui/Transport.java b/shared/src/com/vaadin/shared/ui/ui/Transport.java index 54d0f08434..6263f3039d 100644 --- a/shared/src/com/vaadin/shared/ui/ui/Transport.java +++ b/shared/src/com/vaadin/shared/ui/ui/Transport.java @@ -28,6 +28,12 @@ public enum Transport { */ WEBSOCKET("websocket"), /** + * Websockets for server to client, XHR for client to server + * + * @since 7.6 + */ + WEBSOCKET_XHR("websocket-xhr"), + /** * HTTP streaming * * @deprecated Use the more reliable {@link Transport#LONG_POLLING} instead. diff --git a/shared/src/com/vaadin/shared/ui/ui/UIState.java b/shared/src/com/vaadin/shared/ui/ui/UIState.java index 6f7a531eb6..442c3a1ffb 100644 --- a/shared/src/com/vaadin/shared/ui/ui/UIState.java +++ b/shared/src/com/vaadin/shared/ui/ui/UIState.java @@ -72,6 +72,7 @@ public class UIState extends TabIndexState { * @since 7.3 */ public String theme; + public ReconnectDialogConfigurationState reconnectDialogConfiguration = new ReconnectDialogConfigurationState(); { primaryStyleName = "v-ui"; // Default is 1 for legacy reasons @@ -113,6 +114,7 @@ public class UIState extends TabIndexState { public static final String TRANSPORT_PARAM = "transport"; public static final String FALLBACK_TRANSPORT_PARAM = "fallbackTransport"; + public boolean alwaysUseXhrForServerRequests = false; public PushMode mode = PushMode.DISABLED; public Map<String, String> parameters = new HashMap<String, String>(); { @@ -123,6 +125,16 @@ public class UIState extends TabIndexState { } } + public static class ReconnectDialogConfigurationState implements + Serializable { + public String dialogText = "Server connection lost, trying to reconnect..."; + public String dialogTextGaveUp = "Server connection lost."; + public int reconnectAttempts = 10000; + public int reconnectInterval = 5000; + public int dialogGracePeriod = 1000; + public boolean dialogModal = true; + } + public static class LocaleServiceState implements Serializable { public List<LocaleData> localeData = new ArrayList<LocaleData>(); } diff --git a/shared/src/com/vaadin/shared/ui/upload/UploadServerRpc.java b/shared/src/com/vaadin/shared/ui/upload/UploadServerRpc.java index b576eb9114..b353057ab3 100644 --- a/shared/src/com/vaadin/shared/ui/upload/UploadServerRpc.java +++ b/shared/src/com/vaadin/shared/ui/upload/UploadServerRpc.java @@ -27,4 +27,12 @@ public interface UploadServerRpc extends ServerRpc { */ void change(String filename); + /** + * Called to poll the server to see if any changes have been made e.g. when + * starting upload + * + * @since + */ + void poll(); + } diff --git a/uitest/integration_tests.xml b/uitest/integration_tests.xml index bdbf1f2e5b..20d0d2147d 100644 --- a/uitest/integration_tests.xml +++ b/uitest/integration_tests.xml @@ -230,6 +230,13 @@ <param name="target-server" value="wildfly9" /> </antcall> </target> + <target name="integration-test-wildfly9-nginx"> + <antcall target="run-generic-integration-test"> + <param name="startDelay" value="10" /> + <param name="target-server" value="wildfly9-nginx" /> + <param name="target-port" value="80" /> + </antcall> + </target> <target name="integration-test-glassfish3"> <antcall target="run-generic-integration-test"> <param name="startDelay" value="10" /> diff --git a/uitest/ivy.xml b/uitest/ivy.xml index 281ba1ecdf..e17e094f79 100644 --- a/uitest/ivy.xml +++ b/uitest/ivy.xml @@ -78,15 +78,19 @@ <exclude org="org.eclipse.jetty.orbit"></exclude> </dependency> <dependency org="org.eclipse.jetty" name="jetty-websocket" - rev="&jetty.version;" conf="ide, jetty-run->default"> + rev="&jetty.version;" conf="ide, build-provided, jetty-run->default"> <exclude org="org.eclipse.jetty.orbit"></exclude> </dependency> <dependency org="org.eclipse.jetty" name="jetty-webapp" rev="&jetty.version;" conf="ide, build-provided, jetty-run->default"> <exclude org="org.eclipse.jetty.orbit"></exclude> </dependency> + <dependency org="org.eclipse.jetty" name="jetty-util" + rev="&jetty.version;" conf="ide, build-provided, jetty-run->default"> + <exclude org="org.eclipse.jetty.orbit"></exclude> + </dependency> <dependency org="org.mortbay.jetty" name="jetty-runner" - rev="&jetty.version;" conf="ide, jetty-run->default"> + rev="&jetty.version;" conf="ide, build-provided, jetty-run->default"> <exclude org="org.eclipse.jetty.orbit"></exclude> </dependency> @@ -104,7 +108,7 @@ <dependency org="org.hsqldb" name="hsqldb" rev="2.2.6" conf="build,ide -> default" /> <dependency org="com.vaadin" name="vaadin-testbench" - rev="4.0.2" conf="build-provided,ide -> default" /> + rev="4.0.3" conf="build-provided,ide -> default" /> <!-- This should be removed once tests have been updated to use lang3 --> <dependency org="commons-lang" name="commons-lang" rev="2.6" conf="build,ide -> default" /> @@ -114,7 +118,6 @@ <dependency org="com.vaadin" name="vaadin-buildhelpers" rev="${vaadin.version}" conf="compile-theme->build" /> - <dependency org="org.eclipse.jgit" name="org.eclipse.jgit" rev="3.5.1.201410131835-r" conf="ide,build->default"> <exclude org="org.apache.httpcomponents"></exclude> diff --git a/uitest/src/com/vaadin/tests/application/CommErrorEmulatorServlet.java b/uitest/src/com/vaadin/tests/application/CommErrorEmulatorServlet.java new file mode 100644 index 0000000000..116ef1c1f2 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/CommErrorEmulatorServlet.java @@ -0,0 +1,151 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.application; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.server.DeploymentConfiguration; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServiceException; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinServlet; +import com.vaadin.server.VaadinServletService; +import com.vaadin.server.VaadinSession; +import com.vaadin.server.communication.HeartbeatHandler; +import com.vaadin.server.communication.UidlRequestHandler; +import com.vaadin.ui.UI; + +public class CommErrorEmulatorServlet extends VaadinServlet { + + private Map<UI, Integer> uidlResponseCode = Collections + .synchronizedMap(new HashMap<UI, Integer>()); + private Map<UI, Integer> heartbeatResponseCode = Collections + .synchronizedMap(new HashMap<UI, Integer>()); + + private final CommErrorUIDLRequestHandler uidlHandler = new CommErrorUIDLRequestHandler(); + private final CommErrorHeartbeatHandler heartbeatHandler = new CommErrorHeartbeatHandler(); + + public class CommErrorUIDLRequestHandler extends UidlRequestHandler { + @Override + public boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) + throws IOException { + UI ui = session.getService().findUI(request); + if (ui != null && uidlResponseCode.containsKey(ui)) { + response.sendError(uidlResponseCode.get(ui), "Error set in UI"); + return true; + } + + return super.synchronizedHandleRequest(session, request, response); + } + } + + public class CommErrorHeartbeatHandler extends HeartbeatHandler { + @Override + public boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) + throws IOException { + UI ui = session.getService().findUI(request); + if (ui != null && heartbeatResponseCode.containsKey(ui)) { + response.sendError(heartbeatResponseCode.get(ui), + "Error set in UI"); + return true; + } + + return super.synchronizedHandleRequest(session, request, response); + } + + } + + public class CommErrorEmulatorService extends VaadinServletService { + + public CommErrorEmulatorService(VaadinServlet servlet, + DeploymentConfiguration deploymentConfiguration) + throws ServiceException { + super(servlet, deploymentConfiguration); + } + + @Override + protected List<RequestHandler> createRequestHandlers() + throws ServiceException { + List<RequestHandler> handlers = super.createRequestHandlers(); + handlers.add(uidlHandler); + handlers.add(heartbeatHandler); + return handlers; + } + } + + @Override + protected VaadinServletService createServletService( + DeploymentConfiguration deploymentConfiguration) + throws ServiceException { + CommErrorEmulatorService s = new CommErrorEmulatorService(this, + deploymentConfiguration); + s.init(); + return s; + } + + public void setUIDLResponseCode(final UI ui, int responseCode, + final int delay) { + uidlResponseCode.put(ui, responseCode); + System.out.println("Responding with " + responseCode + + " to UIDL requests for " + ui + " for the next " + delay + + "s"); + + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(delay * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Handing UIDL requests normally again"); + + uidlResponseCode.remove(ui); + } + }).start(); + } + + public void setHeartbeatResponseCode(final UI ui, int responseCode, + final int delay) { + heartbeatResponseCode.put(ui, responseCode); + + System.out.println("Responding with " + responseCode + + " to heartbeat requests for " + ui + " for the next " + delay + + "s"); + + new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(delay * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Handing heartbeat requests normally again"); + heartbeatResponseCode.remove(ui); + } + }).start(); + } + +} diff --git a/uitest/src/com/vaadin/tests/application/CommErrorEmulatorUI.java b/uitest/src/com/vaadin/tests/application/CommErrorEmulatorUI.java new file mode 100644 index 0000000000..080d36fa48 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/CommErrorEmulatorUI.java @@ -0,0 +1,270 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.application; + +import com.vaadin.annotations.Theme; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinServlet; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.Component; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Panel; +import com.vaadin.ui.TextField; +import com.vaadin.ui.VerticalLayout; + +/** + * + * @since + * @author Vaadin Ltd + */ +@Theme("valo") +public class CommErrorEmulatorUI extends AbstractTestUIWithLog { + + private static class Response { + private Integer code; + private Integer time; + + /** + * @param code + * @param time + */ + public Response(Integer code, Integer time) { + super(); + this.code = code; + this.time = time; + } + + } + + private Response uidlResponse = new Response(503, 10); + private Response heartbeatResponse = new Response(200, 10); + + // Server exceptions will occur in this test as we are writing the response + // here and not letting the servlet write it + @Override + protected void setup(VaadinRequest request) { + String transport = request.getParameter("transport"); + + if ("websocket".equalsIgnoreCase(transport)) { + log("Using websocket"); + } else if ("websocket-xhr".equalsIgnoreCase(transport)) { + log("Using websocket for push only"); + } else if ("long-polling".equalsIgnoreCase(transport)) { + log("Using long-polling"); + } else { + log("Using XHR"); + } + getLayout().setSpacing(true); + addComponent(createConfigPanel()); + addComponent(createServerConfigPanel()); + + addComponent(new Button("Say hello", new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + log("Hello"); + } + })); + } + + /** + * @since + * @return + */ + private Component createServerConfigPanel() { + Panel p = new Panel("Server config (NOTE: affects all users)"); + VerticalLayout vl = new VerticalLayout(); + vl.setSpacing(true); + vl.setMargin(true); + p.setContent(vl); + vl.addComponent(createTemporaryResponseCodeSetters("UIDL", uidlResponse)); + vl.addComponent(createTemporaryResponseCodeSetters("Heartbeat", + heartbeatResponse)); + vl.addComponent(new Button("Activate", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + if (uidlResponse.code != null && uidlResponse.code != 200) { + getServlet().setUIDLResponseCode(CommErrorEmulatorUI.this, + uidlResponse.code, uidlResponse.time); + log("Responding with " + uidlResponse.code + + " to UIDL requests for " + uidlResponse.time + + "s"); + } + if (heartbeatResponse.code != null + && heartbeatResponse.code != 200) { + getServlet().setHeartbeatResponseCode( + CommErrorEmulatorUI.this, heartbeatResponse.code, + heartbeatResponse.time); + log("Responding with " + heartbeatResponse.code + + " to heartbeat requests for " + + heartbeatResponse.time + "s"); + } + } + })); + + return p; + } + + private Component createConfigPanel() { + Panel p = new Panel("Reconnect dialog configuration"); + p.setSizeUndefined(); + final TextField reconnectDialogMessage = new TextField( + "Reconnect message"); + reconnectDialogMessage.setWidth("50em"); + reconnectDialogMessage.setValue(getReconnectDialogConfiguration() + .getDialogText()); + reconnectDialogMessage + .addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + getReconnectDialogConfiguration().setDialogText( + reconnectDialogMessage.getValue()); + } + }); + + final TextField reconnectDialogGaveUpMessage = new TextField( + "Reconnect gave up message"); + reconnectDialogGaveUpMessage.setWidth("50em"); + + reconnectDialogGaveUpMessage.setValue(getReconnectDialogConfiguration() + .getDialogTextGaveUp()); + reconnectDialogGaveUpMessage + .addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + getReconnectDialogConfiguration().setDialogTextGaveUp( + reconnectDialogGaveUpMessage.getValue()); + } + }); + final TextField reconnectDialogReconnectAttempts = new TextField( + "Reconnect attempts"); + reconnectDialogReconnectAttempts.setConverter(Integer.class); + reconnectDialogReconnectAttempts + .setConvertedValue(getReconnectDialogConfiguration() + .getReconnectAttempts()); + reconnectDialogReconnectAttempts + .addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + getReconnectDialogConfiguration().setReconnectAttempts( + (Integer) reconnectDialogReconnectAttempts + .getConvertedValue()); + } + }); + final TextField reconnectDialogReconnectInterval = new TextField( + "Reconnect interval (ms)"); + reconnectDialogReconnectInterval.setConverter(Integer.class); + reconnectDialogReconnectInterval + .setConvertedValue(getReconnectDialogConfiguration() + .getReconnectInterval()); + reconnectDialogReconnectInterval + .addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + getReconnectDialogConfiguration().setReconnectInterval( + (Integer) reconnectDialogReconnectInterval + .getConvertedValue()); + } + }); + + final TextField reconnectDialogGracePeriod = new TextField( + "Reconnect dialog grace period (ms)"); + reconnectDialogGracePeriod.setConverter(Integer.class); + reconnectDialogGracePeriod + .setConvertedValue(getReconnectDialogConfiguration() + .getDialogGracePeriod()); + reconnectDialogGracePeriod + .addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + getReconnectDialogConfiguration().setDialogGracePeriod( + (Integer) reconnectDialogGracePeriod + .getConvertedValue()); + } + }); + + final CheckBox reconnectDialogModal = new CheckBox( + "Reconnect dialog modality"); + reconnectDialogModal.setValue(getReconnectDialogConfiguration() + .isDialogModal()); + reconnectDialogModal.addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + getReconnectDialogConfiguration().setDialogModal( + reconnectDialogModal.getValue()); + } + }); + + VerticalLayout vl = new VerticalLayout(); + vl.setMargin(true); + vl.setSpacing(true); + p.setContent(vl); + vl.addComponents(reconnectDialogMessage, reconnectDialogGaveUpMessage, + reconnectDialogGracePeriod, reconnectDialogModal, + reconnectDialogReconnectAttempts, + reconnectDialogReconnectInterval); + return p; + } + + private Component createTemporaryResponseCodeSetters(String type, + final Response response) { + + HorizontalLayout hl = new HorizontalLayout(); + hl.setSpacing(true); + hl.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT); + Label l1 = new Label("Respond to " + type + " requests with code"); + final TextField responseCode = new TextField(null, "" + response.code); + responseCode.setConverter(Integer.class); + responseCode.setWidth("5em"); + Label l2 = new Label("for the following"); + final TextField timeField = new TextField(null, "" + response.time); + timeField.setConverter(Integer.class); + timeField.setWidth("5em"); + Label l3 = new Label("seconds"); + + responseCode.addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + Integer code = (Integer) responseCode.getConvertedValue(); + response.code = code; + } + }); + + timeField.addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + Integer time = (Integer) timeField.getConvertedValue(); + response.time = time; + } + }); + + hl.addComponents(l1, responseCode, l2, timeField, l3); + return hl; + } + + protected CommErrorEmulatorServlet getServlet() { + return (CommErrorEmulatorServlet) VaadinServlet.getCurrent(); + } + +} diff --git a/uitest/src/com/vaadin/tests/application/CriticalNotificationsTestBase.java b/uitest/src/com/vaadin/tests/application/CriticalNotificationsTest.java index f3813fce50..03564cbaf7 100644 --- a/uitest/src/com/vaadin/tests/application/CriticalNotificationsTestBase.java +++ b/uitest/src/com/vaadin/tests/application/CriticalNotificationsTest.java @@ -22,48 +22,7 @@ import com.vaadin.testbench.elements.CheckBoxElement; import com.vaadin.testbench.elements.NotificationElement; import com.vaadin.tests.tb3.MultiBrowserThemeTest; -public abstract class CriticalNotificationsTestBase extends - MultiBrowserThemeTest { - - public static class ValoCriticalNotificationsTest extends - CriticalNotificationsTestBase { - @Override - protected String getTheme() { - return "valo"; - } - } - - public static class ReindeerCriticalNotificationsTest extends - CriticalNotificationsTestBase { - @Override - protected String getTheme() { - return "reindeer"; - } - } - - public static class RunoCriticalNotificationsTest extends - CriticalNotificationsTestBase { - @Override - protected String getTheme() { - return "runo"; - } - } - - public static class ChameleonCriticalNotificationsTest extends - CriticalNotificationsTestBase { - @Override - protected String getTheme() { - return "chameleon"; - } - } - - public static class BaseCriticalNotificationsTest extends - CriticalNotificationsTestBase { - @Override - protected String getTheme() { - return "base"; - } - } +public class CriticalNotificationsTest extends MultiBrowserThemeTest { @Test public void internalError() throws Exception { diff --git a/uitest/src/com/vaadin/tests/application/ReconnectDialogThemeTest.java b/uitest/src/com/vaadin/tests/application/ReconnectDialogThemeTest.java new file mode 100644 index 0000000000..b9e57d39e8 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/ReconnectDialogThemeTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.application; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.parallel.BrowserUtil; +import com.vaadin.testbench.parallel.TestCategory; +import com.vaadin.tests.tb3.CustomTestBenchCommandExecutor; +import com.vaadin.tests.tb3.MultiBrowserThemeTestWithProxy; + +@TestCategory("") +public class ReconnectDialogThemeTest extends MultiBrowserThemeTestWithProxy { + + static By reconnectDialogBy = By.className("v-reconnect-dialog"); + + @Test + public void reconnectDialogTheme() throws IOException { + openTestURL(); + ButtonElement helloButton = $(ButtonElement.class).caption("Say hello") + .first(); + helloButton.click(); + Assert.assertEquals("1. Hello from the server", getLogRow(0)); + disconnectProxy(); + helloButton.click(); + testBench().disableWaitForVaadin(); + waitUntil(new ExpectedCondition<Boolean>() { + + @Override + public Boolean apply(WebDriver input) { + boolean present = isElementPresent(reconnectDialogBy); + return present; + } + }); + + WebElement dialog = findElement(reconnectDialogBy); + WebElement spinner = dialog.findElement(By.className("spinner")); + + // Hide spinner to make screenshot stable + executeScript("arguments[0].style.visibility='hidden';", spinner); + compareScreen("onscreen-without-spinner"); + + // Show spinner and make sure it is shown by comparing to the screenshot + // without a spinner + executeScript("arguments[0].style.visibility='visible';", spinner); + BufferedImage fullScreen = ImageIO.read(new ByteArrayInputStream( + ((TakesScreenshot) getDriver()) + .getScreenshotAs(OutputType.BYTES))); + BufferedImage spinnerImage = CustomTestBenchCommandExecutor + .cropToElement(spinner, fullScreen, + BrowserUtil.isIE8(getDesiredCapabilities())); + assertHasManyColors("Spinner is not shown", spinnerImage); + + } + + private void assertHasManyColors(String message, BufferedImage spinnerImage) { + int backgroundColor = spinnerImage.getRGB(0, 0); + for (int x = 0; x < spinnerImage.getWidth(); x++) { + for (int y = 0; y < spinnerImage.getHeight(); y++) { + if (Math.abs(spinnerImage.getRGB(x, y) - backgroundColor) > 50) { + return; + } + } + } + Assert.fail(message); + + } + + @Override + protected Class<?> getUIClass() { + return ReconnectDialogUI.class; + } + +} diff --git a/uitest/src/com/vaadin/tests/application/ReconnectDialogUI.java b/uitest/src/com/vaadin/tests/application/ReconnectDialogUI.java new file mode 100644 index 0000000000..62de2d49c5 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/ReconnectDialogUI.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.application; + +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; + +public class ReconnectDialogUI extends AbstractTestUIWithLog { + + @Override + protected void setup(VaadinRequest request) { + if (request.getParameter("reconnectAttempts") != null) { + getReconnectDialogConfiguration() + .setReconnectAttempts( + Integer.parseInt(request + .getParameter("reconnectAttempts"))); + } + Button b = new Button("Say hello"); + b.addClickListener(new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + log("Hello from the server"); + } + }); + + addComponent(b); + } + +} diff --git a/uitest/src/com/vaadin/tests/application/ReconnectDialogUITest.java b/uitest/src/com/vaadin/tests/application/ReconnectDialogUITest.java new file mode 100644 index 0000000000..0a31402c13 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/ReconnectDialogUITest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.application; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; + +import com.jcraft.jsch.JSchException; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.tb3.MultiBrowserTestWithProxy; + +public class ReconnectDialogUITest extends MultiBrowserTestWithProxy { + + @Test + public void reconnectDialogShownAndDisappears() throws JSchException { + openTestURL(); + getButton().click(); + Assert.assertEquals("1. Hello from the server", getLogRow(0)); + disconnectProxy(); + getButton().click(); + waitForReconnectDialogWithText("Server connection lost, trying to reconnect..."); + connectProxy(); + waitForReconnectDialogToDisappear(); + Assert.assertEquals("2. Hello from the server", getLogRow(0)); + } + + @Test + public void gaveUpMessageShown() { + openTestURL("reconnectAttempts=3"); + getButton().click(); + Assert.assertEquals("1. Hello from the server", getLogRow(0)); + + disconnectProxy(); + getButton().click(); + + waitForReconnectDialogWithText("Server connection lost."); + } + + private void waitForReconnectDialogWithText(final String text) { + waitForReconnectDialogPresent(); + final WebElement reconnectDialog = findElement(ReconnectDialogThemeTest.reconnectDialogBy); + waitUntil(new ExpectedCondition<Boolean>() { + @Override + public Boolean apply(WebDriver input) { + return reconnectDialog.findElement(By.className("text")) + .getText().equals(text); + } + }, 10); + + } + + private void waitForReconnectDialogToDisappear() { + waitForElementNotPresent(ReconnectDialogThemeTest.reconnectDialogBy); + + } + + private void waitForReconnectDialogPresent() { + waitForElementPresent(ReconnectDialogThemeTest.reconnectDialogBy); + } + + private WebElement getButton() { + return $(ButtonElement.class).first(); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/AbstractTestUI.java b/uitest/src/com/vaadin/tests/components/AbstractTestUI.java index 98b0f63ce1..33ff504d8d 100644 --- a/uitest/src/com/vaadin/tests/components/AbstractTestUI.java +++ b/uitest/src/com/vaadin/tests/components/AbstractTestUI.java @@ -124,6 +124,8 @@ public abstract class AbstractTestUI extends UI { config.setPushMode(PushMode.DISABLED); } else if ("websocket".equals(transport)) { enablePush(Transport.WEBSOCKET); + } else if ("websocket-xhr".equals(transport)) { + enablePush(Transport.WEBSOCKET_XHR); } else if ("streaming".equals(transport)) { enablePush(Transport.STREAMING); } else if ("long-polling".equals(transport)) { diff --git a/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java b/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java index 182bec04c9..1e2b8f4335 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java @@ -36,7 +36,7 @@ public class GridThemeChangeTest extends MultiBrowserTest { @Test public void testThemeChange() { - openTestURL(); + openTestURL("debug"); GridElement grid = $(GridElement.class).first(); @@ -44,6 +44,7 @@ public class GridThemeChangeTest extends MultiBrowserTest { grid.getCell(0, 0).click(); + grid = $(GridElement.class).first(); int valoHeight = grid.getRow(0).getSize().getHeight(); Assert.assertTrue( diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java index cff8ade054..cbd0857bd1 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java @@ -376,9 +376,10 @@ public class GridSortingTest extends GridBasicFeaturesTest { } private void assertLastSortIsUserOriginated(boolean isUserOriginated) { + // Find a message in the log List<WebElement> userOriginatedMessages = getDriver() .findElements( - By.xpath("//*[contains(text(),'SortOrderChangeEvent: isUserOriginated')]")); + By.xpath("//div[@id='Log']//*[contains(text(),'SortOrderChangeEvent: isUserOriginated')]")); Collections.sort(userOriginatedMessages, new Comparator<WebElement>() { @Override diff --git a/uitest/src/com/vaadin/tests/integration/AbstractServletIntegrationTest.java b/uitest/src/com/vaadin/tests/integration/AbstractServletIntegrationTest.java index 7e9a2138e4..eb1b714776 100644 --- a/uitest/src/com/vaadin/tests/integration/AbstractServletIntegrationTest.java +++ b/uitest/src/com/vaadin/tests/integration/AbstractServletIntegrationTest.java @@ -16,8 +16,13 @@ package com.vaadin.tests.integration; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized.Parameters; import com.vaadin.testbench.elements.TableElement; @@ -27,9 +32,12 @@ import com.vaadin.testbench.elements.TableElement; * * @author Vaadin Ltd */ +@RunWith(ParameterizedTB3Runner.class) public abstract class AbstractServletIntegrationTest extends AbstractIntegrationTest { + private String contextPath = "/demo"; + @Test public void runTest() throws IOException, AssertionError { openTestURL(); @@ -40,7 +48,29 @@ public abstract class AbstractServletIntegrationTest extends @Override protected String getDeploymentPath(Class<?> uiClass) { - return "/demo" + super.getDeploymentPath(uiClass); + return contextPath + super.getDeploymentPath(uiClass); + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + @Parameters + public static Collection<String> getContextPaths() { + if (getServerName().equals("wildfly9-nginx")) { + ArrayList<String> paths = new ArrayList<String>(); + paths.add("/buffering/demo"); + paths.add("/nonbuffering/demo"); + paths.add("/buffering-timeout/demo"); + paths.add("/nonbuffering-timeout/demo"); + return paths; + } else { + return Collections.emptyList(); + } + } + + protected static String getServerName() { + return System.getProperty("server-name"); } } diff --git a/uitest/src/com/vaadin/tests/integration/LongPollingProxyServerTest.java b/uitest/src/com/vaadin/tests/integration/LongPollingProxyServerTest.java new file mode 100644 index 0000000000..16c599641b --- /dev/null +++ b/uitest/src/com/vaadin/tests/integration/LongPollingProxyServerTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.integration; + +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.support.ui.ExpectedCondition; + +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.tests.push.BasicPushLongPolling; +import com.vaadin.tests.push.BasicPushTest; +import com.vaadin.tests.tb3.IncludeIfProperty; + +@IncludeIfProperty(property = "server-name", value = "wildfly9-nginx") +public class LongPollingProxyServerTest extends AbstractIntegrationTest { + + @Override + protected Class<?> getUIClass() { + return BasicPushLongPolling.class; + } + + @Test + public void bufferingTimeoutBasicPush() throws Exception { + basicPush("buffering-timeout"); + } + + @Test + public void nonbufferingTimeoutBasicPush() throws Exception { + basicPush("nonbuffering-timeout"); + } + + @Test + public void bufferingBasicPush() throws Exception { + basicPush("buffering"); + } + + @Test + public void nonbufferingBasicPush() throws Exception { + basicPush("nonbuffering"); + } + + @Test + public void bufferingTimeoutActionAfterFirstTimeout() throws Exception { + actionAfterFirstTimeout("buffering-timeout"); + } + + @Test + public void nonbufferingTimeoutActionAfterFirstTimeout() throws Exception { + actionAfterFirstTimeout("nonbuffering-timeout"); + } + + private String getUrl(String bufferingOrNot) { + return getBaseURL() + "/" + bufferingOrNot + "/demo" + + getDeploymentPath(); + } + + private void actionAfterFirstTimeout(String bufferingOrNot) + throws Exception { + String url = getUrl(bufferingOrNot); + getDriver().get(url); + // The wildfly9-nginx server has a configured timeout of 10s for + // *-timeout urls + Thread.sleep(15000); + Assert.assertEquals(0, BasicPushTest.getClientCounter(this)); + BasicPushTest.getIncrementButton(this).click(); + Assert.assertEquals(1, BasicPushTest.getClientCounter(this)); + } + + private void basicPush(String bufferingOrNot) throws Exception { + String url = getUrl(bufferingOrNot); + getDriver().get(url); + + Assert.assertEquals(0, BasicPushTest.getServerCounter(this)); + BasicPushTest.getServerCounterStartButton(this).click(); + waitUntil(new ExpectedCondition<Boolean>() { + @Override + public Boolean apply(WebDriver input) { + return BasicPushTest + .getServerCounter(LongPollingProxyServerTest.this) > 1; + } + }); + } + + @Override + public List<DesiredCapabilities> getBrowsersToTest() { + return Collections.singletonList(Browser.PHANTOMJS + .getDesiredCapabilities()); + } +} diff --git a/uitest/src/com/vaadin/tests/integration/ParameterizedTB3Runner.java b/uitest/src/com/vaadin/tests/integration/ParameterizedTB3Runner.java new file mode 100644 index 0000000000..b051fb51db --- /dev/null +++ b/uitest/src/com/vaadin/tests/integration/ParameterizedTB3Runner.java @@ -0,0 +1,170 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.integration; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; + +import com.vaadin.tests.tb3.TB3Runner; + +/** + * TestBench test runner which supports static @Parameters annotated methods + * providing parameters for the corresponding setter. + * <p> + * {@code @Parameters public static Collection<String> getThemes() } creates one + * permutation for each value returned by {@code getThemes()}. The value is + * automatically assigned to the test instance using {@code setTheme(String)} + * before invoking the test method + * + * @author Vaadin Ltd + */ +public class ParameterizedTB3Runner extends TB3Runner { + + public ParameterizedTB3Runner(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> methods = super.computeTestMethods(); + + Map<Method, Collection<String>> parameters = new LinkedHashMap<Method, Collection<String>>(); + + // Find all @Parameters methods and invoke them to find out permutations + + for (Method m : getTestClass().getJavaClass().getMethods()) { + Parameters p = m.getAnnotation(Parameters.class); + if (p == null) { + continue; + } + + if (!m.getName().startsWith("get") || !m.getName().endsWith("s")) { + throw new IllegalStateException( + "Method " + + m.getName() + + " is annotated with @Parameter but is not named getSomeThings() as it should"); + } + + if (m.getParameterTypes().length != 0) { + throw new IllegalStateException( + "Method " + + m.getName() + + " annotated with @Parameter should not have any arguments"); + } + + if (!Modifier.isStatic(m.getModifiers())) { + throw new IllegalStateException("Method " + m.getName() + + " annotated with @Parameter must be static"); + } + + // getThemes -> setTheme + String setter = "set" + m.getName().substring("get".length()); + setter = setter.substring(0, setter.length() - 1); + // property = property.substring(0, 1).toLowerCase() + // + property.substring(1); + + Method setterMethod; + try { + setterMethod = getTestClass().getJavaClass().getMethod(setter, + String.class); + } catch (Exception e) { + throw new IllegalStateException("No setter " + setter + + " found in " + + getTestClass().getJavaClass().getName(), e); + } + + Collection<String> values; + try { + values = (Collection<String>) m.invoke(null); + if (!values.isEmpty()) { + // Ignore any empty collections to allow e.g. integration + // tests to use "/demo" path by default without adding that + // to the screenshot name + parameters.put(setterMethod, values); + } + } catch (Exception e) { + throw new IllegalStateException("The setter " + m.getName() + + " could not be invoked", e); + } + } + + // Add method permutations for all @Parameters + for (Method setter : parameters.keySet()) { + List<FrameworkMethod> newMethods = new ArrayList<FrameworkMethod>(); + for (FrameworkMethod m : methods) { + + if (!(m instanceof TBMethod)) { + System.err.println("Unknown method type: " + + m.getClass().getName()); + newMethods.add(m); + continue; + } + + // testFoo + // testBar + // -> + // testFoo[valo] + // testFoo[runo] + // testBar[valo] + // testBar[runo] + + for (final String value : parameters.get(setter)) { + newMethods.add(new TBMethodWithBefore((TBMethod) m, setter, + value)); + } + } + // Update methods so next parameters will use all expanded methods + methods = newMethods; + } + return methods; + } + + public static class TBMethodWithBefore extends TBMethod { + + private Method setter; + private String value; + private TBMethod parent; + + public TBMethodWithBefore(TBMethod m, Method setter, String value) { + super(m.getMethod(), m.getCapabilities()); + parent = m; + this.setter = setter; + this.value = value; + } + + @Override + public Object invokeExplosively(Object target, Object... params) + throws Throwable { + setter.invoke(target, value); + return parent.invokeExplosively(target, params); + } + + @Override + public String getName() { + return parent.getName() + "[" + value + "]"; + }; + + } +} diff --git a/uitest/src/com/vaadin/tests/push/BasicPush.java b/uitest/src/com/vaadin/tests/push/BasicPush.java index ffc5395c2c..7379b08f5b 100644 --- a/uitest/src/com/vaadin/tests/push/BasicPush.java +++ b/uitest/src/com/vaadin/tests/push/BasicPush.java @@ -50,7 +50,7 @@ public class BasicPush extends AbstractTestUI { @Override protected void setup(VaadinRequest request) { - + getReconnectDialogConfiguration().setDialogModal(false); spacer(); /* diff --git a/uitest/src/com/vaadin/tests/push/BasicPushTest.java b/uitest/src/com/vaadin/tests/push/BasicPushTest.java index f176008eb0..157e3f74ae 100644 --- a/uitest/src/com/vaadin/tests/push/BasicPushTest.java +++ b/uitest/src/com/vaadin/tests/push/BasicPushTest.java @@ -16,6 +16,7 @@ package com.vaadin.tests.push; import org.junit.Test; +import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedCondition; @@ -52,8 +53,8 @@ public abstract class BasicPushTest extends MultiBrowserTest { } public static int getClientCounter(AbstractTB3Test t) { - WebElement clientCounterElem = t - .vaadinElementById(BasicPush.CLIENT_COUNTER_ID); + WebElement clientCounterElem = t.findElement(By + .id(BasicPush.CLIENT_COUNTER_ID)); return Integer.parseInt(clientCounterElem.getText()); } @@ -66,21 +67,21 @@ public abstract class BasicPushTest extends MultiBrowserTest { } public static int getServerCounter(AbstractTB3Test t) { - WebElement serverCounterElem = t - .vaadinElementById(BasicPush.SERVER_COUNTER_ID); + WebElement serverCounterElem = t.findElement(By + .id(BasicPush.SERVER_COUNTER_ID)); return Integer.parseInt(serverCounterElem.getText()); } public static WebElement getServerCounterStartButton(AbstractTB3Test t) { - return t.vaadinElementById(BasicPush.START_TIMER_ID); + return t.findElement(By.id(BasicPush.START_TIMER_ID)); } public static WebElement getServerCounterStopButton(AbstractTB3Test t) { - return t.vaadinElementById(BasicPush.STOP_TIMER_ID); + return t.findElement(By.id(BasicPush.STOP_TIMER_ID)); } public static WebElement getIncrementButton(AbstractTB3Test t) { - return t.vaadinElementById(BasicPush.INCREMENT_BUTTON_ID); + return t.findElement(By.id(BasicPush.INCREMENT_BUTTON_ID)); } protected void waitUntilClientCounterChanges(final int expectedValue) { diff --git a/uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhr.java b/uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhr.java new file mode 100644 index 0000000000..e31eb2a369 --- /dev/null +++ b/uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhr.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.push; + +import com.vaadin.annotations.Push; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.ui.Transport; +import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; + +@Push(transport = Transport.WEBSOCKET_XHR) +public class BasicPushWebsocketXhr extends BasicPush { + + @Override + public void init(VaadinRequest request) { + super.init(request); + // Don't use fallback so we can easier detect if websocket fails + getPushConfiguration().setParameter( + PushConfigurationState.FALLBACK_TRANSPORT_PARAM, "none"); + } + +} diff --git a/uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhrTest.java b/uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhrTest.java new file mode 100644 index 0000000000..430246d66a --- /dev/null +++ b/uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhrTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.push; + +import java.util.List; + +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.tests.tb3.WebsocketTest; + +public class BasicPushWebsocketXhrTest extends BasicPushTest { + @Override + public List<DesiredCapabilities> getBrowsersToTest() { + return getBrowsersSupportingWebSocket(); + } +} diff --git a/uitest/src/com/vaadin/tests/push/SendMultibyteCharactersTest.java b/uitest/src/com/vaadin/tests/push/SendMultibyteCharactersTest.java index 69e5de960a..95c03d9959 100644 --- a/uitest/src/com/vaadin/tests/push/SendMultibyteCharactersTest.java +++ b/uitest/src/com/vaadin/tests/push/SendMultibyteCharactersTest.java @@ -36,7 +36,7 @@ public abstract class SendMultibyteCharactersTest extends MultiBrowserTest { findElement(By.tagName("body")).click(); - waitForDebugMessage("Variable burst to be sent to server:", 5); + waitForDebugMessage("RPC invocations to be sent to the server:", 5); waitForDebugMessage("Handling message from server", 10); } diff --git a/uitest/src/com/vaadin/tests/requesthandlers/CommunicationError.java b/uitest/src/com/vaadin/tests/requesthandlers/CommunicationError.java index 31ec7658ee..26f3dff1a2 100644 --- a/uitest/src/com/vaadin/tests/requesthandlers/CommunicationError.java +++ b/uitest/src/com/vaadin/tests/requesthandlers/CommunicationError.java @@ -15,6 +15,9 @@ */ package com.vaadin.tests.requesthandlers; +import java.io.IOException; +import java.io.PrintWriter; + import com.vaadin.launcher.ApplicationRunnerServlet; import com.vaadin.server.CustomizedSystemMessages; import com.vaadin.server.SystemMessages; @@ -69,7 +72,17 @@ public class CommunicationError extends UIProvider { @Override public void buttonClick(ClickEvent event) { - VaadinService.getCurrentResponse().setStatus(400); + try { + // An unparseable response will cause + // communication error + PrintWriter writer = VaadinService + .getCurrentResponse().getWriter(); + writer.write("for(;;)[{FOOBAR}]"); + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } } }); addComponent(button); diff --git a/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java b/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java index d76cd616b1..a58575890e 100644 --- a/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java +++ b/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java @@ -37,6 +37,7 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.junit.Assert; import org.junit.Rule; +import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.openqa.selenium.By; import org.openqa.selenium.Dimension; @@ -98,6 +99,9 @@ import elemental.json.impl.JsonUtil; public abstract class AbstractTB3Test extends ParallelTest { @Rule + public TestName testName = new TestName(); + + @Rule public RetryOnFail retry = new RetryOnFail(); /** @@ -465,6 +469,15 @@ public abstract class AbstractTB3Test extends ParallelTest { waitUntil(ExpectedConditions.presenceOfElementLocated(by)); } + protected void waitForElementNotPresent(final By by) { + waitUntil(new ExpectedCondition<Boolean>() { + @Override + public Boolean apply(WebDriver input) { + return input.findElements(by).isEmpty(); + } + }); + } + protected void waitForElementVisible(final By by) { waitUntil(ExpectedConditions.visibilityOfElementLocated(by)); } diff --git a/uitest/src/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java b/uitest/src/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java index a70eeeeb49..00d7788f8b 100644 --- a/uitest/src/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java +++ b/uitest/src/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java @@ -118,7 +118,7 @@ public class CustomTestBenchCommandExecutor { * @return * @throws IOException */ - private BufferedImage cropToElement(WebElement element, + public static BufferedImage cropToElement(WebElement element, BufferedImage fullScreen, boolean isIE8) throws IOException { Point loc = element.getLocation(); Dimension size = element.getSize(); diff --git a/uitest/src/com/vaadin/tests/tb3/IncludeIfProperty.java b/uitest/src/com/vaadin/tests/tb3/IncludeIfProperty.java new file mode 100644 index 0000000000..789422c0c6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/tb3/IncludeIfProperty.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.tb3; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to control inclusion of a test into a test suite. + * <p> + * The test will be included in the suite only if the given System property + * {@code property} has the given {@code value}. + * <p> + * Used by {@link TB3TestLocator} + * + * @since + * @author Vaadin Ltd + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface IncludeIfProperty { + + String property(); + + String value(); + +} diff --git a/uitest/src/com/vaadin/tests/tb3/MultiBrowserTest.java b/uitest/src/com/vaadin/tests/tb3/MultiBrowserTest.java index c53209d1dc..23ead80fce 100644 --- a/uitest/src/com/vaadin/tests/tb3/MultiBrowserTest.java +++ b/uitest/src/com/vaadin/tests/tb3/MultiBrowserTest.java @@ -20,8 +20,6 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.List; -import org.junit.Rule; -import org.junit.rules.TestName; import org.openqa.selenium.ie.InternetExplorerDriver; import org.openqa.selenium.remote.DesiredCapabilities; @@ -46,9 +44,6 @@ import com.vaadin.testbench.parallel.BrowserUtil; */ public abstract class MultiBrowserTest extends PrivateTB3Configuration { - @Rule - public TestName testName = new TestName(); - protected List<DesiredCapabilities> getBrowsersSupportingWebSocket() { // No WebSocket support in IE8-9 and PhantomJS return getBrowserCapabilities(Browser.IE10, Browser.IE11, diff --git a/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTest.java b/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTest.java index 0964e6eb65..9a02b29bc2 100644 --- a/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTest.java +++ b/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTest.java @@ -16,18 +16,34 @@ package com.vaadin.tests.tb3; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized.Parameters; import org.openqa.selenium.remote.DesiredCapabilities; +import com.vaadin.tests.integration.ParameterizedTB3Runner; + /** * Test which uses theme returned by {@link #getTheme()} for running the test */ +@RunWith(ParameterizedTB3Runner.class) public abstract class MultiBrowserThemeTest extends MultiBrowserTest { - protected abstract String getTheme(); + private String theme; + + public void setTheme(String theme) { + this.theme = theme; + } + + @Parameters + public static Collection<String> getThemes() { + return Arrays.asList(new String[] { "valo", "reindeer", "runo", + "chameleon", "base" }); + } @Override protected boolean requireWindowFocusForIE() { @@ -37,7 +53,7 @@ public abstract class MultiBrowserThemeTest extends MultiBrowserTest { @Override protected void openTestURL(Class<?> uiClass, String... parameters) { Set<String> params = new HashSet<String>(Arrays.asList(parameters)); - params.add("theme=" + getTheme()); + params.add("theme=" + theme); super.openTestURL(uiClass, params.toArray(new String[params.size()])); } diff --git a/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTestWithProxy.java b/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTestWithProxy.java new file mode 100644 index 0000000000..26ed8288d1 --- /dev/null +++ b/uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTestWithProxy.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.tb3; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized.Parameters; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.tests.integration.ParameterizedTB3Runner; + +@RunWith(ParameterizedTB3Runner.class) +public abstract class MultiBrowserThemeTestWithProxy extends + MultiBrowserTestWithProxy { + + private String theme; + + public void setTheme(String theme) { + this.theme = theme; + } + + @Parameters + public static Collection<String> getThemes() { + return Arrays.asList(new String[] { "valo", "reindeer", "runo", + "chameleon", "base" }); + } + + @Override + protected boolean requireWindowFocusForIE() { + return true; + } + + @Override + protected void openTestURL(Class<?> uiClass, String... parameters) { + Set<String> params = new HashSet<String>(Arrays.asList(parameters)); + params.add("theme=" + theme); + super.openTestURL(uiClass, params.toArray(new String[params.size()])); + } + + @Override + public List<DesiredCapabilities> getBrowsersToTest() { + List<DesiredCapabilities> browsersToTest = getBrowsersExcludingPhantomJS(); + browsersToTest.add(PHANTOMJS2()); + return browsersToTest; + } +} diff --git a/uitest/src/com/vaadin/tests/tb3/ScreenshotTB3Test.java b/uitest/src/com/vaadin/tests/tb3/ScreenshotTB3Test.java index 6b8fb1741b..1c24720075 100644 --- a/uitest/src/com/vaadin/tests/tb3/ScreenshotTB3Test.java +++ b/uitest/src/com/vaadin/tests/tb3/ScreenshotTB3Test.java @@ -381,8 +381,7 @@ public abstract class ScreenshotTB3Test extends AbstractTB3Test { * fails */ private String getScreenshotFailureName() { - return getScreenshotBaseName() + "_" - + getUniqueIdentifier(getDesiredCapabilities()) + return getScreenshotBaseName() + "_" + getUniqueIdentifier(null) + "-failure.png"; } @@ -418,52 +417,34 @@ public abstract class ScreenshotTB3Test extends AbstractTB3Test { */ private String getScreenshotReferenceName(String identifier, Integer versionOverride) { - String uniqueBrowserIdentifier; - if (versionOverride == null) { - uniqueBrowserIdentifier = getUniqueIdentifier(getDesiredCapabilities()); - } else { - uniqueBrowserIdentifier = getUniqueIdentifier( - getDesiredCapabilities(), "" + versionOverride); - } - - // WindowMaximizeRestoreTest_Windows_InternetExplorer_8_window-1-moved-maximized-restored.png return getScreenshotReferenceDirectory() + File.separator - + getScreenshotBaseName() + "_" + uniqueBrowserIdentifier + "_" - + identifier + ".png"; + + getScreenshotBaseName() + "_" + + getUniqueIdentifier(versionOverride) + "_" + identifier + + ".png"; } - /** - * Returns a string which uniquely (enough) identifies this browser. Used - * mainly in screenshot names. - * - * @param capabilities - * @param versionOverride - * - * @return a unique string for each browser - */ - private String getUniqueIdentifier(DesiredCapabilities capabilities, - String versionOverride) { - return getUniqueIdentifier(BrowserUtil.getPlatform(capabilities), - BrowserUtil.getBrowserIdentifier(capabilities), versionOverride); - } + private String getUniqueIdentifier(Integer versionOverride) { + String testNameAndParameters = testName.getMethodName(); + // runTest-wildfly9-nginx[Windows_Firefox_24][/buffering/demo][valo] - /** - * Returns a string which uniquely (enough) identifies this browser. Used - * mainly in screenshot names. - * - * @param capabilities - * - * @return a unique string for each browser - */ - private String getUniqueIdentifier(DesiredCapabilities capabilities) { - return getUniqueIdentifier(BrowserUtil.getPlatform(capabilities), - BrowserUtil.getBrowserIdentifier(capabilities), - capabilities.getVersion()); - } + String parameters = testNameAndParameters.substring( + testNameAndParameters.indexOf("[") + 1, + testNameAndParameters.length() - 1); + // Windows_Firefox_24][/buffering/demo][valo + + parameters = parameters.replace("][", "_"); + // Windows_Firefox_24_/buffering/demo_valo - private String getUniqueIdentifier(String platform, String browser, - String version) { - return platform + "_" + browser + "_" + version; + parameters = parameters.replace("/", ""); + // Windows_Firefox_24_bufferingdemo_valo + + if (versionOverride != null) { + // Windows_Firefox_17_bufferingdemo_valo + parameters = parameters.replaceFirst("_" + + getDesiredCapabilities().getVersion(), "_" + + versionOverride); + } + return parameters; } /** diff --git a/uitest/src/com/vaadin/tests/tb3/ServletIntegrationTests.java b/uitest/src/com/vaadin/tests/tb3/ServletIntegrationTests.java index 885d3521b7..b06044446e 100644 --- a/uitest/src/com/vaadin/tests/tb3/ServletIntegrationTests.java +++ b/uitest/src/com/vaadin/tests/tb3/ServletIntegrationTests.java @@ -24,7 +24,7 @@ import java.util.Set; import org.junit.runner.RunWith; import org.junit.runners.model.InitializationError; -import com.vaadin.tests.integration.AbstractServletIntegrationTest; +import com.vaadin.tests.integration.AbstractIntegrationTest; import com.vaadin.tests.integration.ServletIntegrationJSR356WebsocketUITest; import com.vaadin.tests.integration.ServletIntegrationWebsocketUITest; import com.vaadin.tests.tb3.ServletIntegrationTests.ServletIntegrationTestSuite; @@ -51,6 +51,7 @@ public class ServletIntegrationTests { notWebsocketCompatible.add("tomcat6"); notWebsocketCompatible.add("tomcat7apacheproxy"); notWebsocketCompatible.add("weblogic10"); + notWebsocketCompatible.add("wildfly9-nginx"); // Requires an update to 8.5.5 and a fix for // https://dev.vaadin.com/ticket/16354 @@ -65,7 +66,7 @@ public class ServletIntegrationTests { public static class ServletIntegrationTestSuite extends TB3TestSuite { public ServletIntegrationTestSuite(Class<?> klass) throws InitializationError, IOException { - super(klass, AbstractServletIntegrationTest.class, + super(klass, AbstractIntegrationTest.class, "com.vaadin.tests.integration", new String[] {}, new ServletTestLocator()); } diff --git a/uitest/src/com/vaadin/tests/tb3/TB3Runner.java b/uitest/src/com/vaadin/tests/tb3/TB3Runner.java index acdef54492..4917e398cf 100644 --- a/uitest/src/com/vaadin/tests/tb3/TB3Runner.java +++ b/uitest/src/com/vaadin/tests/tb3/TB3Runner.java @@ -22,7 +22,7 @@ import java.lang.reflect.Modifier; import org.apache.http.client.HttpClient; import org.junit.runners.Parameterized; import org.junit.runners.model.InitializationError; -import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.internal.ApacheHttpClient; import org.openqa.selenium.remote.internal.HttpClientFactory; import com.vaadin.testbench.parallel.ParallelRunner; @@ -48,8 +48,8 @@ public class TB3Runner extends ParallelRunner { // reduce socket timeout to avoid tests hanging for three hours try { - Field field = HttpCommandExecutor.class - .getDeclaredField("httpClientFactory"); + Field field = ApacheHttpClient.Factory.class + .getDeclaredField("defaultClientFactory"); assert (Modifier.isStatic(field.getModifiers())); field.setAccessible(true); field.set(null, new HttpClientFactory() { diff --git a/uitest/src/com/vaadin/tests/tb3/TB3TestLocator.java b/uitest/src/com/vaadin/tests/tb3/TB3TestLocator.java index a0fbf51195..eb0861757e 100644 --- a/uitest/src/com/vaadin/tests/tb3/TB3TestLocator.java +++ b/uitest/src/com/vaadin/tests/tb3/TB3TestLocator.java @@ -213,6 +213,17 @@ public class TB3TestLocator { return false; } + IncludeIfProperty includeIfProperty = c + .getAnnotation(IncludeIfProperty.class); + if (includeIfProperty != null) { + String includeValue = includeIfProperty.value(); + String systemPropertyValue = System.getProperty(includeIfProperty + .property()); + if (!includeValue.equals(systemPropertyValue)) { + return false; + } + } + return true; } }
\ No newline at end of file diff --git a/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java b/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java index 79da9f902d..211a908ccb 100644 --- a/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java +++ b/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java @@ -71,7 +71,7 @@ public class ThemeChangeOnTheFlyTest extends MultiBrowserTest { public void reindeerToNullToReindeer() throws IOException { openTestURL(); - changeThemeAndCompare("null"); + changeTheme("null"); changeThemeAndCompare("reindeer"); } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java index 0da1c6c775..8237d75c6c 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java @@ -15,17 +15,9 @@ */ package com.vaadin.tests.widgetset.client; -import java.util.Date; -import java.util.logging.Logger; - import com.vaadin.client.ApplicationConnection; -import com.vaadin.client.ValueMap; -import com.vaadin.shared.ApplicationConstants; import com.vaadin.tests.widgetset.server.csrf.ui.CsrfTokenDisabled; -import elemental.json.JsonObject; -import elemental.json.JsonValue; - /** * Mock ApplicationConnection for several issues where we need to hack it. * @@ -34,14 +26,24 @@ import elemental.json.JsonValue; */ public class MockApplicationConnection extends ApplicationConnection { - private static final Logger LOGGER = Logger - .getLogger(MockApplicationConnection.class.getName()); + public MockApplicationConnection() { + super(); + messageHandler = new MockServerMessageHandler(); + messageHandler.setConnection(this); + messageSender = new MockServerCommunicationHandler(); + messageSender.setConnection(this); + } - // The last token received from the server. - private String lastCsrfTokenReceiver; + @Override + public MockServerMessageHandler getMessageHandler() { + return (MockServerMessageHandler) super.getMessageHandler(); + } - // The last token sent to the server. - private String lastCsrfTokenSent; + @Override + public MockServerCommunicationHandler getMessageSender() { + return (MockServerCommunicationHandler) super + .getMessageSender(); + } /** * Provide the last token received from the server. <br/> @@ -50,7 +52,7 @@ public class MockApplicationConnection extends ApplicationConnection { * @see CsrfTokenDisabled */ public String getLastCsrfTokenReceiver() { - return lastCsrfTokenReceiver; + return getMessageHandler().lastCsrfTokenReceiver; } /** @@ -60,23 +62,7 @@ public class MockApplicationConnection extends ApplicationConnection { * @see CsrfTokenDisabled */ public String getLastCsrfTokenSent() { - return lastCsrfTokenSent; - } - - @Override - protected void handleUIDLMessage(Date start, String jsonText, ValueMap json) { - lastCsrfTokenReceiver = json - .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); - - super.handleUIDLMessage(start, jsonText, json); - } - - @Override - protected void doUidlRequest(String uri, JsonObject payload) { - JsonValue jsonValue = payload.get(ApplicationConstants.CSRF_TOKEN); - lastCsrfTokenSent = jsonValue != null ? jsonValue.toJson() : null; - - super.doUidlRequest(uri, payload); + return getMessageSender().lastCsrfTokenSent; } } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java b/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java new file mode 100644 index 0000000000..14b5671181 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client; + +import com.vaadin.client.communication.MessageSender; +import com.vaadin.shared.ApplicationConstants; + +import elemental.json.JsonObject; +import elemental.json.JsonValue; + +public class MockServerCommunicationHandler extends MessageSender { + + // The last token sent to the server. + String lastCsrfTokenSent; + + @Override + public void send(JsonObject payload) { + JsonValue jsonValue = payload.get(ApplicationConstants.CSRF_TOKEN); + lastCsrfTokenSent = jsonValue != null ? jsonValue.toJson() : null; + + super.send(payload); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java b/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java new file mode 100644 index 0000000000..39b89b55ca --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client; + +import com.vaadin.client.ValueMap; +import com.vaadin.client.communication.MessageHandler; +import com.vaadin.shared.ApplicationConstants; + +public class MockServerMessageHandler extends MessageHandler { + + // The last token received from the server. + protected String lastCsrfTokenReceiver; + + @Override + public void handleJSON(ValueMap json) { + lastCsrfTokenReceiver = json + .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); + + super.handleJSON(json); + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/TestingPushConnection.java b/uitest/src/com/vaadin/tests/widgetset/client/TestingPushConnection.java index 4dae8892e7..c9a5a93332 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/TestingPushConnection.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/TestingPushConnection.java @@ -2,7 +2,6 @@ package com.vaadin.tests.widgetset.client; import com.google.gwt.user.client.Window; import com.vaadin.client.ApplicationConnection; -import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler; import com.vaadin.client.communication.AtmospherePushConnection; import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; @@ -12,9 +11,8 @@ public class TestingPushConnection extends AtmospherePushConnection { @Override public void init(ApplicationConnection connection, - PushConfigurationState pushConfiguration, - CommunicationErrorHandler errorHandler) { - super.init(connection, pushConfiguration, errorHandler); + PushConfigurationState pushConfiguration) { + super.init(connection, pushConfiguration); transport = Window.Location.getParameter("transport"); } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java index cf24ed6921..39dca8d799 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java @@ -70,8 +70,8 @@ public class CsrfButtonConnector extends AbstractComponentConnector { } private String csrfTokenInfo() { - return getMockConnection().getCsrfToken() + ", " - + getMockConnection().getLastCsrfTokenReceiver() + ", " + return getMockConnection().getMessageHandler().getCsrfToken() + + ", " + getMockConnection().getLastCsrfTokenReceiver() + ", " + getMockConnection().getLastCsrfTokenSent(); } |