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