package cgeo.geocaching.utils; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.text.Html; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import java.nio.charset.Charset; import java.text.Collator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.models.Geocache; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Misc. utils. All methods don't use Android specific stuff to use these methods in plain JUnit tests. */ public final class TextUtils { public static final Charset CHARSET_UTF8 = Charset.forName("UTF-8"); public static final Charset CHARSET_ASCII = Charset.forName("US-ASCII"); /** * a Collator instance appropriate for comparing strings using the default locale while ignoring the casing */ public static final Collator COLLATOR = getCollator(); private static final Pattern PATTERN_REMOVE_NONPRINTABLE = Pattern.compile("\\p{Cntrl}"); private TextUtils() { // utility class } /** * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned * * @param data * Data to search in * @param pattern * Pattern to search for * @param trim * Set to true if the group found should be trim'ed * @param group * Number of the group to return if found * @param defaultValue * Value to return if the pattern is not found * @param last * Find the last occurring value * @return defaultValue or the n-th group if the pattern matches (trimmed if wanted) */ @SuppressWarnings("RedundantStringConstructorCall") @SuppressFBWarnings("DM_STRING_CTOR") public static String getMatch(@Nullable final String data, final Pattern pattern, final boolean trim, final int group, final String defaultValue, final boolean last) { if (data != null) { final Matcher matcher = pattern.matcher(data); if (matcher.find()) { String result = matcher.group(group); while (last && matcher.find()) { result = matcher.group(group); } if (result != null) { final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(result); final String untrimmed = remover.replaceAll(" "); // Some versions of Java copy the whole page String, when matching with regular expressions // later this would block the garbage collector, as we only need tiny parts of the page // see http://developer.android.com/reference/java/lang/String.html#backing_array // Thus the creation of a new String via String constructor is voluntary here!! // And BTW: You cannot even see that effect in the debugger, but must use a separate memory profiler! return trim ? new String(untrimmed).trim() : new String(untrimmed); } } } return defaultValue; } /** * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned * * @param data * Data to search in * @param pattern * Pattern to search for * @param trim * Set to true if the group found should be trim'ed * @param defaultValue * Value to return if the pattern is not found * @return defaultValue or the first group if the pattern matches (trimmed if wanted) */ public static String getMatch(final String data, final Pattern pattern, final boolean trim, final String defaultValue) { return getMatch(data, pattern, trim, 1, defaultValue, false); } /** * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned * * @param data * Data to search in * @param pattern * Pattern to search for * @param defaultValue * Value to return if the pattern is not found * @return defaultValue or the first group if the pattern matches (trimmed) */ public static String getMatch(@Nullable final String data, final Pattern pattern, final String defaultValue) { return getMatch(data, pattern, true, 1, defaultValue, false); } /** * Searches for the pattern pattern in the data. * * @return true if data contains the pattern pattern */ public static boolean matches(final String data, final Pattern pattern) { // matcher is faster than String.contains() and more flexible - it takes patterns instead of fixed texts return data != null && pattern.matcher(data).find(); } /** * Replaces every \n, \r and \t with a single space. Afterwards multiple spaces * are merged into a single space. Finally leading spaces are deleted. * * This method must be fast, but may not lead to the shortest replacement String. * * You are only allowed to change this code if you can prove it became faster on a device. * see cgeo.geocaching.test.WhiteSpaceTest#replaceWhitespaceManually in the test project. * * @param data * complete HTML page * @return the HTML page as a very long single "line" */ public static String replaceWhitespace(final String data) { final int length = data.length(); final char[] chars = new char[length]; data.getChars(0, length, chars, 0); int resultSize = 0; boolean lastWasWhitespace = true; for (final char c : chars) { if (c == ' ' || c == '\n' || c == '\r' || c == '\t') { if (!lastWasWhitespace) { chars[resultSize++] = ' '; } lastWasWhitespace = true; } else { chars[resultSize++] = c; lastWasWhitespace = false; } } return String.valueOf(chars, 0, resultSize); } /** * Quick and naive check for possible rich HTML content in a string. * * @param str * A string containing HTML code. * @return <tt>true</tt> if <tt>str</tt> contains HTML code that needs to go through a HTML renderer before * being displayed, <tt>false</tt> if it can be displayed as-is without any loss */ public static boolean containsHtml(final String str) { return str.indexOf('<') != -1 || str.indexOf('&') != -1; } /** * Remove all control characters (which are not valid in XML or HTML), as those should not appear in cache texts * anyway * */ public static String removeControlCharacters(final String input) { final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(input); return remover.replaceAll(" ").trim(); } /** * Calculate a simple checksum for change-checking (not usable for security/cryptography!) * * @param input * String to check * @return resulting checksum */ public static long checksum(final String input) { final CRC32 checksum = new CRC32(); checksum.update(input.getBytes(CHARSET_UTF8)); return checksum.getValue(); } /** * Build a Collator instance appropriate for comparing strings using the default locale while ignoring the casing. * * @return a collator */ private static Collator getCollator() { final Collator collator = Collator.getInstance(); collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION); collator.setStrength(Collator.TERTIARY); return collator; } /** * When converting html to text using {@link Html#fromHtml(String)} then the result often contains unwanted trailing * linebreaks (from the conversion of paragraph tags). This method removes those. */ public static CharSequence trimSpanned(final Spanned source) { final int length = source.length(); int i = length; // loop back to the first non-whitespace character while (--i >= 0 && Character.isWhitespace(source.charAt(i))) { } if (i < length - 1) { return source.subSequence(0, i + 1); } return source; } /** * Convert a potentially HTML string into a plain-text one. If the string does not contain HTML markup, * it is returned unchanged. * * @param html a string containing either HTML or plain text * @return a string without any HTML markup */ public static String stripHtml(final String html) { return containsHtml(html) ? trimSpanned(Html.fromHtml(html)).toString() : html; } public static SpannableString coloredCacheText(@NonNull final Geocache cache, @NonNull final String text) { final SpannableString span = new SpannableString(text); if (cache.isDisabled() || cache.isArchived()) { // strike span.setSpan(new StrikethroughSpan(), 0, span.toString().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (cache.isArchived()) { span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(CgeoApplication.getInstance(), R.color.archived_cache_color)), 0, span.toString().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } return span; } }