1cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin/*
2cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * Copyright (C) 2013 DroidDriver committers
3cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin *
4cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * Licensed under the Apache License, Version 2.0 (the "License");
5cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * you may not use this file except in compliance with the License.
6cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * You may obtain a copy of the License at
7cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin *
8cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin *      http://www.apache.org/licenses/LICENSE-2.0
9cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin *
10cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * Unless required by applicable law or agreed to in writing, software
11cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * distributed under the License is distributed on an "AS IS" BASIS,
12cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * See the License for the specific language governing permissions and
14cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * limitations under the License.
15cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin */
16cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
174b31201b5a2dbf8036da5a8d089a68a39cc1dc44Kevin Jinpackage io.appium.droiddriver.finders;
18cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
1917342a5115d7575d44a99fed9c7032e3ab316dccKevin Jinimport android.text.TextUtils;
20d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin
21cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin/**
22cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin * Convenience methods and constants for XPath.
235c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin * <p>
245c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin * DroidDriver implementation uses default XPath library on device, so the
255c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin * support may be limited to <a href="http://www.w3.org/TR/xpath/">XPath
265c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin * 1.0</a>. Newer XPath features may not be supported, for example, the
275c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin * fn:matches function.
28cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin */
29cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jinpublic class XPaths {
30cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
31cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  private XPaths() {}
32cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
33cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  /**
34cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   * @return The tag name used to build UiElement DOM. It is preferable to use
35cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   *         this to build XPath instead of String literals.
36cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   */
37cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  public static String tag(String className) {
38cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    return simpleClassName(className);
39cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  }
40cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
41cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  /**
42cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   * @return The tag name used to build UiElement DOM. It is preferable to use
43cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   *         this to build XPath instead of String literals.
44cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   */
45cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  public static String tag(Class<?> clazz) {
4617342a5115d7575d44a99fed9c7032e3ab316dccKevin Jin    return tag(clazz.getSimpleName());
47cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  }
48cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
49cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  private static String simpleClassName(String name) {
50cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    // the nth anonymous class has a class name ending in "Outer$n"
51cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    // and local inner classes have names ending in "Outer.$1Inner"
52cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    name = name.replaceAll("\\$[0-9]+", "\\$");
53cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
54cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    // we want the name of the inner class all by its lonesome
55cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    int start = name.lastIndexOf('$');
56cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
57cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    // if this isn't an inner class, just find the start of the
58cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    // top level class name.
59cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    if (start == -1) {
60cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin      start = name.lastIndexOf('.');
61cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    }
62cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    return name.substring(start + 1);
63cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  }
64cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
65cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  /**
66cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   * @return XPath predicate (with enclosing []) for boolean attribute that is
67cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   *         present
68cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   */
69cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  public static String is(Attribute attribute) {
70cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    return "[@" + attribute.getName() + "]";
71cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  }
72cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
73cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  /**
74cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   * @return XPath predicate (with enclosing []) for boolean attribute that is
75cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   *         NOT present
76cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin   */
77cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  public static String not(Attribute attribute) {
78cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin    return "[not(@" + attribute.getName() + ")]";
79cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  }
80cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin
81cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  /** @return XPath predicate (with enclosing []) for attribute with value */
82cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  public static String attr(Attribute attribute, String value) {
83d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    return String.format("[@%s=%s]", attribute.getName(), quoteXPathLiteral(value));
84cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin  }
855c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin
869f554eb6d4d25a0a31be3ab88fb715fc3cee4027Tony Wickham  /** @return XPath predicate (with enclosing []) for attribute containing value */
879f554eb6d4d25a0a31be3ab88fb715fc3cee4027Tony Wickham  public static String containsAttr(Attribute attribute, String containedValue) {
88d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    return String.format("[contains(@%s, %s)]", attribute.getName(),
89d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin        quoteXPathLiteral(containedValue));
909f554eb6d4d25a0a31be3ab88fb715fc3cee4027Tony Wickham  }
919f554eb6d4d25a0a31be3ab88fb715fc3cee4027Tony Wickham
925c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin  /** Shorthand for {@link #attr}{@code (Attribute.TEXT, value)} */
935c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin  public static String text(String value) {
945c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin    return attr(Attribute.TEXT, value);
955c0ca5383d9a90b6d5e9c246f387e6261fed6211Kevin Jin  }
96d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin
9770e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin  /** Shorthand for {@link #attr}{@code (Attribute.RESOURCE_ID, value)} */
9870e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin  public static String resourceId(String value) {
9970e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin    return attr(Attribute.RESOURCE_ID, value);
10070e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin  }
10170e34108e0fc19277e642aef3b36b65b8e254899Kevin Jin
102d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin  /**
1030319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin   * @return XPath predicate (with enclosing []) that filters nodes with
1040319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin   *         descendants satisfying {@code descendantPredicate}.
1050319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin   */
1060319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin  public static String withDescendant(String descendantPredicate) {
1070319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin    return "[.//*" + descendantPredicate + "]";
1080319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin  }
1090319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin
1100319e7c14a536a11851cc30cfa57241ce90fec11Kevin Jin  /**
111d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   * Adapted from http://stackoverflow.com/questions/1341847/.
112d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   * <p>
113d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   * Produce an XPath literal equal to the value if possible; if not, produce an
114d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   * XPath expression that will match the value. Note that this function will
115d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   * produce very long XPath expressions if a value contains a long run of
116d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   * double quotes.
117d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin   */
118ffd0f7a9a89c3c19bc5846b23237d96ed8accbfdKevin Jin  static String quoteXPathLiteral(String value) {
119d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    // if the value contains only single or double quotes, construct an XPath
120d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    // literal
121d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    if (!value.contains("\"")) {
122d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin      return "\"" + value + "\"";
123d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    }
124d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    if (!value.contains("'")) {
125d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin      return "'" + value + "'";
126d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    }
127d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin
128d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    // if the value contains both single and double quotes, construct an
129d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    // expression that concatenates all non-double-quote substrings with
130d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    // the quotes, e.g.:
131d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    // concat("foo", '"', "bar")
132d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    StringBuilder sb = new StringBuilder();
133d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    sb.append("concat(\"");
13417342a5115d7575d44a99fed9c7032e3ab316dccKevin Jin    sb.append(TextUtils.join("\",'\"',\"", value.split("\"")));
135d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    sb.append("\")");
136d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin    return sb.toString();
137d88d4ab289d47627418aa500d43b9c11168acba7Kevin Jin  }
138cd9468bc18d0e8250fc495f1ec656667eb206526Kevin Jin}
139