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 17 package io.appium.droiddriver.finders; 18 19 import android.text.TextUtils; 20 21 /** 22 * Convenience methods and constants for XPath. 23 * <p> 24 * DroidDriver implementation uses default XPath library on device, so the 25 * support may be limited to <a href="http://www.w3.org/TR/xpath/">XPath 26 * 1.0</a>. Newer XPath features may not be supported, for example, the 27 * fn:matches function. 28 */ 29 public class XPaths { 30 XPaths()31 private XPaths() {} 32 33 /** 34 * @return The tag name used to build UiElement DOM. It is preferable to use 35 * this to build XPath instead of String literals. 36 */ tag(String className)37 public static String tag(String className) { 38 return simpleClassName(className); 39 } 40 41 /** 42 * @return The tag name used to build UiElement DOM. It is preferable to use 43 * this to build XPath instead of String literals. 44 */ tag(Class<?> clazz)45 public static String tag(Class<?> clazz) { 46 return tag(clazz.getSimpleName()); 47 } 48 simpleClassName(String name)49 private static String simpleClassName(String name) { 50 // the nth anonymous class has a class name ending in "Outer$n" 51 // and local inner classes have names ending in "Outer.$1Inner" 52 name = name.replaceAll("\\$[0-9]+", "\\$"); 53 54 // we want the name of the inner class all by its lonesome 55 int start = name.lastIndexOf('$'); 56 57 // if this isn't an inner class, just find the start of the 58 // top level class name. 59 if (start == -1) { 60 start = name.lastIndexOf('.'); 61 } 62 return name.substring(start + 1); 63 } 64 65 /** 66 * @return XPath predicate (with enclosing []) for boolean attribute that is 67 * present 68 */ is(Attribute attribute)69 public static String is(Attribute attribute) { 70 return "[@" + attribute.getName() + "]"; 71 } 72 73 /** 74 * @return XPath predicate (with enclosing []) for boolean attribute that is 75 * NOT present 76 */ not(Attribute attribute)77 public static String not(Attribute attribute) { 78 return "[not(@" + attribute.getName() + ")]"; 79 } 80 81 /** @return XPath predicate (with enclosing []) for attribute with value */ attr(Attribute attribute, String value)82 public static String attr(Attribute attribute, String value) { 83 return String.format("[@%s=%s]", attribute.getName(), quoteXPathLiteral(value)); 84 } 85 86 /** @return XPath predicate (with enclosing []) for attribute containing value */ containsAttr(Attribute attribute, String containedValue)87 public static String containsAttr(Attribute attribute, String containedValue) { 88 return String.format("[contains(@%s, %s)]", attribute.getName(), 89 quoteXPathLiteral(containedValue)); 90 } 91 92 /** Shorthand for {@link #attr}{@code (Attribute.TEXT, value)} */ text(String value)93 public static String text(String value) { 94 return attr(Attribute.TEXT, value); 95 } 96 97 /** Shorthand for {@link #attr}{@code (Attribute.RESOURCE_ID, value)} */ resourceId(String value)98 public static String resourceId(String value) { 99 return attr(Attribute.RESOURCE_ID, value); 100 } 101 102 /** 103 * @return XPath predicate (with enclosing []) that filters nodes with 104 * descendants satisfying {@code descendantPredicate}. 105 */ withDescendant(String descendantPredicate)106 public static String withDescendant(String descendantPredicate) { 107 return "[.//*" + descendantPredicate + "]"; 108 } 109 110 /** 111 * Adapted from http://stackoverflow.com/questions/1341847/. 112 * <p> 113 * Produce an XPath literal equal to the value if possible; if not, produce an 114 * XPath expression that will match the value. Note that this function will 115 * produce very long XPath expressions if a value contains a long run of 116 * double quotes. 117 */ quoteXPathLiteral(String value)118 static String quoteXPathLiteral(String value) { 119 // if the value contains only single or double quotes, construct an XPath 120 // literal 121 if (!value.contains("\"")) { 122 return "\"" + value + "\""; 123 } 124 if (!value.contains("'")) { 125 return "'" + value + "'"; 126 } 127 128 // if the value contains both single and double quotes, construct an 129 // expression that concatenates all non-double-quote substrings with 130 // the quotes, e.g.: 131 // concat("foo", '"', "bar") 132 StringBuilder sb = new StringBuilder(); 133 sb.append("concat(\""); 134 sb.append(TextUtils.join("\",'\"',\"", value.split("\""))); 135 sb.append("\")"); 136 return sb.toString(); 137 } 138 } 139