/*
* Copyright 2010 IT Mill 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.terminal.gwt.client;
import java.util.ArrayList;
import java.util.Iterator;
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.terminal.gwt.client.ui.SubPartAware;
import com.vaadin.terminal.gwt.client.ui.VView;
import com.vaadin.terminal.gwt.client.ui.VWindow;
/**
* 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;
Element e = targetElement;
while (true) {
pid = client.getPid(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 = (Widget) client.getPaintable(pid);
/*
* 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;
}
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
*/
return path + getDOMPathForElement(targetElement, w.getElement());
}
/**
* 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);
try {
int childIndex = Integer.parseInt(childIndexString);
element = DOM.getChild(element, childIndex);
} catch (Exception e) {
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) {
Element parent = DOM.getParent(e);
if (parent == null) {
return null;
}
int childIndex = -1;
int childCount = DOM.getChildCount(parent);
for (int i = 0; i < childCount; i++) {
if (e == DOM.getChild(parent, i)) {
childIndex = i;
break;
}
}
if (childIndex == -1) {
return null;
}
path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]"
+ path;
if (parent == baseElement) {
break;
}
e = parent;
}
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) {
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 pid = client.getPid(w.getElement());
if (isStaticPid(pid)) {
return pid;
}
if (w instanceof VView) {
return "";
} else if (w instanceof VWindow) {
VWindow win = (VWindow) w;
ArrayList<VWindow> subWindowList = client.getView()
.getSubWindowList();
int indexOfSubWindow = subWindowList.indexOf(win);
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.getView();
} else if (w == null) {
// Must be static pid (PID_S*)
w = (Widget) client.getPaintable(part);
} 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];
int widgetPosition = Integer.parseInt(indexString.substring(0,
indexString.length() - 1));
// Locate the child
Iterator<? extends Widget> iterator;
/*
* VWindow and VContextMenu workarounds for backwards
* compatibility
*/
if (widgetClassName.equals("VWindow")) {
iterator = client.getView().getSubWindowList().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)) {
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;
}
/**
* Checks if the given pid is a static pid.
*
* @param pid
* The pid to check
* @return true if the pid is a static pid, false otherwise
*/
private boolean isStaticPid(String pid) {
if (pid == null) {
return false;
}
return pid.startsWith("PID_S");
}
}