/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.client.componentlocator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.Element; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorMap; import com.vaadin.client.ServerConnector; import com.vaadin.client.Util; import com.vaadin.client.VCaption; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.SubPartAware; import com.vaadin.client.ui.VCssLayout; import com.vaadin.client.ui.VGridLayout; import com.vaadin.client.ui.VOverlay; import com.vaadin.client.ui.VTabsheetPanel; import com.vaadin.client.ui.VUI; import com.vaadin.client.ui.VWindow; import com.vaadin.client.ui.orderedlayout.Slot; import com.vaadin.client.ui.orderedlayout.VAbstractOrderedLayout; import com.vaadin.client.ui.window.WindowConnector; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.Connector; import com.vaadin.shared.communication.SharedState; /** * The LegacyLocatorStrategy class handles the legacy locator syntax that was * introduced in version 5.4 of the framework. The legacy locator strategy is * always used if no other strategy claims responsibility for a locator string. * * @since 7.2 * @author Vaadin Ltd */ public class LegacyLocatorStrategy implements LocatorStrategy { /** * Separator used in the String locator between a parent and a child widget. */ static final String PARENTCHILD_SEPARATOR = "/"; /** * Separator used in the String locator between the part identifying the * containing widget and the part identifying the target element within the * widget. */ static final String SUBPART_SEPARATOR = "#"; /** * String that identifies the root panel when appearing first in the String * locator. */ static final String ROOT_ID = "Root"; private final ApplicationConnection client; private static final RegExp validSyntax = RegExp.compile( "^((\\w+::)?((PID_S)?\\w[-$_a-zA-Z0-9.' ]*)?)?(/[-$_a-zA-Z0-9]+\\[\\d+\\])*/?(#.*)?$"); public LegacyLocatorStrategy(ApplicationConnection clientConnection) { client = clientConnection; } @Override public boolean validatePath(String path) { return validSyntax.test(path); } @Override public String getPathForElement(Element targetElement) { ComponentConnector connector = Util.findPaintable(client, targetElement); Widget w = null; if (connector != null) { // If we found a Paintable then we use that as reference. We should // find the Paintable for all but very special cases (like // overlays). w = connector.getWidget(); /* * Still if the Paintable contains a widget that implements * SubPartAware, we want to use that as a reference */ Widget targetParent = findParentWidget(targetElement, w); while (targetParent != w && targetParent != null) { if (targetParent instanceof SubPartAware) { /* * The targetParent widget is a child of the Paintable and * the first parent (of the targetElement) that implements * SubPartAware */ w = targetParent; break; } targetParent = targetParent.getParent(); } } if (w == null) { // Check if the element is part of a widget that is attached // directly to the root panel RootPanel rootPanel = RootPanel.get(); int rootWidgetCount = rootPanel.getWidgetCount(); for (int i = 0; i < rootWidgetCount; i++) { Widget rootWidget = rootPanel.getWidget(i); if (rootWidget.getElement().isOrHasChild(targetElement)) { // The target element is contained by this root widget w = findParentWidget(targetElement, rootWidget); break; } } if (w != null) { // We found a widget but we should still see if we find a // SubPartAware implementor (we cannot find the Paintable as // there is no link from VOverlay to its paintable/owner). Widget subPartAwareWidget = findSubPartAwareParentWidget(w); if (subPartAwareWidget != null) { w = subPartAwareWidget; } } } if (w == null) { // Containing widget not found return null; } // Determine the path for the target widget String path = getPathForWidget(w); if (path == null) { /* * No path could be determined for the target widget. Cannot create * a locator string. */ return null; } // The parent check is a work around for Firefox 15 which fails to // compare elements properly (#9534) if (w.getElement() == targetElement) { /* * We are done if the target element is the root of the target * widget. */ return path; } else if (w instanceof SubPartAware) { /* * If the widget can provide an identifier for the targetElement we * let it do that */ String elementLocator = ((SubPartAware) w) .getSubPartName(DOM.asOld(targetElement)); if (elementLocator != null) { return path + LegacyLocatorStrategy.SUBPART_SEPARATOR + elementLocator; } } /* * If everything else fails we use the DOM path to identify the target * element */ String domPath = getDOMPathForElement(targetElement, w.getElement()); if (domPath == null) { return path; } else { return path + domPath; } } /** * {@inheritDoc} */ @Override public Element getElementByPath(String path) { return getElementByPathStartingAt(path, null); } /** * {@inheritDoc} */ @Override public Element getElementByPathStartingAt(String path, Element baseElement) { /* * Path is of type "targetWidgetPath#componentPart" or * "targetWidgetPath". */ String parts[] = path.split(LegacyLocatorStrategy.SUBPART_SEPARATOR, 2); String widgetPath = parts[0]; // Note that this only works if baseElement can be mapped to a // widget to which the path is relative. Otherwise, the current // implementation simply interprets the path as if baseElement was // null. Widget baseWidget = WidgetUtil.findWidget(baseElement); Widget w = getWidgetFromPath(widgetPath, baseWidget); if (w == null || !WidgetUtil.isAttachedAndDisplayed(w)) { return null; } if (parts.length == 1) { int pos = widgetPath.indexOf("domChild"); if (pos == -1) { return w.getElement(); } // Contains dom reference to a sub element of the widget String subPath = widgetPath.substring(pos); return getElementByDOMPath(w.getElement(), subPath); } else if (parts.length == 2) { if (w instanceof SubPartAware) { return ((SubPartAware) w).getSubPartElement(parts[1]); } } return null; } /** * {@inheritDoc} */ @Override public List<Element> getElementsByPath(String path) { // This type of search is not supported in LegacyLocator List<Element> array = new ArrayList<>(); Element e = getElementByPath(path); if (e != null) { array.add(e); } return array; } /** * {@inheritDoc} */ @Override public List<Element> getElementsByPathStartingAt(String path, Element root) { // This type of search is not supported in LegacyLocator List<Element> array = new ArrayList<>(); Element e = getElementByPathStartingAt(path, root); if (e != null) { array.add(e); } return array; } /** * Finds the first widget in the hierarchy (moving upwards) that implements * SubPartAware. Returns the SubPartAware implementor or null if none is * found. * * @param w * The widget to start from. This is returned if it implements * SubPartAware. * @return The first widget (upwards in hierarchy) that implements * SubPartAware or null */ Widget findSubPartAwareParentWidget(Widget w) { while (w != null) { if (w instanceof SubPartAware) { return w; } w = w.getParent(); } return null; } /** * Returns the first widget found when going from {@code targetElement} * upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a * parent of {@code targetElement}. * * @param targetElement * @param ancestorWidget * @return The widget whose root element is a parent of * {@code targetElement}. */ private Widget findParentWidget(Element targetElement, Widget ancestorWidget) { /* * As we cannot resolve Widgets from the element we start from the * widget and move downwards to the correct child widget, as long as we * find one. */ if (ancestorWidget instanceof HasWidgets) { for (Widget w : ((HasWidgets) ancestorWidget)) { if (w.getElement().isOrHasChild(targetElement)) { return findParentWidget(targetElement, w); } } } // No children found, this is it return ancestorWidget; } /** * Locates an element based on a DOM path and a base element. * * @param baseElement * The base element which the path is relative to * @param path * String locator (consisting of domChild[x] parts) that * identifies the element * @return The element identified by path, relative to baseElement or null * if the element could not be found. */ private Element getElementByDOMPath(Element baseElement, String path) { String parts[] = path.split(PARENTCHILD_SEPARATOR); Element element = baseElement; for (int i = 0, l = parts.length; i < l; ++i) { String part = parts[i]; if (part.startsWith("domChild[")) { String childIndexString = part.substring("domChild[".length(), part.length() - 1); if (WidgetUtil.findWidget( baseElement) instanceof VAbstractOrderedLayout) { if (element.hasChildNodes()) { Element e = element.getFirstChildElement().cast(); String cn = e.getClassName(); if (cn != null && (cn.equals("v-expand") || cn.contains("v-has-caption"))) { element = e; } } } try { int childIndex = Integer.parseInt(childIndexString); element = DOM.getChild(element, childIndex); } catch (Exception e) { return null; } if (element == null) { return null; } } else { path = parts[i]; for (int j = i + 1; j < l; ++j) { path += PARENTCHILD_SEPARATOR + parts[j]; } return getElementByPathStartingAt(path, element); } } return element; } /** * Generates a String locator using domChild[x] parts for the element * relative to the baseElement. * * @param element * The target element * @param baseElement * The starting point for the locator. The generated path is * relative to this element. * @return A String locator that can be used to locate the target element * using {@link #getElementByDOMPath(Element, String)} or null if * the locator String cannot be created. */ private String getDOMPathForElement(Element element, Element baseElement) { Element e = element; String path = ""; while (true) { int childIndex = -1; Element siblingIterator = e; while (siblingIterator != null) { childIndex++; siblingIterator = siblingIterator.getPreviousSiblingElement() .cast(); } path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]" + path; JavaScriptObject parent = e.getParentElement(); if (parent == null) { return null; } // The parent check is a work around for Firefox 15 which fails to // compare elements properly (#9534) if (parent == baseElement) { break; } e = parent.cast(); } return path; } /** * Creates a locator String for the given widget. The path can be used to * locate the widget using {@link #getWidgetFromPath(String, Widget)}. * <p/> * Returns null if no path can be determined for the widget or if the widget * is null. * * @param w * The target widget * @return A String locator for the widget */ private String getPathForWidget(Widget w) { if (w == null) { return null; } String elementId = w.getElement().getId(); if (elementId != null && !elementId.isEmpty() && !elementId.startsWith("gwt-uid-")) { // Use PID_S+id if the user has set an id but do not use it for auto // generated id:s as these might not be consistent return "PID_S" + elementId; } else if (w instanceof VUI) { return ""; } else if (w instanceof VWindow) { Connector windowConnector = ConnectorMap.get(client) .getConnector(w); List<WindowConnector> subWindowList = client.getUIConnector() .getSubWindows(); int indexOfSubWindow = subWindowList.indexOf(windowConnector); return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]"; } else if (w instanceof RootPanel) { return ROOT_ID; } Widget parent = w.getParent(); String basePath = getPathForWidget(parent); if (basePath == null) { return null; } String simpleName = Util.getSimpleName(w); /* * Check if the parent implements Iterable. At least VPopupView does not * implement HasWdgets so we cannot check for that. */ if (!(parent instanceof Iterable<?>)) { // Parent does not implement Iterable so we cannot find out which // child this is return null; } Iterator<?> i = ((Iterable<?>) parent).iterator(); int pos = 0; while (i.hasNext()) { Object child = i.next(); if (child == w) { return basePath + PARENTCHILD_SEPARATOR + simpleName + "[" + pos + "]"; } String simpleName2 = Util.getSimpleName(child); if (simpleName.equals(simpleName2)) { pos++; } } return null; } /** * Locates the widget based on a String locator. * * @param path * The String locator that identifies the widget. * @param baseWidget * the widget to which the path is relative, null if relative to * root * @return The Widget identified by the String locator or null if the widget * could not be identified. */ @SuppressWarnings("unchecked") private Widget getWidgetFromPath(String path, Widget baseWidget) { Widget w = baseWidget; String parts[] = path.split(PARENTCHILD_SEPARATOR); for (int i = 0; i < parts.length; i++) { String part = parts[i]; if (part.equals(ROOT_ID)) { w = RootPanel.get(); } else if (part.equals("")) { if (w == null) { w = client.getUIConnector().getWidget(); } } else if (w == null) { String id = part; // Must be old static pid (PID_S*) ServerConnector connector = ConnectorMap.get(client) .getConnector(id); if (connector == null) { // Lookup by component id // TODO Optimize this connector = findConnectorById(client.getUIConnector(), id.substring(5)); } if (connector instanceof ComponentConnector) { w = ((ComponentConnector) connector).getWidget(); } else { // Not found return null; } } else if (part.startsWith("domChild[")) { // The target widget has been found and the rest identifies the // element break; } else if (w instanceof Iterable) { // W identifies a widget that contains other widgets, as it // should. Try to locate the child Iterable<?> parent = (Iterable<?>) w; // Part is of type "VVerticalLayout[0]", split this into // VVerticalLayout and 0 String[] split = part.split("\\[", 2); String widgetClassName = split[0]; String indexString = split[1].substring(0, split[1].length() - 1); int widgetPosition; try { widgetPosition = Integer.parseInt(indexString); } catch (NumberFormatException e) { // We've probably been fed a new-style Vaadin locator with a // string-form predicate, that doesn't match anything in the // search space. return null; } // AbsolutePanel in GridLayout has been removed -> skip it if (w instanceof VGridLayout && "AbsolutePanel".equals(widgetClassName)) { continue; } // FlowPane in CSSLayout has been removed -> skip it if (w instanceof VCssLayout && "VCssLayout$FlowPane".equals(widgetClassName)) { continue; } // ChildComponentContainer and VOrderedLayout$Slot have been // replaced with Slot if (w instanceof VAbstractOrderedLayout && ("ChildComponentContainer".equals(widgetClassName) || "VOrderedLayout$Slot" .equals(widgetClassName))) { widgetClassName = "Slot"; } if (w instanceof VTabsheetPanel && widgetPosition != 0) { // TabSheetPanel now only contains 1 connector => the index // is always 0 which indicates the widget in the active tab widgetPosition = 0; } if (w instanceof VOverlay && "VCalendarPanel".equals(widgetClassName)) { // Vaadin 7.1 adds a wrapper for datefield popups parent = (Iterable<?>) ((Iterable<?>) parent).iterator() .next(); } /* * The new grid and ordered layouts do not contain * ChildComponentContainer widgets. This is instead simulated by * constructing a path step that would find the desired widget * from the layout and injecting it as the next search step * (which would originally have found the widget inside the * ChildComponentContainer) */ if ((w instanceof VGridLayout) && "ChildComponentContainer".equals(widgetClassName) && i + 1 < parts.length) { HasWidgets layout = (HasWidgets) w; String nextPart = parts[i + 1]; String[] nextSplit = nextPart.split("\\[", 2); String nextWidgetClassName = nextSplit[0]; // Find the n:th child and count the number of children with // the same type before it int nextIndex = 0; for (Widget child : layout) { boolean matchingType = nextWidgetClassName .equals(Util.getSimpleName(child)); if (matchingType && widgetPosition == 0) { // This is the n:th child that we looked for break; } else if (widgetPosition < 0) { // Error if we're past the desired position without // a match return null; } else if (matchingType) { // If this was another child of the expected type, // increase the count for the next step nextIndex++; } // Don't count captions if (!(child instanceof VCaption)) { widgetPosition--; } } // Advance to the next step, this time checking for the // actual child widget parts[i + 1] = nextWidgetClassName + '[' + nextIndex + ']'; continue; } // Locate the child Iterator<? extends Widget> iterator; /* * VWindow and VContextMenu workarounds for backwards * compatibility */ if (widgetClassName.equals("VWindow")) { List<WindowConnector> windows = client.getUIConnector() .getSubWindows(); List<VWindow> windowWidgets = new ArrayList<>( windows.size()); for (WindowConnector wc : windows) { windowWidgets.add(wc.getWidget()); } iterator = windowWidgets.iterator(); } else if (widgetClassName.equals("VContextMenu")) { return client.getContextMenu(); } else { iterator = (Iterator<? extends Widget>) parent.iterator(); } boolean ok = false; // Find the widgetPosition:th child of type "widgetClassName" while (iterator.hasNext()) { Widget child = iterator.next(); String simpleName2 = Util.getSimpleName(child); if (!widgetClassName.equals(simpleName2) && child instanceof Slot) { /* * Support legacy tests without any selector for the * Slot widget (i.e. /VVerticalLayout[0]/VButton[0]) by * directly checking the stuff inside the slot */ child = ((Slot) child).getWidget(); simpleName2 = Util.getSimpleName(child); } if (widgetClassName.equals(simpleName2)) { if (widgetPosition == 0) { w = child; ok = true; break; } widgetPosition--; } } if (!ok) { // Did not find the child return null; } } else { // W identifies something that is not a "HasWidgets". This // should not happen as all widget containers should implement // HasWidgets. return null; } } return w; } private ServerConnector findConnectorById(ServerConnector root, String id) { SharedState state = root.getState(); if (state instanceof AbstractComponentState && id.equals(((AbstractComponentState) state).id)) { return root; } for (ServerConnector child : root.getChildren()) { ServerConnector found = findConnectorById(child, id); if (found != null) { return found; } } return null; } }