You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ServerCommunicationHandler.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.communication;
  17. import java.util.Date;
  18. import java.util.logging.Logger;
  19. import com.google.gwt.core.client.GWT;
  20. import com.google.gwt.core.client.Scheduler;
  21. import com.google.gwt.http.client.Request;
  22. import com.google.gwt.http.client.RequestBuilder;
  23. import com.google.gwt.http.client.RequestCallback;
  24. import com.google.gwt.http.client.RequestException;
  25. import com.google.gwt.http.client.Response;
  26. import com.google.gwt.user.client.Command;
  27. import com.google.gwt.user.client.Timer;
  28. import com.google.gwt.user.client.Window;
  29. import com.google.gwt.user.client.Window.ClosingEvent;
  30. import com.google.gwt.user.client.Window.ClosingHandler;
  31. import com.vaadin.client.ApplicationConfiguration;
  32. import com.vaadin.client.ApplicationConnection;
  33. import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler;
  34. import com.vaadin.client.ApplicationConnection.RequestStartingEvent;
  35. import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent;
  36. import com.vaadin.client.BrowserInfo;
  37. import com.vaadin.client.Util;
  38. import com.vaadin.client.VLoadingIndicator;
  39. import com.vaadin.shared.ApplicationConstants;
  40. import com.vaadin.shared.JsonConstants;
  41. import com.vaadin.shared.Version;
  42. import com.vaadin.shared.ui.ui.UIConstants;
  43. import com.vaadin.shared.ui.ui.UIState.PushConfigurationState;
  44. import com.vaadin.shared.util.SharedUtil;
  45. import elemental.json.Json;
  46. import elemental.json.JsonArray;
  47. import elemental.json.JsonObject;
  48. /**
  49. * ServerCommunicationHandler is responsible for communicating (sending and
  50. * receiving messages) with the servlet.
  51. *
  52. * It will internally use either XHR or websockets for communicating, depending
  53. * on how the application is configured.
  54. *
  55. * Uses {@link ServerMessageHandler} for processing received messages
  56. *
  57. * @since
  58. * @author Vaadin Ltd
  59. */
  60. public class ServerCommunicationHandler {
  61. private final String JSON_COMMUNICATION_PREFIX = "for(;;);[";
  62. private final String JSON_COMMUNICATION_SUFFIX = "]";
  63. private ApplicationConnection connection;
  64. private PushConnection push;
  65. private boolean hasActiveRequest = false;
  66. private Date requestStartTime;
  67. /**
  68. * Webkit will ignore outgoing requests while waiting for a response to a
  69. * navigation event (indicated by a beforeunload event). When this happens,
  70. * we should keep trying to send the request every now and then until there
  71. * is a response or until it throws an exception saying that it is already
  72. * being sent.
  73. */
  74. private boolean webkitMaybeIgnoringRequests = false;
  75. public ServerCommunicationHandler() {
  76. Window.addWindowClosingHandler(new ClosingHandler() {
  77. @Override
  78. public void onWindowClosing(ClosingEvent event) {
  79. webkitMaybeIgnoringRequests = true;
  80. }
  81. });
  82. }
  83. /**
  84. * Sets the application connection this handler is connected to
  85. *
  86. * @param connection
  87. * the application connection this handler is connected to
  88. */
  89. public void setConnection(ApplicationConnection connection) {
  90. this.connection = connection;
  91. }
  92. public static Logger getLogger() {
  93. return Logger.getLogger(ServerCommunicationHandler.class.getName());
  94. }
  95. public void doSendPendingVariableChanges() {
  96. if (!connection.isApplicationRunning()) {
  97. getLogger()
  98. .warning(
  99. "Trying to send RPC from not yet started or stopped application");
  100. return;
  101. }
  102. if (hasActiveRequest() || (push != null && !push.isActive())) {
  103. // There is an active request or push is enabled but not active
  104. // -> send when current request completes or push becomes active
  105. } else {
  106. sendInvocationsToServer();
  107. }
  108. }
  109. /**
  110. * Sends all pending method invocations (server RPC and legacy variable
  111. * changes) to the server.
  112. *
  113. */
  114. public void sendInvocationsToServer() {
  115. ServerRpcQueue serverRpcQueue = getServerRpcQueue();
  116. if (serverRpcQueue.isEmpty()) {
  117. return;
  118. }
  119. if (ApplicationConfiguration.isDebugMode()) {
  120. Util.logMethodInvocations(connection, serverRpcQueue.getAll());
  121. }
  122. boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator();
  123. JsonArray reqJson = serverRpcQueue.toJson();
  124. serverRpcQueue.clear();
  125. if (reqJson.length() == 0) {
  126. // Nothing to send, all invocations were filtered out (for
  127. // non-existing connectors)
  128. getLogger()
  129. .warning(
  130. "All RPCs filtered out, not sending anything to the server");
  131. return;
  132. }
  133. String extraParams = "";
  134. if (!connection.getConfiguration().isWidgetsetVersionSent()) {
  135. if (!extraParams.isEmpty()) {
  136. extraParams += "&";
  137. }
  138. String widgetsetVersion = Version.getFullVersion();
  139. extraParams += "v-wsver=" + widgetsetVersion;
  140. connection.getConfiguration().setWidgetsetVersionSent();
  141. }
  142. if (showLoadingIndicator) {
  143. connection.getLoadingIndicator().trigger();
  144. }
  145. makeUidlRequest(reqJson, extraParams);
  146. }
  147. private ServerRpcQueue getServerRpcQueue() {
  148. return connection.getServerRpcQueue();
  149. }
  150. /**
  151. * Makes an UIDL request to the server.
  152. *
  153. * @param reqInvocations
  154. * Data containing RPC invocations and all related information.
  155. * @param extraParams
  156. * Parameters that are added as GET parameters to the url.
  157. * Contains key=value pairs joined by & characters or is empty if
  158. * no parameters should be added. Should not start with any
  159. * special character.
  160. */
  161. public void makeUidlRequest(final JsonArray reqInvocations,
  162. final String extraParams) {
  163. startRequest();
  164. JsonObject payload = Json.createObject();
  165. String csrfToken = getServerMessageHandler().getCsrfToken();
  166. if (!csrfToken.equals(ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) {
  167. payload.put(ApplicationConstants.CSRF_TOKEN, csrfToken);
  168. }
  169. payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations);
  170. payload.put(ApplicationConstants.SERVER_SYNC_ID,
  171. getServerMessageHandler().getLastSeenServerSyncId());
  172. getLogger()
  173. .info("Making UIDL Request with params: " + payload.toJson());
  174. String uri = connection
  175. .translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX
  176. + ApplicationConstants.UIDL_PATH + '/');
  177. if (extraParams.equals(connection.getRepaintAllParameters())) {
  178. payload.put(ApplicationConstants.RESYNCHRONIZE_ID, true);
  179. } else {
  180. uri = SharedUtil.addGetParameters(uri, extraParams);
  181. }
  182. uri = SharedUtil.addGetParameters(uri, UIConstants.UI_ID_PARAMETER
  183. + "=" + connection.getConfiguration().getUIId());
  184. doUidlRequest(uri, payload, true);
  185. }
  186. /**
  187. * Sends an asynchronous or synchronous UIDL request to the server using the
  188. * given URI.
  189. *
  190. * @param uri
  191. * The URI to use for the request. May includes GET parameters
  192. * @param payload
  193. * The contents of the request to send
  194. * @param retry
  195. * true when a status code 0 should be retried
  196. */
  197. public void doUidlRequest(final String uri, final JsonObject payload,
  198. final boolean retry) {
  199. RequestCallback requestCallback = new RequestCallback() {
  200. @Override
  201. public void onError(Request request, Throwable exception) {
  202. getCommunicationProblemHandler().xhrException(
  203. payload,
  204. new CommunicationProblemEvent(request, uri, payload,
  205. exception));
  206. }
  207. @Override
  208. public void onResponseReceived(Request request, Response response) {
  209. getLogger().info(
  210. "Server visit took "
  211. + String.valueOf((new Date()).getTime()
  212. - requestStartTime.getTime()) + "ms");
  213. int statusCode = response.getStatusCode();
  214. if (statusCode != 200) {
  215. // There was a problem
  216. CommunicationProblemEvent problemEvent = new CommunicationProblemEvent(
  217. request, uri, payload, response);
  218. getCommunicationProblemHandler().xhrInvalidStatusCode(
  219. problemEvent, retry);
  220. return;
  221. }
  222. String contentType = response.getHeader("Content-Type");
  223. if (contentType == null
  224. || !contentType.startsWith("application/json")) {
  225. getCommunicationProblemHandler().xhrInvalidContent(
  226. new CommunicationProblemEvent(request, uri,
  227. payload, response));
  228. return;
  229. }
  230. // for(;;);["+ realJson +"]"
  231. String responseText = response.getText();
  232. if (!responseText.startsWith(JSON_COMMUNICATION_PREFIX)) {
  233. getCommunicationProblemHandler().xhrInvalidContent(
  234. new CommunicationProblemEvent(request, uri,
  235. payload, response));
  236. return;
  237. }
  238. final String jsonText = responseText.substring(
  239. JSON_COMMUNICATION_PREFIX.length(),
  240. responseText.length()
  241. - JSON_COMMUNICATION_SUFFIX.length());
  242. getServerMessageHandler().handleJSONText(jsonText, statusCode);
  243. }
  244. };
  245. if (push != null) {
  246. push.push(payload);
  247. } else {
  248. try {
  249. doAjaxRequest(uri, payload, requestCallback);
  250. } catch (RequestException e) {
  251. getCommunicationProblemHandler().xhrException(payload,
  252. new CommunicationProblemEvent(null, uri, payload, e));
  253. }
  254. }
  255. }
  256. /**
  257. * Sends an asynchronous UIDL request to the server using the given URI.
  258. *
  259. * @param uri
  260. * The URI to use for the request. May includes GET parameters
  261. * @param payload
  262. * The contents of the request to send
  263. * @param requestCallback
  264. * The handler for the response
  265. * @throws RequestException
  266. * if the request could not be sent
  267. */
  268. protected void doAjaxRequest(String uri, JsonObject payload,
  269. RequestCallback requestCallback) throws RequestException {
  270. RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
  271. // TODO enable timeout
  272. // rb.setTimeoutMillis(timeoutMillis);
  273. // TODO this should be configurable
  274. rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE);
  275. rb.setRequestData(payload.toJson());
  276. rb.setCallback(requestCallback);
  277. final Request request = rb.send();
  278. if (webkitMaybeIgnoringRequests && BrowserInfo.get().isWebkit()) {
  279. final int retryTimeout = 250;
  280. new Timer() {
  281. @Override
  282. public void run() {
  283. // Use native js to access private field in Request
  284. if (resendRequest(request) && webkitMaybeIgnoringRequests) {
  285. // Schedule retry if still needed
  286. schedule(retryTimeout);
  287. }
  288. }
  289. }.schedule(retryTimeout);
  290. }
  291. }
  292. private static native boolean resendRequest(Request request)
  293. /*-{
  294. var xhr = request.@com.google.gwt.http.client.Request::xmlHttpRequest
  295. if (xhr.readyState != 1) {
  296. // Progressed to some other readyState -> no longer blocked
  297. return false;
  298. }
  299. try {
  300. xhr.send();
  301. return true;
  302. } catch (e) {
  303. // send throws exception if it is running for real
  304. return false;
  305. }
  306. }-*/;
  307. /**
  308. * Sets the status for the push connection.
  309. *
  310. * @param enabled
  311. * <code>true</code> to enable the push connection;
  312. * <code>false</code> to disable the push connection.
  313. */
  314. public void setPushEnabled(boolean enabled) {
  315. final PushConfigurationState pushState = connection.getUIConnector()
  316. .getState().pushConfiguration;
  317. if (enabled && push == null) {
  318. push = GWT.create(PushConnection.class);
  319. push.init(connection, pushState, new CommunicationErrorHandler() {
  320. @Override
  321. public boolean onError(String details, int statusCode) {
  322. connection.handleCommunicationError(details, statusCode);
  323. return true;
  324. }
  325. });
  326. } else if (!enabled && push != null && push.isActive()) {
  327. push.disconnect(new Command() {
  328. @Override
  329. public void execute() {
  330. push = null;
  331. /*
  332. * If push has been enabled again while we were waiting for
  333. * the old connection to disconnect, now is the right time
  334. * to open a new connection
  335. */
  336. if (pushState.mode.isEnabled()) {
  337. setPushEnabled(true);
  338. }
  339. /*
  340. * Send anything that was enqueued while we waited for the
  341. * connection to close
  342. */
  343. if (getServerRpcQueue().isFlushPending()) {
  344. getServerRpcQueue().flush();
  345. }
  346. }
  347. });
  348. }
  349. }
  350. public void startRequest() {
  351. if (hasActiveRequest) {
  352. getLogger().severe(
  353. "Trying to start a new request while another is active");
  354. }
  355. hasActiveRequest = true;
  356. requestStartTime = new Date();
  357. connection.fireEvent(new RequestStartingEvent(connection));
  358. }
  359. public void endRequest() {
  360. if (!hasActiveRequest) {
  361. getLogger().severe("No active request");
  362. }
  363. // After sendInvocationsToServer() there may be a new active
  364. // request, so we must set hasActiveRequest to false before, not after,
  365. // the call. Active requests used to be tracked with an integer counter,
  366. // so setting it after used to work but not with the #8505 changes.
  367. hasActiveRequest = false;
  368. webkitMaybeIgnoringRequests = false;
  369. if (connection.isApplicationRunning()) {
  370. if (getServerRpcQueue().isFlushPending()) {
  371. sendInvocationsToServer();
  372. }
  373. ApplicationConnection.runPostRequestHooks(connection
  374. .getConfiguration().getRootPanelId());
  375. }
  376. // deferring to avoid flickering
  377. Scheduler.get().scheduleDeferred(new Command() {
  378. @Override
  379. public void execute() {
  380. if (!connection.isApplicationRunning()
  381. || !(hasActiveRequest() || getServerRpcQueue()
  382. .isFlushPending())) {
  383. getLoadingIndicator().hide();
  384. // If on Liferay and session expiration management is in
  385. // use, extend session duration on each request.
  386. // Doing it here rather than before the request to improve
  387. // responsiveness.
  388. // Postponed until the end of the next request if other
  389. // requests still pending.
  390. ApplicationConnection.extendLiferaySession();
  391. }
  392. }
  393. });
  394. connection.fireEvent(new ResponseHandlingEndedEvent(connection));
  395. }
  396. /**
  397. * Indicates whether or not there are currently active UIDL requests. Used
  398. * internally to sequence requests properly, seldom needed in Widgets.
  399. *
  400. * @return true if there are active requests
  401. */
  402. public boolean hasActiveRequest() {
  403. return hasActiveRequest;
  404. }
  405. /**
  406. * Returns a human readable string representation of the method used to
  407. * communicate with the server.
  408. *
  409. * @return A string representation of the current transport type
  410. */
  411. public String getCommunicationMethodName() {
  412. if (push != null) {
  413. return "Push (" + push.getTransportType() + ")";
  414. } else {
  415. return "XHR";
  416. }
  417. }
  418. private CommunicationProblemHandler getCommunicationProblemHandler() {
  419. return connection.getCommunicationProblemHandler();
  420. }
  421. private ServerMessageHandler getServerMessageHandler() {
  422. return connection.getServerMessageHandler();
  423. }
  424. private VLoadingIndicator getLoadingIndicator() {
  425. return connection.getLoadingIndicator();
  426. }
  427. }