| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.utils; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.google.common.base.CaseFormat; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.io.Closeables; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.text.NumberFormat; |
| import java.text.ParseException; |
| import java.util.List; |
| |
| import static com.android.SdkConstants.DOT_XML; |
| import static com.android.SdkConstants.DOT_PNG; |
| import static com.android.SdkConstants.DOT_GIF; |
| import static com.android.SdkConstants.DOT_9PNG; |
| import static com.android.SdkConstants.DOT_JPEG; |
| import static com.android.SdkConstants.DOT_JPG; |
| import static com.android.SdkConstants.DOT_BMP; |
| |
| /** Miscellaneous utilities used by the Android SDK tools */ |
| public class SdkUtils { |
| /** |
| * Returns true if the given string ends with the given suffix, using a |
| * case-insensitive comparison. |
| * |
| * @param string the full string to be checked |
| * @param suffix the suffix to be checked for |
| * @return true if the string case-insensitively ends with the given suffix |
| */ |
| public static boolean endsWithIgnoreCase(@NonNull String string, @NonNull String suffix) { |
| return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(), |
| suffix, 0, suffix.length()); |
| } |
| |
| /** |
| * Returns true if the given sequence ends with the given suffix (case |
| * sensitive). |
| * |
| * @param sequence the character sequence to be checked |
| * @param suffix the suffix to look for |
| * @return true if the given sequence ends with the given suffix |
| */ |
| public static boolean endsWith(@NonNull CharSequence sequence, @NonNull CharSequence suffix) { |
| return endsWith(sequence, sequence.length(), suffix); |
| } |
| |
| /** |
| * Returns true if the given sequence ends at the given offset with the given suffix (case |
| * sensitive) |
| * |
| * @param sequence the character sequence to be checked |
| * @param endOffset the offset at which the sequence is considered to end |
| * @param suffix the suffix to look for |
| * @return true if the given sequence ends with the given suffix |
| */ |
| public static boolean endsWith(@NonNull CharSequence sequence, int endOffset, |
| @NonNull CharSequence suffix) { |
| if (endOffset < suffix.length()) { |
| return false; |
| } |
| |
| for (int i = endOffset - 1, j = suffix.length() - 1; j >= 0; i--, j--) { |
| if (sequence.charAt(i) != suffix.charAt(j)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Returns true if the given string starts with the given prefix, using a |
| * case-insensitive comparison. |
| * |
| * @param string the full string to be checked |
| * @param prefix the prefix to be checked for |
| * @return true if the string case-insensitively starts with the given prefix |
| */ |
| public static boolean startsWithIgnoreCase(@NonNull String string, @NonNull String prefix) { |
| return string.regionMatches(true /* ignoreCase */, 0, prefix, 0, prefix.length()); |
| } |
| |
| /** |
| * Returns true if the given string starts at the given offset with the |
| * given prefix, case insensitively. |
| * |
| * @param string the full string to be checked |
| * @param offset the offset in the string to start looking |
| * @param prefix the prefix to be checked for |
| * @return true if the string case-insensitively starts at the given offset |
| * with the given prefix |
| */ |
| public static boolean startsWith(@NonNull String string, int offset, @NonNull String prefix) { |
| return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length()); |
| } |
| |
| /** |
| * Strips the whitespace from the given string |
| * |
| * @param string the string to be cleaned up |
| * @return the string, without whitespace |
| */ |
| public static String stripWhitespace(@NonNull String string) { |
| StringBuilder sb = new StringBuilder(string.length()); |
| for (int i = 0, n = string.length(); i < n; i++) { |
| char c = string.charAt(i); |
| if (!Character.isWhitespace(c)) { |
| sb.append(c); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Returns true if the given string has an upper case character. |
| * |
| * @param s the string to check |
| * @return true if it contains uppercase characters |
| */ |
| public static boolean hasUpperCaseCharacter(@NonNull String s) { |
| for (int i = 0; i < s.length(); i++) { |
| if (Character.isUpperCase(s.charAt(i))) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** For use by {@link #getLineSeparator()} */ |
| private static String sLineSeparator; |
| |
| /** |
| * Returns the default line separator to use. |
| * <p> |
| * NOTE: If you have an associated IDocument (Eclipse), it is better to call |
| * TextUtilities#getDefaultLineDelimiter(IDocument) since that will |
| * allow (for example) editing a \r\n-delimited document on a \n-delimited |
| * platform and keep a consistent usage of delimiters in the file. |
| * |
| * @return the delimiter string to use |
| */ |
| @NonNull |
| public static String getLineSeparator() { |
| if (sLineSeparator == null) { |
| // This is guaranteed to exist: |
| sLineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$ |
| } |
| |
| return sLineSeparator; |
| } |
| |
| /** |
| * Wraps the given text at the given line width, with an optional hanging |
| * indent. |
| * |
| * @param text the text to be wrapped |
| * @param lineWidth the number of characters to wrap the text to |
| * @param hangingIndent the hanging indent (to be used for the second and |
| * subsequent lines in each paragraph, or null if not known |
| * @return the string, wrapped |
| */ |
| @NonNull |
| public static String wrap( |
| @NonNull String text, |
| int lineWidth, |
| @Nullable String hangingIndent) { |
| if (hangingIndent == null) { |
| hangingIndent = ""; |
| } |
| int explanationLength = text.length(); |
| StringBuilder sb = new StringBuilder(explanationLength * 2); |
| int index = 0; |
| |
| while (index < explanationLength) { |
| int lineEnd = text.indexOf('\n', index); |
| int next; |
| |
| if (lineEnd != -1 && (lineEnd - index) < lineWidth) { |
| next = lineEnd + 1; |
| } else { |
| // Line is longer than available width; grab as much as we can |
| lineEnd = Math.min(index + lineWidth, explanationLength); |
| if (lineEnd - index < lineWidth) { |
| next = explanationLength; |
| } else { |
| // then back up to the last space |
| int lastSpace = text.lastIndexOf(' ', lineEnd); |
| if (lastSpace > index) { |
| lineEnd = lastSpace; |
| next = lastSpace + 1; |
| } else { |
| // No space anywhere on the line: it contains something wider than |
| // can fit (like a long URL) so just hard break it |
| next = lineEnd + 1; |
| } |
| } |
| } |
| |
| if (sb.length() > 0) { |
| sb.append(hangingIndent); |
| } else { |
| lineWidth -= hangingIndent.length(); |
| } |
| |
| sb.append(text.substring(index, lineEnd)); |
| sb.append('\n'); |
| index = next; |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Returns the given localized string as an int. For example, in the |
| * US locale, "1,000", will return 1000. In the French locale, "1.000" will return |
| * 1000. It will return 0 for empty strings. |
| * <p> |
| * To parse a string without catching parser exceptions, call |
| * {@link #parseLocalizedInt(String, int)} instead, passing the |
| * default value to be returned if the format is invalid. |
| * |
| * @param string the string to be parsed |
| * @return the integer value |
| * @throws ParseException if the format is not correct |
| */ |
| public static int parseLocalizedInt(@NonNull String string) throws ParseException { |
| if (string.isEmpty()) { |
| return 0; |
| } |
| return NumberFormat.getIntegerInstance().parse(string).intValue(); |
| } |
| |
| /** |
| * Returns the given localized string as an int. For example, in the |
| * US locale, "1,000", will return 1000. In the French locale, "1.000" will return |
| * 1000. If the format is invalid, returns the supplied default value instead. |
| * |
| * @param string the string to be parsed |
| * @param defaultValue the value to be returned if there is a parsing error |
| * @return the integer value |
| */ |
| public static int parseLocalizedInt(@NonNull String string, int defaultValue) { |
| try { |
| return parseLocalizedInt(string); |
| } catch (ParseException e) { |
| return defaultValue; |
| } |
| } |
| |
| /** |
| * Returns the given localized string as a double. For example, in the |
| * US locale, "3.14", will return 3.14. In the French locale, "3,14" will return |
| * 3.14. It will return 0 for empty strings. |
| * <p> |
| * To parse a string without catching parser exceptions, call |
| * {@link #parseLocalizedDouble(String, double)} instead, passing the |
| * default value to be returned if the format is invalid. |
| * |
| * @param string the string to be parsed |
| * @return the double value |
| * @throws ParseException if the format is not correct |
| */ |
| public static double parseLocalizedDouble(@NonNull String string) throws ParseException { |
| if (string.isEmpty()) { |
| return 0.0; |
| } |
| return NumberFormat.getNumberInstance().parse(string).doubleValue(); |
| } |
| |
| /** |
| * Returns the given localized string as a double. For example, in the |
| * US locale, "3.14", will return 3.14. In the French locale, "3,14" will return |
| * 3.14. If the format is invalid, returns the supplied default value instead. |
| * |
| * @param string the string to be parsed |
| * @param defaultValue the value to be returned if there is a parsing error |
| * @return the double value |
| */ |
| public static double parseLocalizedDouble(@NonNull String string, double defaultValue) { |
| try { |
| return parseLocalizedDouble(string); |
| } catch (ParseException e) { |
| return defaultValue; |
| } |
| } |
| |
| /** |
| * Returns the corresponding {@link File} for the given file:// url |
| * |
| * @param url the URL string, e.g. file://foo/bar |
| * @return the corresponding {@link File} (which may or may not exist) |
| * @throws MalformedURLException if the URL string is malformed or is not a file: URL |
| */ |
| @NonNull |
| public static File urlToFile(@NonNull String url) throws MalformedURLException { |
| return urlToFile(new URL(url)); |
| } |
| |
| @NonNull |
| public static File urlToFile(@NonNull URL url) throws MalformedURLException { |
| try { |
| return new File(url.toURI()); |
| } |
| catch (IllegalArgumentException e) { |
| MalformedURLException ex = new MalformedURLException(e.getLocalizedMessage()); |
| ex.initCause(e); |
| throw ex; |
| } |
| catch (URISyntaxException e) { |
| return new File(url.getPath()); |
| } |
| } |
| |
| /** |
| * Returns the corresponding URL string for the given {@link File} |
| * |
| * @param file the file to look up the URL for |
| * @return the corresponding URL |
| * @throws MalformedURLException in very unexpected cases |
| */ |
| public static String fileToUrlString(@NonNull File file) throws MalformedURLException { |
| return fileToUrl(file).toExternalForm(); |
| } |
| |
| /** |
| * Returns the corresponding URL for the given {@link File} |
| * |
| * @param file the file to look up the URL for |
| * @return the corresponding URL |
| * @throws MalformedURLException in very unexpected cases |
| */ |
| public static URL fileToUrl(@NonNull File file) throws MalformedURLException { |
| return file.toURI().toURL(); |
| } |
| |
| /** Prefix in comments which mark the source locations for merge results */ |
| public static final String FILENAME_PREFIX = "From: "; |
| |
| /** |
| * Creates the path comment XML string. Note that it does not escape characters |
| * such as & and <; those are expected to be escaped by the caller (for |
| * example, handled by a call to {@link org.w3c.dom.Document#createComment(String)}) |
| * |
| * |
| * @param file the file to create a path comment for |
| * @param includePadding whether to include padding. The final comment recognized by |
| * error recognizers expect padding between the {@code <!--} and |
| * the start marker (From:); you can disable padding if the caller |
| * already is in a context where the padding has been added. |
| * @return the corresponding XML contents of the string |
| */ |
| public static String createPathComment(@NonNull File file, boolean includePadding) |
| throws MalformedURLException { |
| String url = fileToUrlString(file); |
| int dashes = url.indexOf("--"); |
| if (dashes != -1) { // Not allowed inside XML comments - for SGML compatibility. Sigh. |
| url = url.replace("--", "%2D%2D"); |
| } |
| |
| if (includePadding) { |
| return ' ' + FILENAME_PREFIX + url + ' '; |
| } else { |
| return FILENAME_PREFIX + url; |
| } |
| } |
| |
| /** |
| * Copies the given XML file to the given new path. It also inserts a comment at |
| * the end of the file which points to the original source location. This is intended |
| * for use with error parsers which can rewrite for example AAPT error messages in |
| * say layout or manifest files, which occur in the merged (copied) output, and present |
| * it as an error pointing to one of the user's original source files. |
| */ |
| public static void copyXmlWithSourceReference(@NonNull File from, @NonNull File to) |
| throws IOException { |
| copyXmlWithComment(from, to, createPathComment(from, true)); |
| } |
| |
| /** Copies a given XML file, and appends a given comment to the end */ |
| private static void copyXmlWithComment(@NonNull File from, @NonNull File to, |
| @Nullable String comment) throws IOException { |
| assert endsWithIgnoreCase(from.getPath(), DOT_XML) : from; |
| |
| int successfulOps = 0; |
| InputStream in = new FileInputStream(from); |
| try { |
| FileOutputStream out = new FileOutputStream(to, false); |
| try { |
| ByteStreams.copy(in, out); |
| successfulOps++; |
| if (comment != null) { |
| String commentText = "<!--" + XmlUtils.toXmlTextValue(comment) + "-->"; |
| byte[] suffix = commentText.getBytes(Charsets.UTF_8); |
| out.write(suffix); |
| } |
| } finally { |
| Closeables.close(out, successfulOps < 1); |
| successfulOps++; |
| } |
| } finally { |
| Closeables.close(in, successfulOps < 2); |
| } |
| } |
| |
| /** |
| * Translates an XML name (e.g. xml-name) into a Java / C++ constant name (e.g. XML_NAME) |
| * @param xmlName the hyphen separated lower case xml name. |
| * @return the equivalent constant name. |
| */ |
| public static String xmlNameToConstantName(String xmlName) { |
| return CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, xmlName); |
| } |
| |
| /** |
| * Translates a camel case name (e.g. xmlName) into a Java / C++ constant name (e.g. XML_NAME) |
| * @param camelCaseName the camel case name. |
| * @return the equivalent constant name. |
| */ |
| public static String camelCaseToConstantName(String camelCaseName) { |
| return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, camelCaseName); |
| } |
| |
| /** |
| * Translates a Java / C++ constant name (e.g. XML_NAME) into camel case name (e.g. xmlName) |
| * @param constantName the constant name. |
| * @return the equivalent camel case name. |
| */ |
| public static String constantNameToCamelCase(String constantName) { |
| return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, constantName); |
| } |
| |
| /** |
| * Translates a Java / C++ constant name (e.g. XML_NAME) into a XML case name (e.g. xml-name) |
| * @param constantName the constant name. |
| * @return the equivalent XML name. |
| */ |
| public static String constantNameToXmlName(String constantName) { |
| return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, constantName); |
| } |
| |
| |
| /** |
| * Get the R field name from a resource name, since |
| * AAPT will flatten the namespace, turning dots, dashes and colons into _ |
| * |
| * @param resourceName the name to convert |
| * @return the corresponding R field name |
| */ |
| @NonNull |
| public static String getResourceFieldName(@NonNull String resourceName) { |
| // AAPT will flatten the namespace, turning dots, dashes and colons into _ |
| for (int i = 0, n = resourceName.length(); i < n; i++) { |
| char c = resourceName.charAt(i); |
| if (c == '.' || c == ':' || c == '-') { |
| return resourceName.replace('.', '_').replace('-', '_').replace(':', '_'); |
| } |
| } |
| |
| return resourceName; |
| } |
| |
| public static final List<String> IMAGE_EXTENSIONS = Lists.newArrayList( |
| DOT_PNG, DOT_9PNG, DOT_GIF, DOT_JPEG, DOT_JPG, DOT_BMP); |
| |
| /** |
| * Returns true if the given file path points to an image file recognized by |
| * Android. See http://developer.android.com/guide/appendix/media-formats.html |
| * for details. |
| * |
| * @param path the filename to be tested |
| * @return true if the file represents an image file |
| */ |
| public static boolean hasImageExtension(String path) { |
| for (String ext: IMAGE_EXTENSIONS) { |
| if (endsWithIgnoreCase(path, ext)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |