Java tutorial
/* * Copyright 2000-2013 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; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; 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.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; /** * ComponentLocator provides methods for generating a String locator for a given * DOM element and for locating a DOM element using a String locator. */ public class ComponentLocator { /** * Separator used in the String locator between a parent and a child widget. */ private 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. */ private static final String SUBPART_SEPARATOR = "#"; /** * String that identifies the root panel when appearing first in the String * locator. */ private static final String ROOT_ID = "Root"; /** * Reference to ApplicationConnection instance. */ private ApplicationConnection client; /** * Construct a ComponentLocator for the given ApplicationConnection. * * @param client * ApplicationConnection instance for the application. */ public ComponentLocator(ApplicationConnection client) { this.client = client; } /** * Generates a String locator which uniquely identifies the target element. * The {@link #getElementByPath(String)} method can be used for the inverse * operation, i.e. locating an element based on the return value from this * method. * <p> * Note that getElementByPath(getPathForElement(element)) == element is not * always true as {@link #getPathForElement(Element)} can return a path to * another element if the widget determines an action on the other element * will give the same result as the action on the target element. * </p> * * @since 5.4 * @param targetElement * The element to generate a path for. * @return A String locator that identifies the target element or null if a * String locator could not be created. */ public String getPathForElement(Element targetElement) { String pid = null; targetElement = getElement(targetElement); Element e = targetElement; while (true) { pid = ConnectorMap.get(client).getConnectorId(e); if (pid != null) { break; } e = DOM.getParent(e); if (e == null) { break; } } Widget w = null; if (pid != 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 = ((ComponentConnector) ConnectorMap.get(client).getConnector(pid)).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(targetElement); if (elementLocator != null) { return path + 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; } } /** * Returns the element passed to the method. Or in case of Firefox 15, * returns the real element that is in the DOM instead of the element passed * to the method (which is the same element but not ==). * * @param targetElement * the element to return * @return the element passed to the method */ private Element getElement(Element targetElement) { if (targetElement == null) { return null; } if (!BrowserInfo.get().isFirefox()) { return targetElement; } if (BrowserInfo.get().getBrowserMajorVersion() != 15) { return targetElement; } // Firefox 15, you make me sad if (targetElement.getNextSibling() != null) { return (Element) targetElement.getNextSibling().getPreviousSibling(); } if (targetElement.getPreviousSibling() != null) { return (Element) targetElement.getPreviousSibling().getNextSibling(); } // No siblings so this is the only child return (Element) targetElement.getParentNode().getChild(0); } /** * 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 */ private 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 (String part : parts) { if (part.startsWith("domChild[")) { String childIndexString = part.substring("domChild[".length(), part.length() - 1); if (Util.findWidget(baseElement, null) 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; } } } 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; } /** * Locates an element using a String locator (path) which identifies a DOM * element. The {@link #getPathForElement(Element)} method can be used for * the inverse operation, i.e. generating a string expression for a DOM * element. * * @since 5.4 * @param path * The String locater which identifies the target element. * @return The DOM element identified by {@code path} or null if the element * could not be located. */ public Element getElementByPath(String path) { /* * Path is of type "targetWidgetPath#componentPart" or * "targetWidgetPath". */ String parts[] = path.split(SUBPART_SEPARATOR, 2); String widgetPath = parts[0]; Widget w = getWidgetFromPath(widgetPath); if (w == null || !Util.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; } /** * Creates a locator String for the given widget. The path can be used to * locate the widget using {@link #getWidgetFromPath(String)}. * * 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. * @return The Widget identified by the String locator or null if the widget * could not be identified. */ private Widget getWidgetFromPath(String path) { Widget w = null; 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("")) { 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 = Integer.parseInt(indexString); // 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 layotus 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<VWindow>(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; } }