ByXPath.java revision eb4e1921c193bb90eb0122ea7b0fd37cef60e8e1
1c134b29628280a680fdf2696480166de56074b1eKevin Jin/*
2c134b29628280a680fdf2696480166de56074b1eKevin Jin * Copyright (C) 2013 DroidDriver committers
3c134b29628280a680fdf2696480166de56074b1eKevin Jin *
4c134b29628280a680fdf2696480166de56074b1eKevin Jin * Licensed under the Apache License, Version 2.0 (the "License");
5c134b29628280a680fdf2696480166de56074b1eKevin Jin * you may not use this file except in compliance with the License.
6c134b29628280a680fdf2696480166de56074b1eKevin Jin * You may obtain a copy of the License at
7c134b29628280a680fdf2696480166de56074b1eKevin Jin *
8c134b29628280a680fdf2696480166de56074b1eKevin Jin *      http://www.apache.org/licenses/LICENSE-2.0
9c134b29628280a680fdf2696480166de56074b1eKevin Jin *
10c134b29628280a680fdf2696480166de56074b1eKevin Jin * Unless required by applicable law or agreed to in writing, software
11c134b29628280a680fdf2696480166de56074b1eKevin Jin * distributed under the License is distributed on an "AS IS" BASIS,
12c134b29628280a680fdf2696480166de56074b1eKevin Jin * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13c134b29628280a680fdf2696480166de56074b1eKevin Jin * See the License for the specific language governing permissions and
14c134b29628280a680fdf2696480166de56074b1eKevin Jin * limitations under the License.
15c134b29628280a680fdf2696480166de56074b1eKevin Jin */
167576fbbba2bf515908b45293b7156b5bfe088938Kevin Jinpackage com.google.android.droiddriver.finders;
17c134b29628280a680fdf2696480166de56074b1eKevin Jin
1879884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport android.util.Log;
1979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
2079884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport com.google.android.droiddriver.UiElement;
2179884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport com.google.android.droiddriver.base.AbstractUiElement;
22c134b29628280a680fdf2696480166de56074b1eKevin Jinimport com.google.android.droiddriver.exceptions.DroidDriverException;
2379884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport com.google.android.droiddriver.exceptions.ElementNotFoundException;
246316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport com.google.android.droiddriver.util.FileUtils;
2579884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport com.google.android.droiddriver.util.Logs;
26c134b29628280a680fdf2696480166de56074b1eKevin Jinimport com.google.common.base.Objects;
27c134b29628280a680fdf2696480166de56074b1eKevin Jinimport com.google.common.base.Preconditions;
28c134b29628280a680fdf2696480166de56074b1eKevin Jin
2979884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport org.w3c.dom.DOMException;
3079884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport org.w3c.dom.Document;
3179884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport org.w3c.dom.Element;
3279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
336316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport java.io.BufferedOutputStream;
346316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin
3579884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport javax.xml.parsers.DocumentBuilderFactory;
3679884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport javax.xml.parsers.ParserConfigurationException;
376316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport javax.xml.transform.OutputKeys;
386316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport javax.xml.transform.Transformer;
396316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport javax.xml.transform.TransformerFactory;
406316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport javax.xml.transform.dom.DOMSource;
416316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jinimport javax.xml.transform.stream.StreamResult;
42fc21930b0a5cc598f59c0c893274e4ce33abd235Kevin Jinimport javax.xml.xpath.XPath;
4379884ac460f8d357c28091ec3f42fe369964b809Kevin Jinimport javax.xml.xpath.XPathConstants;
44c134b29628280a680fdf2696480166de56074b1eKevin Jinimport javax.xml.xpath.XPathExpression;
45c134b29628280a680fdf2696480166de56074b1eKevin Jinimport javax.xml.xpath.XPathExpressionException;
46c134b29628280a680fdf2696480166de56074b1eKevin Jinimport javax.xml.xpath.XPathFactory;
47c134b29628280a680fdf2696480166de56074b1eKevin Jin
48fc21930b0a5cc598f59c0c893274e4ce33abd235Kevin Jin/**
49fc21930b0a5cc598f59c0c893274e4ce33abd235Kevin Jin * Find matching UiElement by XPath.
50fc21930b0a5cc598f59c0c893274e4ce33abd235Kevin Jin */
517576fbbba2bf515908b45293b7156b5bfe088938Kevin Jinpublic class ByXPath implements Finder {
52fc21930b0a5cc598f59c0c893274e4ce33abd235Kevin Jin  private static final XPath XPATH_COMPILER = XPathFactory.newInstance().newXPath();
5379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  private static final String UI_ELEMENT = "UiElement";
5479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  // document needs to be static so that when buildDomNode is called recursively
5579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  // on children they are in the same document to be appended.
5679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  private static Document document;
578e4d4bb6a6c2b2e4a470c0804833de2a92f154d5Kevin Jin  private final String xPathString;
58c134b29628280a680fdf2696480166de56074b1eKevin Jin  private final XPathExpression xPathExpression;
59c134b29628280a680fdf2696480166de56074b1eKevin Jin
608e4d4bb6a6c2b2e4a470c0804833de2a92f154d5Kevin Jin  protected ByXPath(String xPathString) {
618e4d4bb6a6c2b2e4a470c0804833de2a92f154d5Kevin Jin    this.xPathString = Preconditions.checkNotNull(xPathString);
62c134b29628280a680fdf2696480166de56074b1eKevin Jin    try {
63fc21930b0a5cc598f59c0c893274e4ce33abd235Kevin Jin      xPathExpression = XPATH_COMPILER.compile(xPathString);
64c134b29628280a680fdf2696480166de56074b1eKevin Jin    } catch (XPathExpressionException e) {
656316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      throw new DroidDriverException("xPathString=" + xPathString, e);
66c134b29628280a680fdf2696480166de56074b1eKevin Jin    }
67c134b29628280a680fdf2696480166de56074b1eKevin Jin  }
68c134b29628280a680fdf2696480166de56074b1eKevin Jin
69c134b29628280a680fdf2696480166de56074b1eKevin Jin  @Override
70c134b29628280a680fdf2696480166de56074b1eKevin Jin  public String toString() {
718e4d4bb6a6c2b2e4a470c0804833de2a92f154d5Kevin Jin    return Objects.toStringHelper(this).addValue(xPathString).toString();
72c134b29628280a680fdf2696480166de56074b1eKevin Jin  }
73c134b29628280a680fdf2696480166de56074b1eKevin Jin
7479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  @Override
7579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  public UiElement find(UiElement context) {
7679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    AbstractUiElement contextNode = (AbstractUiElement) context;
7779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    Element domNode = contextNode.getDomNode();
7879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    try {
7979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      getDocument().appendChild(domNode);
8079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      Element foundNode = (Element) xPathExpression.evaluate(domNode, XPathConstants.NODE);
8179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      if (foundNode == null) {
826316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin        Logs.log(Log.DEBUG, "XPath evaluation returns null for " + xPathString);
8379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        throw new ElementNotFoundException(this);
8479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      }
85eb4e1921c193bb90eb0122ea7b0fd37cef60e8e1Kevin Jin
86eb4e1921c193bb90eb0122ea7b0fd37cef60e8e1Kevin Jin      UiElement match = (UiElement) foundNode.getUserData(UI_ELEMENT);
87eb4e1921c193bb90eb0122ea7b0fd37cef60e8e1Kevin Jin      Logs.log(Log.INFO, "Found match: " + match);
88eb4e1921c193bb90eb0122ea7b0fd37cef60e8e1Kevin Jin      return match;
8979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    } catch (XPathExpressionException e) {
9079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      throw new ElementNotFoundException(this, e);
9179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    } finally {
9279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      try {
9379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        getDocument().removeChild(domNode);
9479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      } catch (DOMException ignored) {
9579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        document = null; // getDocument will create new
9679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      }
9779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    }
9879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  }
9979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
10079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  private static Document getDocument() {
10179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    if (document == null) {
10279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      try {
10379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
10479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      } catch (ParserConfigurationException e) {
10579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        throw new DroidDriverException(e);
10679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      }
10779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    }
10879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    return document;
10979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  }
11079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
11179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  /**
11279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin   * Used internally in {@link AbstractUiElement}.
11379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin   */
11479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  public static Element buildDomNode(AbstractUiElement uiElement) {
11579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    String className = uiElement.getClassName();
11679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    if (className == null) {
11779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      className = "UNKNOWN";
11879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    }
11979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    Element element = getDocument().createElement(XPaths.tag(className));
12079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    element.setUserData(UI_ELEMENT, uiElement, null /* UserDataHandler */);
12179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
12279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.CLASS, className);
12379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.RESOURCE_ID, uiElement.getResourceId());
12479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.PACKAGE, uiElement.getPackageName());
12579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.CONTENT_DESC, uiElement.getContentDescription());
12679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.TEXT, uiElement.getText());
12779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.CHECKABLE, uiElement.isCheckable());
12879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.CHECKED, uiElement.isChecked());
12979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.CLICKABLE, uiElement.isClickable());
13079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.ENABLED, uiElement.isEnabled());
13179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.FOCUSABLE, uiElement.isFocusable());
13279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.FOCUSED, uiElement.isFocused());
13379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.SCROLLABLE, uiElement.isScrollable());
13479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.LONG_CLICKABLE, uiElement.isLongClickable());
13579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.PASSWORD, uiElement.isPassword());
13679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    setAttribute(element, Attribute.SELECTED, uiElement.isSelected());
13779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds().toShortString());
13879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
13979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    // TODO: visitor pattern
14079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    int childCount = uiElement.getChildCount();
14179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    for (int i = 0; i < childCount; i++) {
14279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      AbstractUiElement child = uiElement.getChild(i);
14379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      if (child == null) {
1446316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin        Logs.log(Log.WARN, "Skip null child for " + uiElement);
14579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        continue;
14679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      }
14779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      if (!child.isVisible()) {
1486316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin        Logs.log(Log.VERBOSE, "Skip invisible child: " + child);
14979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin        continue;
15079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      }
15179884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
15279884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      element.appendChild(child.getDomNode());
15379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    }
15479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    return element;
15579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  }
15679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin
15779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  private static void setAttribute(Element element, Attribute attr, String value) {
15879884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    if (value != null) {
15979884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      element.setAttribute(attr.getName(), value);
16079884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    }
161c134b29628280a680fdf2696480166de56074b1eKevin Jin  }
1628e4d4bb6a6c2b2e4a470c0804833de2a92f154d5Kevin Jin
16379884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  // add attribute only if it's true
16479884ac460f8d357c28091ec3f42fe369964b809Kevin Jin  private static void setAttribute(Element element, Attribute attr, boolean value) {
16579884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    if (value) {
16679884ac460f8d357c28091ec3f42fe369964b809Kevin Jin      element.setAttribute(attr.getName(), "");
16779884ac460f8d357c28091ec3f42fe369964b809Kevin Jin    }
1688e4d4bb6a6c2b2e4a470c0804833de2a92f154d5Kevin Jin  }
1696316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin
1706316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin  public static boolean dumpDom(String path, AbstractUiElement uiElement) {
1716316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin    BufferedOutputStream bos = null;
1726316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin    try {
1736316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      bos = FileUtils.open(path);
1746316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      Transformer transformer = TransformerFactory.newInstance().newTransformer();
1756316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1766316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      transformer.transform(new DOMSource(uiElement.getDomNode()), new StreamResult(bos));
1776316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      Logs.log(Log.INFO, "Wrote dom to " + path);
1786316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin    } catch (Exception e) {
1796316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      Logs.log(Log.ERROR, e, "Fail to transform node");
1806316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      return false;
1816316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin    } finally {
1826316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      if (bos != null) {
1836316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin        try {
1846316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin          bos.close();
1856316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin        } catch (Exception e) {
1866316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin          // ignore
1876316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin        }
1886316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin      }
1896316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin    }
1906316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin    return true;
1916316362de61fca700d7d5a455ad5c0ac9717c365Kevin Jin  }
192c134b29628280a680fdf2696480166de56074b1eKevin Jin}
193