1 /* 2 * Copyright (C) 2011 The Android Open Source Project 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 com.android.tools.lint.detector.api; 18 19 import static com.android.tools.lint.detector.api.LintConstants.DOT_XML; 20 import static com.android.tools.lint.detector.api.LintConstants.ID_RESOURCE_PREFIX; 21 import static com.android.tools.lint.detector.api.LintConstants.NEW_ID_RESOURCE_PREFIX; 22 23 import com.android.annotations.NonNull; 24 import com.android.annotations.Nullable; 25 import com.android.resources.FolderTypeRelationship; 26 import com.android.resources.ResourceFolderType; 27 import com.android.resources.ResourceType; 28 import com.android.util.PositionXmlParser; 29 import com.google.common.annotations.Beta; 30 import com.google.common.io.Files; 31 32 import org.w3c.dom.Element; 33 import org.w3c.dom.Node; 34 import org.w3c.dom.NodeList; 35 36 import java.io.File; 37 import java.io.IOException; 38 import java.io.UnsupportedEncodingException; 39 import java.util.ArrayList; 40 import java.util.List; 41 42 43 /** 44 * Useful utility methods related to lint. 45 * <p> 46 * <b>NOTE: This is not a public or final API; if you rely on this be prepared 47 * to adjust your code for the next tools release.</b> 48 */ 49 @Beta 50 public class LintUtils { 51 /** 52 * Format a list of strings, and cut of the list at {@code maxItems} if the 53 * number of items are greater. 54 * 55 * @param strings the list of strings to print out as a comma separated list 56 * @param maxItems the maximum number of items to print 57 * @return a comma separated list 58 */ 59 @NonNull formatList(@onNull List<String> strings, int maxItems)60 public static String formatList(@NonNull List<String> strings, int maxItems) { 61 StringBuilder sb = new StringBuilder(20 * strings.size()); 62 63 for (int i = 0, n = strings.size(); i < n; i++) { 64 if (sb.length() > 0) { 65 sb.append(", "); //$NON-NLS-1$ 66 } 67 sb.append(strings.get(i)); 68 69 if (maxItems > 0 && i == maxItems - 1 && n > maxItems) { 70 sb.append(String.format("... (%1$d more)", n - i - 1)); 71 break; 72 } 73 } 74 75 return sb.toString(); 76 } 77 78 /** 79 * Determine if the given type corresponds to a resource that has a unique 80 * file 81 * 82 * @param type the resource type to check 83 * @return true if the given type corresponds to a file-type resource 84 */ isFileBasedResourceType(@onNull ResourceType type)85 public static boolean isFileBasedResourceType(@NonNull ResourceType type) { 86 List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); 87 for (ResourceFolderType folderType : folderTypes) { 88 if (folderType != ResourceFolderType.VALUES) { 89 if (type == ResourceType.ID) { 90 return false; 91 } 92 return true; 93 } 94 } 95 return false; 96 } 97 98 /** 99 * Returns true if the given file represents an XML file 100 * 101 * @param file the file to be checked 102 * @return true if the given file is an xml file 103 */ isXmlFile(@onNull File file)104 public static boolean isXmlFile(@NonNull File file) { 105 String string = file.getName(); 106 return string.regionMatches(true, string.length() - DOT_XML.length(), 107 DOT_XML, 0, DOT_XML.length()); 108 } 109 110 /** 111 * Case insensitive ends with 112 * 113 * @param string the string to be tested whether it ends with the given 114 * suffix 115 * @param suffix the suffix to check 116 * @return true if {@code string} ends with {@code suffix}, 117 * case-insensitively. 118 */ endsWith(@onNull String string, @NonNull String suffix)119 public static boolean endsWith(@NonNull String string, @NonNull String suffix) { 120 return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(), 121 suffix, 0, suffix.length()); 122 } 123 124 /** 125 * Case insensitive starts with 126 * 127 * @param string the string to be tested whether it starts with the given prefix 128 * @param prefix the prefix to check 129 * @param offset the offset to start checking with 130 * @return true if {@code string} starts with {@code prefix}, 131 * case-insensitively. 132 */ startsWith(@onNull String string, @NonNull String prefix, int offset)133 public static boolean startsWith(@NonNull String string, @NonNull String prefix, int offset) { 134 return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length()); 135 } 136 137 /** 138 * Returns the basename of the given filename, unless it's a dot-file such as ".svn". 139 * 140 * @param fileName the file name to extract the basename from 141 * @return the basename (the filename without the file extension) 142 */ getBaseName(@onNull String fileName)143 public static String getBaseName(@NonNull String fileName) { 144 int extension = fileName.indexOf('.'); 145 if (extension > 0) { 146 return fileName.substring(0, extension); 147 } else { 148 return fileName; 149 } 150 } 151 152 /** 153 * Returns the children elements of the given node 154 * 155 * @param node the parent node 156 * @return a list of element children, never null 157 */ 158 @NonNull getChildren(@onNull Node node)159 public static List<Element> getChildren(@NonNull Node node) { 160 NodeList childNodes = node.getChildNodes(); 161 List<Element> children = new ArrayList<Element>(childNodes.getLength()); 162 for (int i = 0, n = childNodes.getLength(); i < n; i++) { 163 Node child = childNodes.item(i); 164 if (child.getNodeType() == Node.ELEMENT_NODE) { 165 children.add((Element) child); 166 } 167 } 168 169 return children; 170 } 171 172 /** 173 * Returns the <b>number</b> of children of the given node 174 * 175 * @param node the parent node 176 * @return the count of element children 177 */ getChildCount(@onNull Node node)178 public static int getChildCount(@NonNull Node node) { 179 NodeList childNodes = node.getChildNodes(); 180 int childCount = 0; 181 for (int i = 0, n = childNodes.getLength(); i < n; i++) { 182 Node child = childNodes.item(i); 183 if (child.getNodeType() == Node.ELEMENT_NODE) { 184 childCount++; 185 } 186 } 187 188 return childCount; 189 } 190 191 /** 192 * Returns true if the given element is the root element of its document 193 * 194 * @param element the element to test 195 * @return true if the element is the root element 196 */ isRootElement(Element element)197 public static boolean isRootElement(Element element) { 198 return element == element.getOwnerDocument().getDocumentElement(); 199 } 200 201 /** 202 * Returns the given id without an {@code @id/} or {@code @+id} prefix 203 * 204 * @param id the id to strip 205 * @return the stripped id, never null 206 */ 207 @NonNull stripIdPrefix(@ullable String id)208 public static String stripIdPrefix(@Nullable String id) { 209 if (id == null) { 210 return ""; 211 } else if (id.startsWith(NEW_ID_RESOURCE_PREFIX)) { 212 return id.substring(NEW_ID_RESOURCE_PREFIX.length()); 213 } else if (id.startsWith(ID_RESOURCE_PREFIX)) { 214 return id.substring(ID_RESOURCE_PREFIX.length()); 215 } 216 217 return id; 218 } 219 220 /** 221 * Returns true if the given two id references match. This is similar to 222 * String equality, but it also considers "{@code @+id/foo == @id/foo}. 223 * 224 * @param id1 the first id to compare 225 * @param id2 the second id to compare 226 * @return true if the two id references refer to the same id 227 */ idReferencesMatch(String id1, String id2)228 public static boolean idReferencesMatch(String id1, String id2) { 229 if (id1.startsWith(NEW_ID_RESOURCE_PREFIX)) { 230 if (id2.startsWith(NEW_ID_RESOURCE_PREFIX)) { 231 return id1.equals(id2); 232 } else { 233 assert id2.startsWith(ID_RESOURCE_PREFIX); 234 return ((id1.length() - id2.length()) 235 == (NEW_ID_RESOURCE_PREFIX.length() - ID_RESOURCE_PREFIX.length())) 236 && id1.regionMatches(NEW_ID_RESOURCE_PREFIX.length(), id2, 237 ID_RESOURCE_PREFIX.length(), 238 id2.length() - ID_RESOURCE_PREFIX.length()); 239 } 240 } else { 241 assert id1.startsWith(ID_RESOURCE_PREFIX); 242 if (id2.startsWith(ID_RESOURCE_PREFIX)) { 243 return id1.equals(id2); 244 } else { 245 assert id2.startsWith(NEW_ID_RESOURCE_PREFIX); 246 return (id2.length() - id1.length() 247 == (NEW_ID_RESOURCE_PREFIX.length() - ID_RESOURCE_PREFIX.length())) 248 && id2.regionMatches(NEW_ID_RESOURCE_PREFIX.length(), id1, 249 ID_RESOURCE_PREFIX.length(), 250 id1.length() - ID_RESOURCE_PREFIX.length()); 251 } 252 } 253 } 254 255 /** 256 * Computes the edit distance (number of insertions, deletions or substitutions 257 * to edit one string into the other) between two strings. In particular, 258 * this will compute the Levenshtein distance. 259 * <p> 260 * See http://en.wikipedia.org/wiki/Levenshtein_distance for details. 261 * 262 * @param s the first string to compare 263 * @param t the second string to compare 264 * @return the edit distance between the two strings 265 */ editDistance(@onNull String s, @NonNull String t)266 public static int editDistance(@NonNull String s, @NonNull String t) { 267 int m = s.length(); 268 int n = t.length(); 269 int[][] d = new int[m + 1][n + 1]; 270 for (int i = 0; i <= m; i++) { 271 d[i][0] = i; 272 } 273 for (int j = 0; j <= n; j++) { 274 d[0][j] = j; 275 } 276 for (int j = 1; j <= n; j++) { 277 for (int i = 1; i <= m; i++) { 278 if (s.charAt(i - 1) == t.charAt(j - 1)) { 279 d[i][j] = d[i - 1][j - 1]; 280 } else { 281 int deletion = d[i - 1][j] + 1; 282 int insertion = d[i][j - 1] + 1; 283 int substitution = d[i - 1][j - 1] + 1; 284 d[i][j] = Math.min(deletion, Math.min(insertion, substitution)); 285 } 286 } 287 } 288 289 return d[m][n]; 290 } 291 292 /** 293 * Returns true if assertions are enabled 294 * 295 * @return true if assertions are enabled 296 */ 297 @SuppressWarnings("all") assertionsEnabled()298 public static boolean assertionsEnabled() { 299 boolean assertionsEnabled = false; 300 assert assertionsEnabled = true; // Intentional side-effect 301 return assertionsEnabled; 302 } 303 304 /** 305 * Returns the layout resource name for the given layout file 306 * 307 * @param layoutFile the file pointing to the layout 308 * @return the layout resource name, not including the {@code @layout} 309 * prefix 310 */ getLayoutName(File layoutFile)311 public static String getLayoutName(File layoutFile) { 312 String name = layoutFile.getName(); 313 int dotIndex = name.indexOf('.'); 314 if (dotIndex != -1) { 315 name = name.substring(0, dotIndex); 316 } 317 return name; 318 } 319 320 /** 321 * Computes the shared parent among a set of files (which may be null). 322 * 323 * @param files the set of files to be checked 324 * @return the closest common ancestor file, or null if none was found 325 */ 326 @Nullable getCommonParent(@onNull List<File> files)327 public static File getCommonParent(@NonNull List<File> files) { 328 int fileCount = files.size(); 329 if (fileCount == 0) { 330 return null; 331 } else if (fileCount == 1) { 332 return files.get(0); 333 } else if (fileCount == 2) { 334 return getCommonParent(files.get(0), files.get(1)); 335 } else { 336 File common = files.get(0); 337 for (int i = 1; i < fileCount; i++) { 338 common = getCommonParent(common, files.get(i)); 339 if (common == null) { 340 return null; 341 } 342 } 343 344 return common; 345 } 346 } 347 348 /** 349 * Computes the closest common parent path between two files. 350 * 351 * @param file1 the first file to be compared 352 * @param file2 the second file to be compared 353 * @return the closest common ancestor file, or null if the two files have 354 * no common parent 355 */ 356 @Nullable getCommonParent(@onNull File file1, @NonNull File file2)357 public static File getCommonParent(@NonNull File file1, @NonNull File file2) { 358 if (file1.equals(file2)) { 359 return file1; 360 } else if (file1.getPath().startsWith(file2.getPath())) { 361 return file2; 362 } else if (file2.getPath().startsWith(file1.getPath())) { 363 return file1; 364 } else { 365 // Dumb and simple implementation 366 File first = file1.getParentFile(); 367 while (first != null) { 368 File second = file2.getParentFile(); 369 while (second != null) { 370 if (first.equals(second)) { 371 return first; 372 } 373 second = second.getParentFile(); 374 } 375 376 first = first.getParentFile(); 377 } 378 } 379 return null; 380 } 381 382 private static final String UTF_8 = "UTF-8"; //$NON-NLS-1$ 383 private static final String UTF_16 = "UTF_16"; //$NON-NLS-1$ 384 private static final String UTF_16LE = "UTF_16LE"; //$NON-NLS-1$ 385 386 /** 387 * Returns the encoded String for the given file. This is usually the 388 * same as {@code Files.toString(file, Charsets.UTF8}, but if there's a UTF byte order mark 389 * (for UTF8, UTF_16 or UTF_16LE), use that instead. 390 * 391 * @param file the file to read from 392 * @return the string 393 * @throws IOException if the file cannot be read properly 394 */ getEncodedString(File file)395 public static String getEncodedString(File file) throws IOException { 396 byte[] bytes = Files.toByteArray(file); 397 if (endsWith(file.getName(), DOT_XML)) { 398 return PositionXmlParser.getXmlString(bytes); 399 } 400 401 return LintUtils.getEncodedString(bytes); 402 } 403 404 /** 405 * Returns the String corresponding to the given data. This is usually the 406 * same as {@code new String(data)}, but if there's a UTF byte order mark 407 * (for UTF8, UTF_16 or UTF_16LE), use that instead. 408 * <p> 409 * NOTE: For XML files, there is the additional complication that there 410 * could be a {@code encoding=} attribute in the prologue. For those files, 411 * use {@link PositionXmlParser#getXmlString(byte[])} instead. 412 * 413 * @param data the byte array to construct the string from 414 * @return the string 415 */ getEncodedString(byte[] data)416 public static String getEncodedString(byte[] data) { 417 if (data == null) { 418 return ""; 419 } 420 421 int offset = 0; 422 String defaultCharset = UTF_8; 423 String charset = null; 424 // Look for the byte order mark, to see if we need to remove bytes from 425 // the input stream (and to determine whether files are big endian or little endian) etc 426 // for files which do not specify the encoding. 427 // See http://unicode.org/faq/utf_bom.html#BOM for more. 428 if (data.length > 4) { 429 if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) { 430 // UTF-8 431 defaultCharset = charset = UTF_8; 432 offset += 3; 433 } else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) { 434 // UTF-16, big-endian 435 defaultCharset = charset = UTF_16; 436 offset += 2; 437 } else if (data[0] == (byte)0x0 && data[1] == (byte)0x0 438 && data[2] == (byte)0xfe && data[3] == (byte)0xff) { 439 // UTF-32, big-endian 440 defaultCharset = charset = "UTF_32"; //$NON-NLS-1$ 441 offset += 4; 442 } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe 443 && data[2] == (byte)0x0 && data[3] == (byte)0x0) { 444 // UTF-32, little-endian. We must check for this *before* looking for 445 // UTF_16LE since UTF_32LE has the same prefix! 446 defaultCharset = charset = "UTF_32LE"; //$NON-NLS-1$ 447 offset += 4; 448 } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) { 449 // UTF-16, little-endian 450 defaultCharset = charset = UTF_16LE; 451 offset += 2; 452 } 453 } 454 int length = data.length - offset; 455 456 // Guess encoding by searching for an encoding= entry in the first line. 457 boolean seenOddZero = false; 458 boolean seenEvenZero = false; 459 for (int lineEnd = offset; lineEnd < data.length; lineEnd++) { 460 if (data[lineEnd] == 0) { 461 if ((lineEnd - offset) % 1 == 0) { 462 seenEvenZero = true; 463 } else { 464 seenOddZero = true; 465 } 466 } else if (data[lineEnd] == '\n' || data[lineEnd] == '\r') { 467 break; 468 } 469 } 470 471 if (charset == null) { 472 charset = seenOddZero ? UTF_16 : seenEvenZero ? UTF_16LE : UTF_8; 473 } 474 475 String text = null; 476 try { 477 text = new String(data, offset, length, charset); 478 } catch (UnsupportedEncodingException e) { 479 try { 480 if (charset != defaultCharset) { 481 text = new String(data, offset, length, defaultCharset); 482 } 483 } catch (UnsupportedEncodingException u) { 484 // Just use the default encoding below 485 } 486 } 487 if (text == null) { 488 text = new String(data, offset, length); 489 } 490 return text; 491 } 492 } 493