1 /* 2 * Copyright (C) 2013 DroidDriver committers 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package io.appium.droiddriver.finders; 17 18 import android.util.Log; 19 20 import org.w3c.dom.DOMException; 21 import org.w3c.dom.Document; 22 import org.w3c.dom.Element; 23 24 import java.io.BufferedOutputStream; 25 import java.util.HashMap; 26 import java.util.Map; 27 28 import javax.xml.parsers.DocumentBuilderFactory; 29 import javax.xml.parsers.ParserConfigurationException; 30 import javax.xml.transform.OutputKeys; 31 import javax.xml.transform.Transformer; 32 import javax.xml.transform.TransformerFactory; 33 import javax.xml.transform.dom.DOMSource; 34 import javax.xml.transform.stream.StreamResult; 35 import javax.xml.xpath.XPath; 36 import javax.xml.xpath.XPathConstants; 37 import javax.xml.xpath.XPathExpression; 38 import javax.xml.xpath.XPathExpressionException; 39 import javax.xml.xpath.XPathFactory; 40 41 import io.appium.droiddriver.UiElement; 42 import io.appium.droiddriver.base.BaseUiElement; 43 import io.appium.droiddriver.exceptions.DroidDriverException; 44 import io.appium.droiddriver.exceptions.ElementNotFoundException; 45 import io.appium.droiddriver.util.FileUtils; 46 import io.appium.droiddriver.util.Logs; 47 import io.appium.droiddriver.util.Preconditions; 48 import io.appium.droiddriver.util.Strings; 49 50 /** 51 * Find matching UiElement by XPath. 52 */ 53 public class ByXPath implements Finder { 54 private static final XPath XPATH_COMPILER = XPathFactory.newInstance().newXPath(); 55 // document needs to be static so that when buildDomNode is called recursively 56 // on children they are in the same document to be appended. 57 private static Document document; 58 // The two maps should be kept in sync 59 private static final Map<BaseUiElement<?, ?>, Element> TO_DOM_MAP = 60 new HashMap<BaseUiElement<?, ?>, Element>(); 61 private static final Map<Element, BaseUiElement<?, ?>> FROM_DOM_MAP = 62 new HashMap<Element, BaseUiElement<?, ?>>(); 63 clearData()64 public static void clearData() { 65 TO_DOM_MAP.clear(); 66 FROM_DOM_MAP.clear(); 67 document = null; 68 } 69 70 private final String xPathString; 71 private final XPathExpression xPathExpression; 72 ByXPath(String xPathString)73 protected ByXPath(String xPathString) { 74 this.xPathString = Preconditions.checkNotNull(xPathString); 75 try { 76 xPathExpression = XPATH_COMPILER.compile(xPathString); 77 } catch (XPathExpressionException e) { 78 throw new DroidDriverException("xPathString=" + xPathString, e); 79 } 80 } 81 82 @Override toString()83 public String toString() { 84 return Strings.toStringHelper(this).addValue(xPathString).toString(); 85 } 86 87 @Override find(UiElement context)88 public UiElement find(UiElement context) { 89 Element domNode = getDomNode((BaseUiElement<?, ?>) context, UiElement.VISIBLE); 90 try { 91 getDocument().appendChild(domNode); 92 Element foundNode = (Element) xPathExpression.evaluate(domNode, XPathConstants.NODE); 93 if (foundNode == null) { 94 Logs.log(Log.DEBUG, "XPath evaluation returns null for " + xPathString); 95 throw new ElementNotFoundException(this); 96 } 97 98 UiElement match = FROM_DOM_MAP.get(foundNode); 99 Logs.log(Log.INFO, "Found match: " + match); 100 return match; 101 } catch (XPathExpressionException e) { 102 throw new ElementNotFoundException(this, e); 103 } finally { 104 try { 105 getDocument().removeChild(domNode); 106 } catch (DOMException e) { 107 Logs.log(Log.ERROR, e, "Failed to clear document"); 108 document = null; // getDocument will create new 109 } 110 } 111 } 112 getDocument()113 private static Document getDocument() { 114 if (document == null) { 115 try { 116 document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); 117 } catch (ParserConfigurationException e) { 118 throw new DroidDriverException(e); 119 } 120 } 121 return document; 122 } 123 124 /** 125 * Returns the DOM node representing this UiElement. 126 */ getDomNode(BaseUiElement<?, ?> uiElement, Predicate<? super UiElement> predicate)127 private static Element getDomNode(BaseUiElement<?, ?> uiElement, 128 Predicate<? super UiElement> predicate) { 129 Element domNode = TO_DOM_MAP.get(uiElement); 130 if (domNode == null) { 131 domNode = buildDomNode(uiElement, predicate); 132 } 133 return domNode; 134 } 135 buildDomNode(BaseUiElement<?, ?> uiElement, Predicate<? super UiElement> predicate)136 private static Element buildDomNode(BaseUiElement<?, ?> uiElement, 137 Predicate<? super UiElement> predicate) { 138 String className = uiElement.getClassName(); 139 if (className == null) { 140 className = "UNKNOWN"; 141 } 142 Element element = getDocument().createElement(XPaths.tag(className)); 143 TO_DOM_MAP.put(uiElement, element); 144 FROM_DOM_MAP.put(element, uiElement); 145 146 setAttribute(element, Attribute.CLASS, className); 147 setAttribute(element, Attribute.RESOURCE_ID, uiElement.getResourceId()); 148 setAttribute(element, Attribute.PACKAGE, uiElement.getPackageName()); 149 setAttribute(element, Attribute.CONTENT_DESC, uiElement.getContentDescription()); 150 setAttribute(element, Attribute.TEXT, uiElement.getText()); 151 setAttribute(element, Attribute.CHECKABLE, uiElement.isCheckable()); 152 setAttribute(element, Attribute.CHECKED, uiElement.isChecked()); 153 setAttribute(element, Attribute.CLICKABLE, uiElement.isClickable()); 154 setAttribute(element, Attribute.ENABLED, uiElement.isEnabled()); 155 setAttribute(element, Attribute.FOCUSABLE, uiElement.isFocusable()); 156 setAttribute(element, Attribute.FOCUSED, uiElement.isFocused()); 157 setAttribute(element, Attribute.SCROLLABLE, uiElement.isScrollable()); 158 setAttribute(element, Attribute.LONG_CLICKABLE, uiElement.isLongClickable()); 159 setAttribute(element, Attribute.PASSWORD, uiElement.isPassword()); 160 if (uiElement.hasSelection()) { 161 element.setAttribute(Attribute.SELECTION_START.getName(), 162 Integer.toString(uiElement.getSelectionStart())); 163 element.setAttribute(Attribute.SELECTION_END.getName(), 164 Integer.toString(uiElement.getSelectionEnd())); 165 } 166 setAttribute(element, Attribute.SELECTED, uiElement.isSelected()); 167 element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds().toShortString()); 168 169 // If we're dumping for debugging, add extra information 170 if (!UiElement.VISIBLE.equals(predicate)) { 171 if (!uiElement.isVisible()) { 172 element.setAttribute(BaseUiElement.ATTRIB_NOT_VISIBLE, ""); 173 } else if (!uiElement.getVisibleBounds().equals(uiElement.getBounds())) { 174 element.setAttribute(BaseUiElement.ATTRIB_VISIBLE_BOUNDS, uiElement.getVisibleBounds() 175 .toShortString()); 176 } 177 } 178 179 for (BaseUiElement<?, ?> child : uiElement.getChildren(predicate)) { 180 element.appendChild(getDomNode(child, predicate)); 181 } 182 return element; 183 } 184 setAttribute(Element element, Attribute attr, String value)185 private static void setAttribute(Element element, Attribute attr, String value) { 186 if (value != null) { 187 element.setAttribute(attr.getName(), value); 188 } 189 } 190 191 // add attribute only if it's true setAttribute(Element element, Attribute attr, boolean value)192 private static void setAttribute(Element element, Attribute attr, boolean value) { 193 if (value) { 194 element.setAttribute(attr.getName(), ""); 195 } 196 } 197 dumpDom(String path, BaseUiElement<?, ?> uiElement)198 public static boolean dumpDom(String path, BaseUiElement<?, ?> uiElement) { 199 BufferedOutputStream bos = null; 200 try { 201 bos = FileUtils.open(path); 202 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 203 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 204 // find() filters invisible UiElements, but this is for debugging and 205 // invisible UiElements may be of interest. 206 clearData(); 207 Element domNode = getDomNode(uiElement, null); 208 transformer.transform(new DOMSource(domNode), new StreamResult(bos)); 209 Logs.log(Log.INFO, "Wrote dom to " + path); 210 } catch (Exception e) { 211 Logs.log(Log.ERROR, e, "Failed to transform node"); 212 return false; 213 } finally { 214 // We built DOM with invisible UiElements. Don't use it for find()! 215 clearData(); 216 if (bos != null) { 217 try { 218 bos.close(); 219 } catch (Exception e) { 220 // ignore 221 } 222 } 223 } 224 return true; 225 } 226 } 227