blob: e230e7fcf505649846c2ed915a0a1b4d31dc4bb9 [file] [log] [blame]
/*
* Copyright (C) 2013 DroidDriver committers
*
* 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 io.appium.droiddriver.finders;
import android.util.Log;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.BufferedOutputStream;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import io.appium.droiddriver.UiElement;
import io.appium.droiddriver.base.BaseUiElement;
import io.appium.droiddriver.exceptions.DroidDriverException;
import io.appium.droiddriver.exceptions.ElementNotFoundException;
import io.appium.droiddriver.util.FileUtils;
import io.appium.droiddriver.util.Logs;
import io.appium.droiddriver.util.Preconditions;
import io.appium.droiddriver.util.Strings;
/**
* Find matching UiElement by XPath.
*/
public class ByXPath implements Finder {
private static final XPath XPATH_COMPILER = XPathFactory.newInstance().newXPath();
// document needs to be static so that when buildDomNode is called recursively
// on children they are in the same document to be appended.
private static Document document;
// The two maps should be kept in sync
private static final Map<BaseUiElement<?, ?>, Element> TO_DOM_MAP =
new HashMap<BaseUiElement<?, ?>, Element>();
private static final Map<Element, BaseUiElement<?, ?>> FROM_DOM_MAP =
new HashMap<Element, BaseUiElement<?, ?>>();
public static void clearData() {
TO_DOM_MAP.clear();
FROM_DOM_MAP.clear();
document = null;
}
private final String xPathString;
private final XPathExpression xPathExpression;
protected ByXPath(String xPathString) {
this.xPathString = Preconditions.checkNotNull(xPathString);
try {
xPathExpression = XPATH_COMPILER.compile(xPathString);
} catch (XPathExpressionException e) {
throw new DroidDriverException("xPathString=" + xPathString, e);
}
}
@Override
public String toString() {
return Strings.toStringHelper(this).addValue(xPathString).toString();
}
@Override
public UiElement find(UiElement context) {
Element domNode = getDomNode((BaseUiElement<?, ?>) context, UiElement.VISIBLE);
try {
getDocument().appendChild(domNode);
Element foundNode = (Element) xPathExpression.evaluate(domNode, XPathConstants.NODE);
if (foundNode == null) {
Logs.log(Log.DEBUG, "XPath evaluation returns null for " + xPathString);
throw new ElementNotFoundException(this);
}
UiElement match = FROM_DOM_MAP.get(foundNode);
Logs.log(Log.INFO, "Found match: " + match);
return match;
} catch (XPathExpressionException e) {
throw new ElementNotFoundException(this, e);
} finally {
try {
getDocument().removeChild(domNode);
} catch (DOMException e) {
Logs.log(Log.ERROR, e, "Failed to clear document");
document = null; // getDocument will create new
}
}
}
private static Document getDocument() {
if (document == null) {
try {
document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
} catch (ParserConfigurationException e) {
throw new DroidDriverException(e);
}
}
return document;
}
/**
* Returns the DOM node representing this UiElement.
*/
private static Element getDomNode(BaseUiElement<?, ?> uiElement,
Predicate<? super UiElement> predicate) {
Element domNode = TO_DOM_MAP.get(uiElement);
if (domNode == null) {
domNode = buildDomNode(uiElement, predicate);
}
return domNode;
}
private static Element buildDomNode(BaseUiElement<?, ?> uiElement,
Predicate<? super UiElement> predicate) {
String className = uiElement.getClassName();
if (className == null) {
className = "UNKNOWN";
}
Element element = getDocument().createElement(XPaths.tag(className));
TO_DOM_MAP.put(uiElement, element);
FROM_DOM_MAP.put(element, uiElement);
setAttribute(element, Attribute.CLASS, className);
setAttribute(element, Attribute.RESOURCE_ID, uiElement.getResourceId());
setAttribute(element, Attribute.PACKAGE, uiElement.getPackageName());
setAttribute(element, Attribute.CONTENT_DESC, uiElement.getContentDescription());
setAttribute(element, Attribute.TEXT, uiElement.getText());
setAttribute(element, Attribute.CHECKABLE, uiElement.isCheckable());
setAttribute(element, Attribute.CHECKED, uiElement.isChecked());
setAttribute(element, Attribute.CLICKABLE, uiElement.isClickable());
setAttribute(element, Attribute.ENABLED, uiElement.isEnabled());
setAttribute(element, Attribute.FOCUSABLE, uiElement.isFocusable());
setAttribute(element, Attribute.FOCUSED, uiElement.isFocused());
setAttribute(element, Attribute.SCROLLABLE, uiElement.isScrollable());
setAttribute(element, Attribute.LONG_CLICKABLE, uiElement.isLongClickable());
setAttribute(element, Attribute.PASSWORD, uiElement.isPassword());
if (uiElement.hasSelection()) {
element.setAttribute(Attribute.SELECTION_START.getName(),
Integer.toString(uiElement.getSelectionStart()));
element.setAttribute(Attribute.SELECTION_END.getName(),
Integer.toString(uiElement.getSelectionEnd()));
}
setAttribute(element, Attribute.SELECTED, uiElement.isSelected());
element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds().toShortString());
// If we're dumping for debugging, add extra information
if (!UiElement.VISIBLE.equals(predicate)) {
if (!uiElement.isVisible()) {
element.setAttribute(BaseUiElement.ATTRIB_NOT_VISIBLE, "");
} else if (!uiElement.getVisibleBounds().equals(uiElement.getBounds())) {
element.setAttribute(BaseUiElement.ATTRIB_VISIBLE_BOUNDS, uiElement.getVisibleBounds()
.toShortString());
}
}
for (BaseUiElement<?, ?> child : uiElement.getChildren(predicate)) {
element.appendChild(getDomNode(child, predicate));
}
return element;
}
private static void setAttribute(Element element, Attribute attr, String value) {
if (value != null) {
element.setAttribute(attr.getName(), value);
}
}
// add attribute only if it's true
private static void setAttribute(Element element, Attribute attr, boolean value) {
if (value) {
element.setAttribute(attr.getName(), "");
}
}
public static boolean dumpDom(String path, BaseUiElement<?, ?> uiElement) {
BufferedOutputStream bos = null;
try {
bos = FileUtils.open(path);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// find() filters invisible UiElements, but this is for debugging and
// invisible UiElements may be of interest.
clearData();
Element domNode = getDomNode(uiElement, null);
transformer.transform(new DOMSource(domNode), new StreamResult(bos));
Logs.log(Log.INFO, "Wrote dom to " + path);
} catch (Exception e) {
Logs.log(Log.ERROR, e, "Failed to transform node");
return false;
} finally {
// We built DOM with invisible UiElements. Don't use it for find()!
clearData();
if (bos != null) {
try {
bos.close();
} catch (Exception e) {
// ignore
}
}
}
return true;
}
}