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

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