Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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