From 24ac02a52362b957c1483026691394e89ec4fd78 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 31 Aug 2015 15:21:10 +0300 Subject: API refactor based on review (#11733) ServerMessageHandler -> MessageHandler ServerCommunicationHandler -> MessageSender State -> ApplicationState CommunicationProblemHandler -> ConnectionStateHandler CommunicationProblemEvent -> XhrConnectionError Change-Id: I2eccfea9cf6a275eba02ac605b6a172e496bd004 --- .../com/vaadin/client/ApplicationConnection.java | 100 +- client/src/com/vaadin/client/LayoutManager.java | 9 +- .../communication/AtmospherePushConnection.java | 26 +- .../communication/CommunicationProblemEvent.java | 106 -- .../communication/CommunicationProblemHandler.java | 193 --- .../communication/ConnectionStateHandler.java | 193 +++ .../DefaultConnectionStateHandler.java | 584 +++++++ .../com/vaadin/client/communication/Heartbeat.java | 6 +- .../client/communication/MessageHandler.java | 1747 ++++++++++++++++++++ .../vaadin/client/communication/MessageSender.java | 410 +++++ .../client/communication/PushConnection.java | 2 +- .../ReconnectingCommunicationProblemHandler.java | 585 ------- .../communication/ServerCommunicationHandler.java | 409 ----- .../client/communication/ServerMessageHandler.java | 1746 ------------------- .../client/communication/ServerRpcQueue.java | 8 +- .../communication/TranslatedURLReference.java | 5 +- .../vaadin/client/communication/XhrConnection.java | 46 +- .../client/communication/XhrConnectionError.java | 106 ++ .../vaadin/client/debug/internal/InfoSection.java | 2 +- client/src/com/vaadin/client/ui/VScrollTable.java | 2 +- .../vaadin/client/ui/dd/VDragAndDropManager.java | 2 +- .../src/com/vaadin/client/ui/ui/UIConnector.java | 4 +- .../communication/ServerMessageHandlerTest.java | 12 +- .../client/MockApplicationConnection.java | 20 +- .../client/MockServerCommunicationHandler.java | 4 +- .../widgetset/client/MockServerMessageHandler.java | 4 +- .../widgetset/client/csrf/CsrfButtonConnector.java | 2 +- 27 files changed, 3168 insertions(+), 3165 deletions(-) delete mode 100644 client/src/com/vaadin/client/communication/CommunicationProblemEvent.java delete mode 100644 client/src/com/vaadin/client/communication/CommunicationProblemHandler.java create mode 100644 client/src/com/vaadin/client/communication/ConnectionStateHandler.java create mode 100644 client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java create mode 100644 client/src/com/vaadin/client/communication/MessageHandler.java create mode 100644 client/src/com/vaadin/client/communication/MessageSender.java delete mode 100644 client/src/com/vaadin/client/communication/ReconnectingCommunicationProblemHandler.java delete mode 100644 client/src/com/vaadin/client/communication/ServerCommunicationHandler.java delete mode 100644 client/src/com/vaadin/client/communication/ServerMessageHandler.java create mode 100644 client/src/com/vaadin/client/communication/XhrConnectionError.java diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index c7d470accc..048bd320ee 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -42,14 +42,15 @@ import com.google.gwt.user.client.Timer; 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.CommunicationProblemHandler; +import com.vaadin.client.communication.ConnectionStateHandler; +import com.vaadin.client.communication.DefaultConnectionStateHandler; import com.vaadin.client.communication.Heartbeat; -import com.vaadin.client.communication.ReconnectingCommunicationProblemHandler; +import com.vaadin.client.communication.MessageHandler; +import com.vaadin.client.communication.MessageSender; import com.vaadin.client.communication.RpcManager; -import com.vaadin.client.communication.ServerCommunicationHandler; -import com.vaadin.client.communication.ServerMessageHandler; import com.vaadin.client.communication.ServerRpcQueue; import com.vaadin.client.componentlocator.ComponentLocator; import com.vaadin.client.metadata.ConnectorBundleLoader; @@ -139,11 +140,11 @@ public class ApplicationConnection implements HasHandlers { /** Event bus for communication events */ private EventBus eventBus = GWT.create(SimpleEventBus.class); - public enum State { + public enum ApplicationState { INITIALIZING, RUNNING, TERMINATED; } - private State state = State.INITIALIZING; + private ApplicationState applicationState = ApplicationState.INITIALIZING; /** * The communication handler methods are called at certain points during @@ -364,14 +365,13 @@ public class ApplicationConnection implements HasHandlers { loadingIndicator.setConnection(this); serverRpcQueue = GWT.create(ServerRpcQueue.class); serverRpcQueue.setConnection(this); - communicationProblemHandler = GWT - .create(ReconnectingCommunicationProblemHandler.class); - communicationProblemHandler.setConnection(this); - serverMessageHandler = GWT.create(ServerMessageHandler.class); - serverMessageHandler.setConnection(this); - serverCommunicationHandler = GWT - .create(ServerCommunicationHandler.class); - serverCommunicationHandler.setConnection(this); + connectionStateHandler = GWT + .create(DefaultConnectionStateHandler.class); + connectionStateHandler.setConnection(this); + messageHandler = GWT.create(MessageHandler.class); + messageHandler.setConnection(this); + messageSender = GWT.create(MessageSender.class); + messageSender.setConnection(this); } public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { @@ -433,14 +433,14 @@ public class ApplicationConnection implements HasHandlers { String jsonText = configuration.getUIDL(); if (jsonText == null) { // initial UIDL not in DOM, request from server - getServerCommunicationHandler().resynchronize(); + getMessageSender().resynchronize(); } else { // initial UIDL provided in DOM, continue as if returned by request // Hack to avoid logging an error in endRequest() - getServerCommunicationHandler().startRequest(); - getServerMessageHandler().handleMessage( - ServerMessageHandler.parseJson(jsonText)); + getMessageSender().startRequest(); + getMessageHandler().handleMessage( + MessageHandler.parseJson(jsonText)); } // Tooltip can't be created earlier because the @@ -463,9 +463,8 @@ public class ApplicationConnection implements HasHandlers { * @return true if the client has some work to be done, false otherwise */ private boolean isActive() { - return !getServerMessageHandler().isInitialUidlHandled() - || isWorkPending() - || getServerCommunicationHandler().hasActiveRequest() + return !getMessageHandler().isInitialUidlHandled() || isWorkPending() + || getMessageSender().hasActiveRequest() || isExecutingDeferredCommands(); } @@ -485,13 +484,13 @@ public class ApplicationConnection implements HasHandlers { } client.getProfilingData = $entry(function() { - var smh = ap.@com.vaadin.client.ApplicationConnection::getServerMessageHandler(); + var smh = ap.@com.vaadin.client.ApplicationConnection::getMessageHandler(); var pd = [ - smh.@com.vaadin.client.communication.ServerMessageHandler::lastProcessingTime, - smh.@com.vaadin.client.communication.ServerMessageHandler::totalProcessingTime + smh.@com.vaadin.client.communication.MessageHandler::lastProcessingTime, + smh.@com.vaadin.client.communication.MessageHandler::totalProcessingTime ]; - pd = pd.concat(smh.@com.vaadin.client.communication.ServerMessageHandler::serverTimingInfo); - pd[pd.length] = smh.@com.vaadin.client.communication.ServerMessageHandler::bootstrapTime; + pd = pd.concat(smh.@com.vaadin.client.communication.MessageHandler::serverTimingInfo); + pd[pd.length] = smh.@com.vaadin.client.communication.MessageHandler::bootstrapTime; return pd; }); @@ -598,9 +597,9 @@ public class ApplicationConnection implements HasHandlers { int cssWaits = 0; protected ServerRpcQueue serverRpcQueue; - protected CommunicationProblemHandler communicationProblemHandler; - protected ServerMessageHandler serverMessageHandler; - protected ServerCommunicationHandler serverCommunicationHandler; + protected ConnectionStateHandler connectionStateHandler; + protected MessageHandler messageHandler; + protected MessageSender messageSender; static final int MAX_CSS_WAITS = 100; @@ -1475,7 +1474,7 @@ public class ApplicationConnection implements HasHandlers { } public void setApplicationRunning(boolean applicationRunning) { - if (getState() == State.TERMINATED) { + if (getApplicationState() == ApplicationState.TERMINATED) { if (applicationRunning) { getLogger() .severe("Tried to restart a terminated application. This is not supported"); @@ -1485,17 +1484,17 @@ public class ApplicationConnection implements HasHandlers { "Tried to stop a terminated application. This should not be done"); } return; - } else if (getState() == State.INITIALIZING) { + } else if (getApplicationState() == ApplicationState.INITIALIZING) { if (applicationRunning) { - state = State.RUNNING; + applicationState = ApplicationState.RUNNING; } else { getLogger() .warning( "Tried to stop the application before it has started. This should not be done"); } - } else if (getState() == State.RUNNING) { + } else if (getApplicationState() == ApplicationState.RUNNING) { if (!applicationRunning) { - state = State.TERMINATED; + applicationState = ApplicationState.TERMINATED; eventBus.fireEvent(new ApplicationStoppedEvent()); } else { getLogger() @@ -1506,13 +1505,14 @@ public class ApplicationConnection implements HasHandlers { } /** - * Checks if the application is in the {@link State#RUNNING} state. + * Checks if the application is in the {@link ApplicationState#RUNNING} + * state. * * @since * @return true if the application is in the running state, false otherwise */ public boolean isApplicationRunning() { - return state == State.RUNNING; + return applicationState == ApplicationState.RUNNING; } public HandlerRegistration addHandler( @@ -1568,11 +1568,11 @@ public class ApplicationConnection implements HasHandlers { * application to go back to a previous state, i.e. a stopped application * can never be re-started * - * @since + * @since 7.6 * @return the current state of this application */ - public State getState() { - return state; + public ApplicationState getApplicationState() { + return applicationState; } /** @@ -1589,17 +1589,17 @@ public class ApplicationConnection implements HasHandlers { * * @return the server RPC queue */ - public CommunicationProblemHandler getCommunicationProblemHandler() { - return communicationProblemHandler; + public ConnectionStateHandler getConnectionStateHandler() { + return connectionStateHandler; } /** - * Gets the server message handler for this application + * Gets the (server to client) message handler for this application * - * @return the server message handler + * @return the message handler */ - public ServerMessageHandler getServerMessageHandler() { - return serverMessageHandler; + public MessageHandler getMessageHandler() { + return messageHandler; } /** @@ -1612,12 +1612,12 @@ public class ApplicationConnection implements HasHandlers { } /** - * Gets the server communication handler for this application + * Gets the (client to server) message sender for this application * - * @return the server communication handler + * @return the message sender */ - public ServerCommunicationHandler getServerCommunicationHandler() { - return serverCommunicationHandler; + public MessageSender getMessageSender() { + return messageSender; } /** @@ -1628,7 +1628,7 @@ public class ApplicationConnection implements HasHandlers { } public int getLastSeenServerSyncId() { - return getServerMessageHandler().getLastSeenServerSyncId(); + return getMessageHandler().getLastSeenServerSyncId(); } } diff --git a/client/src/com/vaadin/client/LayoutManager.java b/client/src/com/vaadin/client/LayoutManager.java index 8c723730a2..c6c172e9c3 100644 --- a/client/src/com/vaadin/client/LayoutManager.java +++ b/client/src/com/vaadin/client/LayoutManager.java @@ -70,6 +70,13 @@ public class LayoutManager { }; private boolean everythingNeedsMeasure = false; + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ public void setConnection(ApplicationConnection connection) { if (this.connection != null) { throw new RuntimeException( @@ -252,7 +259,7 @@ public class LayoutManager { "Can't start a new layout phase before the previous layout phase ends."); } - if (connection.getServerMessageHandler().isUpdatingState()) { + if (connection.getMessageHandler().isUpdatingState()) { // If assertions are enabled, throw an exception assert false : STATE_CHANGE_MESSAGE; diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java index d36dccdc8b..def2f65e2a 100644 --- a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java @@ -194,7 +194,7 @@ public class AtmospherePushConnection implements PushConnection { String extraParams = UIConstants.UI_ID_PARAMETER + "=" + connection.getConfiguration().getUIId(); - String csrfToken = connection.getServerMessageHandler().getCsrfToken(); + String csrfToken = connection.getMessageHandler().getCsrfToken(); if (!csrfToken.equals(ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) { extraParams += "&" + ApplicationConstants.CSRF_TOKEN_PARAMETER + "=" + csrfToken; @@ -270,7 +270,7 @@ public class AtmospherePushConnection implements PushConnection { } if (state == State.CONNECT_PENDING) { - getCommunicationProblemHandler().pushNotConnected(message); + getConnectionStateHandler().pushNotConnected(message); return; } @@ -307,7 +307,7 @@ public class AtmospherePushConnection implements PushConnection { switch (state) { case CONNECT_PENDING: state = State.CONNECTED; - getCommunicationProblemHandler().pushOk(this); + getConnectionStateHandler().pushOk(this); break; case DISCONNECT_PENDING: // Set state to connected to make disconnect close the connection @@ -355,16 +355,16 @@ public class AtmospherePushConnection implements PushConnection { protected void onMessage(AtmosphereResponse response) { String message = response.getResponseBody(); - ValueMap json = ServerMessageHandler.parseWrappedJson(message); + ValueMap json = MessageHandler.parseWrappedJson(message); if (json == null) { // Invalid string (not wrapped as expected) - getCommunicationProblemHandler().pushInvalidContent(this, message); + getConnectionStateHandler().pushInvalidContent(this, message); return; } else { getLogger().info( "Received push (" + getTransportType() + ") message: " + message); - connection.getServerMessageHandler().handleMessage(json); + connection.getMessageHandler().handleMessage(json); } } @@ -386,17 +386,17 @@ public class AtmospherePushConnection implements PushConnection { */ protected void onError(AtmosphereResponse response) { state = State.DISCONNECTED; - getCommunicationProblemHandler().pushError(this); + getConnectionStateHandler().pushError(this); } protected void onClose(AtmosphereResponse response) { state = State.CONNECT_PENDING; - getCommunicationProblemHandler().pushClosed(this); + getConnectionStateHandler().pushClosed(this); } protected void onClientTimeout(AtmosphereResponse response) { state = State.DISCONNECTED; - getCommunicationProblemHandler().pushClientTimeout(this); + getConnectionStateHandler().pushClientTimeout(this); } protected void onReconnect(JavaScriptObject request, @@ -404,7 +404,7 @@ public class AtmospherePushConnection implements PushConnection { if (state == State.CONNECTED) { state = State.CONNECT_PENDING; } - getCommunicationProblemHandler().pushReconnectPending(this); + getConnectionStateHandler().pushReconnectPending(this); } public static abstract class AbstractJSO extends JavaScriptObject { @@ -575,7 +575,7 @@ public class AtmospherePushConnection implements PushConnection { @Override public void onError(ResourceLoadEvent event) { - getCommunicationProblemHandler() + getConnectionStateHandler() .pushScriptLoadError(event.getResourceUrl()); } }); @@ -603,8 +603,8 @@ public class AtmospherePushConnection implements PushConnection { return Logger.getLogger(AtmospherePushConnection.class.getName()); } - private CommunicationProblemHandler getCommunicationProblemHandler() { - return connection.getCommunicationProblemHandler(); + private ConnectionStateHandler getConnectionStateHandler() { + return connection.getConnectionStateHandler(); } } diff --git a/client/src/com/vaadin/client/communication/CommunicationProblemEvent.java b/client/src/com/vaadin/client/communication/CommunicationProblemEvent.java deleted file mode 100644 index 0e1019ec97..0000000000 --- a/client/src/com/vaadin/client/communication/CommunicationProblemEvent.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2000-2014 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.client.communication; - -import com.google.gwt.http.client.Request; -import com.google.gwt.http.client.Response; - -import elemental.json.JsonObject; - -/** - * Event describing a problem which took place during communication with the - * server - * - * @since 7.6 - * @author Vaadin Ltd - */ -public class CommunicationProblemEvent { - - private Throwable exception; - private Request request; - private Response response; - private JsonObject payload; - - /** - * Constructs an event from the given request, payload and exception - * - * @param request - * the request which failed - * @param payload - * the payload which was going to the server - * @param exception - * the exception describing the problem - */ - public CommunicationProblemEvent(Request request, JsonObject payload, - Throwable exception) { - this.request = request; - this.exception = exception; - this.payload = payload; - } - - /** - * Constructs an event from the given request, response and payload - * - * @param request - * the request which failed - * @param payload - * the payload which was going to the server - * @param response - * the response for the request - */ - public CommunicationProblemEvent(Request request, JsonObject payload, - Response response) { - this.request = request; - this.response = response; - this.payload = payload; - } - - /** - * Returns the exception which caused the problem, if available - * - * @return the exception which caused the problem, or null if not available - */ - public Throwable getException() { - return exception; - } - - /** - * Returns the request for which the problem occurred - * - * @return the request where the problem occurred - */ - public Request getRequest() { - return request; - } - - /** - * Returns the received response, if available - * - * @return the received response, or null if not available - */ - public Response getResponse() { - return response; - } - - /** - * Returns the payload which was sent to the server - * - * @return the payload which was sent, never null - */ - public JsonObject getPayload() { - return payload; - } -} \ No newline at end of file diff --git a/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java b/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java deleted file mode 100644 index 1451ec3f93..0000000000 --- a/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2000-2014 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.client.communication; - -import com.google.gwt.http.client.Request; -import com.google.gwt.http.client.Response; -import com.vaadin.client.ApplicationConnection; - -import elemental.json.JsonObject; - -/** - * Interface for handling problems which occur during communication with the - * server. - * - * The handler is responsible for handling any problem in XHR, heartbeat and - * push connections in a way it sees fit. The default implementation is - * {@link ReconnectingCommunicationProblemHandler}. - * - * @since 7.6 - * @author Vaadin Ltd - */ -public interface CommunicationProblemHandler { - - /** - * Sets the application connection this handler is connected to. Called - * internally by the framework. - * - * @param connection - * the application connection this handler is connected to - */ - void setConnection(ApplicationConnection connection); - - /** - * Called when an exception occurs during a {@link Heartbeat} request - * - * @param request - * The heartbeat request - * @param exception - * The exception which occurred - */ - void heartbeatException(Request request, Throwable exception); - - /** - * Called when a heartbeat request returns a status code other than OK (200) - * - * @param request - * The heartbeat request - * @param response - * The heartbeat response - */ - void heartbeatInvalidStatusCode(Request request, Response response); - - /** - * Called when a {@link Heartbeat} request succeeds - */ - void heartbeatOk(); - - /** - * Called when the push connection to the server is closed. This might - * result in the push connection trying a fallback connection method, trying - * to reconnect to the server or might just be an indication that the - * connection was intentionally closed ("unsubscribe"), - * - * @param pushConnection - * The push connection which was closed - */ - void pushClosed(PushConnection pushConnection); - - /** - * Called when a client side timeout occurs before a push connection to the - * server completes. - * - * The client side timeout causes a disconnection of the push connection and - * no reconnect will be attempted after this method is called, - * - * @param pushConnection - * The push connection which timed out - */ - void pushClientTimeout(PushConnection pushConnection); - - /** - * Called when a fatal error fatal error occurs in the push connection. - * - * The push connection will not try to recover from this situation itself - * and typically the problem handler should not try to do automatic recovery - * either. The cause can be e.g. maximum number of reconnection attempts - * have been reached, neither the selected transport nor the fallback - * transport can be used or similar. - * - * @param pushConnection - * The push connection where the error occurred - */ - void pushError(PushConnection pushConnection); - - /** - * Called when the push connection has lost the connection to the server and - * will proceed to try to re-establish the connection - * - * @param pushConnection - * The push connection which will be reconnected - */ - void pushReconnectPending(PushConnection pushConnection); - - /** - * Called when the push connection to the server has been established. - * - * @param pushConnection - * The push connection which was established - */ - void pushOk(PushConnection pushConnection); - - /** - * Called when the required push script could not be loaded - * - * @param resourceUrl - * The URL which was used for loading the script - */ - void pushScriptLoadError(String resourceUrl); - - /** - * Called when an exception occurs during an XmlHttpRequest request to the - * server. - * - * @param communicationProblemEvent - * An event containing what was being sent to the server and what - * exception occurred - */ - void xhrException(CommunicationProblemEvent communicationProblemEvent); - - /** - * Called when invalid content (not JSON) was returned from the server as - * the result of an XmlHttpRequest request - * - * @param communicationProblemEvent - * An event containing what was being sent to the server and what - * was returned - */ - void xhrInvalidContent(CommunicationProblemEvent communicationProblemEvent); - - /** - * Called when invalid status code (not 200) was returned by the server as - * the result of an XmlHttpRequest. - * - * @param communicationProblemEvent - * An event containing what was being sent to the server and what - * was returned - */ - void xhrInvalidStatusCode(CommunicationProblemEvent problemEvent); - - /** - * Called whenever a XmlHttpRequest to the server completes successfully - */ - void xhrOk(); - - /** - * Called when a message is to be sent to the server through the push - * channel but the push channel is not connected - * - * @param payload - * The payload to send to the server - */ - void pushNotConnected(JsonObject payload); - - /** - * Called when invalid content (not JSON) was pushed from the server through - * the push connection - * - * @param communicationProblemEvent - * An event containing what was being sent to the server and what - * was returned - */ - void pushInvalidContent(PushConnection pushConnection, String message); - - /** - * Called when some part of the reconnect dialog configuration has been - * changed. - */ - void configurationUpdated(); - -} diff --git a/client/src/com/vaadin/client/communication/ConnectionStateHandler.java b/client/src/com/vaadin/client/communication/ConnectionStateHandler.java new file mode 100644 index 0000000000..4bbca9055a --- /dev/null +++ b/client/src/com/vaadin/client/communication/ConnectionStateHandler.java @@ -0,0 +1,193 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; +import com.vaadin.client.ApplicationConnection; + +import elemental.json.JsonObject; + +/** + * Interface for handling problems and other events which occur during + * communication with the server. + * + * The handler is responsible for handling any problem in XHR, heartbeat and + * push connections in a way it sees fit. The default implementation is + * {@link DefaultConnectionStateHandler}. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public interface ConnectionStateHandler { + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + void setConnection(ApplicationConnection connection); + + /** + * Called when an exception occurs during a {@link Heartbeat} request + * + * @param request + * The heartbeat request + * @param exception + * The exception which occurred + */ + void heartbeatException(Request request, Throwable exception); + + /** + * Called when a heartbeat request returns a status code other than OK (200) + * + * @param request + * The heartbeat request + * @param response + * The heartbeat response + */ + void heartbeatInvalidStatusCode(Request request, Response response); + + /** + * Called when a {@link Heartbeat} request succeeds + */ + void heartbeatOk(); + + /** + * Called when the push connection to the server is closed. This might + * result in the push connection trying a fallback connection method, trying + * to reconnect to the server or might just be an indication that the + * connection was intentionally closed ("unsubscribe"), + * + * @param pushConnection + * The push connection which was closed + */ + void pushClosed(PushConnection pushConnection); + + /** + * Called when a client side timeout occurs before a push connection to the + * server completes. + * + * The client side timeout causes a disconnection of the push connection and + * no reconnect will be attempted after this method is called, + * + * @param pushConnection + * The push connection which timed out + */ + void pushClientTimeout(PushConnection pushConnection); + + /** + * Called when a fatal error fatal error occurs in the push connection. + * + * The push connection will not try to recover from this situation itself + * and typically the problem handler should not try to do automatic recovery + * either. The cause can be e.g. maximum number of reconnection attempts + * have been reached, neither the selected transport nor the fallback + * transport can be used or similar. + * + * @param pushConnection + * The push connection where the error occurred + */ + void pushError(PushConnection pushConnection); + + /** + * Called when the push connection has lost the connection to the server and + * will proceed to try to re-establish the connection + * + * @param pushConnection + * The push connection which will be reconnected + */ + void pushReconnectPending(PushConnection pushConnection); + + /** + * Called when the push connection to the server has been established. + * + * @param pushConnection + * The push connection which was established + */ + void pushOk(PushConnection pushConnection); + + /** + * Called when the required push script could not be loaded + * + * @param resourceUrl + * The URL which was used for loading the script + */ + void pushScriptLoadError(String resourceUrl); + + /** + * Called when an exception occurs during an XmlHttpRequest request to the + * server. + * + * @param xhrConnectionError + * An event containing what was being sent to the server and what + * exception occurred + */ + void xhrException(XhrConnectionError xhrConnectionError); + + /** + * Called when invalid content (not JSON) was returned from the server as + * the result of an XmlHttpRequest request + * + * @param communicationProblemEvent + * An event containing what was being sent to the server and what + * was returned + */ + void xhrInvalidContent(XhrConnectionError xhrConnectionError); + + /** + * Called when invalid status code (not 200) was returned by the server as + * the result of an XmlHttpRequest. + * + * @param communicationProblemEvent + * An event containing what was being sent to the server and what + * was returned + */ + void xhrInvalidStatusCode(XhrConnectionError xhrConnectionError); + + /** + * Called whenever a XmlHttpRequest to the server completes successfully + */ + void xhrOk(); + + /** + * Called when a message is to be sent to the server through the push + * channel but the push channel is not connected + * + * @param payload + * The payload to send to the server + */ + void pushNotConnected(JsonObject payload); + + /** + * Called when invalid content (not JSON) was pushed from the server through + * the push connection + * + * @param communicationProblemEvent + * An event containing what was being sent to the server and what + * was returned + */ + void pushInvalidContent(PushConnection pushConnection, String message); + + /** + * Called when some part of the reconnect dialog configuration has been + * changed. + */ + void configurationUpdated(); + +} diff --git a/client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java b/client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java new file mode 100644 index 0000000000..be73b9002c --- /dev/null +++ b/client/src/com/vaadin/client/communication/DefaultConnectionStateHandler.java @@ -0,0 +1,584 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.logging.Logger; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.Timer; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; +import com.vaadin.client.ApplicationConnection.ApplicationStoppedHandler; +import com.vaadin.client.WidgetUtil; +import com.vaadin.shared.ui.ui.UIState.ReconnectDialogConfigurationState; + +import elemental.json.JsonObject; + +/** + * Default implementation of the connection state handler. + *

+ * Handles temporary errors by showing a reconnect dialog to the user while + * trying to re-establish the connection to the server and re-send the pending + * message. + *

+ * Handles permanent errors by showing a critical system notification to the + * user + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class DefaultConnectionStateHandler implements ConnectionStateHandler { + + private ApplicationConnection connection; + private ReconnectDialog reconnectDialog = GWT.create(ReconnectDialog.class); + private int reconnectAttempt = 0; + private Type reconnectionCause = null; + + private Timer scheduledReconnect; + private Timer dialogShowTimer = new Timer() { + + @Override + public void run() { + showDialog(); + } + + }; + + protected enum Type { + HEARTBEAT(0), PUSH(1), XHR(2); + + private int priority; + + private Type(int priority) { + this.priority = priority; + } + + public boolean isMessage() { + return this == PUSH || this == XHR; + } + + /** + * Checks if this type is of higher priority than the given type + * + * @param type + * the type to compare to + * @return true if this type has higher priority than the given type, + * false otherwise + */ + public boolean isHigherPriorityThan(Type type) { + return priority > type.priority; + } + } + + @Override + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + + connection.addHandler(ApplicationStoppedEvent.TYPE, + new ApplicationStoppedHandler() { + @Override + public void onApplicationStopped( + ApplicationStoppedEvent event) { + if (isReconnecting()) { + giveUp(); + } + if (scheduledReconnect != null + && scheduledReconnect.isRunning()) { + scheduledReconnect.cancel(); + } + } + + }); + }; + + /** + * Checks if we are currently trying to reconnect + * + * @return true if we have noted a problem and are trying to re-establish + * server connection, false otherwise + */ + private boolean isReconnecting() { + return reconnectionCause != null; + } + + private static Logger getLogger() { + return Logger.getLogger(DefaultConnectionStateHandler.class.getName()); + } + + /** + * Returns the connection this handler is connected to + * + * @return the connection for this handler + */ + protected ApplicationConnection getConnection() { + return connection; + } + + @Override + public void xhrException(XhrConnectionError xhrConnectionError) { + debug("xhrException"); + handleRecoverableError(Type.XHR, xhrConnectionError.getPayload()); + } + + @Override + public void heartbeatException(Request request, Throwable exception) { + getLogger().severe("Heartbeat exception: " + exception.getMessage()); + handleRecoverableError(Type.HEARTBEAT, null); + } + + @Override + public void heartbeatInvalidStatusCode(Request request, Response response) { + int statusCode = response.getStatusCode(); + getLogger().warning("Heartbeat request returned " + statusCode); + + if (response.getStatusCode() == Response.SC_GONE) { + // Session expired + getConnection().showSessionExpiredError(null); + stopApplication(); + } else if (response.getStatusCode() == Response.SC_NOT_FOUND) { + // UI closed, do nothing as the UI will react to this + // Should not trigger reconnect dialog as this will prevent user + // input + } else { + handleRecoverableError(Type.HEARTBEAT, null); + } + } + + @Override + public void heartbeatOk() { + debug("heartbeatOk"); + if (isReconnecting()) { + resolveTemporaryError(Type.HEARTBEAT); + } + } + + private void debug(String msg) { + if (false) { + getLogger().warning(msg); + } + } + + /** + * Called whenever an error occurs in communication which should be handled + * by showing the reconnect dialog and retrying communication until + * successful again + * + * @param type + * The type of failure detected + * @param payload + * The message which did not reach the server, or null if no + * message was involved (heartbeat or push connection failed) + */ + protected void handleRecoverableError(Type type, final JsonObject payload) { + debug("handleTemporaryError(" + type + ")"); + if (!connection.isApplicationRunning()) { + return; + } + + if (!isReconnecting()) { + // First problem encounter + reconnectionCause = type; + getLogger().warning("Reconnecting because of " + type + " failure"); + // Precaution only as there should never be a dialog at this point + // and no timer running + stopDialogTimer(); + if (isDialogVisible()) { + hideDialog(); + } + + // Show dialog after grace period, still continue to try to + // reconnect even before it is shown + dialogShowTimer.schedule(getConfiguration().dialogGracePeriod); + } else { + // We are currently trying to reconnect + // Priority is HEARTBEAT -> PUSH -> XHR + // If a higher priority issues is resolved, we can assume the lower + // one will be also + if (type.isHigherPriorityThan(reconnectionCause)) { + getLogger().warning( + "Now reconnecting because of " + type + " failure"); + reconnectionCause = type; + } + } + + if (reconnectionCause != type) { + return; + } + + reconnectAttempt++; + getLogger().info( + "Reconnect attempt " + reconnectAttempt + " for " + type); + + if (reconnectAttempt >= getConfiguration().reconnectAttempts) { + // Max attempts reached, stop trying + giveUp(); + } else { + updateDialog(); + scheduleReconnect(payload); + } + } + + /** + * Called after a problem occurred. + * + * This method is responsible for re-sending the payload to the server (if + * not null) or re-send a heartbeat request at some point + * + * @param payload + * the payload that did not reach the server, null if the problem + * was detected by a heartbeat + */ + protected void scheduleReconnect(final JsonObject payload) { + // Here and not in timer to avoid TB for getting in between + + // The request is still open at this point to avoid interference, so we + // do not need to start a new one + if (reconnectAttempt == 1) { + // Try once immediately + doReconnect(payload); + } else { + scheduledReconnect = new Timer() { + @Override + public void run() { + scheduledReconnect = null; + doReconnect(payload); + } + }; + scheduledReconnect.schedule(getConfiguration().reconnectInterval); + } + } + + /** + * Re-sends the payload to the server (if not null) or re-sends a heartbeat + * request immediately + * + * @param payload + * the payload that did not reach the server, null if the problem + * was detected by a heartbeat + */ + protected void doReconnect(JsonObject payload) { + if (!connection.isApplicationRunning()) { + // This should not happen as nobody should call this if the + // application has been stopped + getLogger() + .warning( + "Trying to reconnect after application has been stopped. Giving up"); + return; + } + if (payload != null) { + getLogger().info("Re-sending last message to the server..."); + getConnection().getMessageSender().send(payload); + } else { + // Use heartbeat + getLogger().info("Trying to re-establish server connection..."); + getConnection().getHeartbeat().send(); + } + } + + /** + * Called whenever a reconnect attempt fails to allow updating of dialog + * contents + */ + protected void updateDialog() { + reconnectDialog.setText(getDialogText(reconnectAttempt)); + } + + /** + * Called when we should give up trying to reconnect and let the user decide + * how to continue + * + */ + protected void giveUp() { + reconnectionCause = null; + endRequest(); + + stopDialogTimer(); + if (!isDialogVisible()) { + // It SHOULD always be visible at this point, unless you have a + // really strange configuration (grace time longer than total + // reconnect time) + showDialog(); + } + reconnectDialog.setText(getDialogTextGaveUp(reconnectAttempt)); + reconnectDialog.setReconnecting(false); + + // Stopping the application stops heartbeats and push + connection.setApplicationRunning(false); + } + + /** + * Ensures the reconnect dialog does not popup some time from now + */ + private void stopDialogTimer() { + if (dialogShowTimer.isRunning()) { + dialogShowTimer.cancel(); + } + } + + /** + * Checks if the reconnect dialog is visible to the user + * + * @return true if the user can see the dialog, false otherwise + */ + protected boolean isDialogVisible() { + return reconnectDialog.isVisible(); + } + + /** + * Called when the reconnect dialog should be shown. This is typically when + * N seconds has passed since a problem with the connection has been + * detected + */ + protected void showDialog() { + reconnectDialog.setReconnecting(true); + reconnectDialog.show(connection); + + // We never want to show loading indicator and reconnect dialog at the + // same time + connection.getLoadingIndicator().hide(); + } + + /** + * Called when the reconnect dialog should be hidden. + */ + protected void hideDialog() { + reconnectDialog.hide(); + } + + /** + * Gets the text to show in the reconnect dialog after giving up (reconnect + * limit reached) + * + * @param reconnectAttempt + * The number of the current reconnection attempt + * @return The text to show in the reconnect dialog after giving up + */ + protected String getDialogTextGaveUp(int reconnectAttempt) { + return getConfiguration().dialogTextGaveUp.replace("{0}", + reconnectAttempt + ""); + } + + /** + * Gets the text to show in the reconnect dialog + * + * @param reconnectAttempt + * The number of the current reconnection attempt + * @return The text to show in the reconnect dialog + */ + protected String getDialogText(int reconnectAttempt) { + return getConfiguration().dialogText.replace("{0}", reconnectAttempt + + ""); + } + + @Override + public void configurationUpdated() { + // All other properties are fetched directly from the state when needed + reconnectDialog.setModal(getConfiguration().dialogModal); + } + + private ReconnectDialogConfigurationState getConfiguration() { + return connection.getUIConnector().getState().reconnectDialogConfiguration; + } + + @Override + public void xhrInvalidContent(XhrConnectionError xhrConnectionError) { + debug("xhrInvalidContent"); + endRequest(); + + String responseText = xhrConnectionError.getResponse().getText(); + /* + * A servlet filter or equivalent may have intercepted the request and + * served non-UIDL content (for instance, a login page if the session + * has expired.) If the response contains a magic substring, do a + * synchronous refresh. See #8241. + */ + MatchResult refreshToken = RegExp.compile( + ApplicationConnection.UIDL_REFRESH_TOKEN + + "(:\\s*(.*?))?(\\s|$)").exec(responseText); + if (refreshToken != null) { + WidgetUtil.redirect(refreshToken.getGroup(2)); + } else { + handleUnrecoverableCommunicationError( + "Invalid JSON response from server: " + responseText, + xhrConnectionError); + } + + } + + @Override + public void pushInvalidContent(PushConnection pushConnection, String message) { + debug("pushInvalidContent"); + if (pushConnection.isBidirectional()) { + // We can't be sure that what was pushed was actually a response but + // at this point it should not really matter, as something is + // seriously broken. + endRequest(); + } + + // Do nothing special for now. Should likely do the same as + // xhrInvalidContent + handleUnrecoverableCommunicationError("Invalid JSON from server: " + + message, null); + + } + + @Override + public void xhrInvalidStatusCode(XhrConnectionError xhrConnectionError) { + debug("xhrInvalidStatusCode"); + + Response response = xhrConnectionError.getResponse(); + int statusCode = response.getStatusCode(); + getLogger().warning("Server returned " + statusCode + " for xhr"); + + if (statusCode == 401) { + // Authentication/authorization failed, no need to re-try + endRequest(); + handleUnauthorized(xhrConnectionError); + return; + } else { + // 404, 408 and other 4xx codes CAN be temporary when you have a + // proxy between the client and the server and e.g. restart the + // server + // 5xx codes may or may not be temporary + handleRecoverableError(Type.XHR, xhrConnectionError.getPayload()); + } + } + + /** + * @since + */ + private void endRequest() { + getConnection().getMessageSender().endRequest(); + } + + protected void handleUnauthorized(XhrConnectionError xhrConnectionError) { + /* + * Authorization has failed (401). Could be that the session has timed + * out. + */ + connection.showAuthenticationError(""); + stopApplication(); + } + + private void stopApplication() { + // Consider application not running any more and prevent all + // future requests + connection.setApplicationRunning(false); + } + + private void handleUnrecoverableCommunicationError(String details, + XhrConnectionError xhrConnectionError) { + int statusCode = -1; + if (xhrConnectionError != null) { + Response response = xhrConnectionError.getResponse(); + if (response != null) { + statusCode = response.getStatusCode(); + } + } + connection.handleCommunicationError(details, statusCode); + + stopApplication(); + + } + + @Override + public void xhrOk() { + debug("xhrOk"); + if (isReconnecting()) { + resolveTemporaryError(Type.XHR); + } + } + + private void resolveTemporaryError(Type type) { + debug("resolveTemporaryError(" + type + ")"); + + if (reconnectionCause != type) { + // Waiting for some other problem to be resolved + return; + } + + reconnectionCause = null; + reconnectAttempt = 0; + hideDialog(); + + getLogger().info("Re-established connection to server"); + } + + @Override + public void pushOk(PushConnection pushConnection) { + debug("pushOk()"); + if (isReconnecting()) { + resolveTemporaryError(Type.PUSH); + } + } + + @Override + public void pushScriptLoadError(String resourceUrl) { + connection.handleCommunicationError(resourceUrl + + " could not be loaded. Push will not work.", 0); + } + + @Override + public void pushNotConnected(JsonObject payload) { + debug("pushNotConnected()"); + handleRecoverableError(Type.PUSH, payload); + } + + @Override + public void pushReconnectPending(PushConnection pushConnection) { + debug("pushReconnectPending(" + pushConnection.getTransportType() + ")"); + getLogger().info("Reopening push connection"); + if (pushConnection.isBidirectional()) { + // Lost connection for a connection which will tell us when the + // connection is available again + handleRecoverableError(Type.PUSH, null); + } else { + // Lost connection for a connection we do not necessarily know when + // it is available again (long polling behind proxy). Do nothing and + // show reconnect dialog if the user does something and the XHR + // fails + } + } + + @Override + public void pushError(PushConnection pushConnection) { + debug("pushError()"); + connection.handleCommunicationError("Push connection using " + + pushConnection.getTransportType() + " failed!", -1); + } + + @Override + public void pushClientTimeout(PushConnection pushConnection) { + debug("pushClientTimeout()"); + // TODO Reconnect, allowing client timeout to be set + // https://dev.vaadin.com/ticket/18429 + connection + .handleCommunicationError( + "Client unexpectedly disconnected. Ensure client timeout is disabled.", + -1); + } + + @Override + public void pushClosed(PushConnection pushConnection) { + debug("pushClosed()"); + getLogger().info("Push connection closed"); + } + +} diff --git a/client/src/com/vaadin/client/communication/Heartbeat.java b/client/src/com/vaadin/client/communication/Heartbeat.java index 3b6c9dce6d..f38fbca5c8 100644 --- a/client/src/com/vaadin/client/communication/Heartbeat.java +++ b/client/src/com/vaadin/client/communication/Heartbeat.java @@ -96,11 +96,11 @@ public class Heartbeat { int status = response.getStatusCode(); if (status == Response.SC_OK) { - connection.getCommunicationProblemHandler().heartbeatOk(); + connection.getConnectionStateHandler().heartbeatOk(); } else { // Handler should stop the application if heartbeat should // no longer be sent - connection.getCommunicationProblemHandler() + connection.getConnectionStateHandler() .heartbeatInvalidStatusCode(request, response); } @@ -111,7 +111,7 @@ public class Heartbeat { public void onError(Request request, Throwable exception) { // Handler should stop the application if heartbeat should no // longer be sent - connection.getCommunicationProblemHandler().heartbeatException( + connection.getConnectionStateHandler().heartbeatException( request, exception); schedule(); } diff --git a/client/src/com/vaadin/client/communication/MessageHandler.java b/client/src/com/vaadin/client/communication/MessageHandler.java new file mode 100644 index 0000000000..c5b2251310 --- /dev/null +++ b/client/src/com/vaadin/client/communication/MessageHandler.java @@ -0,0 +1,1747 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.ApplicationState; +import com.vaadin.client.ApplicationConnection.MultiStepDuration; +import com.vaadin.client.ApplicationConnection.ResponseHandlingStartedEvent; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.FastStringSet; +import com.vaadin.client.HasComponentsConnector; +import com.vaadin.client.JsArrayObject; +import com.vaadin.client.LayoutManager; +import com.vaadin.client.LocaleService; +import com.vaadin.client.Paintable; +import com.vaadin.client.Profiler; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.UIDL; +import com.vaadin.client.Util; +import com.vaadin.client.VCaption; +import com.vaadin.client.VConsole; +import com.vaadin.client.ValueMap; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.metadata.Type; +import com.vaadin.client.metadata.TypeData; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.client.ui.VNotification; +import com.vaadin.client.ui.dd.VDragAndDropManager; +import com.vaadin.client.ui.ui.UIConnector; +import com.vaadin.client.ui.window.WindowConnector; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.SharedState; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +/** + * A MessageHandler is responsible for handling all incoming messages (JSON) + * from the server (state changes, RPCs and other updates) and ensuring that the + * connectors are updated accordingly. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class MessageHandler { + + public static final String JSON_COMMUNICATION_PREFIX = "for(;;);["; + public static final String JSON_COMMUNICATION_SUFFIX = "]"; + + /** + * Helper used to return two values when updating the connector hierarchy. + */ + private static class ConnectorHierarchyUpdateResult { + /** + * Needed at a later point when the created events are fired + */ + private JsArrayObject 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. + *

+ * This must be -1, 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 responseHandlingLocks = new HashSet(); + + /** Contains all UIDL messages received while response handling is suspended */ + private List pendingUIDLMessages = new ArrayList(); + + // 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. + *

+ * 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. + *

+ * Initial value, i.e. no responses received from the server, is + * {@link #UNDEFINED_SYNC_ID} ({@value #UNDEFINED_SYNC_ID}). This happens + * between the bootstrap HTML being loaded and the first UI being rendered; + */ + private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; + + private ApplicationConnection connection; + + /** + * Data structure holding information about pending UIDL messages. + */ + private static class PendingUIDLMessage { + private ValueMap json; + + public PendingUIDLMessage(ValueMap json) { + this.json = json; + } + + public ValueMap getJson() { + return json; + } + } + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + } + + private static Logger getLogger() { + return Logger.getLogger(MessageHandler.class.getName()); + } + + /** + * Handles a received UIDL JSON text, parsing it, and passing it on to the + * appropriate handlers, while logging timing information. + * + * @param jsonText + * The JSON to handle + */ + public void handleMessage(final ValueMap json) { + if (json == null) { + throw new IllegalArgumentException( + "The json to handle cannot be null"); + } + if (getServerId(json) == -1) { + getLogger() + .severe("Response didn't contain a server id. " + + "Please verify that the server is up-to-date and that the response data has not been modified in transmission."); + } + + if (connection.getApplicationState() == ApplicationState.RUNNING) { + handleJSON(json); + } else if (connection.getApplicationState() == ApplicationState.INITIALIZING) { + // Application is starting up for the first time + connection.setApplicationRunning(true); + connection.executeWhenCSSLoaded(new Command() { + @Override + public void execute() { + handleJSON(json); + } + }); + } else { + getLogger() + .warning( + "Ignored received message because application has already been stopped"); + return; + } + } + + protected void handleJSON(final ValueMap json) { + final int serverId = getServerId(json); + + if (isResynchronize(json) && !isNextExpectedMessage(serverId)) { + // Resynchronize request. We must remove any old pending + // messages and ensure this is handled next. Otherwise we + // would keep waiting for an older message forever (if this + // is triggered by forceHandleMessage) + getLogger().info( + "Received resync message with id " + serverId + + " while waiting for " + getExpectedServerId()); + lastSeenServerSyncId = serverId - 1; + removeOldPendingMessages(); + } + + boolean locked = !responseHandlingLocks.isEmpty(); + + if (locked || !isNextExpectedMessage(serverId)) { + // Cannot or should not handle this message right now, either + // because of locks or because it's an out-of-order message + + if (locked) { + // Some component is doing something that can't be interrupted + // (e.g. animation that should be smooth). Enqueue the UIDL + // message for later processing. + getLogger().info("Postponing UIDL handling due to lock..."); + } else { + // Unexpected server id + if (serverId <= lastSeenServerSyncId) { + // Why is the server re-sending an old package? Ignore it + getLogger().warning( + "Received message with server id " + serverId + + " but have already seen " + + lastSeenServerSyncId + ". Ignoring it"); + endRequestIfResponse(json); + return; + } + + // We are waiting for an earlier message... + getLogger() + .info("Received message with server id " + + serverId + + " but expected " + + getExpectedServerId() + + ". Postponing handling until the missing message(s) have been received"); + } + pendingUIDLMessages.add(new PendingUIDLMessage(json)); + if (!forceHandleMessage.isRunning()) { + forceHandleMessage.schedule(MAX_SUSPENDED_TIMEOUT); + } + return; + } + + final Date start = new Date(); + /* + * Lock response handling to avoid a situation where something pushed + * from the server gets processed while waiting for e.g. lazily loaded + * connectors that are needed for processing the current message. + */ + final Object lock = new Object(); + suspendReponseHandling(lock); + + getLogger().info("Handling message from server"); + connection.fireEvent(new ResponseHandlingStartedEvent(connection)); + + // Client id must be updated before server id, as server id update can + // cause a resync (which must use the updated id) + if (json.containsKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) { + int serverNextExpected = json + .getInt(ApplicationConstants.CLIENT_TO_SERVER_ID); + getMessageSender().setClientToServerMessageId(serverNextExpected, + isResynchronize(json)); + } + + if (serverId != -1) { + /* + * Use sync id unless explicitly set as undefined, as is done by + * e.g. critical server-side notifications + */ + lastSeenServerSyncId = serverId; + } + + // Handle redirect + if (json.containsKey("redirect")) { + String url = json.getValueMap("redirect").getString("url"); + getLogger().info("redirecting to " + url); + WidgetUtil.redirect(url); + return; + } + + final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); + + // Get security key + if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { + csrfToken = json + .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); + } + getLogger().info(" * Handling resources from server"); + + if (json.containsKey("resources")) { + ValueMap resources = json.getValueMap("resources"); + JsArrayString keyArray = resources.getKeyArray(); + int l = keyArray.length(); + for (int i = 0; i < l; i++) { + String key = keyArray.get(i); + connection.setResource(key, resources.getAsString(key)); + } + } + handleUIDLDuration.logDuration( + " * Handling resources from server completed", 10); + + getLogger().info(" * Handling type inheritance map from server"); + + if (json.containsKey("typeInheritanceMap")) { + connection.getConfiguration().addComponentInheritanceInfo( + json.getValueMap("typeInheritanceMap")); + } + handleUIDLDuration.logDuration( + " * Handling type inheritance map from server completed", 10); + + getLogger().info("Handling type mappings from server"); + + if (json.containsKey("typeMappings")) { + connection.getConfiguration() + .addComponentMappings(json.getValueMap("typeMappings"), + connection.getWidgetSet()); + + } + + getLogger().info("Handling resource dependencies"); + if (json.containsKey("scriptDependencies")) { + connection.loadScriptDependencies(json + .getJSStringArray("scriptDependencies")); + } + if (json.containsKey("styleDependencies")) { + connection.loadStyleDependencies(json + .getJSStringArray("styleDependencies")); + } + + handleUIDLDuration.logDuration( + " * Handling type mappings from server completed", 10); + /* + * Hook for e.g. TestBench to get details about server peformance + */ + if (json.containsKey("timings")) { + serverTimingInfo = json.getValueMap("timings"); + } + + Command c = new Command() { + private boolean onlyNoLayoutUpdates = true; + + @Override + public void execute() { + assert serverId == -1 || serverId == lastSeenServerSyncId; + + handleUIDLDuration.logDuration(" * Loading widgets completed", + 10); + + Profiler.enter("Handling meta information"); + ValueMap meta = null; + if (json.containsKey("meta")) { + getLogger().info(" * Handling meta information"); + meta = json.getValueMap("meta"); + if (meta.containsKey("repaintAll")) { + prepareRepaintAll(); + } + if (meta.containsKey("timedRedirect")) { + final ValueMap timedRedirect = meta + .getValueMap("timedRedirect"); + if (redirectTimer != null) { + redirectTimer.cancel(); + } + redirectTimer = new Timer() { + @Override + public void run() { + WidgetUtil.redirect(timedRedirect + .getString("url")); + } + }; + sessionExpirationInterval = timedRedirect + .getInt("interval"); + } + } + Profiler.leave("Handling meta information"); + + if (redirectTimer != null) { + redirectTimer.schedule(1000 * sessionExpirationInterval); + } + + updatingState = true; + + double processUidlStart = Duration.currentTimeMillis(); + + // Ensure that all connectors that we are about to update exist + JsArrayString createdConnectorIds = createConnectorsIfNeeded(json); + + // Update states, do not fire events + JsArrayObject pendingStateChangeEvents = updateConnectorState( + json, createdConnectorIds); + + /* + * Doing this here so that locales are available also to the + * connectors which get a state change event before the UI. + */ + Profiler.enter("Handling locales"); + getLogger().info(" * Handling locales"); + // Store locale data + LocaleService + .addLocales(getUIConnector().getState().localeServiceState.localeData); + Profiler.leave("Handling locales"); + + // Update hierarchy, do not fire events + ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(json); + + // Fire hierarchy change events + sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); + + updateCaptions(pendingStateChangeEvents, + connectorHierarchyUpdateResult.parentChangedIds); + + delegateToWidget(pendingStateChangeEvents); + + // Fire state change events. + sendStateChangeEvents(pendingStateChangeEvents); + + // Update of legacy (UIDL) style connectors + updateVaadin6StyleConnectors(json); + + // Handle any RPC invocations done on the server side + handleRpcInvocations(json); + + if (json.containsKey("dd")) { + // response contains data for drag and drop service + VDragAndDropManager.get().handleServerResponse( + json.getValueMap("dd")); + } + + unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); + + getLogger() + .info("handleUIDLMessage: " + + (Duration.currentTimeMillis() - processUidlStart) + + " ms"); + + updatingState = false; + + if (!onlyNoLayoutUpdates) { + Profiler.enter("Layout processing"); + try { + LayoutManager layoutManager = getLayoutManager(); + layoutManager.setEverythingNeedsMeasure(); + layoutManager.layoutNow(); + } catch (final Throwable e) { + getLogger().log(Level.SEVERE, + "Error processing layouts", e); + } + Profiler.leave("Layout processing"); + } + + if (ApplicationConfiguration.isDebugMode()) { + Profiler.enter("Dumping state changes to the console"); + getLogger().info(" * Dumping state changes to the console"); + VConsole.dirUIDL(json, connection); + Profiler.leave("Dumping state changes to the console"); + } + + if (meta != null) { + Profiler.enter("Error handling"); + if (meta.containsKey("appError")) { + ValueMap error = meta.getValueMap("appError"); + + VNotification.showError(connection, + error.getString("caption"), + error.getString("message"), + error.getString("details"), + error.getString("url")); + + connection.setApplicationRunning(false); + } + Profiler.leave("Error handling"); + } + + // TODO build profiling for widget impl loading time + + lastProcessingTime = (int) ((new Date().getTime()) - start + .getTime()); + totalProcessingTime += lastProcessingTime; + if (bootstrapTime == 0) { + bootstrapTime = calculateBootstrapTime(); + if (Profiler.isEnabled() && bootstrapTime != -1) { + Profiler.logBootstrapTimings(); + } + } + + getLogger().info( + " Processing time was " + + String.valueOf(lastProcessingTime) + "ms"); + getLogger().info( + "Referenced paintables: " + getConnectorMap().size()); + + endRequestIfResponse(json); + resumeResponseHandling(lock); + + if (Profiler.isEnabled()) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + Profiler.logTimings(); + Profiler.reset(); + } + }); + } + } + + /** + * Properly clean up any old stuff to ensure everything is properly + * reinitialized. + */ + private void prepareRepaintAll() { + String uiConnectorId = getUIConnector().getConnectorId(); + if (uiConnectorId == null) { + // Nothing to clear yet + return; + } + + // Create fake server response that says that the uiConnector + // has no children + JsonObject fakeHierarchy = Json.createObject(); + fakeHierarchy.put(uiConnectorId, Json.createArray()); + JsonObject fakeJson = Json.createObject(); + fakeJson.put("hierarchy", fakeHierarchy); + ValueMap fakeValueMap = ((JavaScriptObject) fakeJson.toNative()) + .cast(); + + // Update hierarchy based on the fake response + ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(fakeValueMap); + + // Send hierarchy events based on the fake update + sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); + + // Unregister all the old connectors that have now been removed + unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); + + getLayoutManager().cleanMeasuredSizes(); + } + + private void updateCaptions( + JsArrayObject 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 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 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 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 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 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 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 updateConnectorState( + ValueMap json, JsArrayString createdConnectorIds) { + JsArrayObject 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 newChildren = new ArrayList(); + List newComponents = new ArrayList(); + 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 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 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 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 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. 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 oldChildren = ccc + .getChildComponents(); + if (!oldChildren.isEmpty()) { + /* + * HasComponentsConnector has a separate child component + * list that should also be cleared + */ + ccc.setChildComponents(Collections + . emptyList()); + + // Create event and add it to the list of pending events + ConnectorHierarchyChangeEvent event = GWT + .create(ConnectorHierarchyChangeEvent.class); + event.setConnector(connector); + event.setOldChildren(oldChildren); + events.add(event); + } + } + Profiler.leave(prefix + + "recursivelyDetach create hierarchy event"); + } + + private native void replaceState(AbstractConnector connector, + Object defaultState) + /*-{ + connector.@com.vaadin.client.ui.AbstractConnector::state = defaultState; + }-*/; + + private void handleRpcInvocations(ValueMap json) { + if (json.containsKey("rpc")) { + Profiler.enter("handleRpcInvocations"); + + getLogger() + .info(" * Performing server to client RPC calls"); + + JsonArray rpcCalls = Util.jso2json(json + .getJavaScriptObject("rpc")); + + int rpcLength = rpcCalls.length(); + for (int i = 0; i < rpcLength; i++) { + try { + JsonArray rpcCall = rpcCalls.getArray(i); + MethodInvocation invocation = getRpcManager() + .parseAndApplyInvocation(rpcCall, + connection); + + if (onlyNoLayoutUpdates + && !RpcManager.getMethod(invocation) + .isNoLayout()) { + onlyNoLayoutUpdates = false; + } + + } catch (final Throwable e) { + getLogger() + .log(Level.SEVERE, + "Error performing server to client RPC calls", + e); + } + } + + Profiler.leave("handleRpcInvocations"); + } + } + + }; + ApplicationConfiguration.runWhenDependenciesLoaded(c); + } + + private void endRequestIfResponse(ValueMap json) { + if (isResponse(json)) { + // End the request if the received message was a + // response, not sent asynchronously + getMessageSender().endRequest(); + } + } + + private boolean isResynchronize(ValueMap json) { + return json.containsKey(ApplicationConstants.RESYNCHRONIZE_ID); + } + + private boolean isResponse(ValueMap json) { + ValueMap meta = json.getValueMap("meta"); + if (meta == null || !meta.containsKey("async")) { + return true; + } + return false; + } + + /** + * Checks if the given serverId is the one we are currently waiting for from + * the server + */ + private boolean isNextExpectedMessage(int serverId) { + if (serverId == -1) { + return true; + } + if (serverId == getExpectedServerId()) { + return true; + } + if (lastSeenServerSyncId == UNDEFINED_SYNC_ID) { + // First message is always ok + return true; + } + return false; + + } + + private int getServerId(ValueMap json) { + if (json.containsKey(ApplicationConstants.SERVER_SYNC_ID)) { + return json.getInt(ApplicationConstants.SERVER_SYNC_ID); + } else { + return -1; + } + } + + private int getExpectedServerId() { + return lastSeenServerSyncId + 1; + } + + /** + * Timer used to make sure that no misbehaving components can delay response + * handling forever. + */ + Timer forceHandleMessage = new Timer() { + @Override + public void run() { + if (!responseHandlingLocks.isEmpty()) { + // Lock which was never release -> bug in locker or things just + // too slow + getLogger() + .warning( + "WARNING: reponse handling was never resumed, forcibly removing locks..."); + responseHandlingLocks.clear(); + } else { + // Waited for out-of-order message which never arrived + // Do one final check and resynchronize if the message is not + // there. The final check is only a precaution as this timer + // should have been cancelled if the message has arrived + getLogger().warning( + "Gave up waiting for message " + getExpectedServerId() + + " from the server"); + + } + if (!handlePendingMessages() && !pendingUIDLMessages.isEmpty()) { + // There are messages but the next id was not found, likely it + // has been lost + // Drop pending messages and resynchronize + pendingUIDLMessages.clear(); + getMessageSender().resynchronize(); + } + } + }; + + /** + * This method can be used to postpone rendering of a response for a short + * period of time (e.g. to avoid the rendering process during animation). + * + * @param lock + */ + public void suspendReponseHandling(Object lock) { + responseHandlingLocks.add(lock); + } + + /** + * Resumes the rendering process once all locks have been removed. + * + * @param lock + */ + public void resumeResponseHandling(Object lock) { + responseHandlingLocks.remove(lock); + if (responseHandlingLocks.isEmpty()) { + // Cancel timer that breaks the lock + forceHandleMessage.cancel(); + + if (!pendingUIDLMessages.isEmpty()) { + getLogger() + .info("No more response handling locks, handling pending requests."); + handlePendingMessages(); + } + } + } + + private static native final int calculateBootstrapTime() + /*-{ + if ($wnd.performance && $wnd.performance.timing) { + return (new Date).getTime() - $wnd.performance.timing.responseStart; + } else { + // performance.timing not supported + return -1; + } + }-*/; + + /** + * Finds the next pending UIDL message and handles it (next pending is + * decided based on the server id) + * + * @return true if a message was handled, false otherwise + */ + private boolean handlePendingMessages() { + if (pendingUIDLMessages.isEmpty()) { + return false; + } + + // Try to find the next expected message + PendingUIDLMessage toHandle = null; + for (PendingUIDLMessage message : pendingUIDLMessages) { + if (isNextExpectedMessage(getServerId(message.json))) { + toHandle = message; + break; + } + } + + if (toHandle != null) { + pendingUIDLMessages.remove(toHandle); + handleJSON(toHandle.getJson()); + // Any remaining messages will be handled when this is called + // again at the end of handleJSON + return true; + } else { + return false; + } + + } + + private void removeOldPendingMessages() { + Iterator i = pendingUIDLMessages.iterator(); + while (i.hasNext()) { + PendingUIDLMessage m = i.next(); + int serverId = getServerId(m.json); + if (serverId != -1 && serverId < getExpectedServerId()) { + getLogger().info("Removing old message with id " + serverId); + i.remove(); + } + } + } + + /** + * Gets the server id included in the last received response. + *

+ * 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. + *

+ * 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. + *

+ * 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 true if the internal state might be inconsistent + * because changes are being processed; false if the + * state should be consistent + */ + public boolean isUpdatingState() { + return updatingState; + } + + /** + * Checks if the first UIDL has been handled + * + * @return true if the initial UIDL has already been processed, false + * otherwise + */ + public boolean isInitialUidlHandled() { + return bootstrapTime != 0; + } + + private LayoutManager getLayoutManager() { + return LayoutManager.get(connection); + } + + private ConnectorMap getConnectorMap() { + return ConnectorMap.get(connection); + } + + private UIConnector getUIConnector() { + return connection.getUIConnector(); + } + + private RpcManager getRpcManager() { + return connection.getRpcManager(); + } + + private MessageSender getMessageSender() { + return connection.getMessageSender(); + } + + /** + * Strips the JSON wrapping from the given json string with wrapping. + * + * If the given string is not wrapped as expected, returns null + * + * @since + * @param jsonWithWrapping + * the JSON received from the server + * @return an unwrapped JSON string or null if the given string was not + * wrapped + */ + public static String stripJSONWrapping(String jsonWithWrapping) { + if (jsonWithWrapping == null) { + return null; + } + + if (!jsonWithWrapping.startsWith(JSON_COMMUNICATION_PREFIX) + || !jsonWithWrapping.endsWith(JSON_COMMUNICATION_SUFFIX)) { + return null; + } + return jsonWithWrapping.substring(JSON_COMMUNICATION_PREFIX.length(), + jsonWithWrapping.length() - JSON_COMMUNICATION_SUFFIX.length()); + } + + /** + * Unwraps and parses the given JSON, originating from the server + * + * @param jsonText + * the json from the server + * @return A parsed ValueMap or null if the input could not be parsed (or + * was null) + */ + public static ValueMap parseJson(String jsonText) { + if (jsonText == null) { + return null; + } + final Date start = new Date(); + try { + ValueMap json = parseJSONResponse(jsonText); + getLogger().info( + "JSON parsing took " + + (new Date().getTime() - start.getTime()) + "ms"); + return json; + } catch (final Exception e) { + getLogger().severe("Unable to parse JSON: " + jsonText); + return null; + } + } + + private static native ValueMap parseJSONResponse(String jsonText) + /*-{ + return JSON.parse(jsonText); + }-*/; + + /** + * Parse the given wrapped JSON, received from the server, to a ValueMap + * + * @param wrappedJsonText + * the json, wrapped as done by the server + * @return a ValueMap, or null if the wrapping was incorrect or json could + * not be parsed + */ + public static ValueMap parseWrappedJson(String wrappedJsonText) { + return parseJson(stripJSONWrapping(wrappedJsonText)); + } + +} diff --git a/client/src/com/vaadin/client/communication/MessageSender.java b/client/src/com/vaadin/client/communication/MessageSender.java new file mode 100644 index 0000000000..cde8be48ac --- /dev/null +++ b/client/src/com/vaadin/client/communication/MessageSender.java @@ -0,0 +1,410 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.logging.Logger; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.RequestStartingEvent; +import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent; +import com.vaadin.client.Util; +import com.vaadin.client.VLoadingIndicator; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.Version; +import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +/** + * MessageSender is responsible for sending messages to the server. + *

+ * Internally uses {@link XhrConnection} and/or {@link PushConnection} for + * delivering messages, depending on the application configuration. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class MessageSender { + + private ApplicationConnection connection; + private boolean hasActiveRequest = false; + + /** + * Counter for the messages send to the server. First sent message has id 0. + */ + private int clientToServerMessageId = 0; + private XhrConnection xhrConnection; + private PushConnection push; + + public MessageSender() { + xhrConnection = GWT.create(XhrConnection.class); + } + + /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * + * @param connection + * the application connection this instance is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + xhrConnection.setConnection(connection); + } + + private static Logger getLogger() { + return Logger.getLogger(MessageSender.class.getName()); + } + + public void sendInvocationsToServer() { + if (!connection.isApplicationRunning()) { + getLogger() + .warning( + "Trying to send RPC from not yet started or stopped application"); + return; + } + + if (hasActiveRequest() || (push != null && !push.isActive())) { + // There is an active request or push is enabled but not active + // -> send when current request completes or push becomes active + } else { + doSendInvocationsToServer(); + } + } + + /** + * Sends all pending method invocations (server RPC and legacy variable + * changes) to the server. + * + */ + private void doSendInvocationsToServer() { + + ServerRpcQueue serverRpcQueue = getServerRpcQueue(); + if (serverRpcQueue.isEmpty()) { + return; + } + + if (ApplicationConfiguration.isDebugMode()) { + Util.logMethodInvocations(connection, serverRpcQueue.getAll()); + } + + boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator(); + JsonArray reqJson = serverRpcQueue.toJson(); + serverRpcQueue.clear(); + + if (reqJson.length() == 0) { + // Nothing to send, all invocations were filtered out (for + // non-existing connectors) + getLogger() + .warning( + "All RPCs filtered out, not sending anything to the server"); + return; + } + + JsonObject extraJson = Json.createObject(); + if (!connection.getConfiguration().isWidgetsetVersionSent()) { + extraJson.put(ApplicationConstants.WIDGETSET_VERSION_ID, + Version.getFullVersion()); + connection.getConfiguration().setWidgetsetVersionSent(); + } + if (showLoadingIndicator) { + connection.getLoadingIndicator().trigger(); + } + send(reqJson, extraJson); + } + + private ServerRpcQueue getServerRpcQueue() { + return connection.getServerRpcQueue(); + } + + /** + * Makes an UIDL request to the server. + * + * @param reqInvocations + * Data containing RPC invocations and all related information. + * @param extraParams + * Parameters that are added to the payload + */ + protected void send(final JsonArray reqInvocations, + final JsonObject extraJson) { + startRequest(); + + JsonObject payload = Json.createObject(); + String csrfToken = getMessageHandler().getCsrfToken(); + if (!csrfToken.equals(ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) { + payload.put(ApplicationConstants.CSRF_TOKEN, csrfToken); + } + payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations); + payload.put(ApplicationConstants.SERVER_SYNC_ID, getMessageHandler() + .getLastSeenServerSyncId()); + payload.put(ApplicationConstants.CLIENT_TO_SERVER_ID, + clientToServerMessageId++); + + if (extraJson != null) { + for (String key : extraJson.keys()) { + payload.put(key, extraJson.get(key)); + } + } + + send(payload); + + } + + /** + * Sends an asynchronous or synchronous UIDL request to the server using the + * given URI. + * + * @param uri + * The URI to use for the request. May includes GET parameters + * @param payload + * The contents of the request to send + */ + public void send(final JsonObject payload) { + if (push != null && push.isBidirectional()) { + push.push(payload); + } else { + xhrConnection.send(payload); + } + } + + /** + * Sets the status for the push connection. + * + * @param enabled + * true to enable the push connection; + * false to disable the push connection. + */ + public void setPushEnabled(boolean enabled) { + final PushConfigurationState pushState = connection.getUIConnector() + .getState().pushConfiguration; + + if (enabled && push == null) { + push = GWT.create(PushConnection.class); + push.init(connection, pushState); + } else if (!enabled && push != null && push.isActive()) { + push.disconnect(new Command() { + @Override + public void execute() { + push = null; + /* + * If push has been enabled again while we were waiting for + * the old connection to disconnect, now is the right time + * to open a new connection + */ + if (pushState.mode.isEnabled()) { + setPushEnabled(true); + } + + /* + * Send anything that was enqueued while we waited for the + * connection to close + */ + if (getServerRpcQueue().isFlushPending()) { + getServerRpcQueue().flush(); + } + } + }); + } + } + + public void startRequest() { + if (hasActiveRequest) { + getLogger().severe( + "Trying to start a new request while another is active"); + } + hasActiveRequest = true; + connection.fireEvent(new RequestStartingEvent(connection)); + } + + public void endRequest() { + if (!hasActiveRequest) { + getLogger().severe("No active request"); + } + // After sendInvocationsToServer() there may be a new active + // request, so we must set hasActiveRequest to false before, not after, + // the call. + hasActiveRequest = false; + + if (connection.isApplicationRunning()) { + if (getServerRpcQueue().isFlushPending()) { + sendInvocationsToServer(); + } + runPostRequestHooks(connection.getConfiguration().getRootPanelId()); + } + + // deferring to avoid flickering + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (!connection.isApplicationRunning() + || !(hasActiveRequest() || getServerRpcQueue() + .isFlushPending())) { + getLoadingIndicator().hide(); + + // If on Liferay and session expiration management is in + // use, extend session duration on each request. + // Doing it here rather than before the request to improve + // responsiveness. + // Postponed until the end of the next request if other + // requests still pending. + extendLiferaySession(); + } + } + }); + connection.fireEvent(new ResponseHandlingEndedEvent(connection)); + } + + /** + * Runs possibly registered client side post request hooks. This is expected + * to be run after each uidl request made by Vaadin application. + * + * @param appId + */ + public static native void runPostRequestHooks(String appId) + /*-{ + if ($wnd.vaadin.postRequestHooks) { + for ( var hook in $wnd.vaadin.postRequestHooks) { + if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") { + try { + $wnd.vaadin.postRequestHooks[hook](appId); + } catch (e) { + } + } + } + } + }-*/; + + /** + * If on Liferay and logged in, ask the client side session management + * JavaScript to extend the session duration. + * + * Otherwise, Liferay client side JavaScript will explicitly expire the + * session even though the server side considers the session to be active. + * See ticket #8305 for more information. + */ + public static native void extendLiferaySession() + /*-{ + if ($wnd.Liferay && $wnd.Liferay.Session) { + $wnd.Liferay.Session.extend(); + // if the extend banner is visible, hide it + if ($wnd.Liferay.Session.banner) { + $wnd.Liferay.Session.banner.remove(); + } + } + }-*/; + + /** + * Indicates whether or not there are currently active UIDL requests. Used + * internally to sequence requests properly, seldom needed in Widgets. + * + * @return true if there are active requests + */ + public boolean hasActiveRequest() { + return hasActiveRequest; + } + + /** + * Returns a human readable string representation of the method used to + * communicate with the server. + * + * @return A string representation of the current transport type + */ + public String getCommunicationMethodName() { + String clientToServer = "XHR"; + String serverToClient = "-"; + if (push != null) { + serverToClient = push.getTransportType(); + if (push.isBidirectional()) { + clientToServer = serverToClient; + } + } + + return "Client to server: " + clientToServer + ", " + + "server to client: " + serverToClient; + } + + private ConnectionStateHandler getConnectionStateHandler() { + return connection.getConnectionStateHandler(); + } + + private MessageHandler getMessageHandler() { + return connection.getMessageHandler(); + } + + private VLoadingIndicator getLoadingIndicator() { + return connection.getLoadingIndicator(); + } + + /** + * Resynchronize the client side, i.e. reload all component hierarchy and + * state from the server + */ + public void resynchronize() { + getLogger().info("Resynchronizing from server"); + JsonObject resyncParam = Json.createObject(); + resyncParam.put(ApplicationConstants.RESYNCHRONIZE_ID, true); + send(Json.createArray(), resyncParam); + } + + /** + * Used internally to update what the server expects + * + * @param clientToServerMessageId + * the new client id to set + * @param force + * true if the id must be updated, false otherwise + */ + public void setClientToServerMessageId(int nextExpectedId, boolean force) { + if (nextExpectedId == clientToServerMessageId) { + // No op as everything matches they way it should + return; + } + if (force) { + getLogger().info( + "Forced update of clientId to " + clientToServerMessageId); + clientToServerMessageId = nextExpectedId; + return; + } + + if (nextExpectedId > clientToServerMessageId) { + if (clientToServerMessageId == 0) { + // We have never sent a message to the server, so likely the + // server knows better (typical case is that we refreshed a + // @PreserveOnRefresh UI) + getLogger().info( + "Updating client-to-server id to " + nextExpectedId + + " based on server"); + } else { + getLogger().warning( + "Server expects next client-to-server id to be " + + nextExpectedId + " but we were going to use " + + clientToServerMessageId + ". Will use " + + nextExpectedId + "."); + } + clientToServerMessageId = nextExpectedId; + } else { + // Server has not yet seen all our messages + // Do nothing as they will arrive eventually + } + } + +} diff --git a/client/src/com/vaadin/client/communication/PushConnection.java b/client/src/com/vaadin/client/communication/PushConnection.java index d5affcfa88..489d2c39a4 100644 --- a/client/src/com/vaadin/client/communication/PushConnection.java +++ b/client/src/com/vaadin/client/communication/PushConnection.java @@ -49,7 +49,7 @@ public interface PushConnection { *

* Implementation detail: If the push connection is not connected and the * message can thus not be sent, the implementation must call - * {@link CommunicationProblemHandler#pushNotConnected(JsonObject)}, which + * {@link ConnectionStateHandler#pushNotConnected(JsonObject)}, which * will retry the send later. *

* This method must not be called if the push connection is not diff --git a/client/src/com/vaadin/client/communication/ReconnectingCommunicationProblemHandler.java b/client/src/com/vaadin/client/communication/ReconnectingCommunicationProblemHandler.java deleted file mode 100644 index 36fba675f6..0000000000 --- a/client/src/com/vaadin/client/communication/ReconnectingCommunicationProblemHandler.java +++ /dev/null @@ -1,585 +0,0 @@ -/* - * Copyright 2000-2014 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.client.communication; - -import java.util.logging.Logger; - -import com.google.gwt.core.shared.GWT; -import com.google.gwt.http.client.Request; -import com.google.gwt.http.client.Response; -import com.google.gwt.regexp.shared.MatchResult; -import com.google.gwt.regexp.shared.RegExp; -import com.google.gwt.user.client.Timer; -import com.vaadin.client.ApplicationConnection; -import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; -import com.vaadin.client.ApplicationConnection.ApplicationStoppedHandler; -import com.vaadin.client.WidgetUtil; -import com.vaadin.shared.ui.ui.UIState.ReconnectDialogConfigurationState; - -import elemental.json.JsonObject; - -/** - * Default implementation of the communication problem handler. - *

- * Handles temporary errors by showing a reconnect dialog to the user while - * trying to re-establish the connection to the server and re-send the pending - * message. - *

- * Handles permanent errors by showing a critical system notification to the - * user - * - * @since 7.6 - * @author Vaadin Ltd - */ -public class ReconnectingCommunicationProblemHandler implements - CommunicationProblemHandler { - - private ApplicationConnection connection; - private ReconnectDialog reconnectDialog = GWT.create(ReconnectDialog.class); - private int reconnectAttempt = 0; - private Type reconnectionCause = null; - - private Timer scheduledReconnect; - private Timer dialogShowTimer = new Timer() { - - @Override - public void run() { - showDialog(); - } - - }; - - protected enum Type { - HEARTBEAT(0), PUSH(1), XHR(2); - - private int priority; - - private Type(int priority) { - this.priority = priority; - } - - public boolean isMessage() { - return this == PUSH || this == XHR; - } - - /** - * Checks if this type is of higher priority than the given type - * - * @param type - * the type to compare to - * @return true if this type has higher priority than the given type, - * false otherwise - */ - public boolean isHigherPriorityThan(Type type) { - return priority > type.priority; - } - } - - @Override - public void setConnection(ApplicationConnection connection) { - this.connection = connection; - - connection.addHandler(ApplicationStoppedEvent.TYPE, - new ApplicationStoppedHandler() { - @Override - public void onApplicationStopped( - ApplicationStoppedEvent event) { - if (isReconnecting()) { - giveUp(); - } - if (scheduledReconnect != null - && scheduledReconnect.isRunning()) { - scheduledReconnect.cancel(); - } - } - - }); - }; - - /** - * Checks if we are currently trying to reconnect - * - * @return true if we have noted a problem and are trying to re-establish - * server connection, false otherwise - */ - private boolean isReconnecting() { - return reconnectionCause != null; - } - - private static Logger getLogger() { - return Logger.getLogger(ReconnectingCommunicationProblemHandler.class - .getName()); - } - - /** - * Returns the connection this handler is connected to - * - * @return the connection for this handler - */ - protected ApplicationConnection getConnection() { - return connection; - } - - @Override - public void xhrException(CommunicationProblemEvent event) { - debug("xhrException"); - handleRecoverableError(Type.XHR, event.getPayload()); - } - - @Override - public void heartbeatException(Request request, Throwable exception) { - getLogger().severe("Heartbeat exception: " + exception.getMessage()); - handleRecoverableError(Type.HEARTBEAT, null); - } - - @Override - public void heartbeatInvalidStatusCode(Request request, Response response) { - int statusCode = response.getStatusCode(); - getLogger().warning("Heartbeat request returned " + statusCode); - - if (response.getStatusCode() == Response.SC_GONE) { - // Session expired - getConnection().showSessionExpiredError(null); - stopApplication(); - } else if (response.getStatusCode() == Response.SC_NOT_FOUND) { - // UI closed, do nothing as the UI will react to this - // Should not trigger reconnect dialog as this will prevent user - // input - } else { - handleRecoverableError(Type.HEARTBEAT, null); - } - } - - @Override - public void heartbeatOk() { - debug("heartbeatOk"); - if (isReconnecting()) { - resolveTemporaryError(Type.HEARTBEAT); - } - } - - private void debug(String msg) { - if (false) { - getLogger().warning(msg); - } - } - - /** - * Called whenever an error occurs in communication which should be handled - * by showing the reconnect dialog and retrying communication until - * successful again - * - * @param type - * The type of failure detected - * @param payload - * The message which did not reach the server, or null if no - * message was involved (heartbeat or push connection failed) - */ - protected void handleRecoverableError(Type type, final JsonObject payload) { - debug("handleTemporaryError(" + type + ")"); - if (!connection.isApplicationRunning()) { - return; - } - - if (!isReconnecting()) { - // First problem encounter - reconnectionCause = type; - getLogger().warning("Reconnecting because of " + type + " failure"); - // Precaution only as there should never be a dialog at this point - // and no timer running - stopDialogTimer(); - if (isDialogVisible()) { - hideDialog(); - } - - // Show dialog after grace period, still continue to try to - // reconnect even before it is shown - dialogShowTimer.schedule(getConfiguration().dialogGracePeriod); - } else { - // We are currently trying to reconnect - // Priority is HEARTBEAT -> PUSH -> XHR - // If a higher priority issues is resolved, we can assume the lower - // one will be also - if (type.isHigherPriorityThan(reconnectionCause)) { - getLogger().warning( - "Now reconnecting because of " + type + " failure"); - reconnectionCause = type; - } - } - - if (reconnectionCause != type) { - return; - } - - reconnectAttempt++; - getLogger().info( - "Reconnect attempt " + reconnectAttempt + " for " + type); - - if (reconnectAttempt >= getConfiguration().reconnectAttempts) { - // Max attempts reached, stop trying - giveUp(); - } else { - updateDialog(); - scheduleReconnect(payload); - } - } - - /** - * Called after a problem occurred. - * - * This method is responsible for re-sending the payload to the server (if - * not null) or re-send a heartbeat request at some point - * - * @param payload - * the payload that did not reach the server, null if the problem - * was detected by a heartbeat - */ - protected void scheduleReconnect(final JsonObject payload) { - // Here and not in timer to avoid TB for getting in between - - // The request is still open at this point to avoid interference, so we - // do not need to start a new one - if (reconnectAttempt == 1) { - // Try once immediately - doReconnect(payload); - } else { - scheduledReconnect = new Timer() { - @Override - public void run() { - scheduledReconnect = null; - doReconnect(payload); - } - }; - scheduledReconnect.schedule(getConfiguration().reconnectInterval); - } - } - - /** - * Re-sends the payload to the server (if not null) or re-sends a heartbeat - * request immediately - * - * @param payload - * the payload that did not reach the server, null if the problem - * was detected by a heartbeat - */ - protected void doReconnect(JsonObject payload) { - if (!connection.isApplicationRunning()) { - // This should not happen as nobody should call this if the - // application has been stopped - getLogger() - .warning( - "Trying to reconnect after application has been stopped. Giving up"); - return; - } - if (payload != null) { - getLogger().info("Re-sending last message to the server..."); - getConnection().getServerCommunicationHandler().send(payload); - } else { - // Use heartbeat - getLogger().info("Trying to re-establish server connection..."); - getConnection().getHeartbeat().send(); - } - } - - /** - * Called whenever a reconnect attempt fails to allow updating of dialog - * contents - */ - protected void updateDialog() { - reconnectDialog.setText(getDialogText(reconnectAttempt)); - } - - /** - * Called when we should give up trying to reconnect and let the user decide - * how to continue - * - */ - protected void giveUp() { - reconnectionCause = null; - endRequest(); - - stopDialogTimer(); - if (!isDialogVisible()) { - // It SHOULD always be visible at this point, unless you have a - // really strange configuration (grace time longer than total - // reconnect time) - showDialog(); - } - reconnectDialog.setText(getDialogTextGaveUp(reconnectAttempt)); - reconnectDialog.setReconnecting(false); - - // Stopping the application stops heartbeats and push - connection.setApplicationRunning(false); - } - - /** - * Ensures the reconnect dialog does not popup some time from now - */ - private void stopDialogTimer() { - if (dialogShowTimer.isRunning()) { - dialogShowTimer.cancel(); - } - } - - /** - * Checks if the reconnect dialog is visible to the user - * - * @return true if the user can see the dialog, false otherwise - */ - protected boolean isDialogVisible() { - return reconnectDialog.isVisible(); - } - - /** - * Called when the reconnect dialog should be shown. This is typically when - * N seconds has passed since a problem with the connection has been - * detected - */ - protected void showDialog() { - reconnectDialog.setReconnecting(true); - reconnectDialog.show(connection); - - // We never want to show loading indicator and reconnect dialog at the - // same time - connection.getLoadingIndicator().hide(); - } - - /** - * Called when the reconnect dialog should be hidden. - */ - protected void hideDialog() { - reconnectDialog.hide(); - } - - /** - * Gets the text to show in the reconnect dialog after giving up (reconnect - * limit reached) - * - * @param reconnectAttempt - * The number of the current reconnection attempt - * @return The text to show in the reconnect dialog after giving up - */ - protected String getDialogTextGaveUp(int reconnectAttempt) { - return getConfiguration().dialogTextGaveUp.replace("{0}", - reconnectAttempt + ""); - } - - /** - * Gets the text to show in the reconnect dialog - * - * @param reconnectAttempt - * The number of the current reconnection attempt - * @return The text to show in the reconnect dialog - */ - protected String getDialogText(int reconnectAttempt) { - return getConfiguration().dialogText.replace("{0}", reconnectAttempt - + ""); - } - - @Override - public void configurationUpdated() { - // All other properties are fetched directly from the state when needed - reconnectDialog.setModal(getConfiguration().dialogModal); - } - - private ReconnectDialogConfigurationState getConfiguration() { - return connection.getUIConnector().getState().reconnectDialogConfiguration; - } - - @Override - public void xhrInvalidContent(CommunicationProblemEvent event) { - debug("xhrInvalidContent"); - endRequest(); - - String responseText = event.getResponse().getText(); - /* - * A servlet filter or equivalent may have intercepted the request and - * served non-UIDL content (for instance, a login page if the session - * has expired.) If the response contains a magic substring, do a - * synchronous refresh. See #8241. - */ - MatchResult refreshToken = RegExp.compile( - ApplicationConnection.UIDL_REFRESH_TOKEN - + "(:\\s*(.*?))?(\\s|$)").exec(responseText); - if (refreshToken != null) { - WidgetUtil.redirect(refreshToken.getGroup(2)); - } else { - handleUnrecoverableCommunicationError( - "Invalid JSON response from server: " + responseText, event); - } - - } - - @Override - public void pushInvalidContent(PushConnection pushConnection, String message) { - debug("pushInvalidContent"); - if (pushConnection.isBidirectional()) { - // We can't be sure that what was pushed was actually a response but - // at this point it should not really matter, as something is - // seriously broken. - endRequest(); - } - - // Do nothing special for now. Should likely do the same as - // xhrInvalidContent - handleUnrecoverableCommunicationError("Invalid JSON from server: " - + message, null); - - } - - @Override - public void xhrInvalidStatusCode(CommunicationProblemEvent event) { - debug("xhrInvalidStatusCode"); - - Response response = event.getResponse(); - int statusCode = response.getStatusCode(); - getLogger().warning("Server returned " + statusCode + " for xhr"); - - if (statusCode == 401) { - // Authentication/authorization failed, no need to re-try - endRequest(); - handleUnauthorized(event); - return; - } else { - // 404, 408 and other 4xx codes CAN be temporary when you have a - // proxy between the client and the server and e.g. restart the - // server - // 5xx codes may or may not be temporary - handleRecoverableError(Type.XHR, event.getPayload()); - } - } - - /** - * @since - */ - private void endRequest() { - getConnection().getServerCommunicationHandler().endRequest(); - } - - protected void handleUnauthorized(CommunicationProblemEvent event) { - /* - * Authorization has failed (401). Could be that the session has timed - * out. - */ - connection.showAuthenticationError(""); - stopApplication(); - } - - private void stopApplication() { - // Consider application not running any more and prevent all - // future requests - connection.setApplicationRunning(false); - } - - private void handleUnrecoverableCommunicationError(String details, - CommunicationProblemEvent event) { - int statusCode = -1; - if (event != null) { - Response response = event.getResponse(); - if (response != null) { - statusCode = response.getStatusCode(); - } - } - connection.handleCommunicationError(details, statusCode); - - stopApplication(); - - } - - @Override - public void xhrOk() { - debug("xhrOk"); - if (isReconnecting()) { - resolveTemporaryError(Type.XHR); - } - } - - private void resolveTemporaryError(Type type) { - debug("resolveTemporaryError(" + type + ")"); - - if (reconnectionCause != type) { - // Waiting for some other problem to be resolved - return; - } - - reconnectionCause = null; - reconnectAttempt = 0; - hideDialog(); - - getLogger().info("Re-established connection to server"); - } - - @Override - public void pushOk(PushConnection pushConnection) { - debug("pushOk()"); - if (isReconnecting()) { - resolveTemporaryError(Type.PUSH); - } - } - - @Override - public void pushScriptLoadError(String resourceUrl) { - connection.handleCommunicationError(resourceUrl - + " could not be loaded. Push will not work.", 0); - } - - @Override - public void pushNotConnected(JsonObject payload) { - debug("pushNotConnected()"); - handleRecoverableError(Type.PUSH, payload); - } - - @Override - public void pushReconnectPending(PushConnection pushConnection) { - debug("pushReconnectPending(" + pushConnection.getTransportType() + ")"); - getLogger().info("Reopening push connection"); - if (pushConnection.isBidirectional()) { - // Lost connection for a connection which will tell us when the - // connection is available again - handleRecoverableError(Type.PUSH, null); - } else { - // Lost connection for a connection we do not necessarily know when - // it is available again (long polling behind proxy). Do nothing and - // show reconnect dialog if the user does something and the XHR - // fails - } - } - - @Override - public void pushError(PushConnection pushConnection) { - debug("pushError()"); - connection.handleCommunicationError("Push connection using " - + pushConnection.getTransportType() + " failed!", -1); - } - - @Override - public void pushClientTimeout(PushConnection pushConnection) { - debug("pushClientTimeout()"); - // TODO Reconnect, allowing client timeout to be set - // https://dev.vaadin.com/ticket/18429 - connection - .handleCommunicationError( - "Client unexpectedly disconnected. Ensure client timeout is disabled.", - -1); - } - - @Override - public void pushClosed(PushConnection pushConnection) { - debug("pushClosed()"); - getLogger().info("Push connection closed"); - } - -} diff --git a/client/src/com/vaadin/client/communication/ServerCommunicationHandler.java b/client/src/com/vaadin/client/communication/ServerCommunicationHandler.java deleted file mode 100644 index 12f7dee1d1..0000000000 --- a/client/src/com/vaadin/client/communication/ServerCommunicationHandler.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright 2000-2014 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.client.communication; - -import java.util.logging.Logger; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.user.client.Command; -import com.vaadin.client.ApplicationConfiguration; -import com.vaadin.client.ApplicationConnection; -import com.vaadin.client.ApplicationConnection.RequestStartingEvent; -import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent; -import com.vaadin.client.Util; -import com.vaadin.client.VLoadingIndicator; -import com.vaadin.shared.ApplicationConstants; -import com.vaadin.shared.Version; -import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; - -import elemental.json.Json; -import elemental.json.JsonArray; -import elemental.json.JsonObject; - -/** - * ServerCommunicationHandler is responsible for sending messages to the server. - * - * Internally either XHR or push is used for communicating, depending on the - * application configuration. - * - * @since 7.6 - * @author Vaadin Ltd - */ -public class ServerCommunicationHandler { - - private ApplicationConnection connection; - private boolean hasActiveRequest = false; - - /** - * Counter for the messages send to the server. First sent message has id 0. - */ - private int clientToServerMessageId = 0; - private XhrConnection xhrConnection; - private PushConnection push; - - public ServerCommunicationHandler() { - xhrConnection = GWT.create(XhrConnection.class); - } - - /** - * Sets the application connection this handler is connected to - * - * @param connection - * the application connection this handler is connected to - */ - public void setConnection(ApplicationConnection connection) { - this.connection = connection; - xhrConnection.setConnection(connection); - } - - private static Logger getLogger() { - return Logger.getLogger(ServerCommunicationHandler.class.getName()); - } - - public void sendInvocationsToServer() { - if (!connection.isApplicationRunning()) { - getLogger() - .warning( - "Trying to send RPC from not yet started or stopped application"); - return; - } - - if (hasActiveRequest() || (push != null && !push.isActive())) { - // There is an active request or push is enabled but not active - // -> send when current request completes or push becomes active - } else { - doSendInvocationsToServer(); - } - } - - /** - * Sends all pending method invocations (server RPC and legacy variable - * changes) to the server. - * - */ - private void doSendInvocationsToServer() { - - ServerRpcQueue serverRpcQueue = getServerRpcQueue(); - if (serverRpcQueue.isEmpty()) { - return; - } - - if (ApplicationConfiguration.isDebugMode()) { - Util.logMethodInvocations(connection, serverRpcQueue.getAll()); - } - - boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator(); - JsonArray reqJson = serverRpcQueue.toJson(); - serverRpcQueue.clear(); - - if (reqJson.length() == 0) { - // Nothing to send, all invocations were filtered out (for - // non-existing connectors) - getLogger() - .warning( - "All RPCs filtered out, not sending anything to the server"); - return; - } - - JsonObject extraJson = Json.createObject(); - if (!connection.getConfiguration().isWidgetsetVersionSent()) { - extraJson.put(ApplicationConstants.WIDGETSET_VERSION_ID, - Version.getFullVersion()); - connection.getConfiguration().setWidgetsetVersionSent(); - } - if (showLoadingIndicator) { - connection.getLoadingIndicator().trigger(); - } - send(reqJson, extraJson); - } - - private ServerRpcQueue getServerRpcQueue() { - return connection.getServerRpcQueue(); - } - - /** - * Makes an UIDL request to the server. - * - * @param reqInvocations - * Data containing RPC invocations and all related information. - * @param extraParams - * Parameters that are added to the payload - */ - protected void send(final JsonArray reqInvocations, - final JsonObject extraJson) { - startRequest(); - - JsonObject payload = Json.createObject(); - String csrfToken = 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, - getServerMessageHandler().getLastSeenServerSyncId()); - payload.put(ApplicationConstants.CLIENT_TO_SERVER_ID, - clientToServerMessageId++); - - if (extraJson != null) { - for (String key : extraJson.keys()) { - payload.put(key, extraJson.get(key)); - } - } - - send(payload); - - } - - /** - * Sends an asynchronous or synchronous UIDL request to the server using the - * given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - */ - public void send(final JsonObject payload) { - if (push != null && push.isBidirectional()) { - push.push(payload); - } else { - xhrConnection.send(payload); - } - } - - /** - * Sets the status for the push connection. - * - * @param enabled - * true to enable the push connection; - * false to disable the push connection. - */ - public void setPushEnabled(boolean enabled) { - final PushConfigurationState pushState = connection.getUIConnector() - .getState().pushConfiguration; - - if (enabled && push == null) { - push = GWT.create(PushConnection.class); - push.init(connection, pushState); - } else if (!enabled && push != null && push.isActive()) { - push.disconnect(new Command() { - @Override - public void execute() { - push = null; - /* - * If push has been enabled again while we were waiting for - * the old connection to disconnect, now is the right time - * to open a new connection - */ - if (pushState.mode.isEnabled()) { - setPushEnabled(true); - } - - /* - * Send anything that was enqueued while we waited for the - * connection to close - */ - if (getServerRpcQueue().isFlushPending()) { - getServerRpcQueue().flush(); - } - } - }); - } - } - - public void startRequest() { - if (hasActiveRequest) { - getLogger().severe( - "Trying to start a new request while another is active"); - } - hasActiveRequest = true; - connection.fireEvent(new RequestStartingEvent(connection)); - } - - public void endRequest() { - if (!hasActiveRequest) { - getLogger().severe("No active request"); - } - // After sendInvocationsToServer() there may be a new active - // request, so we must set hasActiveRequest to false before, not after, - // the call. - hasActiveRequest = false; - - if (connection.isApplicationRunning()) { - if (getServerRpcQueue().isFlushPending()) { - sendInvocationsToServer(); - } - runPostRequestHooks(connection.getConfiguration().getRootPanelId()); - } - - // deferring to avoid flickering - Scheduler.get().scheduleDeferred(new Command() { - @Override - public void execute() { - if (!connection.isApplicationRunning() - || !(hasActiveRequest() || getServerRpcQueue() - .isFlushPending())) { - getLoadingIndicator().hide(); - - // If on Liferay and session expiration management is in - // use, extend session duration on each request. - // Doing it here rather than before the request to improve - // responsiveness. - // Postponed until the end of the next request if other - // requests still pending. - extendLiferaySession(); - } - } - }); - connection.fireEvent(new ResponseHandlingEndedEvent(connection)); - } - - /** - * Runs possibly registered client side post request hooks. This is expected - * to be run after each uidl request made by Vaadin application. - * - * @param appId - */ - public static native void runPostRequestHooks(String appId) - /*-{ - if ($wnd.vaadin.postRequestHooks) { - for ( var hook in $wnd.vaadin.postRequestHooks) { - if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") { - try { - $wnd.vaadin.postRequestHooks[hook](appId); - } catch (e) { - } - } - } - } - }-*/; - - /** - * If on Liferay and logged in, ask the client side session management - * JavaScript to extend the session duration. - * - * Otherwise, Liferay client side JavaScript will explicitly expire the - * session even though the server side considers the session to be active. - * See ticket #8305 for more information. - */ - public static native void extendLiferaySession() - /*-{ - if ($wnd.Liferay && $wnd.Liferay.Session) { - $wnd.Liferay.Session.extend(); - // if the extend banner is visible, hide it - if ($wnd.Liferay.Session.banner) { - $wnd.Liferay.Session.banner.remove(); - } - } - }-*/; - - /** - * Indicates whether or not there are currently active UIDL requests. Used - * internally to sequence requests properly, seldom needed in Widgets. - * - * @return true if there are active requests - */ - public boolean hasActiveRequest() { - return hasActiveRequest; - } - - /** - * Returns a human readable string representation of the method used to - * communicate with the server. - * - * @return A string representation of the current transport type - */ - public String getCommunicationMethodName() { - String clientToServer = "XHR"; - String serverToClient = "-"; - if (push != null) { - serverToClient = push.getTransportType(); - if (push.isBidirectional()) { - clientToServer = serverToClient; - } - } - - return "Client to server: " + clientToServer + ", " - + "server to client: " + serverToClient; - } - - private CommunicationProblemHandler getCommunicationProblemHandler() { - return connection.getCommunicationProblemHandler(); - } - - private ServerMessageHandler getServerMessageHandler() { - return connection.getServerMessageHandler(); - } - - private VLoadingIndicator getLoadingIndicator() { - return connection.getLoadingIndicator(); - } - - /** - * Resynchronize the client side, i.e. reload all component hierarchy and - * state from the server - */ - public void resynchronize() { - getLogger().info("Resynchronizing from server"); - JsonObject resyncParam = Json.createObject(); - resyncParam.put(ApplicationConstants.RESYNCHRONIZE_ID, true); - send(Json.createArray(), resyncParam); - } - - /** - * Used internally to update what the server expects - * - * @param clientToServerMessageId - * the new client id to set - * @param force - * true if the id must be updated, false otherwise - */ - public void setClientToServerMessageId(int nextExpectedId, boolean force) { - if (nextExpectedId == clientToServerMessageId) { - // No op as everything matches they way it should - return; - } - if (force) { - getLogger().info( - "Forced update of clientId to " + clientToServerMessageId); - clientToServerMessageId = nextExpectedId; - return; - } - - if (nextExpectedId > clientToServerMessageId) { - if (clientToServerMessageId == 0) { - // We have never sent a message to the server, so likely the - // server knows better (typical case is that we refreshed a - // @PreserveOnRefresh UI) - getLogger().info( - "Updating client-to-server id to " + nextExpectedId - + " based on server"); - } else { - getLogger().warning( - "Server expects next client-to-server id to be " - + nextExpectedId + " but we were going to use " - + clientToServerMessageId + ". Will use " - + nextExpectedId + "."); - } - clientToServerMessageId = nextExpectedId; - } else { - // Server has not yet seen all our messages - // Do nothing as they will arrive eventually - } - } - -} diff --git a/client/src/com/vaadin/client/communication/ServerMessageHandler.java b/client/src/com/vaadin/client/communication/ServerMessageHandler.java deleted file mode 100644 index 0daf0c8fcc..0000000000 --- a/client/src/com/vaadin/client/communication/ServerMessageHandler.java +++ /dev/null @@ -1,1746 +0,0 @@ -/* - * Copyright 2000-2014 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.client.communication; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.google.gwt.core.client.Duration; -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.core.client.JsArray; -import com.google.gwt.core.client.JsArrayString; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.user.client.Command; -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.ui.Widget; -import com.vaadin.client.ApplicationConfiguration; -import com.vaadin.client.ApplicationConnection; -import com.vaadin.client.ApplicationConnection.MultiStepDuration; -import com.vaadin.client.ApplicationConnection.ResponseHandlingStartedEvent; -import com.vaadin.client.ApplicationConnection.State; -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 7.6 - * @author Vaadin Ltd - */ -public class ServerMessageHandler { - - public static final String JSON_COMMUNICATION_PREFIX = "for(;;);["; - public static final String JSON_COMMUNICATION_SUFFIX = "]"; - - /** - * Helper used to return two values when updating the connector hierarchy. - */ - private static class ConnectorHierarchyUpdateResult { - /** - * Needed at a later point when the created events are fired - */ - private JsArrayObject 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. - *

- * This must be -1, 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 responseHandlingLocks = new HashSet(); - - /** Contains all UIDL messages received while response handling is suspended */ - private List pendingUIDLMessages = new ArrayList(); - - // 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. - *

- * 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. - *

- * Initial value, i.e. no responses received from the server, is - * {@link #UNDEFINED_SYNC_ID} ({@value #UNDEFINED_SYNC_ID}). This happens - * between the bootstrap HTML being loaded and the first UI being rendered; - */ - private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; - - private ApplicationConnection connection; - - /** - * Data structure holding information about pending UIDL messages. - */ - private static class PendingUIDLMessage { - private ValueMap json; - - public PendingUIDLMessage(ValueMap json) { - this.json = json; - } - - public ValueMap getJson() { - return json; - } - } - - /** - * Sets the application connection this queue is connected to - * - * @param connection - * the application connection this queue is connected to - */ - public void setConnection(ApplicationConnection connection) { - this.connection = connection; - } - - private static Logger getLogger() { - return Logger.getLogger(ServerMessageHandler.class.getName()); - } - - /** - * Handles a received UIDL JSON text, parsing it, and passing it on to the - * appropriate handlers, while logging timing information. - * - * @param jsonText - * The JSON to handle - */ - public void handleMessage(final ValueMap json) { - if (json == null) { - throw new IllegalArgumentException( - "The json to handle cannot be null"); - } - if (getServerId(json) == -1) { - getLogger() - .severe("Response didn't contain a server id. " - + "Please verify that the server is up-to-date and that the response data has not been modified in transmission."); - } - - if (connection.getState() == State.RUNNING) { - handleJSON(json); - } else if (connection.getState() == State.INITIALIZING) { - // Application is starting up for the first time - connection.setApplicationRunning(true); - connection.executeWhenCSSLoaded(new Command() { - @Override - public void execute() { - handleJSON(json); - } - }); - } else { - getLogger() - .warning( - "Ignored received message because application has already been stopped"); - return; - } - } - - protected void handleJSON(final ValueMap json) { - final int serverId = getServerId(json); - - if (isResynchronize(json) && !isNextExpectedMessage(serverId)) { - // Resynchronize request. We must remove any old pending - // messages and ensure this is handled next. Otherwise we - // would keep waiting for an older message forever (if this - // is triggered by forceHandleMessage) - getLogger().info( - "Received resync message with id " + serverId - + " while waiting for " + getExpectedServerId()); - lastSeenServerSyncId = serverId - 1; - removeOldPendingMessages(); - } - - boolean locked = !responseHandlingLocks.isEmpty(); - - if (locked || !isNextExpectedMessage(serverId)) { - // Cannot or should not handle this message right now, either - // because of locks or because it's an out-of-order message - - if (locked) { - // Some component is doing something that can't be interrupted - // (e.g. animation that should be smooth). Enqueue the UIDL - // message for later processing. - getLogger().info("Postponing UIDL handling due to lock..."); - } else { - // Unexpected server id - if (serverId <= lastSeenServerSyncId) { - // Why is the server re-sending an old package? Ignore it - getLogger().warning( - "Received message with server id " + serverId - + " but have already seen " - + lastSeenServerSyncId + ". Ignoring it"); - endRequestIfResponse(json); - return; - } - - // We are waiting for an earlier message... - getLogger() - .info("Received message with server id " - + serverId - + " but expected " - + getExpectedServerId() - + ". Postponing handling until the missing message(s) have been received"); - } - pendingUIDLMessages.add(new PendingUIDLMessage(json)); - if (!forceHandleMessage.isRunning()) { - forceHandleMessage.schedule(MAX_SUSPENDED_TIMEOUT); - } - return; - } - - final Date start = new Date(); - /* - * Lock response handling to avoid a situation where something pushed - * from the server gets processed while waiting for e.g. lazily loaded - * connectors that are needed for processing the current message. - */ - final Object lock = new Object(); - suspendReponseHandling(lock); - - getLogger().info("Handling message from server"); - connection.fireEvent(new ResponseHandlingStartedEvent(connection)); - - // Client id must be updated before server id, as server id update can - // cause a resync (which must use the updated id) - if (json.containsKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) { - int serverNextExpected = json - .getInt(ApplicationConstants.CLIENT_TO_SERVER_ID); - getServerCommunicationHandler().setClientToServerMessageId( - serverNextExpected, isResynchronize(json)); - } - - if (serverId != -1) { - /* - * Use sync id unless explicitly set as undefined, as is done by - * e.g. critical server-side notifications - */ - lastSeenServerSyncId = serverId; - } - - // Handle redirect - if (json.containsKey("redirect")) { - String url = json.getValueMap("redirect").getString("url"); - getLogger().info("redirecting to " + url); - WidgetUtil.redirect(url); - return; - } - - final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); - - // Get security key - if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { - csrfToken = json - .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); - } - getLogger().info(" * Handling resources from server"); - - if (json.containsKey("resources")) { - ValueMap resources = json.getValueMap("resources"); - JsArrayString keyArray = resources.getKeyArray(); - int l = keyArray.length(); - for (int i = 0; i < l; i++) { - String key = keyArray.get(i); - connection.setResource(key, resources.getAsString(key)); - } - } - handleUIDLDuration.logDuration( - " * Handling resources from server completed", 10); - - getLogger().info(" * Handling type inheritance map from server"); - - if (json.containsKey("typeInheritanceMap")) { - connection.getConfiguration().addComponentInheritanceInfo( - json.getValueMap("typeInheritanceMap")); - } - handleUIDLDuration.logDuration( - " * Handling type inheritance map from server completed", 10); - - getLogger().info("Handling type mappings from server"); - - if (json.containsKey("typeMappings")) { - connection.getConfiguration() - .addComponentMappings(json.getValueMap("typeMappings"), - connection.getWidgetSet()); - - } - - getLogger().info("Handling resource dependencies"); - if (json.containsKey("scriptDependencies")) { - connection.loadScriptDependencies(json - .getJSStringArray("scriptDependencies")); - } - if (json.containsKey("styleDependencies")) { - connection.loadStyleDependencies(json - .getJSStringArray("styleDependencies")); - } - - handleUIDLDuration.logDuration( - " * Handling type mappings from server completed", 10); - /* - * Hook for e.g. TestBench to get details about server peformance - */ - if (json.containsKey("timings")) { - serverTimingInfo = json.getValueMap("timings"); - } - - Command c = new Command() { - private boolean onlyNoLayoutUpdates = true; - - @Override - public void execute() { - assert serverId == -1 || serverId == lastSeenServerSyncId; - - handleUIDLDuration.logDuration(" * Loading widgets completed", - 10); - - Profiler.enter("Handling meta information"); - ValueMap meta = null; - if (json.containsKey("meta")) { - getLogger().info(" * Handling meta information"); - meta = json.getValueMap("meta"); - if (meta.containsKey("repaintAll")) { - prepareRepaintAll(); - } - if (meta.containsKey("timedRedirect")) { - final ValueMap timedRedirect = meta - .getValueMap("timedRedirect"); - if (redirectTimer != null) { - redirectTimer.cancel(); - } - redirectTimer = new Timer() { - @Override - public void run() { - WidgetUtil.redirect(timedRedirect - .getString("url")); - } - }; - sessionExpirationInterval = timedRedirect - .getInt("interval"); - } - } - Profiler.leave("Handling meta information"); - - if (redirectTimer != null) { - redirectTimer.schedule(1000 * sessionExpirationInterval); - } - - updatingState = true; - - double processUidlStart = Duration.currentTimeMillis(); - - // Ensure that all connectors that we are about to update exist - JsArrayString createdConnectorIds = createConnectorsIfNeeded(json); - - // Update states, do not fire events - JsArrayObject pendingStateChangeEvents = updateConnectorState( - json, createdConnectorIds); - - /* - * Doing this here so that locales are available also to the - * connectors which get a state change event before the UI. - */ - Profiler.enter("Handling locales"); - getLogger().info(" * Handling locales"); - // Store locale data - LocaleService - .addLocales(getUIConnector().getState().localeServiceState.localeData); - Profiler.leave("Handling locales"); - - // Update hierarchy, do not fire events - ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(json); - - // Fire hierarchy change events - sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); - - updateCaptions(pendingStateChangeEvents, - connectorHierarchyUpdateResult.parentChangedIds); - - delegateToWidget(pendingStateChangeEvents); - - // Fire state change events. - sendStateChangeEvents(pendingStateChangeEvents); - - // Update of legacy (UIDL) style connectors - updateVaadin6StyleConnectors(json); - - // Handle any RPC invocations done on the server side - handleRpcInvocations(json); - - if (json.containsKey("dd")) { - // response contains data for drag and drop service - VDragAndDropManager.get().handleServerResponse( - json.getValueMap("dd")); - } - - unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); - - getLogger() - .info("handleUIDLMessage: " - + (Duration.currentTimeMillis() - processUidlStart) - + " ms"); - - updatingState = false; - - if (!onlyNoLayoutUpdates) { - Profiler.enter("Layout processing"); - try { - LayoutManager layoutManager = getLayoutManager(); - layoutManager.setEverythingNeedsMeasure(); - layoutManager.layoutNow(); - } catch (final Throwable e) { - getLogger().log(Level.SEVERE, - "Error processing layouts", e); - } - Profiler.leave("Layout processing"); - } - - if (ApplicationConfiguration.isDebugMode()) { - Profiler.enter("Dumping state changes to the console"); - getLogger().info(" * Dumping state changes to the console"); - VConsole.dirUIDL(json, connection); - Profiler.leave("Dumping state changes to the console"); - } - - if (meta != null) { - Profiler.enter("Error handling"); - if (meta.containsKey("appError")) { - ValueMap error = meta.getValueMap("appError"); - - VNotification.showError(connection, - error.getString("caption"), - error.getString("message"), - error.getString("details"), - error.getString("url")); - - connection.setApplicationRunning(false); - } - Profiler.leave("Error handling"); - } - - // TODO build profiling for widget impl loading time - - lastProcessingTime = (int) ((new Date().getTime()) - start - .getTime()); - totalProcessingTime += lastProcessingTime; - if (bootstrapTime == 0) { - bootstrapTime = calculateBootstrapTime(); - if (Profiler.isEnabled() && bootstrapTime != -1) { - Profiler.logBootstrapTimings(); - } - } - - getLogger().info( - " Processing time was " - + String.valueOf(lastProcessingTime) + "ms"); - getLogger().info( - "Referenced paintables: " + getConnectorMap().size()); - - endRequestIfResponse(json); - resumeResponseHandling(lock); - - if (Profiler.isEnabled()) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - Profiler.logTimings(); - Profiler.reset(); - } - }); - } - } - - /** - * Properly clean up any old stuff to ensure everything is properly - * reinitialized. - */ - private void prepareRepaintAll() { - String uiConnectorId = getUIConnector().getConnectorId(); - if (uiConnectorId == null) { - // Nothing to clear yet - return; - } - - // Create fake server response that says that the uiConnector - // has no children - JsonObject fakeHierarchy = Json.createObject(); - fakeHierarchy.put(uiConnectorId, Json.createArray()); - JsonObject fakeJson = Json.createObject(); - fakeJson.put("hierarchy", fakeHierarchy); - ValueMap fakeValueMap = ((JavaScriptObject) fakeJson.toNative()) - .cast(); - - // Update hierarchy based on the fake response - ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(fakeValueMap); - - // Send hierarchy events based on the fake update - sendHierarchyChangeEvents(connectorHierarchyUpdateResult.events); - - // Unregister all the old connectors that have now been removed - unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); - - getLayoutManager().cleanMeasuredSizes(); - } - - private void updateCaptions( - JsArrayObject 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 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 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 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 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 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 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 updateConnectorState( - ValueMap json, JsArrayString createdConnectorIds) { - JsArrayObject 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 newChildren = new ArrayList(); - List newComponents = new ArrayList(); - 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 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 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 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 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. 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 oldChildren = ccc - .getChildComponents(); - if (!oldChildren.isEmpty()) { - /* - * HasComponentsConnector has a separate child component - * list that should also be cleared - */ - ccc.setChildComponents(Collections - . emptyList()); - - // Create event and add it to the list of pending events - ConnectorHierarchyChangeEvent event = GWT - .create(ConnectorHierarchyChangeEvent.class); - event.setConnector(connector); - event.setOldChildren(oldChildren); - events.add(event); - } - } - Profiler.leave(prefix - + "recursivelyDetach create hierarchy event"); - } - - private native void replaceState(AbstractConnector connector, - Object defaultState) - /*-{ - connector.@com.vaadin.client.ui.AbstractConnector::state = defaultState; - }-*/; - - private void handleRpcInvocations(ValueMap json) { - if (json.containsKey("rpc")) { - Profiler.enter("handleRpcInvocations"); - - getLogger() - .info(" * Performing server to client RPC calls"); - - JsonArray rpcCalls = Util.jso2json(json - .getJavaScriptObject("rpc")); - - int rpcLength = rpcCalls.length(); - for (int i = 0; i < rpcLength; i++) { - try { - JsonArray rpcCall = rpcCalls.getArray(i); - MethodInvocation invocation = getRpcManager() - .parseAndApplyInvocation(rpcCall, - connection); - - if (onlyNoLayoutUpdates - && !RpcManager.getMethod(invocation) - .isNoLayout()) { - onlyNoLayoutUpdates = false; - } - - } catch (final Throwable e) { - getLogger() - .log(Level.SEVERE, - "Error performing server to client RPC calls", - e); - } - } - - Profiler.leave("handleRpcInvocations"); - } - } - - }; - ApplicationConfiguration.runWhenDependenciesLoaded(c); - } - - private void endRequestIfResponse(ValueMap json) { - if (isResponse(json)) { - // End the request if the received message was a - // response, not sent asynchronously - getServerCommunicationHandler().endRequest(); - } - } - - private boolean isResynchronize(ValueMap json) { - return json.containsKey(ApplicationConstants.RESYNCHRONIZE_ID); - } - - private boolean isResponse(ValueMap json) { - ValueMap meta = json.getValueMap("meta"); - if (meta == null || !meta.containsKey("async")) { - return true; - } - return false; - } - - /** - * Checks if the given serverId is the one we are currently waiting for from - * the server - */ - private boolean isNextExpectedMessage(int serverId) { - if (serverId == -1) { - return true; - } - if (serverId == getExpectedServerId()) { - return true; - } - if (lastSeenServerSyncId == UNDEFINED_SYNC_ID) { - // First message is always ok - return true; - } - return false; - - } - - private int getServerId(ValueMap json) { - if (json.containsKey(ApplicationConstants.SERVER_SYNC_ID)) { - return json.getInt(ApplicationConstants.SERVER_SYNC_ID); - } else { - return -1; - } - } - - private int getExpectedServerId() { - return lastSeenServerSyncId + 1; - } - - /** - * Timer used to make sure that no misbehaving components can delay response - * handling forever. - */ - Timer forceHandleMessage = new Timer() { - @Override - public void run() { - if (!responseHandlingLocks.isEmpty()) { - // Lock which was never release -> bug in locker or things just - // too slow - getLogger() - .warning( - "WARNING: reponse handling was never resumed, forcibly removing locks..."); - responseHandlingLocks.clear(); - } else { - // Waited for out-of-order message which never arrived - // Do one final check and resynchronize if the message is not - // there. The final check is only a precaution as this timer - // should have been cancelled if the message has arrived - getLogger().warning( - "Gave up waiting for message " + getExpectedServerId() - + " from the server"); - - } - if (!handlePendingMessages() && !pendingUIDLMessages.isEmpty()) { - // There are messages but the next id was not found, likely it - // has been lost - // Drop pending messages and resynchronize - pendingUIDLMessages.clear(); - getServerCommunicationHandler().resynchronize(); - } - } - }; - - /** - * This method can be used to postpone rendering of a response for a short - * period of time (e.g. to avoid the rendering process during animation). - * - * @param lock - */ - public void suspendReponseHandling(Object lock) { - responseHandlingLocks.add(lock); - } - - /** - * Resumes the rendering process once all locks have been removed. - * - * @param lock - */ - public void resumeResponseHandling(Object lock) { - responseHandlingLocks.remove(lock); - if (responseHandlingLocks.isEmpty()) { - // Cancel timer that breaks the lock - forceHandleMessage.cancel(); - - if (!pendingUIDLMessages.isEmpty()) { - getLogger() - .info("No more response handling locks, handling pending requests."); - handlePendingMessages(); - } - } - } - - private static native final int calculateBootstrapTime() - /*-{ - if ($wnd.performance && $wnd.performance.timing) { - return (new Date).getTime() - $wnd.performance.timing.responseStart; - } else { - // performance.timing not supported - return -1; - } - }-*/; - - /** - * Finds the next pending UIDL message and handles it (next pending is - * decided based on the server id) - * - * @return true if a message was handled, false otherwise - */ - private boolean handlePendingMessages() { - if (pendingUIDLMessages.isEmpty()) { - return false; - } - - // Try to find the next expected message - PendingUIDLMessage toHandle = null; - for (PendingUIDLMessage message : pendingUIDLMessages) { - if (isNextExpectedMessage(getServerId(message.json))) { - toHandle = message; - break; - } - } - - if (toHandle != null) { - pendingUIDLMessages.remove(toHandle); - handleJSON(toHandle.getJson()); - // Any remaining messages will be handled when this is called - // again at the end of handleJSON - return true; - } else { - return false; - } - - } - - private void removeOldPendingMessages() { - Iterator i = pendingUIDLMessages.iterator(); - while (i.hasNext()) { - PendingUIDLMessage m = i.next(); - int serverId = getServerId(m.json); - if (serverId != -1 && serverId < getExpectedServerId()) { - getLogger().info("Removing old message with id " + serverId); - i.remove(); - } - } - } - - /** - * Gets the server id included in the last received response. - *

- * 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. - *

- * 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. - *

- * 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 true if the internal state might be inconsistent - * because changes are being processed; false if the - * state should be consistent - */ - public boolean isUpdatingState() { - return updatingState; - } - - /** - * Checks if the first UIDL has been handled - * - * @return true if the initial UIDL has already been processed, false - * otherwise - */ - public boolean isInitialUidlHandled() { - return bootstrapTime != 0; - } - - private LayoutManager getLayoutManager() { - return LayoutManager.get(connection); - } - - private ConnectorMap getConnectorMap() { - return ConnectorMap.get(connection); - } - - private UIConnector getUIConnector() { - return connection.getUIConnector(); - } - - private RpcManager getRpcManager() { - return connection.getRpcManager(); - } - - private ServerCommunicationHandler getServerCommunicationHandler() { - return connection.getServerCommunicationHandler(); - } - - /** - * Strips the JSON wrapping from the given json string with wrapping. - * - * If the given string is not wrapped as expected, returns null - * - * @since - * @param jsonWithWrapping - * the JSON received from the server - * @return an unwrapped JSON string or null if the given string was not - * wrapped - */ - public static String stripJSONWrapping(String jsonWithWrapping) { - if (jsonWithWrapping == null) { - return null; - } - - if (!jsonWithWrapping.startsWith(JSON_COMMUNICATION_PREFIX) - || !jsonWithWrapping.endsWith(JSON_COMMUNICATION_SUFFIX)) { - return null; - } - return jsonWithWrapping.substring(JSON_COMMUNICATION_PREFIX.length(), - jsonWithWrapping.length() - JSON_COMMUNICATION_SUFFIX.length()); - } - - /** - * Unwraps and parses the given JSON, originating from the server - * - * @param jsonText - * the json from the server - * @return A parsed ValueMap or null if the input could not be parsed (or - * was null) - */ - public static ValueMap parseJson(String jsonText) { - if (jsonText == null) { - return null; - } - final Date start = new Date(); - try { - ValueMap json = parseJSONResponse(jsonText); - getLogger().info( - "JSON parsing took " - + (new Date().getTime() - start.getTime()) + "ms"); - return json; - } catch (final Exception e) { - getLogger().severe("Unable to parse JSON: " + jsonText); - return null; - } - } - - private static native ValueMap parseJSONResponse(String jsonText) - /*-{ - return JSON.parse(jsonText); - }-*/; - - /** - * Parse the given wrapped JSON, received from the server, to a ValueMap - * - * @param wrappedJsonText - * the json, wrapped as done by the server - * @return a ValueMap, or null if the wrapping was incorrect or json could - * not be parsed - */ - public static ValueMap parseWrappedJson(String wrappedJsonText) { - return parseJson(stripJSONWrapping(wrappedJsonText)); - } - -} diff --git a/client/src/com/vaadin/client/communication/ServerRpcQueue.java b/client/src/com/vaadin/client/communication/ServerRpcQueue.java index 94cc167c30..2a9a8d4204 100644 --- a/client/src/com/vaadin/client/communication/ServerRpcQueue.java +++ b/client/src/com/vaadin/client/communication/ServerRpcQueue.java @@ -67,10 +67,11 @@ public class ServerRpcQueue { } /** - * Sets the application connection this queue is connected to + * Sets the application connection this instance is connected to. Called + * internally by the framework. * * @param connection - * the application connection this queue is connected to + * the application connection this instance is connected to */ public void setConnection(ApplicationConnection connection) { this.connection = connection; @@ -203,8 +204,7 @@ public class ServerRpcQueue { // Somebody else cleared the queue before we had the chance return; } - connection.getServerCommunicationHandler() - .sendInvocationsToServer(); + connection.getMessageSender().sendInvocationsToServer(); } }; diff --git a/client/src/com/vaadin/client/communication/TranslatedURLReference.java b/client/src/com/vaadin/client/communication/TranslatedURLReference.java index b99f4c6e32..9296662234 100644 --- a/client/src/com/vaadin/client/communication/TranslatedURLReference.java +++ b/client/src/com/vaadin/client/communication/TranslatedURLReference.java @@ -30,8 +30,11 @@ public class TranslatedURLReference extends URLReference { private ApplicationConnection connection; /** + * Sets the application connection this instance is connected to. Called + * internally by the framework. + * * @param connection - * the connection to set + * the application connection this instance is connected to */ public void setConnection(ApplicationConnection connection) { this.connection = connection; diff --git a/client/src/com/vaadin/client/communication/XhrConnection.java b/client/src/com/vaadin/client/communication/XhrConnection.java index e830d465fc..aefdafec87 100644 --- a/client/src/com/vaadin/client/communication/XhrConnection.java +++ b/client/src/com/vaadin/client/communication/XhrConnection.java @@ -71,10 +71,11 @@ public class XhrConnection { } /** - * Sets the application connection this queue is connected to + * Sets the application connection this instance is connected to. Called + * internally by the framework. * * @param connection - * the application connection this queue is connected to + * the application connection this instance is connected to */ public void setConnection(ApplicationConnection connection) { this.connection = connection; @@ -127,12 +128,8 @@ public class XhrConnection { @Override public void onError(Request request, Throwable exception) { - getCommunicationProblemHandler().xhrException( - new CommunicationProblemEvent(request, payload, exception)); - } - - private ServerCommunicationHandler getServerCommunicationHandler() { - return connection.getServerCommunicationHandler(); + getConnectionStateHandler().xhrException( + new XhrConnectionError(request, payload, exception)); } @Override @@ -141,11 +138,10 @@ public class XhrConnection { if (statusCode != 200) { // There was a problem - CommunicationProblemEvent problemEvent = new CommunicationProblemEvent( + XhrConnectionError problemEvent = new XhrConnectionError( request, payload, response); - getCommunicationProblemHandler().xhrInvalidStatusCode( - problemEvent); + getConnectionStateHandler().xhrInvalidStatusCode(problemEvent); return; } @@ -157,27 +153,25 @@ public class XhrConnection { String contentType = response.getHeader("Content-Type"); if (contentType == null || !contentType.startsWith("application/json")) { - getCommunicationProblemHandler().xhrInvalidContent( - new CommunicationProblemEvent(request, payload, - response)); + getConnectionStateHandler().xhrInvalidContent( + new XhrConnectionError(request, payload, response)); return; } // for(;;);["+ realJson +"]" String responseText = response.getText(); - ValueMap json = ServerMessageHandler.parseWrappedJson(responseText); + ValueMap json = MessageHandler.parseWrappedJson(responseText); if (json == null) { // Invalid string (not wrapped as expected or can't parse) - getCommunicationProblemHandler().xhrInvalidContent( - new CommunicationProblemEvent(request, payload, - response)); + getConnectionStateHandler().xhrInvalidContent( + new XhrConnectionError(request, payload, response)); return; } - getCommunicationProblemHandler().xhrOk(); + getConnectionStateHandler().xhrOk(); getLogger().info("Received xhr message: " + responseText); - getServerMessageHandler().handleMessage(json); + getMessageHandler().handleMessage(json); } /** @@ -233,8 +227,8 @@ public class XhrConnection { }.schedule(retryTimeout); } } catch (RequestException e) { - getCommunicationProblemHandler().xhrException( - new CommunicationProblemEvent(null, payload, e)); + getConnectionStateHandler().xhrException( + new XhrConnectionError(null, payload, e)); } } @@ -255,12 +249,12 @@ public class XhrConnection { } - private CommunicationProblemHandler getCommunicationProblemHandler() { - return connection.getCommunicationProblemHandler(); + private ConnectionStateHandler getConnectionStateHandler() { + return connection.getConnectionStateHandler(); } - private ServerMessageHandler getServerMessageHandler() { - return connection.getServerMessageHandler(); + private MessageHandler getMessageHandler() { + return connection.getMessageHandler(); } private static native boolean resendRequest(Request request) diff --git a/client/src/com/vaadin/client/communication/XhrConnectionError.java b/client/src/com/vaadin/client/communication/XhrConnectionError.java new file mode 100644 index 0000000000..025f1d70e7 --- /dev/null +++ b/client/src/com/vaadin/client/communication/XhrConnectionError.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; + +import elemental.json.JsonObject; + +/** + * XhrConnectionError provides detail about an error which occured during an XHR + * request to the server + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class XhrConnectionError { + + private Throwable exception; + private Request request; + private Response response; + private JsonObject payload; + + /** + * Constructs an event from the given request, payload and exception + * + * @param request + * the request which failed + * @param payload + * the payload which was going to the server + * @param exception + * the exception describing the problem + */ + public XhrConnectionError(Request request, JsonObject payload, + Throwable exception) { + this.request = request; + this.exception = exception; + this.payload = payload; + } + + /** + * Constructs an event from the given request, response and payload + * + * @param request + * the request which failed + * @param payload + * the payload which was going to the server + * @param response + * the response for the request + */ + public XhrConnectionError(Request request, JsonObject payload, + Response response) { + this.request = request; + this.response = response; + this.payload = payload; + } + + /** + * Returns the exception which caused the problem, if available + * + * @return the exception which caused the problem, or null if not available + */ + public Throwable getException() { + return exception; + } + + /** + * Returns the request for which the problem occurred + * + * @return the request where the problem occurred + */ + public Request getRequest() { + return request; + } + + /** + * Returns the received response, if available + * + * @return the received response, or null if not available + */ + public Response getResponse() { + return response; + } + + /** + * Returns the payload which was sent to the server + * + * @return the payload which was sent, never null + */ + public JsonObject getPayload() { + return payload; + } +} \ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/InfoSection.java b/client/src/com/vaadin/client/debug/internal/InfoSection.java index aec6433bd9..39ff345cb1 100644 --- a/client/src/com/vaadin/client/debug/internal/InfoSection.java +++ b/client/src/com/vaadin/client/debug/internal/InfoSection.java @@ -166,7 +166,7 @@ public class InfoSection implements Section { addRow("Theme", connection.getUIConnector().getActiveTheme()); String communicationMethodInfo = connection - .getServerCommunicationHandler().getCommunicationMethodName(); + .getMessageSender().getCommunicationMethodName(); int pollInterval = connection.getUIConnector().getState().pollInterval; if (pollInterval > 0) { communicationMethodInfo += " (poll interval " + pollInterval diff --git a/client/src/com/vaadin/client/ui/VScrollTable.java b/client/src/com/vaadin/client/ui/VScrollTable.java index 2c091e4f78..2724daae77 100644 --- a/client/src/com/vaadin/client/ui/VScrollTable.java +++ b/client/src/com/vaadin/client/ui/VScrollTable.java @@ -2616,7 +2616,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets, @Override public void run() { - if (client.getServerCommunicationHandler().hasActiveRequest() + if (client.getMessageSender().hasActiveRequest() || navKeyDown) { // if client connection is busy, don't bother loading it more VConsole.log("Postponed rowfetch"); diff --git a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java index a0be6a195f..109b69f0c4 100644 --- a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java +++ b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java @@ -488,7 +488,7 @@ public class VDragAndDropManager { Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { @Override public boolean execute() { - if (!client.getServerCommunicationHandler() + if (!client.getMessageSender() .hasActiveRequest()) { removeActiveDragSourceStyleName(dragSource); return false; diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index 9fe71beaa0..d67c953c6e 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -748,11 +748,11 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } if (stateChangeEvent.hasPropertyChanged("pushConfiguration")) { - getConnection().getServerCommunicationHandler().setPushEnabled( + getConnection().getMessageSender().setPushEnabled( getState().pushConfiguration.mode.isEnabled()); } if (stateChangeEvent.hasPropertyChanged("reconnectDialogConfiguration")) { - getConnection().getCommunicationProblemHandler() + getConnection().getConnectionStateHandler() .configurationUpdated(); } diff --git a/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java b/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java index 4b788d06cc..c2752f1953 100644 --- a/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java +++ b/client/tests/src/com/vaadin/client/communication/ServerMessageHandlerTest.java @@ -28,29 +28,27 @@ public class ServerMessageHandlerTest { @Test public void unwrapValidJson() { String payload = "{'foo': 'bar'}"; - Assert.assertEquals( - payload, - ServerMessageHandler.stripJSONWrapping("for(;;);[" + payload - + "]")); + Assert.assertEquals(payload, + MessageHandler.stripJSONWrapping("for(;;);[" + payload + "]")); } @Test public void unwrapUnwrappedJson() { String payload = "{'foo': 'bar'}"; - Assert.assertNull(ServerMessageHandler.stripJSONWrapping(payload)); + Assert.assertNull(MessageHandler.stripJSONWrapping(payload)); } @Test public void unwrapNull() { - Assert.assertNull(ServerMessageHandler.stripJSONWrapping(null)); + Assert.assertNull(MessageHandler.stripJSONWrapping(null)); } @Test public void unwrapEmpty() { - Assert.assertNull(ServerMessageHandler.stripJSONWrapping("")); + Assert.assertNull(MessageHandler.stripJSONWrapping("")); } } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java index 2ff8948e29..8237d75c6c 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java @@ -28,21 +28,21 @@ public class MockApplicationConnection extends ApplicationConnection { public MockApplicationConnection() { super(); - serverMessageHandler = new MockServerMessageHandler(); - serverMessageHandler.setConnection(this); - serverCommunicationHandler = new MockServerCommunicationHandler(); - serverCommunicationHandler.setConnection(this); + messageHandler = new MockServerMessageHandler(); + messageHandler.setConnection(this); + messageSender = new MockServerCommunicationHandler(); + messageSender.setConnection(this); } @Override - public MockServerMessageHandler getServerMessageHandler() { - return (MockServerMessageHandler) super.getServerMessageHandler(); + public MockServerMessageHandler getMessageHandler() { + return (MockServerMessageHandler) super.getMessageHandler(); } @Override - public MockServerCommunicationHandler getServerCommunicationHandler() { + public MockServerCommunicationHandler getMessageSender() { return (MockServerCommunicationHandler) super - .getServerCommunicationHandler(); + .getMessageSender(); } /** @@ -52,7 +52,7 @@ public class MockApplicationConnection extends ApplicationConnection { * @see CsrfTokenDisabled */ public String getLastCsrfTokenReceiver() { - return getServerMessageHandler().lastCsrfTokenReceiver; + return getMessageHandler().lastCsrfTokenReceiver; } /** @@ -62,7 +62,7 @@ public class MockApplicationConnection extends ApplicationConnection { * @see CsrfTokenDisabled */ public String getLastCsrfTokenSent() { - return getServerCommunicationHandler().lastCsrfTokenSent; + return getMessageSender().lastCsrfTokenSent; } } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java b/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java index b7aa7fbdcb..14b5671181 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockServerCommunicationHandler.java @@ -15,13 +15,13 @@ */ package com.vaadin.tests.widgetset.client; -import com.vaadin.client.communication.ServerCommunicationHandler; +import com.vaadin.client.communication.MessageSender; import com.vaadin.shared.ApplicationConstants; import elemental.json.JsonObject; import elemental.json.JsonValue; -public class MockServerCommunicationHandler extends ServerCommunicationHandler { +public class MockServerCommunicationHandler extends MessageSender { // The last token sent to the server. String lastCsrfTokenSent; diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java b/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java index 1445553cc6..39b89b55ca 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockServerMessageHandler.java @@ -16,10 +16,10 @@ package com.vaadin.tests.widgetset.client; import com.vaadin.client.ValueMap; -import com.vaadin.client.communication.ServerMessageHandler; +import com.vaadin.client.communication.MessageHandler; import com.vaadin.shared.ApplicationConstants; -public class MockServerMessageHandler extends ServerMessageHandler { +public class MockServerMessageHandler extends MessageHandler { // The last token received from the server. protected String lastCsrfTokenReceiver; diff --git a/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java index 3eba2d2637..39dca8d799 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/csrf/CsrfButtonConnector.java @@ -70,7 +70,7 @@ public class CsrfButtonConnector extends AbstractComponentConnector { } private String csrfTokenInfo() { - return getMockConnection().getServerMessageHandler().getCsrfToken() + return getMockConnection().getMessageHandler().getCsrfToken() + ", " + getMockConnection().getLastCsrfTokenReceiver() + ", " + getMockConnection().getLastCsrfTokenSent(); } -- cgit v1.2.3