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

mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 174a79b03e [CALCITE-7259] Drop commons-lang3 dependency
174a79b03e is described below

commit 174a79b03e54be0712cc9cfe1e14465a37b801b2
Author: Hongyu Guo <[email protected]>
AuthorDate: Thu Mar 12 22:52:58 2026 +0800

    [CALCITE-7259] Drop commons-lang3 dependency
    
    Replace commons-lang3 usages with local helpers and add lint checks to 
prevent re-introducing commons-lang3 imports.
---
 arrow/build.gradle.kts                             |   1 -
 bom/build.gradle.kts                               |   1 -
 .../apache/calcite/test/CassandraExtension.java    |   4 +-
 core/build.gradle.kts                              |   1 -
 .../rel/metadata/RelMdColumnUniqueness.java        |   8 +-
 .../calcite/runtime/CompressionFunctions.java      |   4 +-
 .../org/apache/calcite/runtime/SqlFunctions.java   |   7 +-
 .../org/apache/calcite/runtime/XmlFunctions.java   |   7 +-
 .../main/java/org/apache/calcite/util/Util.java    | 101 +++++++++++++++++++++
 .../calcite/util/format/FormatElementEnum.java     |  15 ++-
 .../calcite/plan/volcano/VolcanoPlannerTest.java   |  18 +++-
 .../java/org/apache/calcite/test/LintTest.java     |  27 ++++++
 .../java/org/apache/calcite/util/UtilTest.java     |  39 ++++++++
 druid/build.gradle.kts                             |   1 -
 .../apache/calcite/adapter/druid/DruidRules.java   |  36 +++++---
 file/build.gradle.kts                              |   1 -
 .../apache/calcite/adapter/file/CsvEnumerator.java |  62 +++++++++----
 geode/build.gradle.kts                             |   1 -
 .../calcite/adapter/geode/util/GeodeUtils.java     |   7 +-
 gradle.properties                                  |   1 -
 innodb/build.gradle.kts                            |   1 -
 .../calcite/adapter/innodb/InnodbSchema.java       |   4 +-
 .../adapter/innodb/InnodbSchemaFactory.java        |   4 +-
 .../adapter/innodb/InnodbAdapterDataTypesTest.java |  54 +++++++----
 .../apache/calcite/adapter/os/OsAdapterTest.java   |  18 ++--
 redis/build.gradle.kts                             |   1 -
 .../calcite/adapter/redis/RedisDataProcess.java    |   6 +-
 .../calcite/adapter/redis/RedisEnumerator.java     |   9 +-
 .../calcite/adapter/redis/RedisJedisManager.java   |   3 +-
 .../apache/calcite/adapter/redis/RedisSchema.java  |  49 ++++++++--
 .../calcite/adapter/redis/RedisCaseBase.java       |   7 +-
 testkit/build.gradle.kts                           |   1 -
 .../org/apache/calcite/test/CalciteAssert.java     |  15 ++-
 33 files changed, 383 insertions(+), 131 deletions(-)

diff --git a/arrow/build.gradle.kts b/arrow/build.gradle.kts
index ecbe01e293..598aa8a879 100644
--- a/arrow/build.gradle.kts
+++ b/arrow/build.gradle.kts
@@ -29,7 +29,6 @@
 
     testImplementation("org.apache.arrow:arrow-jdbc")
     testImplementation("net.hydromatic:scott-data-hsqldb")
-    testImplementation("org.apache.commons:commons-lang3")
     testImplementation(project(":core"))
     testImplementation(project(":testkit"))
 }
diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index 51c4177d10..5a1ad75511 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -106,7 +106,6 @@ fun DependencyConstraintHandlerScope.runtimev(
         apiv("org.apache.calcite.avatica:avatica-server", "calcite.avatica")
         apiv("org.apache.cassandra:cassandra-all")
         apiv("org.apache.commons:commons-dbcp2")
-        apiv("org.apache.commons:commons-lang3")
         apiv("org.apache.commons:commons-math3")
         apiv("org.apache.commons:commons-pool2")
         apiv("org.apache.commons:commons-collections4")
diff --git 
a/cassandra/src/test/java/org/apache/calcite/test/CassandraExtension.java 
b/cassandra/src/test/java/org/apache/calcite/test/CassandraExtension.java
index fa66f78edc..03097925a5 100644
--- a/cassandra/src/test/java/org/apache/calcite/test/CassandraExtension.java
+++ b/cassandra/src/test/java/org/apache/calcite/test/CassandraExtension.java
@@ -19,12 +19,12 @@
 import org.apache.calcite.config.CalciteSystemProperty;
 import org.apache.calcite.util.Sources;
 import org.apache.calcite.util.TestUtil;
+import org.apache.calcite.util.Util;
 
 import org.apache.cassandra.concurrent.Stage;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.service.CassandraDaemon;
 import org.apache.cassandra.service.StorageService;
-import org.apache.commons.lang3.SystemUtils;
 
 import com.datastax.oss.driver.api.core.CqlSession;
 import com.google.common.collect.ImmutableMap;
@@ -134,7 +134,7 @@ private static CassandraResource 
getOrCreate(ExtensionContext context) {
     boolean compatibleGuava = TestUtil.getGuavaMajorVersion() >= 23;
     // remove JVM check once Cassandra supports Eclipse OpenJ9 JVM
     boolean compatibleJVM = !"Eclipse 
OpenJ9".equals(TestUtil.getJavaVirtualMachineVendor());
-    boolean compatibleOS = !SystemUtils.IS_OS_WINDOWS;
+    boolean compatibleOS = !Util.isWindows();
     if (enabled && compatibleJdk && compatibleGuava && compatibleJVM && 
compatibleOS) {
       return ConditionEvaluationResult.enabled("Cassandra tests enabled");
     }
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 842a34fba6..2cb75e9af0 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -69,7 +69,6 @@
     implementation("commons-codec:commons-codec")
     implementation("net.hydromatic:aggdesigner-algorithm")
     implementation("org.apache.commons:commons-dbcp2")
-    implementation("org.apache.commons:commons-lang3")
     implementation("org.apache.commons:commons-math3")
     implementation("org.apache.commons:commons-text")
     implementation("org.jooq:joou-java-6")
diff --git 
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdColumnUniqueness.java 
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdColumnUniqueness.java
index a4ac2e9c5a..e69da27745 100644
--- 
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdColumnUniqueness.java
+++ 
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdColumnUniqueness.java
@@ -55,8 +55,6 @@
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
-import org.apache.commons.lang3.mutable.MutableBoolean;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
@@ -571,17 +569,17 @@ private static void 
addInputRefIfOtherConstant(ImmutableBitSet.Builder builder,
    */
   private static boolean isConstantScalarQuery(RexNode rexNode) {
     if (rexNode.getKind() == SqlKind.SCALAR_QUERY) {
-      MutableBoolean hasCorrelatingVars = new MutableBoolean(false);
+      final boolean[] hasCorrelatingVars = {false};
       ((RexSubQuery) rexNode).rel.accept(new RelShuttleImpl() {
         @Override public RelNode visit(final LogicalFilter filter) {
           if (RexUtil.containsCorrelation(filter.getCondition())) {
-            hasCorrelatingVars.setTrue();
+            hasCorrelatingVars[0] = true;
             return filter;
           }
           return super.visit(filter);
         }
       });
-      return hasCorrelatingVars.isFalse();
+      return !hasCorrelatingVars[0];
     }
     return false;
   }
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/CompressionFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/CompressionFunctions.java
index 1df63573ee..59bac5d5fd 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CompressionFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CompressionFunctions.java
@@ -18,8 +18,6 @@
 
 import org.apache.calcite.avatica.util.ByteString;
 
-import org.apache.commons.lang3.StringUtils;
-
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.io.ByteArrayOutputStream;
@@ -47,7 +45,7 @@ private CompressionFunctions() {
       if (data == null) {
         return null;
       }
-      if (StringUtils.isEmpty(data)) {
+      if (data.isEmpty()) {
         return new ByteString(new byte[0]);
       }
       ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index a729d34916..e2adce8ea1 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -60,7 +60,6 @@
 import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.codec.language.Soundex;
-import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.math3.util.CombinatoricsUtils;
 import org.apache.commons.text.StringEscapeUtils;
 import org.apache.commons.text.similarity.LevenshteinDistance;
@@ -6521,7 +6520,7 @@ public static long customTimestampCeil(DataContext root,
   /** SQL {@code TRANSLATE(string, search_chars, replacement_chars)}
    * function. */
   public static String translate3(String s, String search, String replacement) 
{
-    return org.apache.commons.lang3.StringUtils.replaceChars(s, search, 
replacement);
+    return Util.replaceChars(s, search, replacement);
   }
 
   /** SQL {@code REPLACE(string, search, replacement)} function. */
@@ -6534,7 +6533,7 @@ public static String replace(String s, String search, 
String replacement,
       return s.replace(search, replacement);
     }
     // for MSSQL's REPLACE function, search pattern is case-insensitive during 
matching
-    return org.apache.commons.lang3.Strings.CI.replace(s, search, replacement);
+    return Util.replaceIgnoreCase(s, search, replacement);
   }
 
   /** Helper for "array element reference". Caller has already ensured that
@@ -7699,7 +7698,7 @@ private static String age(long timestamp1, long 
timestamp2) {
       sb.append(
           String.format(Locale.ROOT, "%02d:%02d:%02d.%s", hours, minutes, 
seconds,
               millisString));
-    } else if (ObjectUtils.isNotEmpty(sb)
+    } else if (sb.length() != 0
         && hours == 0 && minutes == 0 && seconds == 0 && millis == 0) {
       return sb.toString().trim();
     } else {
diff --git a/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java
index 2a8af55e23..00660a1b46 100644
--- a/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java
@@ -18,8 +18,7 @@
 
 import org.apache.calcite.util.SimpleNamespaceContext;
 import org.apache.calcite.util.TryThreadLocal;
-
-import org.apache.commons.lang3.StringUtils;
+import org.apache.calcite.util.Util;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.w3c.dom.Node;
@@ -133,7 +132,7 @@ private XmlFunctions() {
                   () -> "firstChild of node " + item);
           result.add(firstChild.getTextContent());
         }
-        return StringUtils.join(result, " ");
+        return Util.joinNullable(result, " ");
       } catch (XPathExpressionException e) {
         return xpathExpression.evaluate(documentNode);
       }
@@ -189,7 +188,7 @@ private XmlFunctions() {
         for (int i = 0; i < nodes.getLength(); i++) {
           result.add(convertNodeToString(castNonNull(nodes.item(i))));
         }
-        return StringUtils.join(result, "");
+        return Util.joinNullable(result, "");
       } catch (XPathExpressionException e) {
         Node node = (Node) xpathExpression
             .evaluate(documentNode, XPathConstants.NODE);
diff --git a/core/src/main/java/org/apache/calcite/util/Util.java 
b/core/src/main/java/org/apache/calcite/util/Util.java
index d911103f26..05b9927192 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -31,6 +31,7 @@
 import org.apache.calcite.sql.util.SqlBasicVisitor;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
@@ -642,6 +643,106 @@ public static String replace(
     return sb.toString();
   }
 
+  /** Right-pads a string with {@code padChar} until it reaches {@code size}.
+   *
+   * <p>Equivalent to {@code 
org.apache.commons.lang3.StringUtils#rightPad(String, int, char)}
+   * for non-null inputs.
+   */
+  public static String rightPad(String s, int size, char padChar) {
+    return Strings.padEnd(s, size, padChar);
+  }
+
+  /** Joins {@code parts} using {@code sep}.
+   *
+   * <p>Null elements are treated as empty strings.
+   */
+  public static String joinNullable(Iterable<? extends @Nullable Object> 
parts, String sep) {
+    final List<String> strings = new ArrayList<>();
+    for (Object o : parts) {
+      strings.add(o == null ? "" : o.toString());
+    }
+    return String.join(sep, strings);
+  }
+
+  /** Returns whether the current runtime is Windows.
+   *
+   * <p>Derived from system property {@code os.name}, using case-insensitive
+   * prefix match against {@code "Windows"}.
+   *
+   * <p>This method is intended to replace commons-lang3's
+   * {@code SystemUtils#IS_OS_WINDOWS}.
+   */
+  public static boolean isWindows() {
+    final String osName = System.getProperty("os.name");
+    return osName != null
+        && osName.regionMatches(true, 0, "Windows", 0, "Windows".length());
+  }
+
+  /** Replaces characters in {@code s} according to {@code search}/{@code 
replacement} mapping.
+   *
+   * <p>Semantics are aligned with {@code 
org.apache.commons.lang3.StringUtils#replaceChars}:
+   * characters found in {@code search} are replaced by the character in the 
same position in
+   * {@code replacement}; if {@code replacement} is shorter, remaining matches 
are removed.
+   */
+  public static @PolyNull String replaceChars(@PolyNull String s, @Nullable 
String search,
+      @Nullable String replacement) {
+    if (s == null || s.isEmpty() || search == null || search.isEmpty()) {
+      return s;
+    }
+    final String repl = replacement == null ? "" : replacement;
+    boolean modified = false;
+    final StringBuilder b = new StringBuilder(s.length());
+    for (int i = 0; i < s.length(); i++) {
+      final char ch = s.charAt(i);
+      final int j = search.indexOf(ch);
+      if (j >= 0) {
+        modified = true;
+        if (j < repl.length()) {
+          b.append(repl.charAt(j));
+        }
+        // else: delete character
+      } else {
+        b.append(ch);
+      }
+    }
+    return modified ? b.toString() : s;
+  }
+
+  /** Case-insensitive replace of all occurrences of {@code search} in {@code 
s}.
+   *
+   * <p>Equivalent to commons-lang's {@code StringUtils.replaceIgnoreCase}, 
but only supports
+   * non-null inputs.
+   */
+  public static String replaceIgnoreCase(String s, String search, String 
replacement) {
+    if (search.isEmpty()) {
+      return s;
+    }
+    final int replLength = search.length();
+    int start = 0;
+    int end = indexOfIgnoreCase(s, search, start);
+    if (end < 0) {
+      return s;
+    }
+    final StringBuilder out = new StringBuilder(s.length());
+    while (end >= 0) {
+      out.append(s, start, end).append(replacement);
+      start = end + replLength;
+      end = indexOfIgnoreCase(s, search, start);
+    }
+    out.append(s, start, s.length());
+    return out.toString();
+  }
+
+  private static int indexOfIgnoreCase(String str, String search, int 
fromIndex) {
+    final int max = str.length() - search.length();
+    for (int i = Math.max(0, fromIndex); i <= max; i++) {
+      if (str.regionMatches(true, i, search, 0, search.length())) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
   /**
    * Creates a file-protocol URL for the given file.
    */
diff --git 
a/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java 
b/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java
index 0e289cf678..66061d3596 100644
--- a/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java
+++ b/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java
@@ -18,8 +18,7 @@
 
 import org.apache.calcite.avatica.util.DateTimeUtils;
 import org.apache.calcite.util.TryThreadLocal;
-
-import org.apache.commons.lang3.StringUtils;
+import org.apache.calcite.util.Util;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
@@ -147,7 +146,7 @@ public enum FormatElementEnum implements FormatElement {
       // Padding zeroes to right as SimpleDateFormat supports precision only 
up to 3 places.
       // Refer to <a 
href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269";>
       // [CALCITE-6269] Fix missing/broken BigQuery date-time format 
elements</a>.
-      sb.append(StringUtils.rightPad(work.sssFormat.format(date), 4, "0"));
+      sb.append(Util.rightPad(work.sssFormat.format(date), 4, '0'));
     }
   },
   FF5("S", "Fractional seconds to 5 digits") {
@@ -156,7 +155,7 @@ public enum FormatElementEnum implements FormatElement {
       // Padding zeroes to right as SimpleDateFormat supports precision only 
up to 3 places.
       // Refer to <a 
href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269";>
       // [CALCITE-6269] Fix missing/broken BigQuery date-time format 
elements</a>.
-      sb.append(StringUtils.rightPad(work.sssFormat.format(date), 5, "0"));
+      sb.append(Util.rightPad(work.sssFormat.format(date), 5, '0'));
     }
   },
   FF6("S", "Fractional seconds to 6 digits") {
@@ -165,7 +164,7 @@ public enum FormatElementEnum implements FormatElement {
       // Padding zeroes to right as SimpleDateFormat supports precision only 
up to 3 places.
       // Refer to <a 
href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269";>
       // [CALCITE-6269] Fix missing/broken BigQuery date-time format 
elements</a>.
-      sb.append(StringUtils.rightPad(work.sssFormat.format(date), 6, "0"));
+      sb.append(Util.rightPad(work.sssFormat.format(date), 6, '0'));
     }
   },
   FF7("S", "Fractional seconds to 6 digits") {
@@ -174,7 +173,7 @@ public enum FormatElementEnum implements FormatElement {
       // Padding zeroes to right as SimpleDateFormat supports precision only 
up to 3 places.
       // Refer to <a 
href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269";>
       // [CALCITE-6269] Fix missing/broken BigQuery date-time format 
elements</a>.
-      sb.append(StringUtils.rightPad(work.sssFormat.format(date), 7, "0"));
+      sb.append(Util.rightPad(work.sssFormat.format(date), 7, '0'));
     }
   },
   FF8("S", "Fractional seconds to 6 digits") {
@@ -183,7 +182,7 @@ public enum FormatElementEnum implements FormatElement {
       // Padding zeroes to right as SimpleDateFormat supports precision only 
up to 3 places.
       // Refer to <a 
href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269";>
       // [CALCITE-6269] Fix missing/broken BigQuery date-time format 
elements</a>.
-      sb.append(StringUtils.rightPad(work.sssFormat.format(date), 8, "0"));
+      sb.append(Util.rightPad(work.sssFormat.format(date), 8, '0'));
     }
   },
   FF9("S", "Fractional seconds to 6 digits") {
@@ -192,7 +191,7 @@ public enum FormatElementEnum implements FormatElement {
       // Padding zeroes to right as SimpleDateFormat supports precision only 
up to 3 places.
       // Refer to <a 
href="https://issues.apache.org/jira/projects/CALCITE/issues/CALCITE-6269";>
       // [CALCITE-6269] Fix missing/broken BigQuery date-time format 
elements</a>.
-      sb.append(StringUtils.rightPad(work.sssFormat.format(date), 9, "0"));
+      sb.append(Util.rightPad(work.sssFormat.format(date), 9, '0'));
     }
   },
   HH12("h", "The hour (12-hour clock) as a decimal number (01-12)") {
diff --git 
a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java 
b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
index 909312dd4e..12c5fbeba3 100644
--- a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
@@ -42,8 +42,6 @@
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.Pair;
 
-import org.apache.commons.lang3.exception.ExceptionUtils;
-
 import org.immutables.value.Value;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -362,7 +360,7 @@ public interface Config extends RelRule.Config {
             "Should throw exception fail since the type mismatches after "
                 + "applying rule.");
 
-    Throwable exception = ExceptionUtils.getRootCause(ex);
+    Throwable exception = getRootCause(ex);
     assertThat(exception, instanceOf(IllegalArgumentException.class));
     assertThat(
         exception.getMessage(), isLinux("Type mismatch:\n"
@@ -372,6 +370,20 @@ public interface Config extends RelRule.Config {
             + "this: JavaType(class java.lang.Integer) -> JavaType(void) NOT 
NULL\n"));
   }
 
+  public static Throwable getRootCause(final Throwable throwable) {
+    final List<Throwable> list = getThrowableList(throwable);
+    return list.isEmpty() ? null : list.get(list.size() - 1);
+  }
+
+  public static List<Throwable> getThrowableList(Throwable throwable) {
+    final List<Throwable> list = new ArrayList<>();
+    while (throwable != null && !list.contains(throwable)) {
+      list.add(throwable);
+      throwable = throwable.getCause();
+    }
+    return list;
+  }
+
   private static <E extends Comparable> List<E> sort(List<E> list) {
     final List<E> list2 = new ArrayList<>(list);
     Collections.sort(list2);
diff --git a/core/src/test/java/org/apache/calcite/test/LintTest.java 
b/core/src/test/java/org/apache/calcite/test/LintTest.java
index ff012bfa16..089d76edba 100644
--- a/core/src/test/java/org/apache/calcite/test/LintTest.java
+++ b/core/src/test/java/org/apache/calcite/test/LintTest.java
@@ -49,6 +49,7 @@
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.hasToString;
 import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
@@ -64,6 +65,10 @@ class LintTest {
       compile("^(\\[CALCITE-[0-9]{1,4}][ ]).*");
   private static final Pattern PATTERN = compile("^ *(// )?");
 
+  private static final Pattern COMMONS_LANG3_IMPORT_PATTERN =
+      compile("^\\s*import\\s+(static\\s+)?"
+          + "org\\.apache\\.commons\\.lang3\\..*;\\s*$");
+
   private static final String TERMINOLOGY_ERROR_MSG =
       "Message contains '%s' word; use one of the following instead: %s";
   private static final List<TermRule> TERM_RULES = initTerminologyRules();
@@ -94,6 +99,13 @@ private Puffin.Program<GlobalState> makeProgram() {
                 && !skipping(line),
             line -> line.state().message("Tab", line))
 
+        // Forbid importing commons-lang3 (Calcite should not use it).
+        .add(line -> isJava(line.filename())
+                && line.matches(COMMONS_LANG3_IMPORT_PATTERN.pattern()),
+            line -> line.state().message(
+                "Forbidden import from 'org.apache.commons.lang3' 
(commons-lang3 is not allowed)",
+                line))
+
         // Comment without space
         .add(line -> line.matches(".* //[^ ].*")
                 && !line.source().fileOpt()
@@ -236,6 +248,21 @@ && isJava(line.filename()),
         .build();
   }
 
+  @Test void testNoCommonsLang3Import() {
+    final Puffin.Program<GlobalState> program = makeProgram();
+    final String code = "import org.apache.commons.lang3.StringUtils;\n"
+        + "class X {}\n";
+    final StringWriter sw = new StringWriter();
+    final GlobalState g;
+    try (PrintWriter pw = new PrintWriter(sw)) {
+      g = program.execute(Stream.of(Sources.of(code)), pw);
+    }
+    final String expected = "[GuavaCharSource{memory}:1:"
+        + "Forbidden import from 'org.apache.commons.lang3' "
+        + "(commons-lang3 is not allowed)]";
+    assertThat(g.messages, hasToString(expected));
+  }
+
   /** Strips the last character from a string. */
   private static String skipLast(String s) {
     return s.substring(0, s.length() - 1);
diff --git a/core/src/test/java/org/apache/calcite/util/UtilTest.java 
b/core/src/test/java/org/apache/calcite/util/UtilTest.java
index 7d8797f19c..2d7ab0f606 100644
--- a/core/src/test/java/org/apache/calcite/util/UtilTest.java
+++ b/core/src/test/java/org/apache/calcite/util/UtilTest.java
@@ -241,6 +241,45 @@ class UtilTest {
     assertThatScientific("-0.0", is("-0.0E0"));
   }
 
+  @Test void testRightPad() {
+    assertThat(Util.rightPad("a", 1, 'x'), is("a"));
+    assertThat(Util.rightPad("a", 2, 'x'), is("ax"));
+    assertThat(Util.rightPad("a", 4, 'x'), is("axxx"));
+    assertThat(Util.rightPad("", 3, 'x'), is("xxx"));
+  }
+
+  @Test void testJoinNullable() {
+    final List<@Nullable Object> parts = Arrays.asList("a", null, "b");
+    assertThat(Util.joinNullable(parts, ":"), is("a::b"));
+    assertThat(Util.joinNullable(Collections.emptyList(), ","), is(""));
+    assertThat(Util.joinNullable(Arrays.asList(null, null), ":"), is(":"));
+  }
+
+  @Test void testReplaceChars() {
+    assertNull(Util.replaceChars((String) null, "ab", "x"));
+    assertThat(Util.replaceChars("", "ab", "x"), is(""));
+
+    final String s = "abc";
+    assertThat(Util.replaceChars(s, null, "x"), sameInstance(s));
+    assertThat(Util.replaceChars(s, "", "x"), sameInstance(s));
+    assertThat(Util.replaceChars(s, "x", "y"), sameInstance(s));
+
+    assertThat(Util.replaceChars("abc", "ab", null), is("c"));
+    assertThat(Util.replaceChars("abc", "abc", "12"), is("12"));
+    assertThat(Util.replaceChars("abc", "a", "a"), is("abc"));
+    assertNotSame("abc", Util.replaceChars("abc", "a", "a"));
+  }
+
+  @Test void testReplaceIgnoreCase() {
+    final String s = "abc";
+    assertThat(Util.replaceIgnoreCase(s, "", "x"), sameInstance(s));
+    assertThat(Util.replaceIgnoreCase(s, "ZZ", "x"), sameInstance(s));
+
+    assertThat(Util.replaceIgnoreCase("aBAba", "ab", "x"), is("xxa"));
+    assertThat(Util.replaceIgnoreCase("xxxx", "X", "yz"), is("yzyzyzyz"));
+    assertThat(Util.replaceIgnoreCase("aaaa", "aa", "b"), is("bb"));
+  }
+
   @Test void testToJavaId() throws UnsupportedEncodingException {
     assertThat(Util.toJavaId("foo", 0), is("ID$0$foo"));
     assertThat(Util.toJavaId("foo bar", 0), is("ID$0$foo_20_bar"));
diff --git a/druid/build.gradle.kts b/druid/build.gradle.kts
index 1b1fcf712e..34cae5bd05 100644
--- a/druid/build.gradle.kts
+++ b/druid/build.gradle.kts
@@ -32,7 +32,6 @@
 
     implementation("com.fasterxml.jackson.core:jackson-databind")
     implementation("com.google.guava:guava")
-    implementation("org.apache.commons:commons-lang3")
 
     testImplementation(project(":testkit"))
     testImplementation("org.mockito:mockito-core")
diff --git 
a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java 
b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
index d313e30e93..ff64d44ed2 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
@@ -54,9 +54,6 @@
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
 
-import org.apache.commons.lang3.tuple.ImmutableTriple;
-import org.apache.commons.lang3.tuple.Triple;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
@@ -220,15 +217,15 @@ protected DruidFilterRule(DruidFilterRuleConfig config) {
           query.getRowType().getFieldNames()
               .indexOf(query.druidTable.timestampFieldName);
       RelNode newDruidQuery = query;
-      final Triple<List<RexNode>, List<RexNode>, List<RexNode>> triple =
+      final SplitFiltersResult split =
           splitFilters(validPreds, nonValidPreds, timestampFieldIdx);
-      if (triple.getLeft().isEmpty() && triple.getMiddle().isEmpty()) {
+      if (split.timeRangeNodes.isEmpty() && split.pushableNodes.isEmpty()) {
         // it sucks, nothing to push
         return;
       }
-      final List<RexNode> residualPreds = new ArrayList<>(triple.getRight());
+      final List<RexNode> residualPreds = new 
ArrayList<>(split.nonPushableNodes);
       List<Interval> intervals = null;
-      if (!triple.getLeft().isEmpty()) {
+      if (!split.timeRangeNodes.isEmpty()) {
         final CalciteConnectionConfig connectionConfig =
             requireNonNull(
                 cluster.getPlanner().getContext()
@@ -236,17 +233,17 @@ protected DruidFilterRule(DruidFilterRuleConfig config) {
         requireNonNull(connectionConfig.timeZone());
         intervals =
             DruidDateTimeUtils.createInterval(
-                RexUtil.composeConjunction(rexBuilder, triple.getLeft()));
+                RexUtil.composeConjunction(rexBuilder, split.timeRangeNodes));
         if (intervals == null || intervals.isEmpty()) {
           // Case we have a filter with extract that can not be written as 
interval push down
-          triple.getMiddle().addAll(triple.getLeft());
+          split.pushableNodes.addAll(split.timeRangeNodes);
         }
       }
 
-      if (!triple.getMiddle().isEmpty()) {
+      if (!split.pushableNodes.isEmpty()) {
         final RelNode newFilter =
             filter.copy(filter.getTraitSet(), Util.last(query.rels),
-                RexUtil.composeConjunction(rexBuilder, triple.getMiddle()));
+                RexUtil.composeConjunction(rexBuilder, split.pushableNodes));
         newDruidQuery = DruidQuery.extendQuery(query, newFilter);
       }
       if (intervals != null && !intervals.isEmpty()) {
@@ -269,7 +266,20 @@ protected DruidFilterRule(DruidFilterRuleConfig config) {
      * 2-m) condition filters that can be pushed to Druid,
      * 3-r) condition filters that cannot be pushed to Druid.
      */
-    private static Triple<List<RexNode>, List<RexNode>, List<RexNode>> 
splitFilters(
+    private static final class SplitFiltersResult {
+      final List<RexNode> timeRangeNodes;
+      final List<RexNode> pushableNodes;
+      final List<RexNode> nonPushableNodes;
+
+      private SplitFiltersResult(List<RexNode> timeRangeNodes, List<RexNode> 
pushableNodes,
+          List<RexNode> nonPushableNodes) {
+        this.timeRangeNodes = timeRangeNodes;
+        this.pushableNodes = pushableNodes;
+        this.nonPushableNodes = nonPushableNodes;
+      }
+    }
+
+    private static SplitFiltersResult splitFilters(
         final List<RexNode> validPreds,
         final List<RexNode> nonValidPreds, final int timestampFieldIdx) {
       final List<RexNode> timeRangeNodes = new ArrayList<>();
@@ -286,7 +296,7 @@ private static Triple<List<RexNode>, List<RexNode>, 
List<RexNode>> splitFilters(
           pushableNodes.add(conj);
         }
       }
-      return ImmutableTriple.of(timeRangeNodes, pushableNodes, 
nonPushableNodes);
+      return new SplitFiltersResult(timeRangeNodes, pushableNodes, 
nonPushableNodes);
     }
 
     /** Rule configuration. */
diff --git a/file/build.gradle.kts b/file/build.gradle.kts
index 37373ccf92..04036a888e 100644
--- a/file/build.gradle.kts
+++ b/file/build.gradle.kts
@@ -31,7 +31,6 @@
     implementation("net.sf.opencsv:opencsv")
     implementation("org.apache.calcite.avatica:avatica-core")
     implementation("commons-io:commons-io")
-    implementation("org.apache.commons:commons-lang3")
     implementation("org.jsoup:jsoup")
     implementation("com.fasterxml.jackson.core:jackson-core")
     implementation("com.fasterxml.jackson.core:jackson-databind")
diff --git 
a/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java 
b/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java
index 4762c20424..012be61450 100644
--- a/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java
+++ b/file/src/main/java/org/apache/calcite/adapter/file/CsvEnumerator.java
@@ -27,8 +27,6 @@
 import org.apache.calcite.util.Source;
 import org.apache.calcite.util.trace.CalciteLogger;
 
-import org.apache.commons.lang3.time.FastDateFormat;
-
 import au.com.bytecode.opencsv.CSVReader;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -40,6 +38,7 @@
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -76,20 +75,43 @@ public class CsvEnumerator<E> implements Enumerator<E> {
   private final RowConverter<E> rowConverter;
   private @Nullable E current;
 
-  private static final FastDateFormat TIME_FORMAT_DATE;
-  private static final FastDateFormat TIME_FORMAT_TIME;
-  private static final FastDateFormat TIME_FORMAT_TIMESTAMP;
+  private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+
+  // FastDateFormat is thread-safe and lenient; mimic with 
ThreadLocal(SimpleDateFormat).
+  private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT_DATE =
+      ThreadLocal.withInitial(() -> {
+        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT);
+        f.setTimeZone(GMT);
+        f.setLenient(true);
+        return f;
+      });
+
+  private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT_TIME =
+      ThreadLocal.withInitial(() -> {
+        SimpleDateFormat f = new SimpleDateFormat("HH:mm:ss", Locale.ROOT);
+        f.setTimeZone(GMT);
+        f.setLenient(true);
+        return f;
+      });
+
+  private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT_TIMESTAMP =
+      ThreadLocal.withInitial(() -> {
+        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", 
Locale.ROOT);
+        f.setTimeZone(GMT);
+        f.setLenient(true);
+        return f;
+      });
+
+  /** Clears per-thread cached date/time formatters to avoid ThreadLocal leaks 
in long-lived
+   * thread pools or container environments. Must be called on the same thread 
that used them. */
+  private static void clearTimeFormats() {
+    TIME_FORMAT_DATE.remove();
+    TIME_FORMAT_TIME.remove();
+    TIME_FORMAT_TIMESTAMP.remove();
+  }
   private static final Pattern DECIMAL_TYPE_PATTERN = Pattern
       .compile("\"decimal\\(([0-9]+),([0-9]+)\\)");
 
-  static {
-    final TimeZone gmt = TimeZone.getTimeZone("GMT");
-    TIME_FORMAT_DATE = FastDateFormat.getInstance("yyyy-MM-dd", gmt);
-    TIME_FORMAT_TIME = FastDateFormat.getInstance("HH:mm:ss", gmt);
-    TIME_FORMAT_TIMESTAMP =
-        FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss", gmt);
-  }
-
   public CsvEnumerator(Source source, AtomicBoolean cancelFlag,
       List<RelDataType> fieldTypes, List<Integer> fields) {
     //noinspection unchecked
@@ -252,7 +274,11 @@ static CSVReader openCsv(Source source) throws IOException 
{
             continue;
           }
           current = null;
-          reader.close();
+          try {
+            reader.close();
+          } finally {
+            clearTimeFormats();
+          }
           return false;
         }
         if (filterValues != null) {
@@ -282,6 +308,8 @@ static CSVReader openCsv(Source source) throws IOException {
       reader.close();
     } catch (IOException e) {
       throw new RuntimeException("Error closing CSV reader", e);
+    } finally {
+      clearTimeFormats();
     }
   }
 
@@ -357,7 +385,7 @@ abstract static class RowConverter<E> {
           return null;
         }
         try {
-          Date date = TIME_FORMAT_DATE.parse(string);
+          Date date = TIME_FORMAT_DATE.get().parse(string);
           return (int) (date.getTime() / DateTimeUtils.MILLIS_PER_DAY);
         } catch (ParseException e) {
           return null;
@@ -367,7 +395,7 @@ abstract static class RowConverter<E> {
           return null;
         }
         try {
-          Date date = TIME_FORMAT_TIME.parse(string);
+          Date date = TIME_FORMAT_TIME.get().parse(string);
           return (int) date.getTime();
         } catch (ParseException e) {
           return null;
@@ -377,7 +405,7 @@ abstract static class RowConverter<E> {
           return null;
         }
         try {
-          Date date = TIME_FORMAT_TIMESTAMP.parse(string);
+          Date date = TIME_FORMAT_TIMESTAMP.get().parse(string);
           return date.getTime();
         } catch (ParseException e) {
           return null;
diff --git a/geode/build.gradle.kts b/geode/build.gradle.kts
index bf51ecdb58..0ef1774167 100644
--- a/geode/build.gradle.kts
+++ b/geode/build.gradle.kts
@@ -30,7 +30,6 @@
 
     implementation("com.google.guava:guava")
     implementation("org.apache.calcite.avatica:avatica-core")
-    implementation("org.apache.commons:commons-lang3")
 
     testImplementation(project(":testkit"))
     testImplementation("com.fasterxml.jackson.core:jackson-databind")
diff --git 
a/geode/src/main/java/org/apache/calcite/adapter/geode/util/GeodeUtils.java 
b/geode/src/main/java/org/apache/calcite/adapter/geode/util/GeodeUtils.java
index 06b74f20b1..2aa212dc5a 100644
--- a/geode/src/main/java/org/apache/calcite/adapter/geode/util/GeodeUtils.java
+++ b/geode/src/main/java/org/apache/calcite/adapter/geode/util/GeodeUtils.java
@@ -22,7 +22,6 @@
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.util.Util;
 
-import org.apache.commons.lang3.Strings;
 import org.apache.geode.cache.CacheClosedException;
 import org.apache.geode.cache.GemFireCache;
 import org.apache.geode.cache.Region;
@@ -84,7 +83,7 @@ public static synchronized ClientCache 
createClientCache(String locatorHost,
       int locatorPort, String autoSerializerPackagePath,
       boolean readSerialized) {
     if (locatorPort != currentLocatorPort
-        || !Strings.CI.equals(currentLocatorHost, locatorHost)) {
+        || !equalsIgnoreCase(currentLocatorHost, locatorHost)) {
       LOGGER.info("Close existing ClientCache ["
           + currentLocatorHost + ":" + currentLocatorPort + "] for new Locator 
connection at: ["
           + locatorHost + ":" + locatorPort + "]");
@@ -117,6 +116,10 @@ public static synchronized void closeClientCache() {
     REGION_MAP.clear();
   }
 
+  private static boolean equalsIgnoreCase(@Nullable String a, @Nullable String 
b) {
+    return a == null ? b == null : b != null && a.equalsIgnoreCase(b);
+  }
+
   /**
    * Obtains a proxy pointing to an existing Region on the server.
    *
diff --git a/gradle.properties b/gradle.properties
index 974408d02b..3005b96424 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -92,7 +92,6 @@ chinook-data-hsqldb.version=0.2
 commons-codec.version=1.16.0
 commons-dbcp2.version=2.11.0
 commons-io.version=2.15.0
-commons-lang3.version=3.18.0
 commons-math3.version=3.6.1
 commons-pool2.version=2.12.0
 commons-collections4.version=4.4
diff --git a/innodb/build.gradle.kts b/innodb/build.gradle.kts
index 88b00aa809..81143af455 100644
--- a/innodb/build.gradle.kts
+++ b/innodb/build.gradle.kts
@@ -31,7 +31,6 @@
     api("com.google.guava:guava")
 
     implementation("org.apache.calcite.avatica:avatica-core")
-    implementation("org.apache.commons:commons-lang3")
     implementation("org.slf4j:slf4j-api")
 
     testImplementation(project(":testkit"))
diff --git 
a/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchema.java 
b/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchema.java
index c6dbf0139a..759f5cd607 100644
--- a/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchema.java
+++ b/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchema.java
@@ -25,8 +25,6 @@
 import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
 import org.apache.calcite.sql.type.SqlTypeName;
 
-import org.apache.commons.lang3.StringUtils;
-
 import com.alibaba.innodb.java.reader.TableReaderFactory;
 import com.alibaba.innodb.java.reader.column.ColumnType;
 import com.alibaba.innodb.java.reader.schema.Column;
@@ -57,7 +55,7 @@ public InnodbSchema(List<String> sqlFilePathList,
       String ibdDataFileBasePath) {
     checkArgument(sqlFilePathList != null && !sqlFilePathList.isEmpty(),
         "SQL file path list cannot be empty");
-    checkArgument(StringUtils.isNotEmpty(ibdDataFileBasePath),
+    checkArgument(ibdDataFileBasePath != null && 
!ibdDataFileBasePath.isEmpty(),
         "InnoDB data file with ibd suffix cannot be empty");
     this.sqlFilePathList = sqlFilePathList;
     this.ibdDataFileBasePath = ibdDataFileBasePath;
diff --git 
a/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchemaFactory.java
 
b/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchemaFactory.java
index 7d9599b049..00b0b287ba 100644
--- 
a/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchemaFactory.java
+++ 
b/innodb/src/main/java/org/apache/calcite/adapter/innodb/InnodbSchemaFactory.java
@@ -20,8 +20,6 @@
 import org.apache.calcite.schema.SchemaFactory;
 import org.apache.calcite.schema.SchemaPlus;
 
-import org.apache.commons.lang3.StringUtils;
-
 import java.util.List;
 import java.util.Map;
 
@@ -37,7 +35,7 @@ public InnodbSchemaFactory() {
     final List<String> sqlFilePathList = (List<String>) 
operand.get("sqlFilePath");
     final String ibdDataFileBasePath = (String) 
operand.get("ibdDataFileBasePath");
     final String timeZone = (String) operand.get("timeZone");
-    if (StringUtils.isNotEmpty(timeZone)) {
+    if (timeZone != null && !timeZone.isEmpty()) {
       System.setProperty("innodb.java.reader.server.timezone", timeZone);
     }
 
diff --git 
a/innodb/src/test/java/org/apache/calcite/adapter/innodb/InnodbAdapterDataTypesTest.java
 
b/innodb/src/test/java/org/apache/calcite/adapter/innodb/InnodbAdapterDataTypesTest.java
index 588e0fa874..d04300303e 100644
--- 
a/innodb/src/test/java/org/apache/calcite/adapter/innodb/InnodbAdapterDataTypesTest.java
+++ 
b/innodb/src/test/java/org/apache/calcite/adapter/innodb/InnodbAdapterDataTypesTest.java
@@ -19,8 +19,6 @@
 import org.apache.calcite.test.CalciteAssert;
 import org.apache.calcite.util.Sources;
 
-import org.apache.commons.lang3.StringUtils;
-
 import com.alibaba.innodb.java.reader.util.Utils;
 import com.google.common.collect.ImmutableMap;
 
@@ -119,18 +117,18 @@ public class InnodbAdapterDataTypesTest {
                 + "f_decimal4=123.100; "
                 + "f_decimal5=12346; "
                 + "f_decimal6=12345.1234567890123456789012345; "
-                + "f_varchar=c" + StringUtils.repeat('x', 31) + "; "
-                + "f_varchar_overflow=c" + StringUtils.repeat("データ", 300) + "; 
"
+                + "f_varchar=c" + repeat('x', 31) + "; "
+                + "f_varchar_overflow=c" + repeat("データ", 300) + "; "
                 + "f_varchar_null=null; "
-                + "f_char_32=c" + StringUtils.repeat("данные", 2) + "; "
-                + "f_char_255=c" + StringUtils.repeat("数据", 100) + "; "
+                + "f_char_32=c" + repeat("данные", 2) + "; "
+                + "f_char_255=c" + repeat("数据", 100) + "; "
                 + "f_char_null=null; "
                 + "f_boolean=false; "
                 + "f_bool=true; "
-                + "f_tinytext=c" + StringUtils.repeat("Data", 50) + "; "
-                + "f_text=c" + StringUtils.repeat("Daten", 200) + "; "
-                + "f_mediumtext=c" + StringUtils.repeat("Datos", 200) + "; "
-                + "f_longtext=c" + StringUtils.repeat("Les données", 800) + "; 
"
+                + "f_tinytext=c" + repeat("Data", 50) + "; "
+                + "f_text=c" + repeat("Daten", 200) + "; "
+                + "f_mediumtext=c" + repeat("Datos", 200) + "; "
+                + "f_longtext=c" + repeat("Les données", 800) + "; "
                 + "f_tinyblob="
                 + genByteArrayString("63", (byte) 0x0a, 100) + "; "
                 + "f_blob="
@@ -164,18 +162,18 @@ public class InnodbAdapterDataTypesTest {
                 + "f_decimal4=456.000; "
                 + "f_decimal5=0; "
                 + "f_decimal6=-0.0123456789012345678912345; "
-                + "f_varchar=d" + StringUtils.repeat('y', 31) + "; "
-                + "f_varchar_overflow=d" + StringUtils.repeat("データ", 300) + "; 
"
+                + "f_varchar=d" + repeat('y', 31) + "; "
+                + "f_varchar_overflow=d" + repeat("データ", 300) + "; "
                 + "f_varchar_null=null; "
-                + "f_char_32=d" + StringUtils.repeat("данные", 2) + "; "
-                + "f_char_255=d" + StringUtils.repeat("数据", 100) + "; "
+                + "f_char_32=d" + repeat("данные", 2) + "; "
+                + "f_char_255=d" + repeat("数据", 100) + "; "
                 + "f_char_null=null; "
                 + "f_boolean=false; "
                 + "f_bool=true; "
-                + "f_tinytext=d" + StringUtils.repeat("Data", 50) + "; "
-                + "f_text=d" + StringUtils.repeat("Daten", 200) + "; "
-                + "f_mediumtext=d" + StringUtils.repeat("Datos", 200) + "; "
-                + "f_longtext=d" + StringUtils.repeat("Les données", 800) + "; 
"
+                + "f_tinytext=d" + repeat("Data", 50) + "; "
+                + "f_text=d" + repeat("Daten", 200) + "; "
+                + "f_mediumtext=d" + repeat("Datos", 200) + "; "
+                + "f_longtext=d" + repeat("Les données", 800) + "; "
                 + "f_tinyblob="
                 + genByteArrayString("64", (byte) 0x0a, 100) + "; "
                 + "f_blob="
@@ -192,6 +190,26 @@ public class InnodbAdapterDataTypesTest {
                 + "f_set=a,e,i,o,u");
   }
 
+  private static String repeat(String s, int n) {
+    if (n <= 0) {
+      return "";
+    }
+    StringBuilder b = new StringBuilder(s.length() * n);
+    for (int i = 0; i < n; i++) {
+      b.append(s);
+    }
+    return b.toString();
+  }
+
+  private static String repeat(char c, int n) {
+    if (n <= 0) {
+      return "";
+    }
+    char[] a = new char[n];
+    java.util.Arrays.fill(a, c);
+    return String.valueOf(a);
+  }
+
   private String genByteArrayString(String prefix, byte b, int repeat) {
     StringBuilder str = new StringBuilder();
     str.append(prefix);
diff --git 
a/plus/src/test/java/org/apache/calcite/adapter/os/OsAdapterTest.java 
b/plus/src/test/java/org/apache/calcite/adapter/os/OsAdapterTest.java
index 9ff5c3f99c..e1158d5aa6 100644
--- a/plus/src/test/java/org/apache/calcite/adapter/os/OsAdapterTest.java
+++ b/plus/src/test/java/org/apache/calcite/adapter/os/OsAdapterTest.java
@@ -71,12 +71,6 @@
  * </ul>
  */
 class OsAdapterTest {
-  private static final String OS_NAME = System.getProperty("os.name");
-
-  private static boolean isWindows() {
-    return OS_NAME.startsWith("Windows");
-  }
-
   /** Returns whether there is a ".git" directory in this directory or in a
    * directory between this directory and root. */
   private static boolean hasGit() {
@@ -114,7 +108,7 @@ private static boolean checkProcessExists(String command) {
   }
 
   @Test void testDu() {
-    assumeFalse(isWindows(), "Skip: the 'du' table does not work on Windows");
+    assumeFalse(Util.isWindows(), "Skip: the 'du' table does not work on 
Windows");
     assumeToolExists("du");
     sql("select * from du")
         .returns(r -> {
@@ -130,7 +124,7 @@ private static boolean checkProcessExists(String command) {
   }
 
   @Test void testDuFilterSortLimit() {
-    assumeFalse(isWindows(), "Skip: the 'du' table does not work on Windows");
+    assumeFalse(Util.isWindows(), "Skip: the 'du' table does not work on 
Windows");
     assumeToolExists("du");
     sql("select * from du where path like '%/src/test/java/%'\n"
         + "order by 1 limit 2")
@@ -149,14 +143,14 @@ private static boolean checkProcessExists(String command) 
{
   }
 
   @Test void testFiles() {
-    assumeFalse(isWindows(), "Skip: the 'files' table does not work on 
Windows");
+    assumeFalse(Util.isWindows(), "Skip: the 'files' table does not work on 
Windows");
     sql("select distinct type from files")
         .returnsUnordered("type=d",
             "type=f");
   }
 
   @Test void testPs() {
-    assumeFalse(isWindows(), "Skip: the 'ps' table does not work on Windows");
+    assumeFalse(Util.isWindows(), "Skip: the 'ps' table does not work on 
Windows");
     assumeToolExists("ps");
     sql("select * from ps")
         .returns(r -> {
@@ -176,7 +170,7 @@ private static boolean checkProcessExists(String command) {
   }
 
   @Test void testPsDistinct() {
-    assumeFalse(isWindows(), "Skip: the 'ps' table does not work on Windows");
+    assumeFalse(Util.isWindows(), "Skip: the 'ps' table does not work on 
Windows");
     assumeToolExists("ps");
     sql("select distinct `user` from ps")
         .returns(r -> {
@@ -229,7 +223,7 @@ private static boolean checkProcessExists(String command) {
   }
 
   @Test void testVmstat() {
-    assumeFalse(isWindows(), "Skip: the 'files' table does not work on 
Windows");
+    assumeFalse(Util.isWindows(), "Skip: the 'files' table does not work on 
Windows");
     assumeToolExists("vmstat");
     sql("select * from vmstat")
         .returns(r -> {
diff --git a/redis/build.gradle.kts b/redis/build.gradle.kts
index 0cbc43112f..11eb99f93d 100644
--- a/redis/build.gradle.kts
+++ b/redis/build.gradle.kts
@@ -24,7 +24,6 @@
     implementation("com.google.guava:guava")
     implementation("commons-io:commons-io")
     implementation("org.apache.calcite.avatica:avatica-core")
-    implementation("org.apache.commons:commons-lang3")
     implementation("org.apache.commons:commons-pool2")
     implementation("org.slf4j:slf4j-api")
 
diff --git 
a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisDataProcess.java 
b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisDataProcess.java
index 853507cd1a..024a0e7df9 100644
--- a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisDataProcess.java
+++ b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisDataProcess.java
@@ -16,8 +16,6 @@
  */
 package org.apache.calcite.adapter.redis;
 
-import org.apache.commons.lang3.StringUtils;
-
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -77,7 +75,7 @@ public List<Object[]> read() {
   }
 
   private Object[] parseJson(String value) {
-    assert StringUtils.isNotEmpty(value);
+    assert value != null && !value.isEmpty();
     Object[] arr = new Object[fields.size()];
     try {
       JsonNode jsonNode = objectMapper.readTree(value);
@@ -97,7 +95,7 @@ private Object[] parseJson(String value) {
   }
 
   private Object[] parseCsv(String value) {
-    assert StringUtils.isNotEmpty(value);
+    assert value != null && !value.isEmpty();
     String[] values = value.split(keyDelimiter);
     Object[] arr = new Object[fields.size()];
     assert values.length == arr.length;
diff --git 
a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisEnumerator.java 
b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisEnumerator.java
index 0643d804fe..dede17cd48 100644
--- a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisEnumerator.java
+++ b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisEnumerator.java
@@ -19,8 +19,6 @@
 import org.apache.calcite.linq4j.Enumerator;
 import org.apache.calcite.linq4j.Linq4j;
 
-import org.apache.commons.lang3.StringUtils;
-
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -38,13 +36,14 @@ class RedisEnumerator implements Enumerator<Object[]> {
   RedisEnumerator(RedisConfig redisConfig, RedisSchema schema, String 
tableName) {
     RedisTableFieldInfo tableFieldInfo = schema.getTableFieldInfo(tableName);
 
+    String password = redisConfig.getPassword();
     RedisJedisManager redisManager =
         new RedisJedisManager(redisConfig.getHost(), redisConfig.getPort(),
-            redisConfig.getDatabase(), redisConfig.getPassword());
+            redisConfig.getDatabase(), password);
 
     try (Jedis jedis = redisManager.getResource()) {
-      if (StringUtils.isNotEmpty(redisConfig.getPassword())) {
-        jedis.auth(redisConfig.getPassword());
+      if (password != null && !password.isEmpty()) {
+        jedis.auth(password);
       }
       RedisDataProcess dataProcess = new RedisDataProcess(jedis, 
tableFieldInfo);
       List<Object[]> objs = dataProcess.read();
diff --git 
a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisJedisManager.java 
b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisJedisManager.java
index 92647d6c14..226af4b7b7 100644
--- 
a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisJedisManager.java
+++ 
b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisJedisManager.java
@@ -18,7 +18,6 @@
 
 import org.apache.calcite.util.trace.CalciteTrace;
 
-import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 
 import com.google.common.cache.CacheBuilder;
@@ -75,7 +74,7 @@ public Jedis getResource() {
 
   private JedisPool createConsumer() {
     String pwd = password;
-    if (StringUtils.isEmpty(pwd)) {
+    if (pwd == null || pwd.isEmpty()) {
       pwd = null;
     }
     return new JedisPool(jedisPoolConfig, host, port, Protocol.DEFAULT_TIMEOUT,
diff --git 
a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisSchema.java 
b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisSchema.java
index 1aad9ae459..42c623895b 100644
--- a/redis/src/main/java/org/apache/calcite/adapter/redis/RedisSchema.java
+++ b/redis/src/main/java/org/apache/calcite/adapter/redis/RedisSchema.java
@@ -20,19 +20,20 @@
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.impl.AbstractSchema;
 
-import org.apache.commons.lang3.ObjectUtils;
-import org.apache.commons.lang3.StringUtils;
-
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 
+import org.checkerframework.checker.nullness.qual.Nullable;
+
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -92,7 +93,7 @@ public RedisTableFieldInfo getTableFieldInfo(String 
tableName) {
       if (jsonCustomTable.name.equals(tableName)) {
         Map<String, Object> map =
             requireNonNull(jsonCustomTable.operand, OPERAND);
-        if (ObjectUtils.isEmpty(map.get(DATA_FORMAT))) {
+        if (isEmptyObject(map.get(DATA_FORMAT))) {
           throw new RuntimeException("dataFormat is invalid, it must be raw, 
csv or json");
         }
         RedisDataFormat dataFormatEnum =
@@ -100,7 +101,7 @@ public RedisTableFieldInfo getTableFieldInfo(String 
tableName) {
         if (dataFormatEnum == null) {
           throw new RuntimeException("dataFormat is invalid, it must be raw, 
csv or json");
         }
-        if (ObjectUtils.isEmpty(map.get(FIELDS))) {
+        if (isEmptyObject(map.get(FIELDS))) {
           throw new RuntimeException("fields is null");
         }
         dataFormat = map.get(DATA_FORMAT).toString();
@@ -114,9 +115,45 @@ public RedisTableFieldInfo getTableFieldInfo(String 
tableName) {
     tableFieldInfo.setTableName(tableName);
     tableFieldInfo.setDataFormat(dataFormat);
     tableFieldInfo.setFields(fields);
-    if (StringUtils.isNotEmpty(keyDelimiter)) {
+    if (!keyDelimiter.isEmpty()) {
       tableFieldInfo.setKeyDelimiter(keyDelimiter);
     }
     return tableFieldInfo;
   }
+
+  /** Returns whether an object should be considered "empty" for 
configuration/validation.
+   *
+   * <p>Semantics are aligned with
+   * {@code org.apache.commons.lang3.ObjectUtils#isEmptyObject(Object)}.
+   * <ul>
+   *   <li>{@code null} is empty
+   *   <li>{@link CharSequence}: {@code length()==0} is empty
+   *   <li>{@link Collection}/{@link Map}: {@code isEmpty()} is empty
+   *   <li>{@link Optional}: {@code !isPresent()} is empty
+   *   <li>Arrays: {@code length==0} is empty
+   * </ul>
+   */
+  public static boolean isEmptyObject(@Nullable Object o) {
+    if (o == null) {
+      return true;
+    }
+    if (o instanceof CharSequence) {
+      return ((CharSequence) o).length() == 0;
+    }
+    if (o instanceof Collection<?>) {
+      return ((Collection<?>) o).isEmpty();
+    }
+    if (o instanceof Map) {
+      return ((Map<?, ?>) o).isEmpty();
+    }
+    if (o instanceof Optional) {
+      return !((Optional<?>) o).isPresent();
+    }
+    final Class<?> c = o.getClass();
+    if (c.isArray()) {
+      return java.lang.reflect.Array.getLength(o) == 0;
+    }
+    return false;
+  }
+
 }
diff --git 
a/redis/src/test/java/org/apache/calcite/adapter/redis/RedisCaseBase.java 
b/redis/src/test/java/org/apache/calcite/adapter/redis/RedisCaseBase.java
index 44d82385aa..adfc7dd6c6 100644
--- a/redis/src/test/java/org/apache/calcite/adapter/redis/RedisCaseBase.java
+++ b/redis/src/test/java/org/apache/calcite/adapter/redis/RedisCaseBase.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.adapter.redis;
 
 import org.apache.calcite.config.CalciteSystemProperty;
+import org.apache.calcite.util.Util;
 
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
@@ -71,7 +72,7 @@ public static void startRedisContainer() {
   @BeforeEach
   public void createRedisServer() throws IOException {
     if (!REDIS_CONTAINER.isRunning()) {
-      if (isWindows()) {
+      if (Util.isWindows()) {
         redisServer = 
RedisServer.builder().port(PORT).setting(MAX_HEAP).build();
       } else {
         redisServer = new RedisServer(PORT);
@@ -81,10 +82,6 @@ public void createRedisServer() throws IOException {
     }
   }
 
-  private static boolean isWindows() {
-    return System.getProperty("os.name").startsWith("Windows");
-  }
-
   @AfterEach
   public void stopRedisServer() {
     if (!REDIS_CONTAINER.isRunning()) {
diff --git a/testkit/build.gradle.kts b/testkit/build.gradle.kts
index 2154cbf82c..613526ebd0 100644
--- a/testkit/build.gradle.kts
+++ b/testkit/build.gradle.kts
@@ -30,7 +30,6 @@
     implementation("net.hydromatic:scott-data-hsqldb")
     implementation("net.hydromatic:steelwheels-data-hsqldb")
     implementation("org.apache.commons:commons-dbcp2")
-    implementation("org.apache.commons:commons-lang3")
     implementation("org.apache.commons:commons-pool2")
     implementation("org.hamcrest:hamcrest")
     implementation("org.hsqldb:hsqldb::jdk8")
diff --git a/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java 
b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
index 8a846da87e..cbc2f6c799 100644
--- a/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
+++ b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
@@ -137,8 +137,6 @@
 import static org.apache.calcite.test.Matchers.containsStringLinux;
 import static org.apache.calcite.test.Matchers.isLinux;
 
-import static org.apache.commons.lang3.StringUtils.countMatches;
-
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.hasItem;
@@ -500,6 +498,19 @@ public static Consumer<ResultSet> checkResultContains(
     };
   }
 
+  private static int countMatches(@Nullable String str, @Nullable String sub) {
+    if (str == null || sub == null || sub.isEmpty()) {
+      return 0;
+    }
+    int count = 0;
+    int idx = 0;
+    while ((idx = str.indexOf(sub, idx)) >= 0) {
+      count++;
+      idx += sub.length();
+    }
+    return count;
+  }
+
   public static Consumer<ResultSet> checkMaskedResultContains(
       final String expected) {
     return s -> {

Reply via email to