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.

ComponentLocator.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. /*
  2. @VaadinApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client;
  5. import java.util.ArrayList;
  6. import java.util.Iterator;
  7. import com.google.gwt.user.client.DOM;
  8. import com.google.gwt.user.client.Element;
  9. import com.google.gwt.user.client.ui.HasWidgets;
  10. import com.google.gwt.user.client.ui.RootPanel;
  11. import com.google.gwt.user.client.ui.Widget;
  12. import com.vaadin.terminal.gwt.client.ui.SubPartAware;
  13. import com.vaadin.terminal.gwt.client.ui.VView;
  14. import com.vaadin.terminal.gwt.client.ui.VWindow;
  15. /**
  16. * ComponentLocator provides methods for generating a String locator for a given
  17. * DOM element and for locating a DOM element using a String locator.
  18. */
  19. public class ComponentLocator {
  20. /**
  21. * Separator used in the String locator between a parent and a child widget.
  22. */
  23. private static final String PARENTCHILD_SEPARATOR = "/";
  24. /**
  25. * Separator used in the String locator between the part identifying the
  26. * containing widget and the part identifying the target element within the
  27. * widget.
  28. */
  29. private static final String SUBPART_SEPARATOR = "#";
  30. /**
  31. * String that identifies the root panel when appearing first in the String
  32. * locator.
  33. */
  34. private static final String ROOT_ID = "Root";
  35. /**
  36. * Reference to ApplicationConnection instance.
  37. */
  38. private ApplicationConnection client;
  39. /**
  40. * Construct a ComponentLocator for the given ApplicationConnection.
  41. *
  42. * @param client
  43. * ApplicationConnection instance for the application.
  44. */
  45. public ComponentLocator(ApplicationConnection client) {
  46. this.client = client;
  47. }
  48. /**
  49. * Generates a String locator which uniquely identifies the target element.
  50. * The {@link #getElementByPath(String)} method can be used for the inverse
  51. * operation, i.e. locating an element based on the return value from this
  52. * method.
  53. * <p>
  54. * Note that getElementByPath(getPathForElement(element)) == element is not
  55. * always true as {@link #getPathForElement(Element)} can return a path to
  56. * another element if the widget determines an action on the other element
  57. * will give the same result as the action on the target element.
  58. * </p>
  59. *
  60. * @since 5.4
  61. * @param targetElement
  62. * The element to generate a path for.
  63. * @return A String locator that identifies the target element or null if a
  64. * String locator could not be created.
  65. */
  66. public String getPathForElement(Element targetElement) {
  67. String pid = null;
  68. Element e = targetElement;
  69. while (true) {
  70. pid = VPaintableMap.get(client).getPid(e);
  71. if (pid != null) {
  72. break;
  73. }
  74. e = DOM.getParent(e);
  75. if (e == null) {
  76. break;
  77. }
  78. }
  79. Widget w = null;
  80. if (pid != null) {
  81. // If we found a Paintable then we use that as reference. We should
  82. // find the Paintable for all but very special cases (like
  83. // overlays).
  84. w = ((VPaintableWidget) VPaintableMap.get(client).getPaintable(pid))
  85. .getWidgetForPaintable();
  86. /*
  87. * Still if the Paintable contains a widget that implements
  88. * SubPartAware, we want to use that as a reference
  89. */
  90. Widget targetParent = findParentWidget(targetElement, w);
  91. while (targetParent != w && targetParent != null) {
  92. if (targetParent instanceof SubPartAware) {
  93. /*
  94. * The targetParent widget is a child of the Paintable and
  95. * the first parent (of the targetElement) that implements
  96. * SubPartAware
  97. */
  98. w = targetParent;
  99. break;
  100. }
  101. targetParent = targetParent.getParent();
  102. }
  103. }
  104. if (w == null) {
  105. // Check if the element is part of a widget that is attached
  106. // directly to the root panel
  107. RootPanel rootPanel = RootPanel.get();
  108. int rootWidgetCount = rootPanel.getWidgetCount();
  109. for (int i = 0; i < rootWidgetCount; i++) {
  110. Widget rootWidget = rootPanel.getWidget(i);
  111. if (rootWidget.getElement().isOrHasChild(targetElement)) {
  112. // The target element is contained by this root widget
  113. w = findParentWidget(targetElement, rootWidget);
  114. break;
  115. }
  116. }
  117. if (w != null) {
  118. // We found a widget but we should still see if we find a
  119. // SubPartAware implementor (we cannot find the Paintable as
  120. // there is no link from VOverlay to its paintable/owner).
  121. Widget subPartAwareWidget = findSubPartAwareParentWidget(w);
  122. if (subPartAwareWidget != null) {
  123. w = subPartAwareWidget;
  124. }
  125. }
  126. }
  127. if (w == null) {
  128. // Containing widget not found
  129. return null;
  130. }
  131. // Determine the path for the target widget
  132. String path = getPathForWidget(w);
  133. if (path == null) {
  134. /*
  135. * No path could be determined for the target widget. Cannot create
  136. * a locator string.
  137. */
  138. return null;
  139. }
  140. if (w.getElement() == targetElement) {
  141. /*
  142. * We are done if the target element is the root of the target
  143. * widget.
  144. */
  145. return path;
  146. } else if (w instanceof SubPartAware) {
  147. /*
  148. * If the widget can provide an identifier for the targetElement we
  149. * let it do that
  150. */
  151. String elementLocator = ((SubPartAware) w)
  152. .getSubPartName(targetElement);
  153. if (elementLocator != null) {
  154. return path + SUBPART_SEPARATOR + elementLocator;
  155. }
  156. }
  157. /*
  158. * If everything else fails we use the DOM path to identify the target
  159. * element
  160. */
  161. return path + getDOMPathForElement(targetElement, w.getElement());
  162. }
  163. /**
  164. * Finds the first widget in the hierarchy (moving upwards) that implements
  165. * SubPartAware. Returns the SubPartAware implementor or null if none is
  166. * found.
  167. *
  168. * @param w
  169. * The widget to start from. This is returned if it implements
  170. * SubPartAware.
  171. * @return The first widget (upwards in hierarchy) that implements
  172. * SubPartAware or null
  173. */
  174. private Widget findSubPartAwareParentWidget(Widget w) {
  175. while (w != null) {
  176. if (w instanceof SubPartAware) {
  177. return w;
  178. }
  179. w = w.getParent();
  180. }
  181. return null;
  182. }
  183. /**
  184. * Returns the first widget found when going from {@code targetElement}
  185. * upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a
  186. * parent of {@code targetElement}.
  187. *
  188. * @param targetElement
  189. * @param ancestorWidget
  190. * @return The widget whose root element is a parent of
  191. * {@code targetElement}.
  192. */
  193. private Widget findParentWidget(Element targetElement, Widget ancestorWidget) {
  194. /*
  195. * As we cannot resolve Widgets from the element we start from the
  196. * widget and move downwards to the correct child widget, as long as we
  197. * find one.
  198. */
  199. if (ancestorWidget instanceof HasWidgets) {
  200. for (Widget w : ((HasWidgets) ancestorWidget)) {
  201. if (w.getElement().isOrHasChild(targetElement)) {
  202. return findParentWidget(targetElement, w);
  203. }
  204. }
  205. }
  206. // No children found, this is it
  207. return ancestorWidget;
  208. }
  209. /**
  210. * Locates an element based on a DOM path and a base element.
  211. *
  212. * @param baseElement
  213. * The base element which the path is relative to
  214. * @param path
  215. * String locator (consisting of domChild[x] parts) that
  216. * identifies the element
  217. * @return The element identified by path, relative to baseElement or null
  218. * if the element could not be found.
  219. */
  220. private Element getElementByDOMPath(Element baseElement, String path) {
  221. String parts[] = path.split(PARENTCHILD_SEPARATOR);
  222. Element element = baseElement;
  223. for (String part : parts) {
  224. if (part.startsWith("domChild[")) {
  225. String childIndexString = part.substring("domChild[".length(),
  226. part.length() - 1);
  227. try {
  228. int childIndex = Integer.parseInt(childIndexString);
  229. element = DOM.getChild(element, childIndex);
  230. } catch (Exception e) {
  231. return null;
  232. }
  233. }
  234. }
  235. return element;
  236. }
  237. /**
  238. * Generates a String locator using domChild[x] parts for the element
  239. * relative to the baseElement.
  240. *
  241. * @param element
  242. * The target element
  243. * @param baseElement
  244. * The starting point for the locator. The generated path is
  245. * relative to this element.
  246. * @return A String locator that can be used to locate the target element
  247. * using {@link #getElementByDOMPath(Element, String)} or null if
  248. * the locator String cannot be created.
  249. */
  250. private String getDOMPathForElement(Element element, Element baseElement) {
  251. Element e = element;
  252. String path = "";
  253. while (true) {
  254. Element parent = DOM.getParent(e);
  255. if (parent == null) {
  256. return null;
  257. }
  258. int childIndex = -1;
  259. int childCount = DOM.getChildCount(parent);
  260. for (int i = 0; i < childCount; i++) {
  261. if (e == DOM.getChild(parent, i)) {
  262. childIndex = i;
  263. break;
  264. }
  265. }
  266. if (childIndex == -1) {
  267. return null;
  268. }
  269. path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]"
  270. + path;
  271. if (parent == baseElement) {
  272. break;
  273. }
  274. e = parent;
  275. }
  276. return path;
  277. }
  278. /**
  279. * Locates an element using a String locator (path) which identifies a DOM
  280. * element. The {@link #getPathForElement(Element)} method can be used for
  281. * the inverse operation, i.e. generating a string expression for a DOM
  282. * element.
  283. *
  284. * @since 5.4
  285. * @param path
  286. * The String locater which identifies the target element.
  287. * @return The DOM element identified by {@code path} or null if the element
  288. * could not be located.
  289. */
  290. public Element getElementByPath(String path) {
  291. /*
  292. * Path is of type "targetWidgetPath#componentPart" or
  293. * "targetWidgetPath".
  294. */
  295. String parts[] = path.split(SUBPART_SEPARATOR, 2);
  296. String widgetPath = parts[0];
  297. Widget w = getWidgetFromPath(widgetPath);
  298. if (w == null || !Util.isAttachedAndDisplayed(w)) {
  299. return null;
  300. }
  301. if (parts.length == 1) {
  302. int pos = widgetPath.indexOf("domChild");
  303. if (pos == -1) {
  304. return w.getElement();
  305. }
  306. // Contains dom reference to a sub element of the widget
  307. String subPath = widgetPath.substring(pos);
  308. return getElementByDOMPath(w.getElement(), subPath);
  309. } else if (parts.length == 2) {
  310. if (w instanceof SubPartAware) {
  311. return ((SubPartAware) w).getSubPartElement(parts[1]);
  312. }
  313. }
  314. return null;
  315. }
  316. /**
  317. * Creates a locator String for the given widget. The path can be used to
  318. * locate the widget using {@link #getWidgetFromPath(String)}.
  319. *
  320. * Returns null if no path can be determined for the widget or if the widget
  321. * is null.
  322. *
  323. * @param w
  324. * The target widget
  325. * @return A String locator for the widget
  326. */
  327. private String getPathForWidget(Widget w) {
  328. if (w == null) {
  329. return null;
  330. }
  331. String pid = VPaintableMap.get(client).getPid(w.getElement());
  332. if (isStaticPid(pid)) {
  333. return pid;
  334. }
  335. if (w instanceof VView) {
  336. return "";
  337. } else if (w instanceof VWindow) {
  338. VWindow win = (VWindow) w;
  339. ArrayList<VWindow> subWindowList = client.getView()
  340. .getSubWindowList();
  341. int indexOfSubWindow = subWindowList.indexOf(win);
  342. return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]";
  343. } else if (w instanceof RootPanel) {
  344. return ROOT_ID;
  345. }
  346. Widget parent = w.getParent();
  347. String basePath = getPathForWidget(parent);
  348. if (basePath == null) {
  349. return null;
  350. }
  351. String simpleName = Util.getSimpleName(w);
  352. /*
  353. * Check if the parent implements Iterable. At least VPopupView does not
  354. * implement HasWdgets so we cannot check for that.
  355. */
  356. if (!(parent instanceof Iterable<?>)) {
  357. // Parent does not implement Iterable so we cannot find out which
  358. // child this is
  359. return null;
  360. }
  361. Iterator<?> i = ((Iterable<?>) parent).iterator();
  362. int pos = 0;
  363. while (i.hasNext()) {
  364. Object child = i.next();
  365. if (child == w) {
  366. return basePath + PARENTCHILD_SEPARATOR + simpleName + "["
  367. + pos + "]";
  368. }
  369. String simpleName2 = Util.getSimpleName(child);
  370. if (simpleName.equals(simpleName2)) {
  371. pos++;
  372. }
  373. }
  374. return null;
  375. }
  376. /**
  377. * Locates the widget based on a String locator.
  378. *
  379. * @param path
  380. * The String locator that identifies the widget.
  381. * @return The Widget identified by the String locator or null if the widget
  382. * could not be identified.
  383. */
  384. private Widget getWidgetFromPath(String path) {
  385. Widget w = null;
  386. String parts[] = path.split(PARENTCHILD_SEPARATOR);
  387. for (int i = 0; i < parts.length; i++) {
  388. String part = parts[i];
  389. if (part.equals(ROOT_ID)) {
  390. w = RootPanel.get();
  391. } else if (part.equals("")) {
  392. w = client.getView();
  393. } else if (w == null) {
  394. // Must be static pid (PID_S*)
  395. w = ((VPaintableWidget) VPaintableMap.get(client).getPaintable(
  396. part)).getWidgetForPaintable();
  397. } else if (part.startsWith("domChild[")) {
  398. // The target widget has been found and the rest identifies the
  399. // element
  400. break;
  401. } else if (w instanceof Iterable) {
  402. // W identifies a widget that contains other widgets, as it
  403. // should. Try to locate the child
  404. Iterable<?> parent = (Iterable<?>) w;
  405. // Part is of type "VVerticalLayout[0]", split this into
  406. // VVerticalLayout and 0
  407. String[] split = part.split("\\[", 2);
  408. String widgetClassName = split[0];
  409. String indexString = split[1];
  410. int widgetPosition = Integer.parseInt(indexString.substring(0,
  411. indexString.length() - 1));
  412. // Locate the child
  413. Iterator<? extends Widget> iterator;
  414. /*
  415. * VWindow and VContextMenu workarounds for backwards
  416. * compatibility
  417. */
  418. if (widgetClassName.equals("VWindow")) {
  419. iterator = client.getView().getSubWindowList().iterator();
  420. } else if (widgetClassName.equals("VContextMenu")) {
  421. return client.getContextMenu();
  422. } else {
  423. iterator = (Iterator<? extends Widget>) parent.iterator();
  424. }
  425. boolean ok = false;
  426. // Find the widgetPosition:th child of type "widgetClassName"
  427. while (iterator.hasNext()) {
  428. Widget child = iterator.next();
  429. String simpleName2 = Util.getSimpleName(child);
  430. if (widgetClassName.equals(simpleName2)) {
  431. if (widgetPosition == 0) {
  432. w = child;
  433. ok = true;
  434. break;
  435. }
  436. widgetPosition--;
  437. }
  438. }
  439. if (!ok) {
  440. // Did not find the child
  441. return null;
  442. }
  443. } else {
  444. // W identifies something that is not a "HasWidgets". This
  445. // should not happen as all widget containers should implement
  446. // HasWidgets.
  447. return null;
  448. }
  449. }
  450. return w;
  451. }
  452. /**
  453. * Checks if the given pid is a static pid.
  454. *
  455. * @param pid
  456. * The pid to check
  457. * @return true if the pid is a static pid, false otherwise
  458. */
  459. private boolean isStaticPid(String pid) {
  460. if (pid == null) {
  461. return false;
  462. }
  463. return pid.startsWith("PID_S");
  464. }
  465. }