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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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.Map;
  21. import java.util.Map.Entry;
  22. import java.util.Set;
  23. import java.util.logging.Logger;
  24. import com.google.gwt.core.client.JavaScriptObject;
  25. import com.google.gwt.core.client.JsArray;
  26. import com.google.gwt.dom.client.Element;
  27. import com.vaadin.client.communication.JavaScriptMethodInvocation;
  28. import com.vaadin.client.communication.ServerRpcQueue;
  29. import com.vaadin.client.communication.StateChangeEvent;
  30. import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
  31. import com.vaadin.client.ui.layout.ElementResizeEvent;
  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<String, JavaScriptObject>();
  42. private final Map<String, Set<String>> rpcMethods = new HashMap<String, Set<String>>();
  43. private final Map<Element, Map<JavaScriptObject, ElementResizeListener>> resizeListeners = new HashMap<Element, Map<JavaScriptObject, ElementResizeListener>>();
  44. private JavaScriptObject connectorWrapper;
  45. private int tag;
  46. private String initFunctionName;
  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<String>();
  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. ApplicationConfiguration conf = connector.getConnection()
  131. .getConfiguration();
  132. ArrayList<String> attemptedNames = new ArrayList<String>();
  133. Integer tag = Integer.valueOf(this.tag);
  134. while (tag != null) {
  135. String serverSideClassName = conf.getServerSideClassNameForTag(tag);
  136. String initFunctionName = serverSideClassName.replaceAll("\\.",
  137. "_");
  138. if (tryInitJs(initFunctionName, getConnectorWrapper())) {
  139. getLogger().info("JavaScript connector initialized using "
  140. + initFunctionName);
  141. this.initFunctionName = initFunctionName;
  142. return true;
  143. } else {
  144. getLogger().warning("No JavaScript function " + initFunctionName
  145. + " found");
  146. attemptedNames.add(initFunctionName);
  147. tag = conf.getParentTag(tag.intValue());
  148. }
  149. }
  150. getLogger().info("No JavaScript init for connector found");
  151. showInitProblem(attemptedNames);
  152. return false;
  153. }
  154. protected void showInitProblem(ArrayList<String> attemptedNames) {
  155. // Default does nothing
  156. }
  157. private static native boolean tryInitJs(String initFunctionName,
  158. JavaScriptObject connectorWrapper)
  159. /*-{
  160. if (typeof $wnd[initFunctionName] == 'function') {
  161. $wnd[initFunctionName].apply(connectorWrapper);
  162. return true;
  163. } else {
  164. return false;
  165. }
  166. }-*/;
  167. public JavaScriptObject getConnectorWrapper() {
  168. if (connectorWrapper == null) {
  169. connectorWrapper = createConnectorWrapper(this,
  170. connector.getConnection(), nativeState, rpcMap,
  171. connector.getConnectorId(), rpcObjects);
  172. }
  173. return connectorWrapper;
  174. }
  175. private static native JavaScriptObject createConnectorWrapper(
  176. JavaScriptConnectorHelper h, ApplicationConnection c,
  177. JavaScriptObject nativeState, JavaScriptObject registeredRpc,
  178. String connectorId, Map<String, JavaScriptObject> rpcObjects)
  179. /*-{
  180. return {
  181. 'getConnectorId': function() {
  182. return connectorId;
  183. },
  184. 'getParentId': $entry(function(connectorId) {
  185. return h.@com.vaadin.client.JavaScriptConnectorHelper::getParentId(Ljava/lang/String;)(connectorId);
  186. }),
  187. 'getState': function() {
  188. return nativeState;
  189. },
  190. 'getRpcProxy': $entry(function(iface) {
  191. if (!iface) {
  192. iface = '';
  193. }
  194. return rpcObjects.@java.util.Map::get(Ljava/lang/Object;)(iface);
  195. }),
  196. 'getElement': $entry(function(connectorId) {
  197. return h.@com.vaadin.client.JavaScriptConnectorHelper::getWidgetElement(Ljava/lang/String;)(connectorId);
  198. }),
  199. 'registerRpc': function(iface, rpcHandler) {
  200. //registerRpc(handler) -> registerRpc('', handler);
  201. if (!rpcHandler) {
  202. rpcHandler = iface;
  203. iface = '';
  204. }
  205. if (!registeredRpc[iface]) {
  206. registeredRpc[iface] = [];
  207. }
  208. registeredRpc[iface].push(rpcHandler);
  209. },
  210. 'translateVaadinUri': $entry(function(uri) {
  211. return c.@com.vaadin.client.ApplicationConnection::translateVaadinUri(Ljava/lang/String;)(uri);
  212. }),
  213. 'addResizeListener': function(element, resizeListener) {
  214. if (!element || element.nodeType != 1) throw "element must be defined";
  215. if (typeof resizeListener != "function") throw "resizeListener must be defined";
  216. $entry(h.@com.vaadin.client.JavaScriptConnectorHelper::addResizeListener(*)).call(h, element, resizeListener);
  217. },
  218. 'removeResizeListener': function(element, resizeListener) {
  219. if (!element || element.nodeType != 1) throw "element must be defined";
  220. if (typeof resizeListener != "function") throw "resizeListener must be defined";
  221. $entry(h.@com.vaadin.client.JavaScriptConnectorHelper::removeResizeListener(*)).call(h, element, resizeListener);
  222. }
  223. };
  224. }-*/;
  225. // Called from JSNI to add a listener
  226. private void addResizeListener(Element element,
  227. final JavaScriptObject callbackFunction) {
  228. Map<JavaScriptObject, ElementResizeListener> elementListeners = resizeListeners
  229. .get(element);
  230. if (elementListeners == null) {
  231. elementListeners = new HashMap<JavaScriptObject, ElementResizeListener>();
  232. resizeListeners.put(element, elementListeners);
  233. }
  234. ElementResizeListener listener = elementListeners.get(callbackFunction);
  235. if (listener == null) {
  236. LayoutManager layoutManager = LayoutManager
  237. .get(connector.getConnection());
  238. listener = new ElementResizeListener() {
  239. @Override
  240. public void onElementResize(ElementResizeEvent e) {
  241. invokeElementResizeCallback(e.getElement(),
  242. callbackFunction);
  243. }
  244. };
  245. layoutManager.addElementResizeListener(element, listener);
  246. elementListeners.put(callbackFunction, listener);
  247. }
  248. }
  249. private static native void invokeElementResizeCallback(Element element,
  250. JavaScriptObject callbackFunction)
  251. /*-{
  252. // Call with a simple event object and 'this' pointing to the global scope
  253. callbackFunction.call($wnd, {'element': element});
  254. }-*/;
  255. // Called from JSNI to remove a listener
  256. private void removeResizeListener(Element element,
  257. JavaScriptObject callbackFunction) {
  258. Map<JavaScriptObject, ElementResizeListener> listenerMap = resizeListeners
  259. .get(element);
  260. if (listenerMap == null) {
  261. return;
  262. }
  263. ElementResizeListener listener = listenerMap.remove(callbackFunction);
  264. if (listener != null) {
  265. LayoutManager.get(connector.getConnection())
  266. .removeElementResizeListener(element, listener);
  267. if (listenerMap.isEmpty()) {
  268. resizeListeners.remove(element);
  269. }
  270. }
  271. }
  272. private native void attachRpcMethod(JavaScriptObject rpc, String iface,
  273. String method)
  274. /*-{
  275. var self = this;
  276. rpc[method] = $entry(function() {
  277. self.@com.vaadin.client.JavaScriptConnectorHelper::fireRpc(Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(iface, method, arguments);
  278. });
  279. }-*/;
  280. private String getParentId(String connectorId) {
  281. ServerConnector target = getConnector(connectorId);
  282. if (target == null) {
  283. return null;
  284. }
  285. ServerConnector parent = target.getParent();
  286. if (parent == null) {
  287. return null;
  288. } else {
  289. return parent.getConnectorId();
  290. }
  291. }
  292. private Element getWidgetElement(String connectorId) {
  293. ServerConnector target = getConnector(connectorId);
  294. if (target instanceof ComponentConnector) {
  295. return ((ComponentConnector) target).getWidget().getElement();
  296. } else {
  297. return null;
  298. }
  299. }
  300. private ServerConnector getConnector(String connectorId) {
  301. if (connectorId == null || connectorId.length() == 0) {
  302. return connector;
  303. }
  304. return ConnectorMap.get(connector.getConnection())
  305. .getConnector(connectorId);
  306. }
  307. private void fireRpc(String iface, String method,
  308. JsArray<JavaScriptObject> arguments) {
  309. if (iface == null) {
  310. iface = findWildcardInterface(method);
  311. }
  312. JsonArray argumentsArray = Util.jso2json(arguments);
  313. Object[] parameters = new Object[arguments.length()];
  314. for (int i = 0; i < parameters.length; i++) {
  315. parameters[i] = argumentsArray.get(i);
  316. }
  317. ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection());
  318. rpcQueue.add(new JavaScriptMethodInvocation(connector.getConnectorId(),
  319. iface, method, parameters), false);
  320. rpcQueue.flush();
  321. }
  322. private String findWildcardInterface(String method) {
  323. Set<String> interfaces = rpcMethods.get(method);
  324. if (interfaces.size() == 1) {
  325. return interfaces.iterator().next();
  326. } else {
  327. // TODO Resolve conflicts using argument count and types
  328. String interfaceList = "";
  329. for (String iface : interfaces) {
  330. if (interfaceList.length() != 0) {
  331. interfaceList += ", ";
  332. }
  333. interfaceList += getJsInterfaceName(iface);
  334. }
  335. throw new IllegalStateException("Can not call method " + method
  336. + " for wildcard rpc proxy because the function is defined for multiple rpc interfaces: "
  337. + interfaceList
  338. + ". Retrieve a rpc proxy for a specific interface using getRpcProxy(interfaceName) to use the function.");
  339. }
  340. }
  341. private void fireCallback(String name,
  342. JsArray<JavaScriptObject> arguments) {
  343. MethodInvocation invocation = new JavaScriptMethodInvocation(
  344. connector.getConnectorId(),
  345. "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call",
  346. new Object[] { name, arguments });
  347. ServerRpcQueue rpcQueue = ServerRpcQueue.get(connector.getConnection());
  348. rpcQueue.add(invocation, false);
  349. rpcQueue.flush();
  350. }
  351. public void setNativeState(JavaScriptObject state) {
  352. updateNativeState(nativeState, state);
  353. }
  354. private static native void updateNativeState(JavaScriptObject state,
  355. JavaScriptObject input)
  356. /*-{
  357. // Copy all fields to existing state object
  358. for(var key in state) {
  359. if (state.hasOwnProperty(key)) {
  360. delete state[key];
  361. }
  362. }
  363. for(var key in input) {
  364. if (input.hasOwnProperty(key)) {
  365. state[key] = input[key];
  366. }
  367. }
  368. }-*/;
  369. public Object[] decodeRpcParameters(JsonArray parametersJson) {
  370. return new Object[] { Util.json2jso(parametersJson) };
  371. }
  372. public void setTag(int tag) {
  373. this.tag = tag;
  374. }
  375. public void invokeJsRpc(MethodInvocation invocation,
  376. JsonArray parametersJson) {
  377. String iface = invocation.getInterfaceName();
  378. String method = invocation.getMethodName();
  379. if ("com.vaadin.ui.JavaScript$JavaScriptCallbackRpc".equals(iface)
  380. && "call".equals(method)) {
  381. String callbackName = parametersJson.getString(0);
  382. JavaScriptObject arguments = Util.json2jso(parametersJson.get(1));
  383. invokeCallback(getConnectorWrapper(), callbackName, arguments);
  384. } else {
  385. JavaScriptObject arguments = Util.json2jso(parametersJson);
  386. invokeJsRpc(rpcMap, iface, method, arguments);
  387. // Also invoke wildcard interface
  388. invokeJsRpc(rpcMap, "", method, arguments);
  389. }
  390. }
  391. private static native void invokeCallback(JavaScriptObject connector,
  392. String name, JavaScriptObject arguments)
  393. /*-{
  394. connector[name].apply(connector, arguments);
  395. }-*/;
  396. private static native void invokeJsRpc(JavaScriptObject rpcMap,
  397. String interfaceName, String methodName,
  398. JavaScriptObject parameters)
  399. /*-{
  400. var targets = rpcMap[interfaceName];
  401. if (!targets) {
  402. return;
  403. }
  404. for(var i = 0; i < targets.length; i++) {
  405. var target = targets[i];
  406. target[methodName].apply(target, parameters);
  407. }
  408. }-*/;
  409. private static native void ensureCallback(JavaScriptConnectorHelper h,
  410. JavaScriptObject connector, String name)
  411. /*-{
  412. connector[name] = $entry(function() {
  413. var args = Array.prototype.slice.call(arguments, 0);
  414. h.@com.vaadin.client.JavaScriptConnectorHelper::fireCallback(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args);
  415. });
  416. }-*/;
  417. private JavaScriptConnectorState getConnectorState() {
  418. return (JavaScriptConnectorState) connector.getState();
  419. }
  420. public void onUnregister() {
  421. invokeIfPresent(connectorWrapper, "onUnregister");
  422. if (!resizeListeners.isEmpty()) {
  423. LayoutManager layoutManager = LayoutManager
  424. .get(connector.getConnection());
  425. for (Entry<Element, Map<JavaScriptObject, ElementResizeListener>> entry : resizeListeners
  426. .entrySet()) {
  427. Element element = entry.getKey();
  428. for (ElementResizeListener listener : entry.getValue()
  429. .values()) {
  430. layoutManager.removeElementResizeListener(element,
  431. listener);
  432. }
  433. }
  434. resizeListeners.clear();
  435. }
  436. }
  437. private static native void invokeIfPresent(
  438. JavaScriptObject connectorWrapper, String functionName)
  439. /*-{
  440. if (typeof connectorWrapper[functionName] == 'function') {
  441. connectorWrapper[functionName].apply(connectorWrapper, arguments);
  442. }
  443. }-*/;
  444. public String getInitFunctionName() {
  445. return initFunctionName;
  446. }
  447. private static Logger getLogger() {
  448. return Logger.getLogger(JavaScriptConnectorHelper.class.getName());
  449. }
  450. }