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 19KB

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