]> source.dussan.org Git - vaadin-framework.git/commitdiff
Separate server message handling to its own class (#11733)
authorArtur Signell <artur@vaadin.com>
Thu, 16 Apr 2015 11:31:16 +0000 (14:31 +0300)
committerArtur Signell <artur@vaadin.com>
Mon, 13 Jul 2015 14:19:07 +0000 (17:19 +0300)
Change-Id: Ic4342171ecbdae4b6e6075fa9ed6d4eebe399a87

12 files changed:
client/src/com/vaadin/client/ApplicationConfiguration.java
client/src/com/vaadin/client/ApplicationConnection.java
client/src/com/vaadin/client/JavaScriptConnectorHelper.java
client/src/com/vaadin/client/LayoutManager.java
client/src/com/vaadin/client/LayoutManagerIE8.java
client/src/com/vaadin/client/ValueMap.java
client/src/com/vaadin/client/communication/AtmospherePushConnection.java
client/src/com/vaadin/client/communication/ServerMessageHandler.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java
uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java
uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java

index 0db8dc297e99009541304fad00e4fe983d3b985b..d20e0568cd39fd56a797501fc3694777f8048232 100644 (file)
@@ -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 {
index 89ad931b319498d492b64bdb73daa0636c9a02c2..8865e6efcb966c8d2efc70ef37b45f836000e044 100644 (file)
 
 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.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;
@@ -33,10 +27,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;
@@ -59,43 +51,32 @@ 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.CommunicationProblemEvent;
 import com.vaadin.client.communication.CommunicationProblemHandler;
-import com.vaadin.client.communication.HasJavaScriptConnectorHelper;
 import com.vaadin.client.communication.Heartbeat;
-import com.vaadin.client.communication.JsonDecoder;
 import com.vaadin.client.communication.PushConnection;
 import com.vaadin.client.communication.RpcManager;
+import com.vaadin.client.communication.ServerMessageHandler;
 import com.vaadin.client.communication.ServerRpcQueue;
-import com.vaadin.client.communication.StateChangeEvent;
 import com.vaadin.client.componentlocator.ComponentLocator;
-import com.vaadin.client.extensions.AbstractExtensionConnector;
 import com.vaadin.client.metadata.ConnectorBundleLoader;
-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.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.AbstractComponentState;
 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;
@@ -120,26 +101,6 @@ import elemental.json.JsonObject;
  */
 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;
 
@@ -179,9 +140,6 @@ public class ApplicationConnection implements HasHandlers {
     private final String JSON_COMMUNICATION_PREFIX = "for(;;);[";
     private final String JSON_COMMUNICATION_SUFFIX = "]";
 
-    // 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>();
 
     private WidgetSet widgetSet;
@@ -206,12 +164,6 @@ public class ApplicationConnection implements HasHandlers {
     /** Parameters for this application connection loaded from the web-page */
     private ApplicationConfiguration configuration;
 
-    /** Timer for automatic refirect to SessionExpiredURL */
-    private Timer redirectTimer;
-
-    /** redirectTimer scheduling interval in seconds */
-    private int sessionExpirationInterval;
-
     private Date requestStartTime;
 
     private final LayoutManager layoutManager;
@@ -220,45 +172,6 @@ public class ApplicationConnection implements HasHandlers {
 
     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;
-        }
-
-        public ValueMap getJson() {
-            return json;
-        }
-    }
-
-    /** 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);
 
@@ -474,8 +387,6 @@ public class ApplicationConnection implements HasHandlers {
         }
     }
 
-    private boolean updatingState = false;
-
     public ApplicationConnection() {
         // Assuming UI data is eagerly loaded
         ConnectorBundleLoader.get().loadBundle(
@@ -492,6 +403,8 @@ public class ApplicationConnection implements HasHandlers {
         communicationProblemHandler = GWT
                 .create(CommunicationProblemHandler.class);
         communicationProblemHandler.setConnection(this);
+        serverMessageHandler = GWT.create(ServerMessageHandler.class);
+        serverMessageHandler.setConnection(this);
     }
 
     public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) {
@@ -562,10 +475,9 @@ public class ApplicationConnection implements HasHandlers {
             // inital UIDL not in DOM, request later
             repaintAll();
         } else {
-            // Update counter so TestBench knows something is still going on
-            hasActiveRequest = true;
-
             // initial UIDL provided in DOM, continue as if returned by request
+            // Hack to avoid logging an error in endRequest()
+            startRequest();
             handleJSONText(jsonText, -1);
         }
 
@@ -589,7 +501,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 !getServerMessageHandler().isInitialUidlHandled()
+                || isWorkPending() || hasActiveRequest()
                 || isExecutingDeferredCommands();
     }
 
@@ -609,12 +522,13 @@ public class ApplicationConnection implements HasHandlers {
         }
 
         client.getProfilingData = $entry(function() {
+            var smh = ap.@com.vaadin.client.ApplicationConnection::getServerMessageHandler();
             var pd = [
-                ap.@com.vaadin.client.ApplicationConnection::lastProcessingTime,
-                    ap.@com.vaadin.client.ApplicationConnection::totalProcessingTime
+                smh.@com.vaadin.client.communication.ServerMessageHandler::lastProcessingTime,
+                    smh.@com.vaadin.client.communication.ServerMessageHandler::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.ServerMessageHandler::serverTimingInfo);
+            pd[pd.length] = smh.@com.vaadin.client.communication.ServerMessageHandler::bootstrapTime;
             return pd;
         });
 
@@ -638,16 +552,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
      */
@@ -758,7 +662,7 @@ public class ApplicationConnection implements HasHandlers {
         return parameters;
     }
 
-    protected void repaintAll() {
+    public void repaintAll() {
         makeUidlRequest(Json.createArray(), getRepaintAllParameters());
     }
 
@@ -803,12 +707,13 @@ public class ApplicationConnection implements HasHandlers {
         startRequest();
 
         JsonObject payload = Json.createObject();
-        if (!getCsrfToken().equals(
-                ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) {
-            payload.put(ApplicationConstants.CSRF_TOKEN, getCsrfToken());
+        String csrfToken = getServerMessageHandler().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, lastSeenServerSyncId);
+        payload.put(ApplicationConstants.SERVER_SYNC_ID,
+                getServerMessageHandler().getLastSeenServerSyncId());
 
         getLogger()
                 .info("Making UIDL Request with params: " + payload.toJson());
@@ -934,7 +839,7 @@ public class ApplicationConnection implements HasHandlers {
                 "JSON parsing took " + (new Date().getTime() - start.getTime())
                         + "ms");
         if (getState() == State.RUNNING) {
-            handleReceivedJSONMessage(start, jsonText, json);
+            getServerMessageHandler().handleUIDLMessage(start, jsonText, json);
         } else if (getState() == State.INITIALIZING) {
             // Application is starting up for the first time
             setApplicationRunning(true);
@@ -1003,59 +908,9 @@ public class ApplicationConnection implements HasHandlers {
 
     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;
-
     protected ServerRpcQueue serverRpcQueue;
     protected CommunicationProblemHandler communicationProblemHandler;
-
-    /**
-     * 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 ServerMessageHandler serverMessageHandler;
 
     static final int MAX_CSS_WAITS = 100;
 
@@ -1081,7 +936,9 @@ public class ApplicationConnection implements HasHandlers {
             if (cssWaits >= MAX_CSS_WAITS) {
                 getLogger().severe("CSS files may have not loaded properly.");
             }
-            handleReceivedJSONMessage(new Date(), jsonText, json);
+
+            getServerMessageHandler().handleUIDLMessage(new Date(), jsonText,
+                    json);
         }
     }
 
@@ -1276,1189 +1133,7 @@ public class ApplicationConnection implements HasHandlers {
        }
     }-*/;
 
-    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);
-            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);
-                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() {
-                                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, 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
@@ -2483,7 +1158,7 @@ public class ApplicationConnection implements HasHandlers {
         }
     }
 
-    private void loadScriptDependencies(final JsArrayString dependencies) {
+    public void loadScriptDependencies(final JsArrayString dependencies) {
         if (dependencies.length() == 0) {
             return;
         }
@@ -2943,7 +1618,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
@@ -2954,6 +1629,19 @@ public class ApplicationConnection implements HasHandlers {
         return resourcesMap.get(name);
     }
 
+    /**
+     * 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.
      * 
@@ -3019,20 +1707,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.
@@ -3208,70 +1882,6 @@ 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());
-            }
-        }
-    }
-
     public void handleCommunicationError(String details, int statusCode) {
         boolean handled = false;
         if (communicationErrorDelegate != null) {
@@ -3447,21 +2057,6 @@ public class ApplicationConnection implements HasHandlers {
         return heartbeat;
     }
 
-    /**
-     * 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.
-     * 
-     * @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
-     */
-    public boolean isUpdatingState() {
-        return updatingState;
-    }
-
     /**
      * Returns the state of this application. An application state goes from
      * "initializing" to "running" to "stopped". There is no way for an
@@ -3492,4 +2087,37 @@ public class ApplicationConnection implements HasHandlers {
     public CommunicationProblemHandler getCommunicationProblemHandler() {
         return communicationProblemHandler;
     }
+
+    /**
+     * Gets the server message handler for this application
+     * 
+     * @return the server message handler
+     */
+    public ServerMessageHandler getServerMessageHandler() {
+        return serverMessageHandler;
+    }
+
+    /**
+     * Gets the server rpc manager for this application
+     * 
+     * @return the server rpc manager
+     */
+    public RpcManager getRpcManager() {
+        return rpcManager;
+    }
+
+    /**
+     * @return the widget set
+     */
+    public WidgetSet getWidgetSet() {
+        return widgetSet;
+    }
+
+    /**
+     * @since
+     * @return
+     */
+    public int getLastSeenServerSyncId() {
+        return getServerMessageHandler().getLastSeenServerSyncId();
+    }
 }
index eee597b70696ce5aeb4b6e8aa37e40b165d380c0..1833b370e5d3cd39588fdb187cfa4d593373c3c7 100644 (file)
@@ -64,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;
@@ -93,7 +93,7 @@ public class JavaScriptConnectorHelper {
     }
 
     private void processStateChanges() {
-        int lastResponseId = connector.getConnection().getLastResponseId();
+        int lastResponseId = connector.getConnection().getLastSeenServerSyncId();
         if (processedResponseId == lastResponseId) {
             return;
         }
index 102e618f5e346492d2aec1013186e7620b4a0fb0..8c723730a2e300e170b80026bad41c81d9df5507 100644 (file)
@@ -252,7 +252,7 @@ public class LayoutManager {
                     "Can't start a new layout phase before the previous layout phase ends.");
         }
 
-        if (connection.isUpdatingState()) {
+        if (connection.getServerMessageHandler().isUpdatingState()) {
             // If assertions are enabled, throw an exception
             assert false : STATE_CHANGE_MESSAGE;
 
@@ -1793,7 +1793,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() {
index 9fb6819e832db25b4ed598e1259bd856b4112e17..4464c3bee856198f312e576ae21b1774c0c60116 100644 (file)
@@ -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.
index 172fd84a84ebc3ad0d94380f9a3b0241ced48d0c..460b6491d07bae2f9362d39b29524bfdbf16c2a9 100644 (file)
@@ -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];
     }-*/;
index a81ab616cf0745b5dbeb218016050fa1132ad23f..f67ef091c9cba16444b67b8d594cf30d58f15d89 100644 (file)
@@ -201,10 +201,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.getServerMessageHandler().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
diff --git a/client/src/com/vaadin/client/communication/ServerMessageHandler.java b/client/src/com/vaadin/client/communication/ServerMessageHandler.java
new file mode 100644 (file)
index 0000000..cb0dc89
--- /dev/null
@@ -0,0 +1,1542 @@
+/*
+ * 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.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.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;
+
+/**
+ * ServerMessageHandler 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
+ * @author Vaadin Ltd
+ */
+public class ServerMessageHandler {
+
+    /**
+     * 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 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;
+        }
+
+        public ValueMap getJson() {
+            return json;
+        }
+    }
+
+    /**
+     * Sets the application connection this queue is connected to
+     *
+     * @param connection
+     *            the application connection this queue is connected to
+     */
+    public void setConnection(ApplicationConnection connection) {
+        this.connection = connection;
+    }
+
+    public static Logger getLogger() {
+        return Logger.getLogger(ServerMessageHandler.class.getName());
+    }
+
+    public 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");
+        connection.fireEvent(new ResponseHandlingStartedEvent(connection));
+
+        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
+                        connection.endRequest();
+                    }
+
+                    resumeResponseHandling(lock);
+                    connection.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);
+            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 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() {
+                                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 for " + jsonText.length()
+                                + " characters of JSON");
+                getLogger().info(
+                        "Referenced paintables: " + getConnectorMap().size());
+
+                if (meta == null || !meta.containsKey("async")) {
+                    // End the request if the received message was a response,
+                    // not sent asynchronously
+                    // FIXME
+                    connection.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 = 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);
+    }
+
+    /**
+     * 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();
+            }
+        }
+    }
+
+    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;
+        }
+    }-*/;
+
+    /**
+     * 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) {
+                handleUIDLMessage(pending.getStart(), pending.getJsonText(),
+                        pending.getJson());
+            }
+        }
+    }
+
+    /**
+     * 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();
+    }
+
+}
index 8fa885c2b9b30335d8a0c1e01719231ae8118fa7..dbd530dde1f3994d3a261a188f0ccc5223a44a02 100644 (file)
@@ -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;
         }
index 139ee7cf3adf41e21edc8467d89654f77e67c16f..b6d8495a64fe530056c82b6d57bef6baca375fdb 100644 (file)
  */
 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;
 
@@ -34,15 +30,20 @@ import elemental.json.JsonValue;
  */
 public class MockApplicationConnection extends ApplicationConnection {
 
-    private static final Logger LOGGER = Logger
-            .getLogger(MockApplicationConnection.class.getName());
-
-    // The last token received from the server.
-    private String lastCsrfTokenReceiver;
+    public MockApplicationConnection() {
+        super();
+        serverMessageHandler = new MockServerMessageHandler();
+        serverMessageHandler.setConnection(this);
+    }
 
     // The last token sent to the server.
     private String lastCsrfTokenSent;
 
+    @Override
+    public MockServerMessageHandler getServerMessageHandler() {
+        return (MockServerMessageHandler) super.getServerMessageHandler();
+    }
+
     /**
      * Provide the last token received from the server. <br/>
      * We added this to test the change done on CSRF token.
@@ -50,7 +51,7 @@ public class MockApplicationConnection extends ApplicationConnection {
      * @see CsrfTokenDisabled
      */
     public String getLastCsrfTokenReceiver() {
-        return lastCsrfTokenReceiver;
+        return getServerMessageHandler().lastCsrfTokenReceiver;
     }
 
     /**
@@ -63,14 +64,6 @@ public class MockApplicationConnection extends ApplicationConnection {
         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
     public void doUidlRequest(String uri, JsonObject payload, boolean retry) {
         JsonValue jsonValue = payload.get(ApplicationConstants.CSRF_TOKEN);
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 (file)
index 0000000..499a482
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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 java.util.Date;
+
+import com.vaadin.client.ValueMap;
+import com.vaadin.client.communication.ServerMessageHandler;
+import com.vaadin.shared.ApplicationConstants;
+
+public class MockServerMessageHandler extends ServerMessageHandler {
+
+    // The last token received from the server.
+    protected String lastCsrfTokenReceiver;
+
+    @Override
+    public void handleUIDLMessage(Date start, String jsonText, ValueMap json) {
+        lastCsrfTokenReceiver = json
+                .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID);
+
+        super.handleUIDLMessage(start, jsonText, json);
+    }
+
+}
index cf24ed69213ca1f9ddc5af1ad331dbe755d3c127..3eba2d26374f823b0b6bd4004cbf537d9e6092dd 100644 (file)
@@ -70,8 +70,8 @@ public class CsrfButtonConnector extends AbstractComponentConnector {
     }
 
     private String csrfTokenInfo() {
-        return getMockConnection().getCsrfToken() + ", "
-                + getMockConnection().getLastCsrfTokenReceiver() + ", "
+        return getMockConnection().getServerMessageHandler().getCsrfToken()
+                + ", " + getMockConnection().getLastCsrfTokenReceiver() + ", "
                 + getMockConnection().getLastCsrfTokenSent();
     }