• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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