From 8cf74d31aceb30b0d0922773d8d06aeaade928b3 Mon Sep 17 00:00:00 2001 From: evenliu Date: Wed, 25 Sep 2024 12:05:07 +0800 Subject: [PATCH] enhance escape and replace method. (#531) (#541) Co-authored-by: liujianjun.ljj Co-authored-by: Growth <37107073+ZijieSong@users.noreply.github.com> (cherry picked from commit aedf03dd8198178c130c0bbff830712abbb72e5d) --- .../core/appender/builder/XStringBuilder.java | 3 + .../common/tracer/core/utils/StringUtils.java | 304 ++++++++++++++++-- .../tracer/core/utils/StringUtilsTest.java | 38 +++ 3 files changed, 320 insertions(+), 25 deletions(-) diff --git a/tracer-core/src/main/java/com/alipay/common/tracer/core/appender/builder/XStringBuilder.java b/tracer-core/src/main/java/com/alipay/common/tracer/core/appender/builder/XStringBuilder.java index 0c05c380f..a590ce4cc 100644 --- a/tracer-core/src/main/java/com/alipay/common/tracer/core/appender/builder/XStringBuilder.java +++ b/tracer-core/src/main/java/com/alipay/common/tracer/core/appender/builder/XStringBuilder.java @@ -34,10 +34,13 @@ public class XStringBuilder { public static final char DEFAULT_SEPARATOR = ','; public static final String DEFAULT_SEPARATOR_ESCAPE = "%2C"; public static final String AND_SEPARATOR = "&"; + public static final char AND_SEPARATOR_CHAR = '&'; public static final String AND_SEPARATOR_ESCAPE = "%26"; public static final String EQUAL_SEPARATOR = "="; + public static final char EQUAL_SEPARATOR_CHAR = '='; public static final String EQUAL_SEPARATOR_ESCAPE = "%3D"; public static final String PERCENT = "%"; + public static final char PERCENT_CHAR = '%'; public static final String PERCENT_ESCAPE = "%25"; private static char separator = DEFAULT_SEPARATOR; diff --git a/tracer-core/src/main/java/com/alipay/common/tracer/core/utils/StringUtils.java b/tracer-core/src/main/java/com/alipay/common/tracer/core/utils/StringUtils.java index 4b8faccec..25e8a8db4 100644 --- a/tracer-core/src/main/java/com/alipay/common/tracer/core/utils/StringUtils.java +++ b/tracer-core/src/main/java/com/alipay/common/tracer/core/utils/StringUtils.java @@ -57,6 +57,8 @@ public class StringUtils { private static final String CURRENT_PATH = "."; + public static final int INDEX_NOT_FOUND = -1; + public static boolean isBlank(String str) { int strLen; if (str == null || (strLen = str.length()) == 0) { @@ -74,6 +76,14 @@ public static boolean isNotBlank(String str) { return !isBlank(str); } + public static boolean isEmpty(final CharSequence cs) { + return cs == null || cs.length() == 0; + } + + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + /** * Convert the map to a string and add the specified prefix to each key, such as {"k1":"v1"} * @@ -221,10 +231,23 @@ public static String arrayToString(Object[] items, char separator, String prefix * @param str origin data */ public static String escapePercentEqualAnd(String str) { - //You must first escape the % - return escape( - escape(escape(str, PERCENT, PERCENT_ESCAPE), AND_SEPARATOR, AND_SEPARATOR_ESCAPE), - EQUAL_SEPARATOR, EQUAL_SEPARATOR_ESCAPE); + if (str == null) { + return StringUtils.EMPTY_STRING; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch == PERCENT_CHAR) { + sb.append(PERCENT_ESCAPE); + } else if (ch == AND_SEPARATOR_CHAR) { + sb.append(AND_SEPARATOR_ESCAPE); + } else if (ch == EQUAL_SEPARATOR_CHAR) { + sb.append(EQUAL_SEPARATOR_ESCAPE); + } else { + sb.append(ch); + } + } + return sb.toString(); } /** @@ -249,7 +272,7 @@ private static String escape(String str, String oldStr, String newStr) { if (str == null) { return StringUtils.EMPTY_STRING; } - return str.replace(oldStr, newStr); + return replace(str, oldStr, newStr); } public static boolean hasText(CharSequence str) { @@ -434,33 +457,264 @@ public static String deleteAny(String inString, String charsToDelete) { * @return a {@code String} with the replacements */ public static String replace(String inString, String oldPattern, String newPattern) { - if (isBlank(inString) || isBlank(oldPattern) || newPattern == null) { - return inString; + return replace(inString, oldPattern, newPattern, -1); + } + + /** + * Replaces a String with another String inside a larger String, + * for the first {@code max} values of the search String. + * + *

A {@code null} reference passed to this method is a no-op.

+ * + *
+     * StringUtils.replace(null, *, *, *)         = null
+     * StringUtils.replace("", *, *, *)           = ""
+     * StringUtils.replace("any", null, *, *)     = "any"
+     * StringUtils.replace("any", *, null, *)     = "any"
+     * StringUtils.replace("any", "", *, *)       = "any"
+     * StringUtils.replace("any", *, *, 0)        = "any"
+     * StringUtils.replace("abaa", "a", null, -1) = "abaa"
+     * StringUtils.replace("abaa", "a", "", -1)   = "b"
+     * StringUtils.replace("abaa", "a", "z", 0)   = "abaa"
+     * StringUtils.replace("abaa", "a", "z", 1)   = "zbaa"
+     * StringUtils.replace("abaa", "a", "z", 2)   = "zbza"
+     * StringUtils.replace("abaa", "a", "z", -1)  = "zbzz"
+     * 
+ * + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace it with, may be null + * @param max maximum number of values to replace, or {@code -1} if no maximum + * @return the text with any replacements processed, + * {@code null} if null String input + */ + public static String replace(final String text, final String searchString, + final String replacement, final int max) { + return replace(text, searchString, replacement, max, false); + } + + /** + * Replaces a String with another String inside a larger String, + * for the first {@code max} values of the search String, + * case-sensitively/insensitively based on {@code ignoreCase} value. + * + *

A {@code null} reference passed to this method is a no-op.

+ * + *
+     * StringUtils.replace(null, *, *, *, false)         = null
+     * StringUtils.replace("", *, *, *, false)           = ""
+     * StringUtils.replace("any", null, *, *, false)     = "any"
+     * StringUtils.replace("any", *, null, *, false)     = "any"
+     * StringUtils.replace("any", "", *, *, false)       = "any"
+     * StringUtils.replace("any", *, *, 0, false)        = "any"
+     * StringUtils.replace("abaa", "a", null, -1, false) = "abaa"
+     * StringUtils.replace("abaa", "a", "", -1, false)   = "b"
+     * StringUtils.replace("abaa", "a", "z", 0, false)   = "abaa"
+     * StringUtils.replace("abaa", "A", "z", 1, false)   = "abaa"
+     * StringUtils.replace("abaa", "A", "z", 1, true)   = "zbaa"
+     * StringUtils.replace("abAa", "a", "z", 2, true)   = "zbza"
+     * StringUtils.replace("abAa", "a", "z", -1, true)  = "zbzz"
+     * 
+ * + * @param text text to search and replace in, may be null + * @param searchString the String to search for (case-insensitive), may be null + * @param replacement the String to replace it with, may be null + * @param max maximum number of values to replace, or {@code -1} if no maximum + * @param ignoreCase if true replace is case-insensitive, otherwise case-sensitive + * @return the text with any replacements processed, + * {@code null} if null String input + */ + private static String replace(final String text, String searchString, final String replacement, + int max, final boolean ignoreCase) { + if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) { + return text; + } + if (ignoreCase) { + searchString = searchString.toLowerCase(); + } + int start = 0; + int end = ignoreCase ? indexOfIgnoreCase(text, searchString, start) : indexOf(text, + searchString, start); + if (end == INDEX_NOT_FOUND) { + return text; + } + final int replLength = searchString.length(); + int increase = Math.max(replacement.length() - replLength, 0); + increase *= max < 0 ? 16 : Math.min(max, 64); + final StringBuilder buf = new StringBuilder(text.length() + increase); + while (end != INDEX_NOT_FOUND) { + buf.append(text, start, end).append(replacement); + start = end + replLength; + if (--max == 0) { + break; + } + end = ignoreCase ? indexOfIgnoreCase(text, searchString, start) : indexOf(text, + searchString, start); } - int index = inString.indexOf(oldPattern); - if (index == -1) { - // no occurrence -> can return input as-is - return inString; + buf.append(text, start, text.length()); + return buf.toString(); + } + + /** + * Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String, int)} if possible. + * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ * + *
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf(*, null, *)          = -1
+     * StringUtils.indexOf("", "", 0)           = 0
+     * StringUtils.indexOf("", *, 0)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.indexOf("aabaabaa", "b", 0)  = 2
+     * StringUtils.indexOf("aabaabaa", "ab", 0) = 1
+     * StringUtils.indexOf("aabaabaa", "b", 3)  = 5
+     * StringUtils.indexOf("aabaabaa", "b", 9)  = -1
+     * StringUtils.indexOf("aabaabaa", "b", -1) = 2
+     * StringUtils.indexOf("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOf("abc", "", 9)        = 3
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence (always ≥ startPos), + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from indexOf(String, String, int) to indexOf(CharSequence, CharSequence, int) + */ + public static int indexOf(final CharSequence seq, final CharSequence searchSeq, + final int startPos) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + if (seq instanceof String) { + return ((String) seq).indexOf(searchSeq.toString(), startPos); + } else if (seq instanceof StringBuilder) { + return ((StringBuilder) seq).indexOf(searchSeq.toString(), startPos); + } else if (seq instanceof StringBuffer) { + return ((StringBuffer) seq).indexOf(searchSeq.toString(), startPos); + } + return seq.toString().indexOf(searchSeq.toString(), startPos); + } + + /** + * Case in-sensitive find of the first index within a CharSequence + * from the specified position. + * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ * + *
+     * StringUtils.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOfIgnoreCase("abc", "", 9)        = -1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence (always ≥ startPos), + * -1 if no match or {@code null} string input + * @since 2.5 + * @since 3.0 Changed signature from indexOfIgnoreCase(String, String, int) to indexOfIgnoreCase(CharSequence, CharSequence, int) + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, + int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos < 0) { + startPos = 0; + } + final int endLimit = str.length() - searchStr.length() + 1; + if (startPos > endLimit) { + return INDEX_NOT_FOUND; } + if (searchStr.length() == 0) { + return startPos; + } + for (int i = startPos; i < endLimit; i++) { + if (regionMatches(str, true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } - int capacity = inString.length(); - if (newPattern.length() > oldPattern.length()) { - capacity += 16; + /** + * Green implementation of regionMatches. + * + * @param cs the {@code CharSequence} to be processed + * @param ignoreCase whether or not to be case insensitive + * @param thisStart the index to start on the {@code cs} CharSequence + * @param substring the {@code CharSequence} to be looked for + * @param start the index to start on the {@code substring} CharSequence + * @param length character length of the region + * @return whether the region matched + */ + public static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, + final int thisStart, final CharSequence substring, + final int start, final int length) { + if (cs instanceof String && substring instanceof String) { + return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, + length); + } + int index1 = thisStart; + int index2 = start; + int tmpLen = length; + + // Extract these first so we detect NPEs the same as the java.lang.String version + final int srcLen = cs.length() - thisStart; + final int otherLen = substring.length() - start; + + // Check for invalid parameters + if (thisStart < 0 || start < 0 || length < 0) { + return false; } - StringBuilder sb = new StringBuilder(capacity); - int pos = 0; // our position in the old string - int patLen = oldPattern.length(); - while (index >= 0) { - sb.append(inString.substring(pos, index)); - sb.append(newPattern); - pos = index + patLen; - index = inString.indexOf(oldPattern, pos); + // Check that the regions are long enough + if (srcLen < length || otherLen < length) { + return false; } - // append any characters to the right of a match - sb.append(inString.substring(pos)); - return sb.toString(); + while (tmpLen-- > 0) { + final char c1 = cs.charAt(index1++); + final char c2 = substring.charAt(index2++); + + if (c1 == c2) { + continue; + } + + if (!ignoreCase) { + return false; + } + + // The real same check as in String.regionMatches(): + final char u1 = Character.toUpperCase(c1); + final char u2 = Character.toUpperCase(c2); + if (u1 != u2 && Character.toLowerCase(u1) != Character.toLowerCase(u2)) { + return false; + } + } + + return true; } /** diff --git a/tracer-core/src/test/java/com/alipay/common/tracer/core/utils/StringUtilsTest.java b/tracer-core/src/test/java/com/alipay/common/tracer/core/utils/StringUtilsTest.java index 86bdc0b03..ba11b3b87 100644 --- a/tracer-core/src/test/java/com/alipay/common/tracer/core/utils/StringUtilsTest.java +++ b/tracer-core/src/test/java/com/alipay/common/tracer/core/utils/StringUtilsTest.java @@ -16,8 +16,12 @@ */ package com.alipay.common.tracer.core.utils; +import org.junit.Assert; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + import static org.junit.Assert.assertEquals; /** @@ -178,4 +182,38 @@ public void testCountMatches() throws Exception { } + @Test + public void testEscapePercentEqualAnd() { + String replaceStr = "test%test2&test3="; + String replacedStr = StringUtils.escapePercentEqualAnd(replaceStr); + Assert.assertEquals("test%25test2%26test3%3D", replacedStr); + String replaceRevertStr = StringUtils.unescapeEqualAndPercent(replacedStr); + Assert.assertEquals(replaceStr, replaceRevertStr); + } + + @Test + public void testIsEmpty() { + Assert.assertFalse(StringUtils.isEmpty(" ")); + Assert.assertTrue(StringUtils.isNotEmpty(" ")); + Assert.assertTrue(StringUtils.isEmpty("")); + Assert.assertFalse(StringUtils.isNotEmpty("")); + } + + @Test + public void testMapToStringAndStringToMap() { + Map map = new HashMap<>(); + map.put("key1%", "value1%"); + map.put("key2=", "value2="); + map.put(null, "value3&"); + map.put("key3&", null); + String mapStr = StringUtils.mapToString(map); + Assert.assertEquals("key1%25=value1%25&=value3%26&key3%26=&key2%3D=value2%3D&", mapStr); + Map map2 = new HashMap<>(); + StringUtils.stringToMap(mapStr, map2); + Assert.assertEquals("value1%", map2.get("key1%")); + Assert.assertEquals("value2=", map2.get("key2=")); + Assert.assertEquals("value3&", map2.get("")); + Assert.assertEquals("", map2.get("key3&")); + } + }