diff options
author | Artur Signell <artur@vaadin.com> | 2015-04-16 16:40:36 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2015-07-13 17:19:07 +0300 |
commit | 90b4c678d139fc4927811cdcad87d0b91eca98b6 (patch) | |
tree | 67dc629b9627efcebeabb0d0360f20f304e8615a /client | |
parent | 2e8d06f89b99a63f4b4fa6ece11b4119ae55fd57 (diff) | |
download | vaadin-framework-90b4c678d139fc4927811cdcad87d0b91eca98b6.tar.gz vaadin-framework-90b4c678d139fc4927811cdcad87d0b91eca98b6.zip |
Separate server message sending to its own class (#11733)
Change-Id: Ib3c4ac687387f2a239908b7e25e2753dbbf7e98b
Diffstat (limited to 'client')
11 files changed, 581 insertions, 476 deletions
diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index 8865e6efcb..0e84392cf0 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -36,29 +36,20 @@ import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.event.shared.HasHandlers; import com.google.gwt.event.shared.SimpleEventBus; -import com.google.gwt.http.client.Request; -import com.google.gwt.http.client.RequestBuilder; -import com.google.gwt.http.client.RequestCallback; -import com.google.gwt.http.client.RequestException; -import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.Window.ClosingEvent; -import com.google.gwt.user.client.Window.ClosingHandler; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConfiguration.ErrorMessage; import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; import com.vaadin.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.client.ResourceLoader.ResourceLoadListener; -import com.vaadin.client.communication.CommunicationProblemEvent; import com.vaadin.client.communication.CommunicationProblemHandler; import com.vaadin.client.communication.Heartbeat; -import com.vaadin.client.communication.PushConnection; 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; @@ -73,17 +64,12 @@ import com.vaadin.client.ui.VOverlay; import com.vaadin.client.ui.ui.UIConnector; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.ApplicationConstants; -import com.vaadin.shared.JsonConstants; import com.vaadin.shared.VaadinUriResolver; import com.vaadin.shared.Version; import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; -import com.vaadin.shared.ui.ui.UIConstants; -import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; import com.vaadin.shared.util.SharedUtil; import elemental.json.Json; -import elemental.json.JsonArray; -import elemental.json.JsonObject; /** * This is the client side communication "engine", managing client-server @@ -137,9 +123,6 @@ public class ApplicationConnection implements HasHandlers { */ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; - private final String JSON_COMMUNICATION_PREFIX = "for(;;);["; - private final String JSON_COMMUNICATION_SUFFIX = "]"; - private final HashMap<String, String> resourcesMap = new HashMap<String, String>(); private WidgetSet widgetSet; @@ -148,30 +131,15 @@ public class ApplicationConnection implements HasHandlers { private final UIConnector uIConnector; - private boolean hasActiveRequest = false; - - /** - * Webkit will ignore outgoing requests while waiting for a response to a - * navigation event (indicated by a beforeunload event). When this happens, - * we should keep trying to send the request every now and then until there - * is a response or until it throws an exception saying that it is already - * being sent. - */ - private boolean webkitMaybeIgnoringRequests = false; - protected boolean cssLoaded = false; /** Parameters for this application connection loaded from the web-page */ private ApplicationConfiguration configuration; - private Date requestStartTime; - private final LayoutManager layoutManager; private final RpcManager rpcManager; - private PushConnection push; - /** Event bus for communication events */ private EventBus eventBus = GWT.create(SimpleEventBus.class); @@ -405,6 +373,9 @@ public class ApplicationConnection implements HasHandlers { communicationProblemHandler.setConnection(this); serverMessageHandler = GWT.create(ServerMessageHandler.class); serverMessageHandler.setConnection(this); + serverCommunicationHandler = GWT + .create(ServerCommunicationHandler.class); + serverCommunicationHandler.setConnection(this); } public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { @@ -443,13 +414,6 @@ public class ApplicationConnection implements HasHandlers { heartbeat.init(this); - Window.addWindowClosingHandler(new ClosingHandler() { - @Override - public void onWindowClosing(ClosingEvent event) { - webkitMaybeIgnoringRequests = true; - } - }); - // Ensure the overlay container is added to the dom and set as a live // area for assistive devices Element overlayContainer = VOverlay.getOverlayContainer(this); @@ -476,9 +440,10 @@ public class ApplicationConnection implements HasHandlers { repaintAll(); } else { // initial UIDL provided in DOM, continue as if returned by request + // Hack to avoid logging an error in endRequest() - startRequest(); - handleJSONText(jsonText, -1); + getServerCommunicationHandler().startRequest(); + getServerMessageHandler().handleJSONText(jsonText, -1); } // Tooltip can't be created earlier because the @@ -502,7 +467,8 @@ public class ApplicationConnection implements HasHandlers { */ private boolean isActive() { return !getServerMessageHandler().isInitialUidlHandled() - || isWorkPending() || hasActiveRequest() + || isWorkPending() + || getServerCommunicationHandler().hasActiveRequest() || isExecutingDeferredCommands(); } @@ -613,7 +579,7 @@ public class ApplicationConnection implements HasHandlers { * * @param appId */ - private static native void runPostRequestHooks(String appId) + public static native void runPostRequestHooks(String appId) /*-{ if ($wnd.vaadin.postRequestHooks) { for ( var hook in $wnd.vaadin.postRequestHooks) { @@ -635,7 +601,7 @@ public class ApplicationConnection implements HasHandlers { * session even though the server side considers the session to be active. * See ticket #8305 for more information. */ - protected native void extendLiferaySession() + public static native void extendLiferaySession() /*-{ if ($wnd.Liferay && $wnd.Liferay.Session) { $wnd.Liferay.Session.extend(); @@ -646,24 +612,15 @@ public class ApplicationConnection implements HasHandlers { } }-*/; - /** - * Indicates whether or not there are currently active UIDL requests. Used - * internally to sequence requests properly, seldom needed in Widgets. - * - * @return true if there are active requests - */ - public boolean hasActiveRequest() { - return hasActiveRequest; - } - - private String getRepaintAllParameters() { + public String getRepaintAllParameters() { String parameters = ApplicationConstants.URL_PARAMETER_REPAINT_ALL + "=1"; return parameters; } public void repaintAll() { - makeUidlRequest(Json.createArray(), getRepaintAllParameters()); + getServerCommunicationHandler().makeUidlRequest(Json.createArray(), + getRepaintAllParameters()); } /** @@ -691,231 +648,16 @@ public class ApplicationConnection implements HasHandlers { getUIConnector().showServerDebugInfo(serverConnector); } - /** - * Makes an UIDL request to the server. - * - * @param reqInvocations - * Data containing RPC invocations and all related information. - * @param extraParams - * Parameters that are added as GET parameters to the url. - * Contains key=value pairs joined by & characters or is empty if - * no parameters should be added. Should not start with any - * special character. - */ - protected void makeUidlRequest(final JsonArray reqInvocations, - final String extraParams) { - startRequest(); - - JsonObject payload = Json.createObject(); - 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()); - - getLogger() - .info("Making UIDL Request with params: " + payload.toJson()); - String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX - + ApplicationConstants.UIDL_PATH + '/'); - - if (extraParams != null && extraParams.length() > 0) { - if (extraParams.equals(getRepaintAllParameters())) { - payload.put(ApplicationConstants.RESYNCHRONIZE_ID, true); - } else { - uri = SharedUtil.addGetParameters(uri, extraParams); - } - } - uri = SharedUtil.addGetParameters(uri, UIConstants.UI_ID_PARAMETER - + "=" + configuration.getUIId()); - - doUidlRequest(uri, payload, true); - - } - - /** - * Sends an asynchronous or synchronous UIDL request to the server using the - * given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - * @param retry - * true when a status code 0 should be retried - * @since 7.3.7 - */ - public void doUidlRequest(final String uri, final JsonObject payload, - final boolean retry) { - RequestCallback requestCallback = new RequestCallback() { - - @Override - public void onError(Request request, Throwable exception) { - getCommunicationProblemHandler().xhrException( - payload, - new CommunicationProblemEvent(request, uri, payload, - exception)); - } - - @Override - public void onResponseReceived(Request request, Response response) { - getLogger().info( - "Server visit took " - + String.valueOf((new Date()).getTime() - - requestStartTime.getTime()) + "ms"); - - int statusCode = response.getStatusCode(); - - if (statusCode != 200) { - // There was a problem - CommunicationProblemEvent problemEvent = new CommunicationProblemEvent( - request, uri, payload, response); - - getCommunicationProblemHandler().xhrInvalidStatusCode( - problemEvent, retry); - return; - } - - String contentType = response.getHeader("Content-Type"); - if (contentType == null - || !contentType.startsWith("application/json")) { - getCommunicationProblemHandler().xhrInvalidContent( - new CommunicationProblemEvent(request, uri, - payload, response)); - return; - } - - // for(;;);["+ realJson +"]" - String responseText = response.getText(); - - if (!responseText.startsWith(JSON_COMMUNICATION_PREFIX)) { - getCommunicationProblemHandler().xhrInvalidContent( - new CommunicationProblemEvent(request, uri, - payload, response)); - return; - } - - final String jsonText = responseText.substring( - JSON_COMMUNICATION_PREFIX.length(), - responseText.length() - - JSON_COMMUNICATION_SUFFIX.length()); - - handleJSONText(jsonText, statusCode); - } - }; - if (push != null) { - push.push(payload); - } else { - try { - doAjaxRequest(uri, payload, requestCallback); - } catch (RequestException e) { - getCommunicationProblemHandler().xhrException(payload, - new CommunicationProblemEvent(null, uri, payload, e)); - } - } - } - - /** - * Handles received UIDL JSON text, parsing it, and passing it on to the - * appropriate handlers, while logging timing information. - * - * @param jsonText - * @param statusCode - */ - private void handleJSONText(String jsonText, int statusCode) { - final Date start = new Date(); - final ValueMap json; - try { - json = parseJSONResponse(jsonText); - } catch (final Exception e) { - endRequest(); - showCommunicationError(e.getMessage() + " - Original JSON-text:" - + jsonText, statusCode); - return; - } - - getLogger().info( - "JSON parsing took " + (new Date().getTime() - start.getTime()) - + "ms"); - if (getState() == State.RUNNING) { - getServerMessageHandler().handleUIDLMessage(start, jsonText, json); - } else if (getState() == State.INITIALIZING) { - // Application is starting up for the first time - setApplicationRunning(true); - handleWhenCSSLoaded(jsonText, json); - } else { - getLogger() - .warning( - "Ignored received message because application has already been stopped"); - return; - } - } - - /** - * Sends an asynchronous UIDL request to the server using the given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - * @param requestCallback - * The handler for the response - * @throws RequestException - * if the request could not be sent - */ - protected void doAjaxRequest(String uri, JsonObject payload, - RequestCallback requestCallback) throws RequestException { - RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); - // TODO enable timeout - // rb.setTimeoutMillis(timeoutMillis); - // TODO this should be configurable - rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE); - rb.setRequestData(payload.toJson()); - rb.setCallback(requestCallback); - - final Request request = rb.send(); - if (webkitMaybeIgnoringRequests && BrowserInfo.get().isWebkit()) { - final int retryTimeout = 250; - new Timer() { - @Override - public void run() { - // Use native js to access private field in Request - if (resendRequest(request) && webkitMaybeIgnoringRequests) { - // Schedule retry if still needed - schedule(retryTimeout); - } - } - }.schedule(retryTimeout); - } - } - - private static native boolean resendRequest(Request request) - /*-{ - var xhr = request.@com.google.gwt.http.client.Request::xmlHttpRequest - if (xhr.readyState != 1) { - // Progressed to some other readyState -> no longer blocked - return false; - } - try { - xhr.send(); - return true; - } catch (e) { - // send throws exception if it is running for real - return false; - } - }-*/; - int cssWaits = 0; protected ServerRpcQueue serverRpcQueue; protected CommunicationProblemHandler communicationProblemHandler; protected ServerMessageHandler serverMessageHandler; + protected ServerCommunicationHandler serverCommunicationHandler; static final int MAX_CSS_WAITS = 100; - protected void handleWhenCSSLoaded(final String jsonText, - final ValueMap json) { + public void handleWhenCSSLoaded(final String jsonText, final ValueMap json) { if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) { (new Timer() { @Override @@ -962,7 +704,7 @@ public class ApplicationConnection implements HasHandlers { * The status code returned for the request * */ - protected void showCommunicationError(String details, int statusCode) { + public void showCommunicationError(String details, int statusCode) { getLogger().severe("Communication error: " + details); showError(details, configuration.getCommunicationError()); } @@ -1002,57 +744,6 @@ public class ApplicationConnection implements HasHandlers { message.getMessage(), details, message.getUrl()); } - protected void startRequest() { - if (hasActiveRequest) { - getLogger().severe( - "Trying to start a new request while another is active"); - } - hasActiveRequest = true; - requestStartTime = new Date(); - eventBus.fireEvent(new RequestStartingEvent(this)); - } - - 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. Active requests used to be tracked with an integer counter, - // so setting it after used to work but not with the #8505 changes. - hasActiveRequest = false; - - webkitMaybeIgnoringRequests = false; - - if (isApplicationRunning()) { - if (serverRpcQueue.isFlushPending()) { - sendInvocationsToServer(); - } - runPostRequestHooks(configuration.getRootPanelId()); - } - - // deferring to avoid flickering - Scheduler.get().scheduleDeferred(new Command() { - @Override - public void execute() { - if (!isApplicationRunning() - || !(hasActiveRequest() || serverRpcQueue - .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(); - } - } - }); - eventBus.fireEvent(new ResponseHandlingEndedEvent(this)); - } - /** * Checks if the client has running or scheduled commands */ @@ -1124,15 +815,6 @@ public class ApplicationConnection implements HasHandlers { return getLoadingIndicator().isVisible(); } - private static native ValueMap parseJSONResponse(String jsonText) - /*-{ - try { - return JSON.parse(jsonText); - } catch (ignored) { - return eval('(' + jsonText + ')'); - } - }-*/; - public void loadStyleDependencies(JsArrayString dependencies) { // Assuming no reason to interpret in a defined order ResourceLoadListener resourceLoadListener = new ResourceLoadListener() { @@ -1226,65 +908,6 @@ public class ApplicationConnection implements HasHandlers { serverRpcQueue.flush(); } - public void doSendPendingVariableChanges() { - if (!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 { - sendInvocationsToServer(); - } - } - - /** - * Sends all pending method invocations (server RPC and legacy variable - * changes) to the server. - * - */ - private void sendInvocationsToServer() { - if (serverRpcQueue.isEmpty()) { - return; - } - - if (ApplicationConfiguration.isDebugMode()) { - Util.logMethodInvocations(this, 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; - } - - String extraParams = ""; - if (!getConfiguration().isWidgetsetVersionSent()) { - if (!extraParams.isEmpty()) { - extraParams += "&"; - } - String widgetsetVersion = Version.getFullVersion(); - extraParams += "v-wsver=" + widgetsetVersion; - - getConfiguration().setWidgetsetVersionSent(); - } - if (showLoadingIndicator) { - getLoadingIndicator().trigger(); - } - makeUidlRequest(reqJson, extraParams); - } - /** * Sends a new value for the given paintables given variable to the server. * <p> @@ -1982,70 +1605,6 @@ public class ApplicationConnection implements HasHandlers { focusedElement); } - /** - * Sets the status for the push connection. - * - * @param enabled - * <code>true</code> to enable the push connection; - * <code>false</code> to disable the push connection. - */ - public void setPushEnabled(boolean enabled) { - final PushConfigurationState pushState = uIConnector.getState().pushConfiguration; - - if (enabled && push == null) { - push = GWT.create(PushConnection.class); - push.init(this, pushState, new CommunicationErrorHandler() { - @Override - public boolean onError(String details, int statusCode) { - handleCommunicationError(details, statusCode); - return true; - } - }); - } else if (!enabled && push != null && push.isActive()) { - push.disconnect(new Command() { - @Override - public void execute() { - push = null; - /* - * If push has been enabled again while we were waiting for - * the old connection to disconnect, now is the right time - * to open a new connection - */ - if (pushState.mode.isEnabled()) { - setPushEnabled(true); - } - - /* - * Send anything that was enqueued while we waited for the - * connection to close - */ - if (serverRpcQueue.isFlushPending()) { - sendPendingVariableChanges(); - } - } - }); - } - } - - public void handlePushMessage(String message) { - handleJSONText(message, 200); - } - - /** - * Returns a human readable string representation of the method used to - * communicate with the server. - * - * @since 7.1 - * @return A string representation of the current transport type - */ - public String getCommunicationMethodName() { - if (push != null) { - return "Push (" + push.getTransportType() + ")"; - } else { - return "XHR"; - } - } - private static Logger getLogger() { return Logger.getLogger(ApplicationConnection.class.getName()); } @@ -2107,17 +1666,23 @@ public class ApplicationConnection implements HasHandlers { } /** + * Gets the server communication handler for this application + * + * @return the server communication handler + */ + public ServerCommunicationHandler getServerCommunicationHandler() { + return serverCommunicationHandler; + } + + /** * @return the widget set */ public WidgetSet getWidgetSet() { return widgetSet; } - /** - * @since - * @return - */ public int getLastSeenServerSyncId() { return getServerMessageHandler().getLastSeenServerSyncId(); } + } diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java index b4f777f50e..43963e14c2 100644 --- a/client/src/com/vaadin/client/Util.java +++ b/client/src/com/vaadin/client/Util.java @@ -780,7 +780,7 @@ public class Util { + "(" + formattedParams + ")"; } - static void logMethodInvocations(ApplicationConnection c, + public static void logMethodInvocations(ApplicationConnection c, Collection<MethodInvocation> methodInvocations) { try { getLogger().info("RPC invocations to be sent to the server:"); diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java index f67ef091c9..45095bedb1 100644 --- a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java @@ -339,7 +339,7 @@ public class AtmospherePushConnection implements PushConnection { getLogger().info("Received push message: " + message); // "for(;;);[{json}]" -> "{json}" message = message.substring(9, message.length() - 1); - connection.handlePushMessage(message); + connection.getServerMessageHandler().handleJSONText(message, 200); } } diff --git a/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java b/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java index 23b2678365..d8740a50e6 100644 --- a/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java +++ b/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java @@ -108,8 +108,8 @@ public class CommunicationProblemHandler { public void run() { // doUidlRequest does not call startRequest so we do // not call endRequest before it - connection.doUidlRequest(event.getUri(), - event.getPayload(), true); + getServerCommunicationHandler().doUidlRequest( + event.getUri(), event.getPayload(), true); } }).schedule(Integer.parseInt(delay)); return; @@ -138,7 +138,7 @@ public class CommunicationProblemHandler { * @since */ private void endRequestAndStopApplication() { - connection.endRequest(); + getServerCommunicationHandler().endRequest(); // Consider application not running any more and prevent all // future requests @@ -174,8 +174,8 @@ public class CommunicationProblemHandler { public void run() { // doUidlRequest does not call startRequest so we do // not call endRequest before it - connection.doUidlRequest(event.getUri(), - event.getPayload(), false); + getServerCommunicationHandler().doUidlRequest( + event.getUri(), event.getPayload(), false); } }).schedule(100); } else { @@ -220,4 +220,8 @@ public class CommunicationProblemHandler { } } + + private ServerCommunicationHandler getServerCommunicationHandler() { + return connection.getServerCommunicationHandler(); + } } diff --git a/client/src/com/vaadin/client/communication/ServerCommunicationHandler.java b/client/src/com/vaadin/client/communication/ServerCommunicationHandler.java new file mode 100644 index 0000000000..ef215220e2 --- /dev/null +++ b/client/src/com/vaadin/client/communication/ServerCommunicationHandler.java @@ -0,0 +1,482 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.Date; +import java.util.logging.Logger; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.Response; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.Window.ClosingEvent; +import com.google.gwt.user.client.Window.ClosingHandler; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler; +import com.vaadin.client.ApplicationConnection.RequestStartingEvent; +import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.Util; +import com.vaadin.client.VLoadingIndicator; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.JsonConstants; +import com.vaadin.shared.Version; +import com.vaadin.shared.ui.ui.UIConstants; +import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; +import com.vaadin.shared.util.SharedUtil; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +/** + * ServerCommunicationHandler is responsible for communicating (sending and + * receiving messages) with the servlet. + * + * It will internally use either XHR or websockets for communicating, depending + * on how the application is configured. + * + * Uses {@link ServerMessageHandler} for processing received messages + * + * @since + * @author Vaadin Ltd + */ +public class ServerCommunicationHandler { + + private final String JSON_COMMUNICATION_PREFIX = "for(;;);["; + private final String JSON_COMMUNICATION_SUFFIX = "]"; + + private ApplicationConnection connection; + private PushConnection push; + private boolean hasActiveRequest = false; + private Date requestStartTime; + + /** + * Webkit will ignore outgoing requests while waiting for a response to a + * navigation event (indicated by a beforeunload event). When this happens, + * we should keep trying to send the request every now and then until there + * is a response or until it throws an exception saying that it is already + * being sent. + */ + private boolean webkitMaybeIgnoringRequests = false; + + public ServerCommunicationHandler() { + Window.addWindowClosingHandler(new ClosingHandler() { + @Override + public void onWindowClosing(ClosingEvent event) { + webkitMaybeIgnoringRequests = true; + } + }); + + } + + /** + * 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; + } + + public static Logger getLogger() { + return Logger.getLogger(ServerCommunicationHandler.class.getName()); + } + + public void doSendPendingVariableChanges() { + 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 { + sendInvocationsToServer(); + } + } + + /** + * Sends all pending method invocations (server RPC and legacy variable + * changes) to the server. + * + */ + public void sendInvocationsToServer() { + 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; + } + + String extraParams = ""; + if (!connection.getConfiguration().isWidgetsetVersionSent()) { + if (!extraParams.isEmpty()) { + extraParams += "&"; + } + String widgetsetVersion = Version.getFullVersion(); + extraParams += "v-wsver=" + widgetsetVersion; + + connection.getConfiguration().setWidgetsetVersionSent(); + } + if (showLoadingIndicator) { + connection.getLoadingIndicator().trigger(); + } + makeUidlRequest(reqJson, extraParams); + } + + 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 as GET parameters to the url. + * Contains key=value pairs joined by & characters or is empty if + * no parameters should be added. Should not start with any + * special character. + */ + public void makeUidlRequest(final JsonArray reqInvocations, + final String extraParams) { + 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()); + + getLogger() + .info("Making UIDL Request with params: " + payload.toJson()); + String uri = connection + .translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.UIDL_PATH + '/'); + + if (extraParams.equals(connection.getRepaintAllParameters())) { + payload.put(ApplicationConstants.RESYNCHRONIZE_ID, true); + } else { + uri = SharedUtil.addGetParameters(uri, extraParams); + } + uri = SharedUtil.addGetParameters(uri, UIConstants.UI_ID_PARAMETER + + "=" + connection.getConfiguration().getUIId()); + + doUidlRequest(uri, payload, true); + + } + + /** + * Sends an asynchronous or synchronous UIDL request to the server using the + * given URI. + * + * @param uri + * The URI to use for the request. May includes GET parameters + * @param payload + * The contents of the request to send + * @param retry + * true when a status code 0 should be retried + */ + public void doUidlRequest(final String uri, final JsonObject payload, + final boolean retry) { + RequestCallback requestCallback = new RequestCallback() { + + @Override + public void onError(Request request, Throwable exception) { + getCommunicationProblemHandler().xhrException( + payload, + new CommunicationProblemEvent(request, uri, payload, + exception)); + } + + @Override + public void onResponseReceived(Request request, Response response) { + getLogger().info( + "Server visit took " + + String.valueOf((new Date()).getTime() + - requestStartTime.getTime()) + "ms"); + + int statusCode = response.getStatusCode(); + + if (statusCode != 200) { + // There was a problem + CommunicationProblemEvent problemEvent = new CommunicationProblemEvent( + request, uri, payload, response); + + getCommunicationProblemHandler().xhrInvalidStatusCode( + problemEvent, retry); + return; + } + + String contentType = response.getHeader("Content-Type"); + if (contentType == null + || !contentType.startsWith("application/json")) { + getCommunicationProblemHandler().xhrInvalidContent( + new CommunicationProblemEvent(request, uri, + payload, response)); + return; + } + + // for(;;);["+ realJson +"]" + String responseText = response.getText(); + + if (!responseText.startsWith(JSON_COMMUNICATION_PREFIX)) { + getCommunicationProblemHandler().xhrInvalidContent( + new CommunicationProblemEvent(request, uri, + payload, response)); + return; + } + + final String jsonText = responseText.substring( + JSON_COMMUNICATION_PREFIX.length(), + responseText.length() + - JSON_COMMUNICATION_SUFFIX.length()); + + getServerMessageHandler().handleJSONText(jsonText, statusCode); + } + }; + if (push != null) { + push.push(payload); + } else { + try { + doAjaxRequest(uri, payload, requestCallback); + } catch (RequestException e) { + getCommunicationProblemHandler().xhrException(payload, + new CommunicationProblemEvent(null, uri, payload, e)); + } + } + } + + /** + * Sends an asynchronous UIDL request to the server using the given URI. + * + * @param uri + * The URI to use for the request. May includes GET parameters + * @param payload + * The contents of the request to send + * @param requestCallback + * The handler for the response + * @throws RequestException + * if the request could not be sent + */ + protected void doAjaxRequest(String uri, JsonObject payload, + RequestCallback requestCallback) throws RequestException { + RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); + // TODO enable timeout + // rb.setTimeoutMillis(timeoutMillis); + // TODO this should be configurable + rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE); + rb.setRequestData(payload.toJson()); + rb.setCallback(requestCallback); + + final Request request = rb.send(); + if (webkitMaybeIgnoringRequests && BrowserInfo.get().isWebkit()) { + final int retryTimeout = 250; + new Timer() { + @Override + public void run() { + // Use native js to access private field in Request + if (resendRequest(request) && webkitMaybeIgnoringRequests) { + // Schedule retry if still needed + schedule(retryTimeout); + } + } + }.schedule(retryTimeout); + } + } + + private static native boolean resendRequest(Request request) + /*-{ + var xhr = request.@com.google.gwt.http.client.Request::xmlHttpRequest + if (xhr.readyState != 1) { + // Progressed to some other readyState -> no longer blocked + return false; + } + try { + xhr.send(); + return true; + } catch (e) { + // send throws exception if it is running for real + return false; + } + }-*/; + + /** + * Sets the status for the push connection. + * + * @param enabled + * <code>true</code> to enable the push connection; + * <code>false</code> to disable the push connection. + */ + public void setPushEnabled(boolean enabled) { + final PushConfigurationState pushState = connection.getUIConnector() + .getState().pushConfiguration; + + if (enabled && push == null) { + push = GWT.create(PushConnection.class); + push.init(connection, pushState, new CommunicationErrorHandler() { + @Override + public boolean onError(String details, int statusCode) { + connection.handleCommunicationError(details, statusCode); + return true; + } + }); + } else if (!enabled && push != null && push.isActive()) { + push.disconnect(new Command() { + @Override + public void execute() { + push = null; + /* + * If push has been enabled again while we were waiting for + * the old connection to disconnect, now is the right time + * to open a new connection + */ + if (pushState.mode.isEnabled()) { + setPushEnabled(true); + } + + /* + * Send anything that was enqueued while we waited for the + * connection to close + */ + if (getServerRpcQueue().isFlushPending()) { + getServerRpcQueue().flush(); + } + } + }); + } + } + + public void startRequest() { + if (hasActiveRequest) { + getLogger().severe( + "Trying to start a new request while another is active"); + } + hasActiveRequest = true; + requestStartTime = new Date(); + 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. Active requests used to be tracked with an integer counter, + // so setting it after used to work but not with the #8505 changes. + hasActiveRequest = false; + + webkitMaybeIgnoringRequests = false; + + if (connection.isApplicationRunning()) { + if (getServerRpcQueue().isFlushPending()) { + sendInvocationsToServer(); + } + ApplicationConnection.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. + ApplicationConnection.extendLiferaySession(); + } + } + }); + connection.fireEvent(new ResponseHandlingEndedEvent(connection)); + } + + /** + * 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() { + if (push != null) { + return "Push (" + push.getTransportType() + ")"; + } else { + return "XHR"; + } + } + + private CommunicationProblemHandler getCommunicationProblemHandler() { + return connection.getCommunicationProblemHandler(); + } + + private ServerMessageHandler getServerMessageHandler() { + return connection.getServerMessageHandler(); + } + + private VLoadingIndicator getLoadingIndicator() { + return connection.getLoadingIndicator(); + } + +} diff --git a/client/src/com/vaadin/client/communication/ServerMessageHandler.java b/client/src/com/vaadin/client/communication/ServerMessageHandler.java index cb0dc898c0..a9874d99ea 100644 --- a/client/src/com/vaadin/client/communication/ServerMessageHandler.java +++ b/client/src/com/vaadin/client/communication/ServerMessageHandler.java @@ -38,6 +38,7 @@ 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; @@ -225,6 +226,52 @@ public class ServerMessageHandler { return Logger.getLogger(ServerMessageHandler.class.getName()); } + /** + * Handles received UIDL JSON text, parsing it, and passing it on to the + * appropriate handlers, while logging timing information. + * + * @param jsonText + * @param statusCode + */ + public void handleJSONText(String jsonText, int statusCode) { + final Date start = new Date(); + final ValueMap json; + try { + json = parseJSONResponse(jsonText); + } catch (final Exception e) { + // FIXME + getServerCommunicationHandler().endRequest(); + connection.showCommunicationError(e.getMessage() + + " - Original JSON-text:" + jsonText, statusCode); + return; + } + + getLogger().info( + "JSON parsing took " + (new Date().getTime() - start.getTime()) + + "ms"); + if (connection.getState() == State.RUNNING) { + handleUIDLMessage(start, jsonText, json); + } else if (connection.getState() == State.INITIALIZING) { + // Application is starting up for the first time + connection.setApplicationRunning(true); + connection.handleWhenCSSLoaded(jsonText, json); + } else { + getLogger() + .warning( + "Ignored received message because application has already been stopped"); + return; + } + } + + private static native ValueMap parseJSONResponse(String jsonText) + /*-{ + try { + return JSON.parse(jsonText); + } catch (ignored) { + return eval('(' + jsonText + ')'); + } + }-*/; + public void handleUIDLMessage(final Date start, final String jsonText, final ValueMap json) { if (!responseHandlingLocks.isEmpty()) { @@ -274,7 +321,7 @@ public class ServerMessageHandler { if (meta == null || !meta.containsKey("async")) { // End the request if the received message was a // response, not sent asynchronously - connection.endRequest(); + getServerCommunicationHandler().endRequest(); } resumeResponseHandling(lock); @@ -513,7 +560,7 @@ public class ServerMessageHandler { // End the request if the received message was a response, // not sent asynchronously // FIXME - connection.endRequest(); + getServerCommunicationHandler().endRequest(); } resumeResponseHandling(lock); @@ -1539,4 +1586,8 @@ public class ServerMessageHandler { return connection.getRpcManager(); } + private ServerCommunicationHandler getServerCommunicationHandler() { + return connection.getServerCommunicationHandler(); + } + } diff --git a/client/src/com/vaadin/client/communication/ServerRpcQueue.java b/client/src/com/vaadin/client/communication/ServerRpcQueue.java index d9241e4c63..2b7bab447f 100644 --- a/client/src/com/vaadin/client/communication/ServerRpcQueue.java +++ b/client/src/com/vaadin/client/communication/ServerRpcQueue.java @@ -201,7 +201,8 @@ public class ServerRpcQueue { // Somebody else cleared the queue before we had the chance return; } - connection.doSendPendingVariableChanges(); + connection.getServerCommunicationHandler() + .doSendPendingVariableChanges(); } }; diff --git a/client/src/com/vaadin/client/debug/internal/InfoSection.java b/client/src/com/vaadin/client/debug/internal/InfoSection.java index dfb31cdd18..aec6433bd9 100644 --- a/client/src/com/vaadin/client/debug/internal/InfoSection.java +++ b/client/src/com/vaadin/client/debug/internal/InfoSection.java @@ -166,7 +166,7 @@ public class InfoSection implements Section { addRow("Theme", connection.getUIConnector().getActiveTheme()); String communicationMethodInfo = connection - .getCommunicationMethodName(); + .getServerCommunicationHandler().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 6bb3199b08..2c091e4f78 100644 --- a/client/src/com/vaadin/client/ui/VScrollTable.java +++ b/client/src/com/vaadin/client/ui/VScrollTable.java @@ -2616,7 +2616,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets, @Override public void run() { - if (client.hasActiveRequest() || navKeyDown) { + if (client.getServerCommunicationHandler().hasActiveRequest() + || navKeyDown) { // if client connection is busy, don't bother loading it more VConsole.log("Postponed rowfetch"); schedule(250); diff --git a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java index efca46b522..a0be6a195f 100644 --- a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java +++ b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java @@ -488,7 +488,8 @@ public class VDragAndDropManager { Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { @Override public boolean execute() { - if (!client.hasActiveRequest()) { + if (!client.getServerCommunicationHandler() + .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 60e9587493..6d708a9f14 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -748,7 +748,7 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } if (stateChangeEvent.hasPropertyChanged("pushConfiguration")) { - getConnection().setPushEnabled( + getConnection().getServerCommunicationHandler().setPushEnabled( getState().pushConfiguration.mode.isEnabled()); } |