Merge branch 'main' into java_21
diff --git a/.github/workflows/distribution.yml b/.github/workflows/distribution.yml
index 3ed1998..1c2b39c 100644
--- a/.github/workflows/distribution.yml
+++ b/.github/workflows/distribution.yml
@@ -35,7 +35,7 @@
       uses: actions/setup-java@v3
       with:
         distribution: 'temurin'
-        java-version: 17
+        java-version: 21
         java-package: jdk
     - name: Prepare caches
       uses: ./.github/actions/gradle-caches
diff --git a/.github/workflows/gradle-precommit.yml b/.github/workflows/gradle-precommit.yml
index 076fb0e..0e3c2d6 100644
--- a/.github/workflows/gradle-precommit.yml
+++ b/.github/workflows/gradle-precommit.yml
@@ -25,7 +25,7 @@
         # Operating systems to run on.
         os: [ubuntu-latest]
         # Test JVMs.
-        java: [ '17' ]
+        java: [ '21' ]
 
     env:
       GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }}
@@ -59,7 +59,7 @@
         # macos-latest: a tad slower than ubuntu and pretty much the same (?) so leaving out.
         os: [ubuntu-latest]
         # Test JVMs.
-        java: [ '17' ]
+        java: [ '21' ]
 
     env:
       GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }}
diff --git a/.github/workflows/hunspell.yml b/.github/workflows/hunspell.yml
index ed040ec..f44e2ea 100644
--- a/.github/workflows/hunspell.yml
+++ b/.github/workflows/hunspell.yml
@@ -25,7 +25,7 @@
       uses: actions/setup-java@v3
       with:
         distribution: 'temurin'
-        java-version: 17
+        java-version: 21
         java-package: jdk
 
     - name: Prepare caches
diff --git a/build.gradle b/build.gradle
index f396a77..9c64ab1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -73,7 +73,7 @@
   }
 
   // Minimum Java version required to compile and run Lucene.
-  minJavaVersion = JavaVersion.VERSION_17
+  minJavaVersion = JavaVersion.VERSION_21
 
   // snapshot build marker used in scripts.
   snapshotBuild = version.contains("SNAPSHOT")
@@ -101,6 +101,12 @@
   groovy "org.codehaus.groovy:groovy-all:3.0.12"
 }
 
+repositories {
+  maven {
+    url "https://repo.eclipse.org/content/repositories/eclipse-snapshots/"
+  }
+}
+
 apply from: file('buildSrc/scriptDepVersions.gradle')
 
 // Include smaller chunks configuring dedicated build areas.
diff --git a/buildSrc/scriptDepVersions.gradle b/buildSrc/scriptDepVersions.gradle
index 991ff18..795ce08 100644
--- a/buildSrc/scriptDepVersions.gradle
+++ b/buildSrc/scriptDepVersions.gradle
@@ -24,7 +24,7 @@
       "apache-rat": "0.14",
       "asm": "9.6",
       "commons-codec": "1.13",
-      "ecj": "3.30.0",
+      "ecj": "3.36.0-SNAPSHOT",
       "flexmark": "0.61.24",
       "javacc": "7.0.12",
       "jflex": "1.8.2",
diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/br/BrazilianStemmer.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/br/BrazilianStemmer.java
index f5647fc..f999c25 100644
--- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/br/BrazilianStemmer.java
+++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/br/BrazilianStemmer.java
@@ -20,7 +20,7 @@
 
 /** A stemmer for Brazilian Portuguese words. */
 class BrazilianStemmer {
-  private static final Locale locale = new Locale("pt", "BR");
+  private static final Locale locale = new Locale.Builder().setLanguageTag("pt-BR").build();
 
   /** Changed term */
   private String TERM;
diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/de/GermanStemmer.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/de/GermanStemmer.java
index ba668b2..866b160 100644
--- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/de/GermanStemmer.java
+++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/de/GermanStemmer.java
@@ -33,7 +33,7 @@
   /** Amount of characters that are removed with <code>substitute()</code> while stemming. */
   private int substCount = 0;
 
-  private static final Locale locale = new Locale("de", "DE");
+  private static final Locale locale = new Locale.Builder().setLanguageTag("de-DE").build();
 
   /**
    * Stemms the given term to an unique <code>discriminator</code>.
diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/th/ThaiTokenizer.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/th/ThaiTokenizer.java
index d098d75..50674f1 100644
--- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/th/ThaiTokenizer.java
+++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/th/ThaiTokenizer.java
@@ -38,7 +38,8 @@
    */
   public static final boolean DBBI_AVAILABLE;
 
-  private static final BreakIterator proto = BreakIterator.getWordInstance(new Locale("th"));
+  private static final BreakIterator proto =
+      BreakIterator.getWordInstance(new Locale.Builder().setLanguageTag("th").build());
 
   static {
     // check that we have a working dictionary-based break iterator for thai
diff --git a/lucene/analysis/common/src/test/org/apache/lucene/collation/TestCollationKeyAnalyzer.java b/lucene/analysis/common/src/test/org/apache/lucene/collation/TestCollationKeyAnalyzer.java
index 7ecfb7b..b76c1d0 100644
--- a/lucene/analysis/common/src/test/org/apache/lucene/collation/TestCollationKeyAnalyzer.java
+++ b/lucene/analysis/common/src/test/org/apache/lucene/collation/TestCollationKeyAnalyzer.java
@@ -26,7 +26,8 @@
   // Neither Java 1.4.2 nor 1.5.0 has Farsi Locale collation available in
   // RuleBasedCollator.  However, the Arabic Locale seems to order the Farsi
   // characters properly.
-  private Collator collator = Collator.getInstance(new Locale("ar"));
+  private Collator collator =
+      Collator.getInstance(new Locale.Builder().setLanguageTag("ar").build());
   private Analyzer analyzer;
 
   @Override
diff --git a/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUCollationKeyAnalyzer.java b/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUCollationKeyAnalyzer.java
index e9141c2..fa867e2 100644
--- a/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUCollationKeyAnalyzer.java
+++ b/lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/TestICUCollationKeyAnalyzer.java
@@ -24,7 +24,8 @@
 
 public class TestICUCollationKeyAnalyzer extends CollationTestBase {
 
-  private Collator collator = Collator.getInstance(new Locale("fa"));
+  private Collator collator =
+      Collator.getInstance(new Locale.Builder().setLanguageTag("fa").build());
   private Analyzer analyzer;
 
   @Override
diff --git a/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/PerfRunData.java b/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/PerfRunData.java
index 5c34666..72253d3 100644
--- a/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/PerfRunData.java
+++ b/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/PerfRunData.java
@@ -46,6 +46,7 @@
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.store.FSDirectory;
 import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.SuppressForbidden;
 
 /**
  * Data maintained by a performance test run.
@@ -204,7 +205,7 @@
     resetInputs();
 
     // release unused stuff
-    System.runFinalization();
+    runFinalization();
     System.gc();
 
     // Re-init clock
@@ -482,4 +483,10 @@
   public Map<String, AnalyzerFactory> getAnalyzerFactories() {
     return analyzerFactories;
   }
+
+  @SuppressWarnings("removal")
+  @SuppressForbidden(reason = "requires to run finalization")
+  private static void runFinalization() {
+    System.runFinalization();
+  }
 }
diff --git a/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/tasks/NewLocaleTask.java b/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/tasks/NewLocaleTask.java
index cbeab25..7137bc1 100644
--- a/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/tasks/NewLocaleTask.java
+++ b/lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/tasks/NewLocaleTask.java
@@ -17,47 +17,44 @@
 package org.apache.lucene.benchmark.byTask.tasks;
 
 import java.util.Locale;
-import java.util.StringTokenizer;
 import org.apache.lucene.benchmark.byTask.PerfRunData;
 
 /**
  * Set a {@link java.util.Locale} for use in benchmarking.
  *
- * <p>Locales can be specified in the following ways:
+ * <p>Locales can be specified as BCP-47 language tag or as ROOT or empty string.
  *
  * <ul>
  *   <li><code>de</code>: Language "de"
- *   <li><code>en,US</code>: Language "en", country "US"
- *   <li><code>no,NO,NY</code>: Language "no", country "NO", variant "NY"
+ *   <li><code>en-US</code>: Language "en", country "US"
  *   <li><code>ROOT</code>: The root (language-agnostic) Locale
  *   <li>&lt;empty string&gt;: Erase the Locale (null)
  * </ul>
  */
 public class NewLocaleTask extends PerfTask {
-  private String language;
-  private String country;
-  private String variant;
+  private String tag;
 
   /**
-   * Create a new {@link java.util.Locale} and set it it in the getRunData() for use by all future
+   * Create a new {@link java.util.Locale} and set it in the getRunData() for use by all future
    * tasks.
    */
   public NewLocaleTask(PerfRunData runData) {
     super(runData);
   }
 
-  static Locale createLocale(String language, String country, String variant) {
-    if (language == null || language.length() == 0) return null;
+  static Locale createLocale(String tag) {
+    if (tag == null || tag.length() == 0) return null;
 
-    String lang = language;
-    if (lang.equalsIgnoreCase("ROOT")) lang = ""; // empty language is the root locale in the JDK
+    if (tag.equalsIgnoreCase("ROOT")) {
+      return Locale.ROOT;
+    }
 
-    return new Locale(lang, country, variant);
+    return new Locale.Builder().setLanguageTag(tag).build();
   }
 
   @Override
   public int doLogic() throws Exception {
-    Locale locale = createLocale(language, country, variant);
+    Locale locale = createLocale(tag);
     getRunData().setLocale(locale);
     System.out.println(
         "Changed Locale to: "
@@ -70,11 +67,7 @@
   @Override
   public void setParams(String params) {
     super.setParams(params);
-    language = country = variant = "";
-    StringTokenizer st = new StringTokenizer(params, ",");
-    if (st.hasMoreTokens()) language = st.nextToken();
-    if (st.hasMoreTokens()) country = st.nextToken();
-    if (st.hasMoreTokens()) variant = st.nextToken();
+    tag = params;
   }
 
   @Override
diff --git a/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/TestPerfTasksLogic.java b/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/TestPerfTasksLogic.java
index 0ded600..312e1c1 100644
--- a/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/TestPerfTasksLogic.java
+++ b/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/TestPerfTasksLogic.java
@@ -793,19 +793,17 @@
 
     // ROOT locale
     benchmark = execBenchmark(getLocaleConfig("ROOT"));
-    assertEquals(new Locale(""), benchmark.getRunData().getLocale());
+    assertEquals(Locale.ROOT, benchmark.getRunData().getLocale());
 
     // specify just a language
     benchmark = execBenchmark(getLocaleConfig("de"));
-    assertEquals(new Locale("de"), benchmark.getRunData().getLocale());
+    assertEquals(
+        new Locale.Builder().setLanguageTag("de").build(), benchmark.getRunData().getLocale());
 
     // specify language + country
-    benchmark = execBenchmark(getLocaleConfig("en,US"));
-    assertEquals(new Locale("en", "US"), benchmark.getRunData().getLocale());
-
-    // specify language + country + variant
-    benchmark = execBenchmark(getLocaleConfig("no,NO,NY"));
-    assertEquals(new Locale("no", "NO", "NY"), benchmark.getRunData().getLocale());
+    benchmark = execBenchmark(getLocaleConfig("en-US"));
+    assertEquals(
+        new Locale.Builder().setLanguageTag("en-US").build(), benchmark.getRunData().getLocale());
   }
 
   private String[] getLocaleConfig(String localeParam) {
@@ -832,22 +830,28 @@
   public void testCollator() throws Exception {
     // ROOT locale
     Benchmark benchmark = execBenchmark(getCollatorConfig("ROOT", "impl:jdk"));
-    CollationKeyAnalyzer expected = new CollationKeyAnalyzer(Collator.getInstance(new Locale("")));
+    CollationKeyAnalyzer expected = new CollationKeyAnalyzer(Collator.getInstance(Locale.ROOT));
     assertEqualCollation(expected, benchmark.getRunData().getAnalyzer(), "foobar");
 
     // specify just a language
     benchmark = execBenchmark(getCollatorConfig("de", "impl:jdk"));
-    expected = new CollationKeyAnalyzer(Collator.getInstance(new Locale("de")));
+    expected =
+        new CollationKeyAnalyzer(
+            Collator.getInstance(new Locale.Builder().setLanguageTag("de").build()));
     assertEqualCollation(expected, benchmark.getRunData().getAnalyzer(), "foobar");
 
     // specify language + country
-    benchmark = execBenchmark(getCollatorConfig("en,US", "impl:jdk"));
-    expected = new CollationKeyAnalyzer(Collator.getInstance(new Locale("en", "US")));
+    benchmark = execBenchmark(getCollatorConfig("en-US", "impl:jdk"));
+    expected =
+        new CollationKeyAnalyzer(
+            Collator.getInstance(new Locale.Builder().setLanguageTag("en-US").build()));
     assertEqualCollation(expected, benchmark.getRunData().getAnalyzer(), "foobar");
 
     // specify language + country + variant
-    benchmark = execBenchmark(getCollatorConfig("no,NO,NY", "impl:jdk"));
-    expected = new CollationKeyAnalyzer(Collator.getInstance(new Locale("no", "NO", "NY")));
+    benchmark = execBenchmark(getCollatorConfig("nn-NO", "impl:jdk"));
+    expected =
+        new CollationKeyAnalyzer(
+            Collator.getInstance(new Locale.Builder().setLanguageTag("nn-NO").build()));
     assertEqualCollation(expected, benchmark.getRunData().getAnalyzer(), "foobar");
   }
 
diff --git a/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/feeds/TestHtmlParser.java b/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/feeds/TestHtmlParser.java
index 9d14783..c318a01 100644
--- a/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/feeds/TestHtmlParser.java
+++ b/lucene/benchmark/src/test/org/apache/lucene/benchmark/byTask/feeds/TestHtmlParser.java
@@ -96,7 +96,7 @@
   public void testTurkish() throws Exception {
     final Locale saved = Locale.getDefault();
     try {
-      Locale.setDefault(new Locale("tr", "TR"));
+      Locale.setDefault(new Locale.Builder().setLanguageTag("tr-TR").build());
       String text =
           "<html><HEAD><TITLE>ııı</TITLE></head><body>"
               + "<IMG SRC=\"../images/head.jpg\" WIDTH=570 HEIGHT=47 BORDER=0 ALT=\"ş\">"
diff --git a/lucene/core/src/test/org/apache/lucene/util/TestWeakIdentityMap.java b/lucene/core/src/test/org/apache/lucene/util/TestWeakIdentityMap.java
index 3db8540..86098cb 100644
--- a/lucene/core/src/test/org/apache/lucene/util/TestWeakIdentityMap.java
+++ b/lucene/core/src/test/org/apache/lucene/util/TestWeakIdentityMap.java
@@ -119,7 +119,7 @@
     int size = map.size();
     for (int i = 0; size > 0 && i < 10; i++)
       try {
-        System.runFinalization();
+        runFinalization();
         System.gc();
         int newSize = map.size();
         assertTrue("previousSize(" + size + ")>=newSize(" + newSize + ")", size >= newSize);
@@ -232,7 +232,7 @@
     int size = map.size();
     for (int i = 0; size > 0 && i < 10; i++)
       try {
-        System.runFinalization();
+        runFinalization();
         System.gc();
         int newSize = map.size();
         assertTrue("previousSize(" + size + ")>=newSize(" + newSize + ")", size >= newSize);
@@ -252,4 +252,10 @@
           InterruptedException ie) {
       }
   }
+
+  @SuppressWarnings("removal")
+  @SuppressForbidden(reason = "requires to run finalization")
+  private static void runFinalization() {
+    System.runFinalization();
+  }
 }
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/search/QueryParserPaneProvider.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/search/QueryParserPaneProvider.java
index 4dc585e..0c32b2a 100644
--- a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/search/QueryParserPaneProvider.java
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/components/fragments/search/QueryParserPaneProvider.java
@@ -421,7 +421,7 @@
         .fuzzyMinSim(fuzzyMinSimFloat)
         .fuzzyPrefixLength(fuzzyPrefLenInt)
         .dateResolution(DateTools.Resolution.valueOf((String) dateResCB.getSelectedItem()))
-        .locale(new Locale(locationTF.getText()))
+        .locale(new Locale.Builder().setLanguageTag(locationTF.getText()).build())
         .timeZone(TimeZone.getTimeZone(timezoneTF.getText()))
         .typeMap(typeMap)
         .build();
diff --git a/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/messages/TestNLS.java b/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/messages/TestNLS.java
index ba6b413..0065c45 100644
--- a/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/messages/TestNLS.java
+++ b/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/messages/TestNLS.java
@@ -68,7 +68,7 @@
   }
 
   public void testNLSLoading_xx_XX() {
-    Locale locale = new Locale("xx", "XX", "");
+    Locale locale = new Locale.Builder().setLanguageTag("xx-XX").build();
     String message =
         NLS.getLocalizedMessage(
             MessagesTestBundle.Q0004E_INVALID_SYNTAX_ESCAPE_UNICODE_TRUNCATION, locale);