// Copyright 2012 Google Inc. All Rights Reserved. // // 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.google.collide.shared.util; import com.google.collide.json.shared.JsonArray; import com.google.collide.json.shared.JsonIntegerMap; import javax.annotation.Nonnull; /** * Utility methods for string operations. * */ public class StringUtils { /** * Map [N] -> string of N spaces. Used by {@link #getSpaces} to cache strings * of spaces. */ private static final JsonIntegerMap<String> cachedSpaces = JsonCollections.createIntegerMap(); /** * Interface that defines string utility methods used by shared code but have * differing client and server implementations. */ public interface Implementation { JsonArray<String> split(String string, String separator); } private static class PureJavaImplementation implements Implementation { @Override public JsonArray<String> split(String string, String separator) { JsonArray<String> result = JsonCollections.createArray(); int sepLength = separator.length(); if (sepLength == 0) { for (int i = 0, n = string.length(); i < n; i++) { result.add(string.substring(i, i + 1)); } return result; } int position = 0; while (true) { int index = string.indexOf(separator, position); if (index == -1) { result.add(string.substring(position)); return result; } result.add(string.substring(position, index)); position = index + sepLength; } } } /** * By default, this is a pure java implementation, but can be set to a more * optimized version by the client */ private static Implementation implementation = new PureJavaImplementation(); /** * Sets the implementation for methods */ public static void setImplementation(Implementation implementation) { StringUtils.implementation = implementation; } /** * @return largest n such that * {@code string1.substring(0, n).equals(string2.substring(0, n))} */ public static int findCommonPrefixLength(String string1, String string2) { int limit = Math.min(string1.length(), string2.length()); int result = 0; while (result < limit) { if (string2.charAt(result) != string1.charAt(result)) { break; } result++; } return result; } public static int countNumberOfOccurrences(String s, String pattern) { int count = 0; int i = 0; while ((i = s.indexOf(pattern, i)) >= 0) { count++; i += pattern.length(); } return count; } /** * Check if a String ends with a specified suffix, ignoring case * * @param s the String to check, may be null * @param suffix the suffix to find, may be null * @return true if s ends with suffix or both s and suffix are null, false * otherwise. */ public static boolean endsWithIgnoreCase(String s, String suffix) { if (s == null || suffix == null) { return (s == null && suffix == null); } if (suffix.length() > s.length()) { return false; } return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length()); } public static boolean looksLikeImage(String path) { String lowercase = path.toLowerCase(); return lowercase.endsWith(".jpg") || lowercase.endsWith(".jpeg") || lowercase.endsWith(".ico") || lowercase.endsWith(".png") || lowercase.endsWith(".gif"); } public static <T> String join(T[] items, String separator) { StringBuilder s = new StringBuilder(); for (int i = 0; i < items.length; i++) { s.append(items[i]).append(separator); } s.setLength(s.length() - separator.length()); return s.toString(); } public static <T> String join(JsonArray<T> items, String separator) { StringBuilder s = new StringBuilder(); for (int i = 0; i < items.size(); i++) { s.append(items.get(i)).append(separator); } s.setLength(s.length() - separator.length()); return s.toString(); } /** * Check that string starts with specified prefix. * * <p>If {@code caseInsensitive == false} this check is equivalent * to {@link String#startsWith(String)}. * * <p>Otherwise {@code prefix} should be lower-case and check ignores * case of {@code string}. */ public static boolean startsWith( @Nonnull String prefix, @Nonnull String string, boolean caseInsensitive) { if (caseInsensitive) { int prefixLength = prefix.length(); if (string.length() < prefixLength) { return false; } return prefix.equals(string.substring(0, prefixLength).toLowerCase()); } else { return string.startsWith(prefix); } } /** * @return the length of the starting whitespace for the line, or the string * length if it is all whitespace */ public static int lengthOfStartingWhitespace(String s) { int n = s.length(); for (int i = 0; i < n; i++) { char c = s.charAt(i); // TODO: This currently only deals with ASCII whitespace. // Read until a non-space if (c != ' ' && c != '\t') { return i; } } return n; } /** * @return first character in the string that is not a whitespace, or * {@code 0} if there is no such characters */ public static char firstNonWhitespaceCharacter(String s) { for (int i = 0, n = s.length(); i < n; ++i) { char c = s.charAt(i); if (!isWhitespace(c)) { return c; } } return 0; } /** * @return last character in the string that is not a whitespace, or * {@code 0} if there is no such characters */ public static char lastNonWhitespaceCharacter(String s) { for (int i = s.length() - 1; i >= 0; --i) { char c = s.charAt(i); if (!isWhitespace(c)) { return c; } } return 0; } public static String nullToEmpty(String s) { return s == null ? "" : s; } public static boolean isNullOrEmpty(String s) { return s == null || "".equals(s); } public static boolean isNullOrWhitespace(String s) { return s == null || "".equals(s.trim()); } public static String trimNullToEmpty(String s) { return s == null ? "" : s.trim(); } public static String ensureNotEmpty(String s, String defaultStr) { return isNullOrEmpty(s) ? defaultStr : s; } public static boolean isWhitespace(char ch) { return (ch <= ' '); } public static boolean isAlpha(char ch) { return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z'); } public static boolean isNumeric(char ch) { return ('0' <= ch && ch <= '9'); } public static boolean isQuote(char ch) { return ch == '\'' || ch == '\"'; } public static boolean isAlphaNumOrUnderscore(char ch) { return isAlpha(ch) || isNumeric(ch) || ch == '_'; } public static long toLong(String longStr) { return longStr == null ? 0 : Long.parseLong(longStr); } /** * @return true, if both strings are empty or {@code null}, or if they are * equal */ public static boolean equalStringsOrEmpty(String a, String b) { return nullToEmpty(a).equals(nullToEmpty(b)); } /** * @return true, if the strings are not empty or {@code null}, and equal */ public static boolean equalNonEmptyStrings(String a, String b) { return !isNullOrEmpty(a) && a.equals(b); } /** * Splits with the contract of the contract of the JavaScript String.split(). * * <p>More specifically: empty segments will be produced for adjacent and * trailing separators. * * <p>If an empty string is used as the separator, the string is split * between each character. * * <p>Examples:<ul> * <li>{@code split("a", "a")} should produce {@code ["", ""]} * <li>{@code split("a\n", "\n")} should produce {@code ["a", ""]} * <li>{@code split("ab", "")} should produce {@code ["a", "b"]} * </ul> */ public static JsonArray<String> split(String s, String separator) { return implementation.split(s, separator); } /** * @return the number of editor lines this text would take up */ public static int countNumberOfVisibleLines(String text) { return countNumberOfOccurrences(text, "\n") + (text.endsWith("\n") ? 0 : 1); } public static String capitalizeFirstLetter(String s) { if (!isNullOrEmpty(s)) { s = s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); } return s; } public static String ensureStartsWith(String s, String startText) { if (isNullOrEmpty(s)) { return startText; } else if (!s.startsWith(startText)) { return startText + s; } else { return s; } } /** * Like {@link String#substring(int)} but allows for the {@code count} * parameter to extend past the string's bounds. */ public static String substringGuarded(String s, int position, int count) { int sLength = s.length(); if (sLength - position <= count) { return position == 0 ? s : s.substring(position); } else { return s.substring(position, position + count); } } public static String repeatString(String s, int count) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < count; i++) { builder.append(s); } return builder.toString(); } /** * Gets a {@link String} consisting of the given number of spaces. * * <p>NB: The result is cached in {@link #cachedSpaces}. * * @param size the number of spaces * @return a {@link String} consisting of {@code size} spaces */ public static String getSpaces(int size) { if (cachedSpaces.hasKey(size)) { return cachedSpaces.get(size); } char[] fill = new char[size]; for (int i = 0; i < size; i++) { fill[i] = ' '; } String spaces = new String(fill); cachedSpaces.put(size, spaces); return spaces; } /** * If this given string is of length {@code maxLength} or less, it will * be returned as-is. * Otherwise it will be trucated to {@code maxLength}, regardless of whether * there are any space characters in the String. If an ellipsis is requested * to be appended to the truncated String, the String will be truncated so * that the ellipsis will also fit within maxLength. * If no truncation was necessary, no ellipsis will be added. * @param source the String to truncate if necessary * @param maxLength the maximum number of characters to keep * @param addEllipsis if true, and if the String had to be truncated, * add "..." to the end of the String before returning. Additionally, * the ellipsis will only be added if maxLength is greater than 3. * @return the original string if it's length is less than or equal to * maxLength, otherwise a truncated string as mentioned above */ public static String truncateAtMaxLength(String source, int maxLength, boolean addEllipsis) { if (source.length() <= maxLength) { return source; } if (addEllipsis && maxLength > 3) { return unicodePreservingSubstring(source, 0, maxLength - 3) + "..."; } return unicodePreservingSubstring(source, 0, maxLength); } /** * Normalizes {@code index} such that it respects Unicode character * boundaries in {@code str}. * * <p>If {@code index} is the low surrogate of a unicode character, * the method returns {@code index - 1}. Otherwise, {@code index} is * returned. * * <p>In the case in which {@code index} falls in an invalid surrogate pair * (e.g. consecutive low surrogates, consecutive high surrogates), or if * if it is not a valid index into {@code str}, the original value of * {@code index} is returned. * * @param str the String * @param index the index to be normalized * @return a normalized index that does not split a Unicode character */ private static int unicodePreservingIndex(String str, int index) { if (index > 0 && index < str.length()) { if (Character.isHighSurrogate(str.charAt(index - 1)) && Character.isLowSurrogate(str.charAt(index))) { return index - 1; } } return index; } /** * Returns a substring of {@code str} that respects Unicode character * boundaries. * * <p>The string will never be split between a [high, low] surrogate pair, * as defined by {@link Character#isHighSurrogate} and * {@link Character#isLowSurrogate}. * * <p>If {@code begin} or {@code end} are the low surrogate of a unicode * character, it will be offset by -1. * * <p>This behavior guarantees that * {@code str.equals(StringUtil.unicodePreservingSubstring(str, 0, n) + * StringUtil.unicodePreservingSubstring(str, n, str.length())) } is * true for all {@code n}. * </pre> * * <p>This means that unlike {@link String#substring(int, int)}, the length of * the returned substring may not necessarily be equivalent to * {@code end - begin}. * * @param str the original String * @param begin the beginning index, inclusive * @param end the ending index, exclusive * @return the specified substring, possibly adjusted in order to not * split unicode surrogate pairs * @throws IndexOutOfBoundsException if the {@code begin} is negative, * or {@code end} is larger than the length of {@code str}, or * {@code begin} is larger than {@code end} */ private static String unicodePreservingSubstring( String str, int begin, int end) { return str.substring(unicodePreservingIndex(str, begin), unicodePreservingIndex(str, end)); } }