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.

LegacyLocatorStrategy.java 24KB

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