This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 972aa7b29 fix: default ttl in recursive replacement (#1297)
972aa7b29 is described below

commit 972aa7b29a7cb7306b9e0737bac86fa42d6218ac
Author: Capt. Cutlass <5120290+paranoidu...@users.noreply.github.com>
AuthorDate: Tue Oct 15 13:31:23 2024 -0400

    fix: default ttl in recursive replacement (#1297)
    
    https://issues.apache.org/jira/browse/LANG-1753
---
 .../java/org/apache/commons/lang3/StringUtils.java | 26 ++++++------
 .../org/apache/commons/lang3/StringUtilsTest.java  | 48 +++++++++++++++++++++-
 2 files changed, 59 insertions(+), 15 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java 
b/src/main/java/org/apache/commons/lang3/StringUtils.java
index b9403c793..70c26ee31 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -21,12 +21,10 @@ import java.nio.charset.Charset;
 import java.text.Normalizer;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
-import java.util.Set;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
 
@@ -181,6 +179,11 @@ public class StringUtils {
      */
     private static final int PAD_LIMIT = 8192;
 
+    /**
+     * The default maximum depth at which recursive replacement will continue 
until no further search replacements are possible.
+     */
+    private static final int DEFAULT_TTL = 5;
+
     /**
      * Pattern used in {@link #stripAccents(String)}.
      */
@@ -6450,20 +6453,14 @@ public class StringUtils {
 
         // mchyzer Performance note: This creates very few new objects (one 
major goal)
         // let me know if there are performance requests, we can create a 
harness to measure
+        if (isEmpty(text) || ArrayUtils.isEmpty(searchList) || 
ArrayUtils.isEmpty(replacementList)) {
+            return text;
+        }
 
         // if recursing, this shouldn't be less than 0
         if (timeToLive < 0) {
-            final Set<String> searchSet = new 
HashSet<>(Arrays.asList(searchList));
-            final Set<String> replacementSet = new 
HashSet<>(Arrays.asList(replacementList));
-            searchSet.retainAll(replacementSet);
-            if (!searchSet.isEmpty()) {
-                throw new IllegalStateException("Aborting to protect against 
StackOverflowError - " +
-                        "output of one loop is the input of another");
-            }
-        }
-
-        if (isEmpty(text) || ArrayUtils.isEmpty(searchList) || 
ArrayUtils.isEmpty(replacementList) || ArrayUtils.isNotEmpty(searchList) && 
timeToLive == -1) {
-            return text;
+            throw new IllegalStateException("Aborting to protect against 
StackOverflowError - " +
+                "output of one loop is the input of another");
         }
 
         final int searchLength = searchList.length;
@@ -6611,7 +6608,8 @@ public class StringUtils {
      * @since 2.4
      */
     public static String replaceEachRepeatedly(final String text, final 
String[] searchList, final String[] replacementList) {
-        return replaceEach(text, searchList, replacementList, true, 
ArrayUtils.getLength(searchList));
+        int timeToLive = Math.max(ArrayUtils.getLength(searchList), 
DEFAULT_TTL);
+        return replaceEach(text, searchList, replacementList, true, 
timeToLive);
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
index e1102f74e..8f15669b3 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
@@ -1990,7 +1990,53 @@ public class StringUtilsTest extends AbstractLangTest {
         assertEquals("aba", StringUtils.replaceEachRepeatedly("aba", new 
String[]{null}, new String[]{"a"}));
         assertEquals("wcte", StringUtils.replaceEachRepeatedly("abcde", new 
String[]{"ab", "d"}, new String[]{"w", "t"}));
         assertEquals("tcte", StringUtils.replaceEachRepeatedly("abcde", new 
String[]{"ab", "d"}, new String[]{"d", "t"}));
-        assertEquals("blaan", StringUtils.replaceEachRepeatedly("blllaan", new 
String[]{"llaan"}, new String[]{"laan"}) );
+
+        // Test recursive replacement - LANG-1528 & LANG-1753
+        assertEquals("blaan", StringUtils.replaceEachRepeatedly("blllaan", new 
String[]{"llaan"}, new String[]{"laan"}));
+        assertEquals("blaan", StringUtils.replaceEachRepeatedly("bllllaan", 
new String[]{"llaan"}, new String[]{"laan"}));
+
+        // Test default TTL for smaller search lists. 32 characters reduced to 
16, then 8, 4, 2, 1.
+        assertEquals("a", 
StringUtils.replaceEachRepeatedly("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+                new String[]{"aa"}, new String[]{"a"}));
+
+        // Test default TTL exceeded. 33 characters reduced to 17, then 9, 5, 
3, 2 (still found).
+        assertThrows(
+                IllegalStateException.class,
+                () -> 
StringUtils.replaceEachRepeatedly("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+                    new String[]{"aa"}, new String[]{"a"}),
+               "Cannot be resolved within the default time-to-live limit");
+
+        // Test larger TTL for larger search lists. Replace repeatedly until 
there are no more possible replacements.
+        assertEquals("000000000", 
StringUtils.replaceEachRepeatedly("aA0aA0aA0",
+                new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 
"k", "l", "m", "n",
+                        "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", 
"z", "A", "B", "C", "D",
+                        "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", 
"P", "Q", "R", "S", "T",
+                        "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", 
"6", "7", "8", "9"},
+                new String[]{"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", 
"l", "m", "n", "o",
+                        "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", 
"A", "B", "C", "D", "E",
+                        "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", 
"Q", "R", "S", "T", "U",
+                        "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", 
"7", "8", "9", "0"}));
+
+        // Test long infinite cycle: a -> b -> ... -> 9 -> 0 -> a -> b -> ...
+        assertThrows(
+                IllegalStateException.class,
+                () -> StringUtils.replaceEachRepeatedly("a",
+                    new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", 
"j", "k", "l", "m", "n",
+                            "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", 
"y", "z", "A", "B", "C", "D",
+                            "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", 
"O", "P", "Q", "R", "S", "T",
+                            "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", 
"5", "6", "7", "8", "9", "0"},
+                    new String[]{"b", "c", "d", "e", "f", "g", "h", "i", "j", 
"k", "l", "m", "n", "o",
+                            "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", 
"z", "A", "B", "C", "D", "E",
+                            "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", 
"P", "Q", "R", "S", "T", "U",
+                            "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", 
"6", "7", "8", "9", "0", "a"}),
+                "Should be a circular reference");
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> StringUtils.replaceEachRepeatedly("%{key1}",
+                    new String[] {"%{key1}", "%{key2}", "%{key3}"},
+                    new String[] {"Key1 %{key2}", "Key2 %{key3}", "Key3 
%{key1}"}),
+                "Should be a circular reference");
 
         assertThrows(
                 IllegalStateException.class,

Reply via email to