aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2015-09-07 10:49:38 +0300
committerArtur Signell <artur@vaadin.com>2015-09-07 10:49:52 +0300
commite54434a93266757616be1362fa5475fc5431c48f (patch)
tree94b306109f32ca17e18b1fa2cf7cfe9838bb1c98
parentebceef4d44bcd61605fa92fcf7be8d3678537599 (diff)
parentc8f92cd539d08962ed324ff2e7322aa8dfac2124 (diff)
downloadvaadin-framework-e54434a93266757616be1362fa5475fc5431c48f.tar.gz
vaadin-framework-e54434a93266757616be1362fa5475fc5431c48f.zip
Merge remote-tracking branch 'origin/reconnect-dialog' (#11733)
Change-Id: Id148ac8a5b86a76ed966f96ea7732c35ad0d056d
-rw-r--r--WebContent/VAADIN/themes/base/base.scss2
-rw-r--r--WebContent/VAADIN/themes/base/common/img/reconnect-spinner.gifbin0 -> 3223 bytes
-rw-r--r--WebContent/VAADIN/themes/base/common/reconnect-dialog.scss32
-rw-r--r--WebContent/VAADIN/themes/valo/shared/_global.scss2
-rw-r--r--WebContent/VAADIN/themes/valo/shared/_reconnect-dialog.scss31
-rw-r--r--WebContent/WEB-INF/web.xml18
-rwxr-xr-xclient/src/com/vaadin/DefaultWidgetSet.gwt.xml4
-rw-r--r--client/src/com/vaadin/client/ApplicationConfiguration.java2
-rw-r--r--client/src/com/vaadin/client/ApplicationConnection.java2439
-rw-r--r--client/src/com/vaadin/client/JavaScriptConnectorHelper.java17
-rw-r--r--client/src/com/vaadin/client/LayoutManager.java11
-rw-r--r--client/src/com/vaadin/client/LayoutManagerIE8.java2
-rw-r--r--client/src/com/vaadin/client/Util.java61
-rw-r--r--client/src/com/vaadin/client/ValueMap.java4
-rw-r--r--client/src/com/vaadin/client/WidgetUtil.java17
-rw-r--r--client/src/com/vaadin/client/communication/AtmospherePushConnection.java126
-rw-r--r--client/src/com/vaadin/client/communication/ConnectionStateHandler.java202
-rw-r--r--client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java597
-rw-r--r--client/src/com/vaadin/client/communication/DefaultReconnectDialog.java117
-rw-r--r--client/src/com/vaadin/client/communication/Heartbeat.java36
-rw-r--r--client/src/com/vaadin/client/communication/MessageHandler.java1747
-rw-r--r--client/src/com/vaadin/client/communication/MessageSender.java410
-rw-r--r--client/src/com/vaadin/client/communication/PushConnection.java32
-rw-r--r--client/src/com/vaadin/client/communication/ReconnectDialog.java93
-rw-r--r--client/src/com/vaadin/client/communication/RpcProxy.java8
-rw-r--r--client/src/com/vaadin/client/communication/ServerRpcQueue.java342
-rw-r--r--client/src/com/vaadin/client/communication/TranslatedURLReference.java5
-rw-r--r--client/src/com/vaadin/client/communication/XhrConnection.java276
-rw-r--r--client/src/com/vaadin/client/communication/XhrConnectionError.java106
-rw-r--r--client/src/com/vaadin/client/debug/internal/InfoSection.java2
-rw-r--r--client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java10
-rw-r--r--client/src/com/vaadin/client/ui/VNotification.java4
-rw-r--r--client/src/com/vaadin/client/ui/VScrollTable.java3
-rw-r--r--client/src/com/vaadin/client/ui/VUpload.java7
-rw-r--r--client/src/com/vaadin/client/ui/datefield/DateFieldConnector.java3
-rw-r--r--client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java3
-rw-r--r--client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java4
-rw-r--r--client/src/com/vaadin/client/ui/ui/UIConnector.java13
-rw-r--r--client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java54
-rw-r--r--ivysettings.xml2
-rw-r--r--server/src/com/vaadin/server/LegacyCommunicationManager.java4
-rw-r--r--server/src/com/vaadin/server/LocaleService.java20
-rw-r--r--server/src/com/vaadin/server/communication/AtmospherePushConnection.java2
-rw-r--r--server/src/com/vaadin/server/communication/ServerRpcHandler.java98
-rw-r--r--server/src/com/vaadin/server/communication/UIInitHandler.java2
-rw-r--r--server/src/com/vaadin/server/communication/UidlRequestHandler.java48
-rw-r--r--server/src/com/vaadin/server/communication/UidlWriter.java17
-rw-r--r--server/src/com/vaadin/ui/PushConfiguration.java24
-rw-r--r--server/src/com/vaadin/ui/ReconnectDialogConfiguration.java201
-rw-r--r--server/src/com/vaadin/ui/UI.java45
-rw-r--r--server/src/com/vaadin/ui/Upload.java5
-rw-r--r--server/tests/src/com/vaadin/ui/PushConfigurationTransportTest.java6
-rw-r--r--shared/src/com/vaadin/shared/ApplicationConstants.java12
-rw-r--r--shared/src/com/vaadin/shared/ui/ui/Transport.java6
-rw-r--r--shared/src/com/vaadin/shared/ui/ui/UIState.java12
-rw-r--r--shared/src/com/vaadin/shared/ui/upload/UploadServerRpc.java8
-rw-r--r--uitest/integration_tests.xml7
-rw-r--r--uitest/ivy.xml11
-rw-r--r--uitest/src/com/vaadin/tests/application/CommErrorEmulatorServlet.java151
-rw-r--r--uitest/src/com/vaadin/tests/application/CommErrorEmulatorUI.java270
-rw-r--r--uitest/src/com/vaadin/tests/application/CriticalNotificationsTest.java (renamed from uitest/src/com/vaadin/tests/application/CriticalNotificationsTestBase.java)43
-rw-r--r--uitest/src/com/vaadin/tests/application/ReconnectDialogThemeTest.java101
-rw-r--r--uitest/src/com/vaadin/tests/application/ReconnectDialogUI.java46
-rw-r--r--uitest/src/com/vaadin/tests/application/ReconnectDialogUITest.java82
-rw-r--r--uitest/src/com/vaadin/tests/components/AbstractTestUI.java2
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java3
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java3
-rw-r--r--uitest/src/com/vaadin/tests/integration/AbstractServletIntegrationTest.java32
-rw-r--r--uitest/src/com/vaadin/tests/integration/LongPollingProxyServerTest.java107
-rw-r--r--uitest/src/com/vaadin/tests/integration/ParameterizedTB3Runner.java170
-rw-r--r--uitest/src/com/vaadin/tests/push/BasicPush.java2
-rw-r--r--uitest/src/com/vaadin/tests/push/BasicPushTest.java15
-rw-r--r--uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhr.java34
-rw-r--r--uitest/src/com/vaadin/tests/push/BasicPushWebsocketXhrTest.java29
-rw-r--r--uitest/src/com/vaadin/tests/push/SendMultibyteCharactersTest.java2
-rw-r--r--uitest/src/com/vaadin/tests/requesthandlers/CommunicationError.java15
-rw-r--r--uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java13
-rw-r--r--uitest/src/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java2
-rw-r--r--uitest/src/com/vaadin/tests/tb3/IncludeIfProperty.java44
-rw-r--r--uitest/src/com/vaadin/tests/tb3/MultiBrowserTest.java5
-rw-r--r--uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTest.java20
-rw-r--r--uitest/src/com/vaadin/tests/tb3/MultiBrowserThemeTestWithProxy.java64
-rw-r--r--uitest/src/com/vaadin/tests/tb3/ScreenshotTB3Test.java67
-rw-r--r--uitest/src/com/vaadin/tests/tb3/ServletIntegrationTests.java5
-rw-r--r--uitest/src/com/vaadin/tests/tb3/TB3Runner.java6
-rw-r--r--uitest/src/com/vaadin/tests/tb3/TB3TestLocator.java11
-rw-r--r--uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java2
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java50
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java36
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java35
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/TestingPushConnection.java6
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java4
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
new file mode 100644
index 0000000000..bc1a496d11
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/common/img/reconnect-spinner.gif
Binary files differ
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();
}