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.

MessageSender.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /*
  2. * Copyright 2000-2018 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.logging.Logger;
  18. import com.google.gwt.core.client.GWT;
  19. import com.google.gwt.core.client.Scheduler;
  20. import com.vaadin.client.ApplicationConfiguration;
  21. import com.vaadin.client.ApplicationConnection;
  22. import com.vaadin.client.ApplicationConnection.RequestStartingEvent;
  23. import com.vaadin.client.ApplicationConnection.ResponseHandlingEndedEvent;
  24. import com.vaadin.client.Util;
  25. import com.vaadin.client.VLoadingIndicator;
  26. import com.vaadin.shared.ApplicationConstants;
  27. import com.vaadin.shared.Version;
  28. import com.vaadin.shared.ui.ui.UIState.PushConfigurationState;
  29. import elemental.json.Json;
  30. import elemental.json.JsonArray;
  31. import elemental.json.JsonObject;
  32. import elemental.json.JsonValue;
  33. /**
  34. * MessageSender is responsible for sending messages to the server.
  35. * <p>
  36. * Internally uses {@link XhrConnection} and/or {@link PushConnection} for
  37. * delivering messages, depending on the application configuration.
  38. *
  39. * @since 7.6
  40. * @author Vaadin Ltd
  41. */
  42. public class MessageSender {
  43. private ApplicationConnection connection;
  44. private boolean hasActiveRequest = false;
  45. private boolean resynchronizeRequested = false;
  46. /**
  47. * Counter for the messages send to the server. First sent message has id 0.
  48. */
  49. private int clientToServerMessageId = 0;
  50. private XhrConnection xhrConnection;
  51. private PushConnection push;
  52. public MessageSender() {
  53. xhrConnection = GWT.create(XhrConnection.class);
  54. }
  55. /**
  56. * Sets the application connection this instance is connected to. Called
  57. * internally by the framework.
  58. *
  59. * @param connection
  60. * the application connection this instance is connected to
  61. */
  62. public void setConnection(ApplicationConnection connection) {
  63. this.connection = connection;
  64. xhrConnection.setConnection(connection);
  65. }
  66. private static Logger getLogger() {
  67. return Logger.getLogger(MessageSender.class.getName());
  68. }
  69. public void sendInvocationsToServer() {
  70. if (!connection.isApplicationRunning()) {
  71. getLogger().warning(
  72. "Trying to send RPC from not yet started or stopped application");
  73. return;
  74. }
  75. if (hasActiveRequest() || (push != null && !push.isActive())) {
  76. // There is an active request or push is enabled but not active
  77. // -> send when current request completes or push becomes active
  78. } else {
  79. doSendInvocationsToServer();
  80. }
  81. }
  82. /**
  83. * Sends all pending method invocations (server RPC and legacy variable
  84. * changes) to the server.
  85. *
  86. */
  87. private void doSendInvocationsToServer() {
  88. ServerRpcQueue serverRpcQueue = getServerRpcQueue();
  89. if (serverRpcQueue.isEmpty() && !resynchronizeRequested) {
  90. return;
  91. }
  92. if (ApplicationConfiguration.isDebugMode()) {
  93. Util.logMethodInvocations(connection, serverRpcQueue.getAll());
  94. }
  95. boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator();
  96. JsonArray reqJson = serverRpcQueue.toJson();
  97. serverRpcQueue.clear();
  98. if (reqJson.length() == 0 && !resynchronizeRequested) {
  99. // Nothing to send, all invocations were filtered out (for
  100. // non-existing connectors)
  101. getLogger().warning(
  102. "All RPCs filtered out, not sending anything to the server");
  103. return;
  104. }
  105. JsonObject extraJson = Json.createObject();
  106. if (!connection.getConfiguration().isWidgetsetVersionSent()) {
  107. extraJson.put(ApplicationConstants.WIDGETSET_VERSION_ID,
  108. Version.getFullVersion());
  109. connection.getConfiguration().setWidgetsetVersionSent();
  110. }
  111. if (resynchronizeRequested) {
  112. getLogger().info("Resynchronizing from server");
  113. extraJson.put(ApplicationConstants.RESYNCHRONIZE_ID, true);
  114. resynchronizeRequested = false;
  115. }
  116. if (showLoadingIndicator) {
  117. connection.getLoadingIndicator().trigger();
  118. }
  119. send(reqJson, extraJson);
  120. }
  121. private ServerRpcQueue getServerRpcQueue() {
  122. return connection.getServerRpcQueue();
  123. }
  124. /**
  125. * Makes an UIDL request to the server.
  126. *
  127. * @param reqInvocations
  128. * Data containing RPC invocations and all related information.
  129. * @param extraJson
  130. * The JsonObject whose parameters are added to the payload
  131. */
  132. protected void send(final JsonArray reqInvocations,
  133. final JsonObject extraJson) {
  134. startRequest();
  135. JsonObject payload = Json.createObject();
  136. String csrfToken = getMessageHandler().getCsrfToken();
  137. if (!csrfToken.equals(ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) {
  138. payload.put(ApplicationConstants.CSRF_TOKEN, csrfToken);
  139. }
  140. payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations);
  141. payload.put(ApplicationConstants.SERVER_SYNC_ID,
  142. getMessageHandler().getLastSeenServerSyncId());
  143. payload.put(ApplicationConstants.CLIENT_TO_SERVER_ID,
  144. clientToServerMessageId++);
  145. if (extraJson != null) {
  146. for (String key : extraJson.keys()) {
  147. JsonValue value = extraJson.get(key);
  148. payload.put(key, value);
  149. }
  150. }
  151. send(payload);
  152. }
  153. /**
  154. * Sends an asynchronous or synchronous UIDL request to the server using the
  155. * given URI.
  156. *
  157. * @param payload
  158. * The contents of the request to send
  159. */
  160. public void send(final JsonObject payload) {
  161. if (push != null && push.isBidirectional()) {
  162. push.push(payload);
  163. } else {
  164. xhrConnection.send(payload);
  165. }
  166. }
  167. /**
  168. * Sets the status for the push connection.
  169. *
  170. * @param enabled
  171. * <code>true</code> to enable the push connection;
  172. * <code>false</code> to disable the push connection.
  173. */
  174. public void setPushEnabled(boolean enabled) {
  175. final PushConfigurationState pushState = connection.getUIConnector()
  176. .getState().pushConfiguration;
  177. if (enabled && push == null) {
  178. push = GWT.create(PushConnection.class);
  179. push.init(connection, pushState);
  180. } else if (!enabled && push != null && push.isActive()) {
  181. push.disconnect(() -> {
  182. push = null;
  183. /*
  184. * If push has been enabled again while we were waiting for the
  185. * old connection to disconnect, now is the right time to open a
  186. * new connection
  187. */
  188. if (pushState.mode.isEnabled()) {
  189. setPushEnabled(true);
  190. }
  191. /*
  192. * Send anything that was enqueued while we waited for the
  193. * connection to close
  194. */
  195. if (getServerRpcQueue().isFlushPending()) {
  196. getServerRpcQueue().flush();
  197. }
  198. });
  199. }
  200. }
  201. public void startRequest() {
  202. if (hasActiveRequest) {
  203. getLogger().severe(
  204. "Trying to start a new request while another is active");
  205. }
  206. hasActiveRequest = true;
  207. connection.fireEvent(new RequestStartingEvent(connection));
  208. }
  209. public void endRequest() {
  210. if (!hasActiveRequest) {
  211. getLogger().severe("No active request");
  212. }
  213. // After sendInvocationsToServer() there may be a new active
  214. // request, so we must set hasActiveRequest to false before, not after,
  215. // the call.
  216. hasActiveRequest = false;
  217. if (connection.isApplicationRunning()) {
  218. if (getServerRpcQueue().isFlushPending()
  219. || resynchronizeRequested) {
  220. sendInvocationsToServer();
  221. }
  222. runPostRequestHooks(connection.getConfiguration().getRootPanelId());
  223. }
  224. // deferring to avoid flickering
  225. Scheduler.get().scheduleDeferred(() -> {
  226. if (!connection.isApplicationRunning() || !(hasActiveRequest()
  227. || getServerRpcQueue().isFlushPending())) {
  228. getLoadingIndicator().hide();
  229. // If on Liferay and session expiration management is in
  230. // use, extend session duration on each request.
  231. // Doing it here rather than before the request to improve
  232. // responsiveness.
  233. // Postponed until the end of the next request if other
  234. // requests still pending.
  235. extendLiferaySession();
  236. }
  237. });
  238. connection.fireEvent(new ResponseHandlingEndedEvent(connection));
  239. }
  240. /**
  241. * Runs possibly registered client side post request hooks. This is expected
  242. * to be run after each uidl request made by Vaadin application.
  243. *
  244. * @param appId
  245. */
  246. public static native void runPostRequestHooks(String appId)
  247. /*-{
  248. if ($wnd.vaadin.postRequestHooks) {
  249. for ( var hook in $wnd.vaadin.postRequestHooks) {
  250. if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") {
  251. try {
  252. $wnd.vaadin.postRequestHooks[hook](appId);
  253. } catch (e) {
  254. }
  255. }
  256. }
  257. }
  258. }-*/;
  259. /**
  260. * If on Liferay and logged in, ask the client side session management
  261. * JavaScript to extend the session duration.
  262. *
  263. * Otherwise, Liferay client side JavaScript will explicitly expire the
  264. * session even though the server side considers the session to be active.
  265. * See ticket #8305 for more information.
  266. */
  267. public static native void extendLiferaySession()
  268. /*-{
  269. if ($wnd.Liferay && $wnd.Liferay.Session) {
  270. $wnd.Liferay.Session.extend();
  271. // if the extend banner is visible, hide it
  272. if ($wnd.Liferay.Session.banner) {
  273. $wnd.Liferay.Session.banner.remove();
  274. }
  275. }
  276. }-*/;
  277. /**
  278. * Indicates whether or not there are currently active UIDL requests. Used
  279. * internally to sequence requests properly, seldom needed in Widgets.
  280. *
  281. * @return true if there are active requests
  282. */
  283. public boolean hasActiveRequest() {
  284. return hasActiveRequest;
  285. }
  286. /**
  287. * Returns a human readable string representation of the method used to
  288. * communicate with the server.
  289. *
  290. * @return A string representation of the current transport type
  291. */
  292. public String getCommunicationMethodName() {
  293. String clientToServer = "XHR";
  294. String serverToClient = "-";
  295. if (push != null) {
  296. serverToClient = push.getTransportType();
  297. if (push.isBidirectional()) {
  298. clientToServer = serverToClient;
  299. }
  300. }
  301. return "Client to server: " + clientToServer + ", "
  302. + "server to client: " + serverToClient;
  303. }
  304. private ConnectionStateHandler getConnectionStateHandler() {
  305. return connection.getConnectionStateHandler();
  306. }
  307. private MessageHandler getMessageHandler() {
  308. return connection.getMessageHandler();
  309. }
  310. private VLoadingIndicator getLoadingIndicator() {
  311. return connection.getLoadingIndicator();
  312. }
  313. /**
  314. * Resynchronize the client side, i.e. reload all component hierarchy and
  315. * state from the server
  316. */
  317. public void resynchronize() {
  318. getMessageHandler().onResynchronize();
  319. getLogger().info("Resynchronize from server requested");
  320. resynchronizeRequested = true;
  321. sendInvocationsToServer();
  322. }
  323. /**
  324. * Used internally to update what the server expects.
  325. *
  326. * @param nextExpectedId
  327. * the new client id to set
  328. * @param force
  329. * true if the id must be updated, false otherwise
  330. */
  331. public void setClientToServerMessageId(int nextExpectedId, boolean force) {
  332. if (nextExpectedId == clientToServerMessageId) {
  333. // No op as everything matches they way it should
  334. return;
  335. }
  336. if (force) {
  337. getLogger().info(
  338. "Forced update of clientId to " + clientToServerMessageId);
  339. clientToServerMessageId = nextExpectedId;
  340. return;
  341. }
  342. if (nextExpectedId > clientToServerMessageId) {
  343. if (clientToServerMessageId == 0) {
  344. // We have never sent a message to the server, so likely the
  345. // server knows better (typical case is that we refreshed a
  346. // @PreserveOnRefresh UI)
  347. getLogger().info("Updating client-to-server id to "
  348. + nextExpectedId + " based on server");
  349. } else {
  350. getLogger().warning(
  351. "Server expects next client-to-server id to be "
  352. + nextExpectedId + " but we were going to use "
  353. + clientToServerMessageId + ". Will use "
  354. + nextExpectedId + ".");
  355. }
  356. clientToServerMessageId = nextExpectedId;
  357. } else {
  358. // Server has not yet seen all our messages
  359. // Do nothing as they will arrive eventually
  360. }
  361. }
  362. }