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.

JavaScriptConnectorHelper.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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;
  17. import java.util.ArrayList;
  18. import java.util.HashMap;
  19. import java.util.HashSet;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Map.Entry;
  23. import java.util.Set;
  24. import java.util.logging.Logger;
  25. import com.google.gwt.core.client.JavaScriptObject;
  26. import com.google.gwt.core.client.JsArray;
  27. import com.google.gwt.dom.client.Element;
  28. import com.vaadin.client.communication.JavaScriptMethodInvocation;
  29. import com.vaadin.client.communication.ServerRpcQueue;
  30. import com.vaadin.client.ui.layout.ElementResizeListener;
  31. import com.vaadin.shared.JavaScriptConnectorState;
  32. import com.vaadin.shared.communication.MethodInvocation;
  33. import elemental.json.JsonArray;
  34. public class JavaScriptConnectorHelper {
  35. private final ServerConnector connector;
  36. private final JavaScriptObject nativeState = JavaScriptObject
  37. .createObject();
  38. private final JavaScriptObject rpcMap = JavaScriptObject.createObject();
  39. private final Map<String, JavaScriptObject> rpcObjects = new HashMap<>();
  40. private final Map<String, Set<String>> rpcMethods = new HashMap<>();
  41. private final Map<Element, Map<JavaScriptObject, ElementResizeListener>> resizeListeners = new HashMap<>();
  42. private JavaScriptObject connectorWrapper;
  43. private String initFunctionName;
  44. private String tagName;
  45. public JavaScriptConnectorHelper(ServerConnector connector) {
  46. this.connector = connector;
  47. // Wildcard rpc object
  48. rpcObjects.put("", JavaScriptObject.createObject());
  49. }
  50. /**
  51. * The id of the previous response for which state changes have been
  52. * processed. If this is the same as the
  53. * {@link ApplicationConnection#getLastSeenServerSyncId()}, it means that
  54. * the state change has already been handled and should not be done again.
  55. */
  56. private int processedResponseId = -1;
  57. public void init() {
  58. connector.addStateChangeHandler(event -> processStateChanges());
  59. }
  60. /**
  61. * Makes sure the javascript part of the connector has been initialized. The
  62. * javascript is usually initalized the first time a state change event is
  63. * received, but it might in some cases be necessary to make this happen
  64. * earlier.
  65. *
  66. * @since 7.4.0
  67. */
  68. public void ensureJavascriptInited() {
  69. if (initFunctionName == null) {
  70. processStateChanges();
  71. }
  72. }
  73. private void processStateChanges() {
  74. int lastResponseId = connector.getConnection()
  75. .getLastSeenServerSyncId();
  76. if (processedResponseId == lastResponseId) {
  77. return;
  78. }
  79. processedResponseId = lastResponseId;
  80. JavaScriptObject wrapper = getConnectorWrapper();
  81. JavaScriptConnectorState state = getConnectorState();
  82. for (String callback : state.getCallbackNames()) {
  83. ensureCallback(JavaScriptConnectorHelper.this, wrapper, callback);
  84. }
  85. for (Entry<String, Set<String>> entry : state.getRpcInterfaces()
  86. .entrySet()) {
  87. String rpcName = entry.getKey();
  88. String jsName = getJsInterfaceName(rpcName);
  89. if (!rpcObjects.containsKey(jsName)) {
  90. Set<String> methods = entry.getValue();
  91. rpcObjects.put(jsName, createRpcObject(rpcName, methods));
  92. // Init all methods for wildcard rpc
  93. for (String method : methods) {
  94. JavaScriptObject wildcardRpcObject = rpcObjects.get("");
  95. Set<String> interfaces = rpcMethods.get(method);
  96. if (interfaces == null) {
  97. interfaces = new HashSet<>();
  98. rpcMethods.put(method, interfaces);
  99. attachRpcMethod(wildcardRpcObject, null, method);
  100. }
  101. interfaces.add(rpcName);
  102. }
  103. }
  104. }
  105. // Init after setting up callbacks & rpc
  106. if (initFunctionName == null) {
  107. initJavaScript();
  108. }
  109. invokeIfPresent(wrapper, "onStateChange");
  110. }
  111. private static String getJsInterfaceName(String rpcName) {
  112. return rpcName.replace('$', '.');
  113. }
  114. protected JavaScriptObject createRpcObject(String iface,
  115. Set<String> methods) {
  116. JavaScriptObject object = JavaScriptObject.createObject();
  117. for (String method : methods) {
  118. attachRpcMethod(object, iface, method);
  119. }
  120. return object;
  121. }
  122. protected boolean initJavaScript() {
  123. List<String> initFunctionNames = getPotentialInitFunctionNames();
  124. for (String initFunctionName : initFunctionNames) {
  125. if (tryInitJs(initFunctionName, getConnectorWrapper())) {
  126. getLogger().info("JavaScript connector initialized using "
  127. + initFunctionName);
  128. this.initFunctionName = initFunctionName;
  129. return true;
  130. } else {
  131. getLogger().warning("No JavaScript function " + initFunctionName
  132. + " found");
  133. }
  134. }
  135. getLogger().info("No JavaScript init for connector found");
  136. showInitProblem(initFunctionNames);
  137. return false;
  138. }
  139. protected void showInitProblem(List<String> attemptedNames) {
  140. // Default does nothing
  141. }
  142. private static native boolean tryInitJs(String initFunctionName,
  143. JavaScriptObject connectorWrapper)
  144. /*-{
  145. if (typeof $wnd[initFunctionName] == 'function') {
  146. $wnd[initFunctionName].apply(connectorWrapper);
  147. return true;
  148. } else {
  149. return false;
  150. }
  151. }-*/;
  152. public JavaScriptObject getConnectorWrapper() {
  153. if (connectorWrapper == null) {
  154. connectorWrapper = createConnectorWrapper(this,
  155. connector.getConnection(), nativeState, rpcMap,
  156. connector.getConnectorId(), rpcObjects);
  157. }
  158. return connectorWrapper;
  159. }
  160. private static native JavaScriptObject createConnectorWrapper(
  161. JavaScriptConnectorHelper h, ApplicationConnection c,
  162. JavaScriptObject nativeState, JavaScriptObject registeredRpc,
  163. String connectorId, Map<String, JavaScriptObject> rpcObjects)
  164. /*-{
  165. return {
  166. 'getConnectorId': function() {
  167. return connectorId;
  168. },
  169. 'getParentId': $entry(function(connectorId) {
  170. return h.@com.vaadin.client.JavaScriptConnectorHelper::getParentId(Ljava/lang/String;)(connectorId);
  171. }),
  172. 'getState': function() {
  173. return nativeState;
  174. },
  175. 'getRpcProxy': $entry(function(iface) {
  176. if (!iface) {
  177. iface = '';
  178. }
  179. return rpcObjects.@java.util.Map::get(Ljava/lang/Object;)(iface);
  180. }),
  181. 'getElement': $entry(function(connectorId) {
  182. return h.@com.vaadin.client.JavaScriptConnectorHelper::getWidgetElement(Ljava/lang/String;)(connectorId);
  183. }),
  184. 'registerRpc': function(iface, rpcHandler) {
  185. //registerRpc(handler) -> registerRpc('', handler);
  186. if (!rpcHandler) {
  187. rpcHandler = iface;
  188. iface = '';
  189. }
  190. if (!registeredRpc[iface]) {
  191. registeredRpc[iface] = [];
  192. }
  193. registeredRpc[iface].push(rpcHandler);
  194. },
  195. 'translateVaadinUri': $entry(function(uri) {
  196. return c.@com.vaadin.client.ApplicationConnection::translateVaadinUri(Ljava/lang/String;)(uri);
  197. }),
  198. 'addResizeListener': function(element, resizeListener) {
  199. if (!element || element.nodeType != 1) throw "element must be defined";
  200. if (typeof resizeListener != "function") throw "resizeListener must be defined";
  201. $entry(h.@com.vaadin.client.JavaScriptConnectorHelper::addResizeListener(*)).call(h, element, resizeListener);
  202. },
  203. 'removeResizeListener': function(element, resizeListener) {
  204. if (!element || element.nodeType != 1) throw "element must be defined";
  205. if (typeof resizeListener != "function") throw "resizeListener must be defined";
  206. $entry(h.@com.vaadin.client.JavaScriptConnectorHelper::removeResizeListener(*)).call(h, element, resizeListener);
  207. }
  208. };
  209. }-*/;
  210. // Called from JSNI to add a listener
  211. private void addResizeListener(Element element,
  212. final JavaScriptObject callbackFunction) {
  213. Map<JavaScriptObject, ElementResizeListener> elementListeners = resizeListeners
  214. .get(element);
  215. if (elementListeners == null) {
  216. elementListeners = new HashMap<>();
  217. resizeListeners.put(element, elementListeners);
  218. }
  219. ElementResizeListener listener = elementListeners.get(callbackFunction);
  220. if (listener == null) {
  221. LayoutManager layoutManager = LayoutManager
  222. .get(connector.getConnection());
  223. listener = event -> invokeElementResizeCallback(event.getElement(),
  224. callbackFunction);
  225. layoutManager.addElementResizeListener(element, listener);
  226. elementListeners.put(callbackFunction, listener);
  227. }
  228. }
  229. private static native void invokeElementResizeCallback(Element element,
  230. JavaScriptObject callbackFunction)
  231. /*-{
  232. // Call with a simple event object and 'this' pointing to the global scope
  233. callbackFunction.call($wnd, {'element': element});
  234. }-*/;
  235. // Called from JSNI to remove a listener
  236. private void removeResizeListener(Element element,
  237. JavaScriptObject callbackFunction) {
  238. Map<JavaScriptObject, ElementResizeListener> listenerMap = resizeListeners
  239. .get(element);
  240. if (listenerMap == null) {
  241. return;
  242. }
  243. ElementResizeListener listener = listenerMap.remove(callbackFunction);
  244. if (listener != null) {
  245. LayoutManager.get(connector.getConnection())
  246. .removeElementResizeListener(element, listener);
  247. if (listenerMap.isEmpty()) {
  248. resizeListeners.remove(element);
  249. }
  250. }
  251. }
  252. private native void attachRpcMethod(JavaScriptObject rpc, String iface,
  253. String method)
  254. /*-{
  255. var self = this;
  256. rpc[method] = $entry(function() {
  257. self.@com.vaadin.client.JavaScriptConnectorHelper::fireRpc(Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(iface, method, arguments);
  258. });
  259. }-*/;
  260. private String getParentId(String connectorId) {
  261. ServerConnector target = getConnector(connectorId);
  262. if (target == null) {
  263. return null;
  264. }
  265. ServerConnector parent = target.getParent();
  266. if (parent == null) {
  267. return null;
  268. } else {
  269. return parent.getConnectorId();
  270. }
  271. }
  272. private Element getWidgetElement(String connectorId) {
  273. ServerConnector target = getConnector(connectorId);
  274. if (target instanceof ComponentConnector) {
  275. return ((ComponentConnector) target).getWidget().getElement();
  276. } else {
  277. return null;
  278. }
  279. }
  280. private ServerConnector getConnector(String connectorId) {
  281. if (connectorId == null || connectorId.isEmpty()) {
  282. return connector;
  283. }
  284. return ConnectorMap.get(connector.getConnection())
  285. .getConnector(connectorId);
  286. }
  287. private void fireRpc(String iface, String method,
  288. JsArray<JavaScriptObject> arguments) {
  289. if (iface == null) {
  290. iface = findWildcardInterface(method);
  291. }
  292. JsonArray argumentsArray = Util.jso2json(arguments);
  293. Object[] parameters = new Object[arguments.length()];
  294. for (int i = 0; i < parameters.length; i++) {
  295. parameters[i] = argumentsArray.get(i);
  296. }
  297. ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection());
  298. rpcQueue.add(new JavaScriptMethodInvocation(connector.getConnectorId(),
  299. iface, method, parameters), false);
  300. rpcQueue.flush();
  301. }
  302. private String findWildcardInterface(String method) {
  303. Set<String> interfaces = rpcMethods.get(method);
  304. if (interfaces.size() == 1) {
  305. return interfaces.iterator().next();
  306. } else {
  307. // TODO Resolve conflicts using argument count and types
  308. String interfaceList = "";
  309. for (String iface : interfaces) {
  310. if (!interfaceList.isEmpty()) {
  311. interfaceList += ", ";
  312. }
  313. interfaceList += getJsInterfaceName(iface);
  314. }
  315. throw new IllegalStateException("Can not call method " + method
  316. + " for wildcard rpc proxy because the function is defined for multiple rpc interfaces: "
  317. + interfaceList
  318. + ". Retrieve a rpc proxy for a specific interface using getRpcProxy(interfaceName) to use the function.");
  319. }
  320. }
  321. private void fireCallback(String name,
  322. JsArray<JavaScriptObject> arguments) {
  323. MethodInvocation invocation = new JavaScriptMethodInvocation(
  324. connector.getConnectorId(),
  325. "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call",
  326. new Object[] { name, arguments });
  327. ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection());
  328. rpcQueue.add(invocation, false);
  329. rpcQueue.flush();
  330. }
  331. public void setNativeState(JavaScriptObject state) {
  332. updateNativeState(nativeState, state);
  333. }
  334. private static native void updateNativeState(JavaScriptObject state,
  335. JavaScriptObject input)
  336. /*-{
  337. // Copy all fields to existing state object
  338. for (var key in input) {
  339. if (input.hasOwnProperty(key)) {
  340. state[key] = input[key];
  341. }
  342. }
  343. }-*/;
  344. public Object[] decodeRpcParameters(JsonArray parametersJson) {
  345. return new Object[] { Util.json2jso(parametersJson) };
  346. }
  347. public void invokeJsRpc(MethodInvocation invocation,
  348. JsonArray parametersJson) {
  349. String iface = invocation.getInterfaceName();
  350. String method = invocation.getMethodName();
  351. if ("com.vaadin.ui.JavaScript$JavaScriptCallbackRpc".equals(iface)
  352. && "call".equals(method)) {
  353. String callbackName = parametersJson.getString(0);
  354. JavaScriptObject arguments = Util.json2jso(parametersJson.get(1));
  355. invokeCallback(getConnectorWrapper(), callbackName, arguments);
  356. } else {
  357. JavaScriptObject arguments = Util.json2jso(parametersJson);
  358. invokeJsRpc(rpcMap, iface, method, arguments);
  359. // Also invoke wildcard interface
  360. invokeJsRpc(rpcMap, "", method, arguments);
  361. }
  362. }
  363. private static native void invokeCallback(JavaScriptObject connector,
  364. String name, JavaScriptObject arguments)
  365. /*-{
  366. connector[name].apply(connector, arguments);
  367. }-*/;
  368. private static native void invokeJsRpc(JavaScriptObject rpcMap,
  369. String interfaceName, String methodName,
  370. JavaScriptObject parameters)
  371. /*-{
  372. var targets = rpcMap[interfaceName];
  373. if (!targets) {
  374. return;
  375. }
  376. for (var i = 0; i < targets.length; i++) {
  377. var target = targets[i];
  378. target[methodName].apply(target, parameters);
  379. }
  380. }-*/;
  381. private static native void ensureCallback(JavaScriptConnectorHelper h,
  382. JavaScriptObject connector, String name)
  383. /*-{
  384. connector[name] = $entry(function() {
  385. var args = Array.prototype.slice.call(arguments, 0);
  386. h.@com.vaadin.client.JavaScriptConnectorHelper::fireCallback(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args);
  387. });
  388. }-*/;
  389. private JavaScriptConnectorState getConnectorState() {
  390. return (JavaScriptConnectorState) connector.getState();
  391. }
  392. public void onUnregister() {
  393. invokeIfPresent(connectorWrapper, "onUnregister");
  394. if (!resizeListeners.isEmpty()) {
  395. LayoutManager layoutManager = LayoutManager
  396. .get(connector.getConnection());
  397. for (Entry<Element, Map<JavaScriptObject, ElementResizeListener>> entry : resizeListeners
  398. .entrySet()) {
  399. Element element = entry.getKey();
  400. for (ElementResizeListener listener : entry.getValue()
  401. .values()) {
  402. layoutManager.removeElementResizeListener(element,
  403. listener);
  404. }
  405. }
  406. resizeListeners.clear();
  407. }
  408. }
  409. private static native void invokeIfPresent(
  410. JavaScriptObject connectorWrapper, String functionName)
  411. /*-{
  412. if (typeof connectorWrapper[functionName] == 'function') {
  413. connectorWrapper[functionName].apply(connectorWrapper, arguments);
  414. }
  415. }-*/;
  416. public String getInitFunctionName() {
  417. return initFunctionName;
  418. }
  419. private List<String> getPotentialInitFunctionNames() {
  420. ApplicationConfiguration conf = connector.getConnection()
  421. .getConfiguration();
  422. List<String> initFunctionNames = new ArrayList<String>();
  423. Integer tag = Integer.valueOf(connector.getTag());
  424. while (tag != null) {
  425. String initFunctionName = conf.getServerSideClassNameForTag(tag);
  426. initFunctionName = initFunctionName.replaceAll("\\.", "_");
  427. initFunctionNames.add(initFunctionName);
  428. tag = conf.getParentTag(tag);
  429. }
  430. return initFunctionNames;
  431. }
  432. public String getTagName() {
  433. if (tagName != null) {
  434. return tagName;
  435. }
  436. for (String initFunctionName : getPotentialInitFunctionNames()) {
  437. tagName = getTagJs(initFunctionName);
  438. if (tagName != null) {
  439. return tagName;
  440. }
  441. }
  442. // No tagName found, use default
  443. tagName = "div";
  444. return tagName;
  445. }
  446. private static native String getTagJs(String initFunctionName)
  447. /*-{
  448. if ($wnd[initFunctionName] && typeof $wnd[initFunctionName].tag == 'string') {
  449. return $wnd[initFunctionName].tag;
  450. } else {
  451. return null;
  452. }
  453. }-*/;
  454. private static Logger getLogger() {
  455. return Logger.getLogger(JavaScriptConnectorHelper.class.getName());
  456. }
  457. }