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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  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.StateChangeEvent;
  29. import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
  30. import com.vaadin.client.ui.layout.ElementResizeEvent;
  31. import com.vaadin.client.ui.layout.ElementResizeListener;
  32. import com.vaadin.shared.JavaScriptConnectorState;
  33. import com.vaadin.shared.communication.MethodInvocation;
  34. import elemental.json.JsonArray;
  35. public class JavaScriptConnectorHelper {
  36. private final ServerConnector connector;
  37. private final JavaScriptObject nativeState = JavaScriptObject
  38. .createObject();
  39. private final JavaScriptObject rpcMap = JavaScriptObject.createObject();
  40. private final Map<String, JavaScriptObject> rpcObjects = new HashMap<String, JavaScriptObject>();
  41. private final Map<String, Set<String>> rpcMethods = new HashMap<String, Set<String>>();
  42. private final Map<Element, Map<JavaScriptObject, ElementResizeListener>> resizeListeners = new HashMap<Element, Map<JavaScriptObject, ElementResizeListener>>();
  43. private JavaScriptObject connectorWrapper;
  44. private int tag;
  45. private String initFunctionName;
  46. public JavaScriptConnectorHelper(ServerConnector connector) {
  47. this.connector = connector;
  48. // Wildcard rpc object
  49. rpcObjects.put("", JavaScriptObject.createObject());
  50. }
  51. /**
  52. * The id of the previous response for which state changes have been
  53. * processed. If this is the same as the
  54. * {@link ApplicationConnection#getLastResponseId()}, it means that the
  55. * state change has already been handled and should not be done again.
  56. */
  57. private int processedResponseId = -1;
  58. public void init() {
  59. connector.addStateChangeHandler(new StateChangeHandler() {
  60. @Override
  61. public void onStateChanged(StateChangeEvent stateChangeEvent) {
  62. processStateChanges();
  63. }
  64. });
  65. }
  66. /**
  67. * Makes sure the javascript part of the connector has been initialized. The
  68. * javascript is usually initalized the first time a state change event is
  69. * received, but it might in some cases be necessary to make this happen
  70. * earlier.
  71. *
  72. * @since 7.4.0
  73. */
  74. public void ensureJavascriptInited() {
  75. if (initFunctionName == null) {
  76. processStateChanges();
  77. }
  78. }
  79. private void processStateChanges() {
  80. int lastResponseId = connector.getConnection().getLastResponseId();
  81. if (processedResponseId == lastResponseId) {
  82. return;
  83. }
  84. processedResponseId = lastResponseId;
  85. JavaScriptObject wrapper = getConnectorWrapper();
  86. JavaScriptConnectorState state = getConnectorState();
  87. for (String callback : state.getCallbackNames()) {
  88. ensureCallback(JavaScriptConnectorHelper.this, wrapper, callback);
  89. }
  90. for (Entry<String, Set<String>> entry : state.getRpcInterfaces()
  91. .entrySet()) {
  92. String rpcName = entry.getKey();
  93. String jsName = getJsInterfaceName(rpcName);
  94. if (!rpcObjects.containsKey(jsName)) {
  95. Set<String> methods = entry.getValue();
  96. rpcObjects.put(jsName, createRpcObject(rpcName, methods));
  97. // Init all methods for wildcard rpc
  98. for (String method : methods) {
  99. JavaScriptObject wildcardRpcObject = rpcObjects.get("");
  100. Set<String> interfaces = rpcMethods.get(method);
  101. if (interfaces == null) {
  102. interfaces = new HashSet<String>();
  103. rpcMethods.put(method, interfaces);
  104. attachRpcMethod(wildcardRpcObject, null, method);
  105. }
  106. interfaces.add(rpcName);
  107. }
  108. }
  109. }
  110. // Init after setting up callbacks & rpc
  111. if (initFunctionName == null) {
  112. initJavaScript();
  113. }
  114. invokeIfPresent(wrapper, "onStateChange");
  115. }
  116. private static String getJsInterfaceName(String rpcName) {
  117. return rpcName.replace('$', '.');
  118. }
  119. protected JavaScriptObject createRpcObject(String iface, Set<String> methods) {
  120. JavaScriptObject object = JavaScriptObject.createObject();
  121. for (String method : methods) {
  122. attachRpcMethod(object, iface, method);
  123. }
  124. return object;
  125. }
  126. protected boolean initJavaScript() {
  127. ApplicationConfiguration conf = connector.getConnection()
  128. .getConfiguration();
  129. ArrayList<String> attemptedNames = new ArrayList<String>();
  130. Integer tag = Integer.valueOf(this.tag);
  131. while (tag != null) {
  132. String serverSideClassName = conf.getServerSideClassNameForTag(tag);
  133. String initFunctionName = serverSideClassName
  134. .replaceAll("\\.", "_");
  135. if (tryInitJs(initFunctionName, getConnectorWrapper())) {
  136. getLogger().info(
  137. "JavaScript connector initialized using "
  138. + initFunctionName);
  139. this.initFunctionName = initFunctionName;
  140. return true;
  141. } else {
  142. getLogger()
  143. .warning(
  144. "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.get(connector
  237. .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()).getConnector(
  305. 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. connector.getConnection().addMethodInvocationToQueue(
  318. new JavaScriptMethodInvocation(connector.getConnectorId(),
  319. iface, method, parameters), false, false);
  320. }
  321. private String findWildcardInterface(String method) {
  322. Set<String> interfaces = rpcMethods.get(method);
  323. if (interfaces.size() == 1) {
  324. return interfaces.iterator().next();
  325. } else {
  326. // TODO Resolve conflicts using argument count and types
  327. String interfaceList = "";
  328. for (String iface : interfaces) {
  329. if (interfaceList.length() != 0) {
  330. interfaceList += ", ";
  331. }
  332. interfaceList += getJsInterfaceName(iface);
  333. }
  334. throw new IllegalStateException(
  335. "Can not call method "
  336. + method
  337. + " for wildcard rpc proxy because the function is defined for multiple rpc interfaces: "
  338. + interfaceList
  339. + ". Retrieve a rpc proxy for a specific interface using getRpcProxy(interfaceName) to use the function.");
  340. }
  341. }
  342. private void fireCallback(String name, JsArray<JavaScriptObject> arguments) {
  343. MethodInvocation invocation = new JavaScriptMethodInvocation(
  344. connector.getConnectorId(),
  345. "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call",
  346. new Object[] { name, arguments });
  347. connector.getConnection().addMethodInvocationToQueue(invocation, false,
  348. false);
  349. }
  350. public void setNativeState(JavaScriptObject state) {
  351. updateNativeState(nativeState, state);
  352. }
  353. private static native void updateNativeState(JavaScriptObject state,
  354. JavaScriptObject input)
  355. /*-{
  356. // Copy all fields to existing state object
  357. for(var key in state) {
  358. if (state.hasOwnProperty(key)) {
  359. delete state[key];
  360. }
  361. }
  362. for(var key in input) {
  363. if (input.hasOwnProperty(key)) {
  364. state[key] = input[key];
  365. }
  366. }
  367. }-*/;
  368. public Object[] decodeRpcParameters(JsonArray parametersJson) {
  369. return new Object[] { Util.json2jso(parametersJson) };
  370. }
  371. public void setTag(int tag) {
  372. this.tag = tag;
  373. }
  374. public void invokeJsRpc(MethodInvocation invocation,
  375. JsonArray parametersJson) {
  376. String iface = invocation.getInterfaceName();
  377. String method = invocation.getMethodName();
  378. if ("com.vaadin.ui.JavaScript$JavaScriptCallbackRpc".equals(iface)
  379. && "call".equals(method)) {
  380. String callbackName = parametersJson.getString(0);
  381. JavaScriptObject arguments = Util.json2jso(parametersJson.get(1));
  382. invokeCallback(getConnectorWrapper(), callbackName, arguments);
  383. } else {
  384. JavaScriptObject arguments = Util.json2jso(parametersJson);
  385. invokeJsRpc(rpcMap, iface, method, arguments);
  386. // Also invoke wildcard interface
  387. invokeJsRpc(rpcMap, "", method, arguments);
  388. }
  389. }
  390. private static native void invokeCallback(JavaScriptObject connector,
  391. String name, JavaScriptObject arguments)
  392. /*-{
  393. connector[name].apply(connector, arguments);
  394. }-*/;
  395. private static native void invokeJsRpc(JavaScriptObject rpcMap,
  396. String interfaceName, String methodName, JavaScriptObject parameters)
  397. /*-{
  398. var targets = rpcMap[interfaceName];
  399. if (!targets) {
  400. return;
  401. }
  402. for(var i = 0; i < targets.length; i++) {
  403. var target = targets[i];
  404. target[methodName].apply(target, parameters);
  405. }
  406. }-*/;
  407. private static native void ensureCallback(JavaScriptConnectorHelper h,
  408. JavaScriptObject connector, String name)
  409. /*-{
  410. connector[name] = $entry(function() {
  411. var args = Array.prototype.slice.call(arguments, 0);
  412. h.@com.vaadin.client.JavaScriptConnectorHelper::fireCallback(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args);
  413. });
  414. }-*/;
  415. private JavaScriptConnectorState getConnectorState() {
  416. return (JavaScriptConnectorState) connector.getState();
  417. }
  418. public void onUnregister() {
  419. invokeIfPresent(connectorWrapper, "onUnregister");
  420. if (!resizeListeners.isEmpty()) {
  421. LayoutManager layoutManager = LayoutManager.get(connector
  422. .getConnection());
  423. for (Entry<Element, Map<JavaScriptObject, ElementResizeListener>> entry : resizeListeners
  424. .entrySet()) {
  425. Element element = entry.getKey();
  426. for (ElementResizeListener listener : entry.getValue().values()) {
  427. layoutManager
  428. .removeElementResizeListener(element, listener);
  429. }
  430. }
  431. resizeListeners.clear();
  432. }
  433. }
  434. private static native void invokeIfPresent(
  435. JavaScriptObject connectorWrapper, String functionName)
  436. /*-{
  437. if (typeof connectorWrapper[functionName] == 'function') {
  438. connectorWrapper[functionName].apply(connectorWrapper, arguments);
  439. }
  440. }-*/;
  441. public String getInitFunctionName() {
  442. return initFunctionName;
  443. }
  444. private static Logger getLogger() {
  445. return Logger.getLogger(JavaScriptConnectorHelper.class.getName());
  446. }
  447. }