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

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

commit 436f44183b7e730b1a4053b96d3c29e250432fe2
Author: Martin Wiesner <[email protected]>
AuthorDate: Sat Mar 14 21:50:22 2026 +0100

    OPENNLP-1735: Upgrade minimum JDK level to 21 LTS
    - adjusts pom.xml
    - adjusts GH actions
    - replace new Locale(..) with Locale.of(..)
    - replaces new URL(..) with new URI(..).toURL()
---
 .github/workflows/license.yml                      |  4 +-
 .github/workflows/maven.yml                        |  8 +--
 .github/workflows/publish-snapshots.yml            |  4 +-
 .github/workflows/shell-tests.yml                  | 18 +++----
 README.md                                          | 10 ++--
 .../opennlp/tools/AbstractModelLoaderTest.java     | 14 ++++-
 .../opennlp/tools/EnabledWhenCDNAvailable.java     |  7 ++-
 .../formats/NameFinderCensus90NameStream.java      |  4 +-
 .../opennlp/tools/formats/conllu/ConlluStream.java |  2 +-
 .../tools/formats/conllu/ConlluStreamTest.java     |  6 +--
 .../models/simple/SimpleClassPathModelFinder.java  | 20 ++++---
 .../main/java/opennlp/tools/util/DownloadUtil.java | 63 +++++++++++++---------
 .../opennlp/tools/AbstractModelLoaderTest.java     | 14 ++++-
 .../opennlp/tools/EnabledWhenCDNAvailable.java     |  7 ++-
 .../sentdetect/AbstractSentenceDetectorTest.java   |  8 +--
 .../tools/tokenize/TokenizerFactoryTest.java       |  8 +--
 .../java/opennlp/uima/normalizer/NumberUtil.java   |  4 +-
 .../src/test/java/opennlp/uima/AbstractIT.java     | 12 ++++-
 .../opennlp/tools/EnabledWhenCDNAvailable.java     |  7 ++-
 .../tools/namefind/AbstractNameFinderTest.java     | 12 ++++-
 .../opennlp/tools/util/DownloadParserTest.java     | 44 +++++++++------
 pom.xml                                            |  8 +--
 22 files changed, 186 insertions(+), 98 deletions(-)

diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml
index 9ebde146..b3f0c245 100644
--- a/.github/workflows/license.yml
+++ b/.github/workflows/license.yml
@@ -27,10 +27,10 @@ jobs:
 
     steps:
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
-      - name: Set up JDK 17
+      - name: Set up JDK 21
         uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 
v5.2.0
         with:
-          java-version: '17'
+          java-version: '21'
           distribution: 'temurin'
 
       - name: Cache Maven packages
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index bf7aa0d4..a0ad6a78 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -27,14 +27,14 @@ on:
 jobs:
   build:
     runs-on: ${{ matrix.os }}
-    continue-on-error: ${{ matrix.experimental }}
     strategy:
+      fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        java: [ 17, 21, 25 ]
+        java: [ 21, 25 ]
         experimental: [false]
 #        include:
-#          - java: 25-ea
+#          - java: 26-ea
 #            os: ubuntu-latest
 #            experimental: true
 
@@ -54,6 +54,6 @@ jobs:
         distribution: temurin
         java-version: ${{ matrix.java }}
     - name: Build with Maven
-      run: mvn -V clean test install --show-version --batch-mode 
--no-transfer-progress -Pjacoco -Pci
+      run: mvn -V clean test verify --show-version --batch-mode 
--no-transfer-progress -Pjacoco -Pci
     - name: Jacoco
       run: mvn jacoco:report
diff --git a/.github/workflows/publish-snapshots.yml 
b/.github/workflows/publish-snapshots.yml
index b03082a2..c01b974b 100644
--- a/.github/workflows/publish-snapshots.yml
+++ b/.github/workflows/publish-snapshots.yml
@@ -43,8 +43,8 @@ jobs:
       - name: Setup Java
         uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 
v5.2.0
         with:
-          distribution: adopt
-          java-version: 17
+          distribution: temurin
+          java-version: 21
       - id: extract_version
         name: Extract version
         shell: bash
diff --git a/.github/workflows/shell-tests.yml 
b/.github/workflows/shell-tests.yml
index 5cacc0a9..888cc197 100644
--- a/.github/workflows/shell-tests.yml
+++ b/.github/workflows/shell-tests.yml
@@ -36,14 +36,14 @@ jobs:
           sudo apt-get update
           sudo apt-get install -y bats
 
-      - name: Set up JDK 17
+      - name: Set up JDK 21
         uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 
v5.2.0
         with:
           distribution: temurin
-          java-version: 17
+          java-version: 21
 
       - name: Build with Maven
-        run: mvn -V clean install --no-transfer-progress -Pci -DskipTests=true
+        run: mvn -V clean verify --no-transfer-progress -Pci -DskipTests=true
 
       - name: Find and Extract OpenNLP Distribution
         run: |
@@ -91,14 +91,14 @@ jobs:
           brew update
           brew install bats-core
 
-      - name: Set up JDK 17
+      - name: Set up JDK 21
         uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 
v5.2.0
         with:
           distribution: temurin
-          java-version: 17
+          java-version: 21
 
       - name: Build with Maven
-        run: mvn -V clean install --no-transfer-progress -Pci -DskipTests=true
+        run: mvn -V clean verify --no-transfer-progress -Pci -DskipTests=true
 
       - name: Find and Extract OpenNLP Distribution
         run: |
@@ -138,14 +138,14 @@ jobs:
           Import-Module Pester
         shell: pwsh
 
-      - name: Set up JDK 17
+      - name: Set up JDK 21
         uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 
v5.2.0
         with:
           distribution: temurin
-          java-version: 17
+          java-version: 21
 
       - name: Build with Maven
-        run: mvn -V clean install --no-transfer-progress -Pci -DskipTests=true
+        run: mvn -V clean verify --no-transfer-progress -Pci -DskipTests=true
 
       - name: Run Pester Tests # (one step to avoid environment issues on 
Windows)
         run: |
diff --git a/README.md b/README.md
index e263f2b6..060792a4 100644
--- a/README.md
+++ b/README.md
@@ -132,7 +132,7 @@ For users of the traditional CLI toolkit, nothing changes 
with the 3.x release l
 The Apache OpenNLP team is planning to change the package namespace from 
`opennlp` to `org.apache.opennlp` in a future release (potentially 4.x). 
 This change will be made to align with standard Java package naming 
conventions and to avoid potential conflicts with other libraries.
 
-In addition, the Apache OpenNLP team is considering the raise of the minimal 
Java version to JDK 21+ in a future release (potentially 4.x) 
+In addition, the Apache OpenNLP team raised the minimal Java version to JDK 
21+ for the 3.0.0 release 
 to take advantage of the latest language features and improvements.
 
 ## Branches and Merging Strategy
@@ -141,8 +141,10 @@ To support ongoing development and stable maintenance of 
Apache OpenNLP, the pro
 
 ### Branch overview
 
-- **`main`**: Development branch for version **3.0** and beyond. All feature 
development and 3.x releases occur here.
-- **`opennlp-2.x`**: Maintains the stable **2.x** release line. This branch 
will receive selective updates and patch releases.
+- **`main`**: Development branch for version **3.0** and beyond. 
+              All feature development and 3.x releases occur here. Minimum 
Java level: 21.
+- **`opennlp-2.x`**: Maintains the stable **2.x** release line. 
+              This branch will receive selective updates and patch releases. 
Minimum Java level: 17.
 
 ### Workflow summary
 
@@ -159,7 +161,7 @@ To support ongoing development and stable maintenance of 
Apache OpenNLP, the pro
 
 ## Building OpenNLP
 
-At least JDK 17 and Maven 3.3.9 are required to build the library.
+For the main branch, at least JDK 21 and Maven 3.9.x are required to build the 
library.
 
 After cloning the repository go into the destination directory and run:
 
diff --git 
a/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
 
b/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
index 059a07d3..8da15743 100644
--- 
a/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
+++ 
b/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
@@ -20,6 +20,8 @@ package opennlp.tools;
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -47,11 +49,19 @@ public abstract class AbstractModelLoaderTest {
           "sv", "tr", "uk");
 
   protected static void downloadVersion15Model(String modelName) throws 
IOException {
-    downloadModel(new URL(BASE_URL_MODELS_V15 + modelName));
+    downloadModel(toModelURL(BASE_URL_MODELS_V15 + modelName));
   }
 
   protected static void downloadVersion183Model(String modelName) throws 
IOException {
-    downloadModel(new URL(BASE_URL_MODELS_V183 + modelName));
+    downloadModel(toModelURL(BASE_URL_MODELS_V183 + modelName));
+  }
+
+  private static URL toModelURL(String location) throws IOException {
+    try {
+      return new URI(location).toURL();
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
+    }
   }
 
   private static void downloadModel(URL url) throws IOException {
diff --git 
a/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
 
b/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
index fccc5526..d15f2707 100644
--- 
a/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
+++ 
b/opennlp-core/opennlp-cli/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import javax.net.ssl.HttpsURLConnection;
 
@@ -57,7 +59,7 @@ public @interface EnabledWhenCDNAvailable {
           socket.connect(new InetSocketAddress(host, 443), TIMEOUT_MS);
 
           // Then, try to check the HTTP status by making an HTTPS request
-          final URL url = new URL("https://"; + host);
+          final URL url = new URI("https://"; + host).toURL();
           final HttpsURLConnection connection = (HttpsURLConnection) 
url.openConnection();
           connection.setConnectTimeout(TIMEOUT_MS);
           connection.setReadTimeout(TIMEOUT_MS);
@@ -72,6 +74,9 @@ public @interface EnabledWhenCDNAvailable {
                 "Resource (CDN) reachable, but HTTP status code: " + 
statusCode);
 
           }
+        } catch (URISyntaxException e) {
+          return ConditionEvaluationResult.disabled(
+              "Resource (CDN) identifier has an invalid form.");
         } catch (IOException e) {
           return ConditionEvaluationResult.disabled(
               "Resource (CDN) unreachable.");
diff --git 
a/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/NameFinderCensus90NameStream.java
 
b/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/NameFinderCensus90NameStream.java
index 43fe6f38..e8acc2c4 100644
--- 
a/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/NameFinderCensus90NameStream.java
+++ 
b/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/NameFinderCensus90NameStream.java
@@ -58,7 +58,7 @@ public class NameFinderCensus90NameStream implements 
ObjectStream<StringList> {
    *                    input file to be attached to this class.
    */
   public NameFinderCensus90NameStream(ObjectStream<String> lineStream) {
-    this.locale = new Locale("en");   // locale is English
+    this.locale = Locale.of("en");   // locale is English
     this.encoding = Charset.defaultCharset();
     // todo how do we find the encoding for an already open ObjectStream() ?
     this.lineStream = lineStream;
@@ -76,7 +76,7 @@ public class NameFinderCensus90NameStream implements 
ObjectStream<StringList> {
    */
   public NameFinderCensus90NameStream(InputStreamFactory in, Charset encoding)
       throws IOException {
-    this.locale = new Locale("en"); // locale is English
+    this.locale = Locale.of("en"); // locale is English
     this.encoding = encoding;
     this.lineStream = new PlainTextByLineStream(in, this.encoding);
   }
diff --git 
a/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/conllu/ConlluStream.java
 
b/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/conllu/ConlluStream.java
index e4a9d8fa..17c991e6 100644
--- 
a/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/conllu/ConlluStream.java
+++ 
b/opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/conllu/ConlluStream.java
@@ -240,7 +240,7 @@ public class ConlluStream implements 
ObjectStream<ConlluSentence> {
       throw new InvalidFormatException(e);
     }
     if (!lang.isEmpty()) {
-      textLang.put(new Locale(lang), secondPart);
+      textLang.put(Locale.of(lang), secondPart);
     }
     else {
       throw new InvalidFormatException(String.format("Locale language code is 
invalid: %s", lang));
diff --git 
a/opennlp-core/opennlp-formats/src/test/java/opennlp/tools/formats/conllu/ConlluStreamTest.java
 
b/opennlp-core/opennlp-formats/src/test/java/opennlp/tools/formats/conllu/ConlluStreamTest.java
index 5545f321..5716a84a 100644
--- 
a/opennlp-core/opennlp-formats/src/test/java/opennlp/tools/formats/conllu/ConlluStreamTest.java
+++ 
b/opennlp-core/opennlp-formats/src/test/java/opennlp/tools/formats/conllu/ConlluStreamTest.java
@@ -83,8 +83,8 @@ public class ConlluStreamTest extends 
AbstractConlluSampleStreamTest<SentenceSam
       Assertions.assertEquals(3, sent3.getWordLines().size());
       Assertions.assertTrue(sent3.isNewParagraph());
       Map<Object, Object> textLang3 = new HashMap<>();
-      textLang3.put(new Locale("fr"), "VoilĂ  ce qui nous est parvenu par la 
tradition orale.");
-      textLang3.put(new Locale("en"), "This is what is heard.");
+      textLang3.put(Locale.of("fr"), "VoilĂ  ce qui nous est parvenu par la 
tradition orale.");
+      textLang3.put(Locale.of("en"), "This is what is heard.");
       Assertions.assertEquals(Optional.of(textLang3)
           , sent3.getTextLang());
 
@@ -99,7 +99,7 @@ public class ConlluStreamTest extends 
AbstractConlluSampleStreamTest<SentenceSam
       Assertions.assertTrue(sent4.isNewParagraph());
       Assertions.assertEquals(Optional.of("mf920901-001"), 
sent4.getDocumentId());
       Assertions.assertEquals(Optional.of("mf920901-001-p1"), 
sent4.getParagraphId());
-      Assertions.assertEquals(Optional.of(Collections.singletonMap(new 
Locale("en"),
+      
Assertions.assertEquals(Optional.of(Collections.singletonMap(Locale.of("en"),
               "Slovak constitution: pros and cons"))
           , sent4.getTextLang());
 
diff --git 
a/opennlp-core/opennlp-models/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
 
b/opennlp-core/opennlp-models/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
index ebc7da9f..0a85fb98 100644
--- 
a/opennlp-core/opennlp-models/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
+++ 
b/opennlp-core/opennlp-models/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
@@ -148,20 +148,28 @@ public class SimpleClassPathModelFinder extends 
AbstractClassPathModelFinder imp
     return pattern.matcher(url.getFile()).matches();
   }
 
+  private static URL toURL(String location) throws IOException {
+    try {
+      return new URI(location).toURL();
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
+    }
+  }
+
   private List<URI> getURIsFromJar(URL fileUrl, boolean isWindows) throws 
IOException {
     final List<URI> uris = new ArrayList<>();
-    final URL jarUrl = new URL(JAR + ":" +
+    final String location = JAR + ":" +
         (isWindows ? fileUrl.toString().replace("\\", "/")
-            : fileUrl.toString()) + "!/");
+            : fileUrl.toString()) + "!/";
+    final URL jarUrl = toURL(location);
     final JarURLConnection jarConnection = (JarURLConnection) 
jarUrl.openConnection();
     try (JarFile jarFile = jarConnection.getJarFile()) {
       final Enumeration<JarEntry> entries = jarFile.entries();
       while (entries.hasMoreElements()) {
         final JarEntry entry = entries.nextElement();
         if (!entry.isDirectory()) {
-          final URL entryUrl = new URL(jarUrl + entry.getName());
           try {
-            uris.add(entryUrl.toURI());
+            uris.add(new URI(jarUrl + entry.getName()));
           } catch (URISyntaxException ignored) {
             //if we cannot convert to URI here, we ignore that entry.
           }
@@ -211,8 +219,8 @@ public class SimpleClassPathModelFinder extends 
AbstractClassPathModelFinder imp
     final List<URL> jarUrls = new ArrayList<>();
     for (String classPath: matches) {
       try {
-        jarUrls.add(new URL(FILE_PREFIX, "", classPath));
-      } catch (MalformedURLException ignored) {
+        jarUrls.add(new URI(FILE_PREFIX, "", classPath, null).toURL());
+      } catch (MalformedURLException | URISyntaxException ignored) {
         //if we cannot parse a URL from the system property, just ignore it...
         //we couldn't load it anyway
       }
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/DownloadUtil.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/DownloadUtil.java
index 03f6f064..7554c064 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/DownloadUtil.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/DownloadUtil.java
@@ -22,6 +22,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -61,7 +63,7 @@ public class DownloadUtil {
       System.getProperty("OPENNLP_DOWNLOAD_MODEL_PATH", 
"models/ud-models-1.3/");
   private static final String OPENNLP_DOWNLOAD_HOME = "OPENNLP_DOWNLOAD_HOME";
 
-  private static Map<String, Map<ModelType, String>> availableModels;
+  private static Map<String, Map<ModelType, URL>> availableModels;
 
   /**
    * Checks if a model of the specified {@code modelType} has been downloaded 
already
@@ -71,22 +73,23 @@ public class DownloadUtil {
    * @param modelType The {@link ModelType type} of model.
    * @return {@code true} if a model exists locally, {@code false} otherwise.
    * @throws IOException Thrown if IO errors occurred or the computed hash sum
-   * of an associated, local model file was incorrect.
+   *                     of an associated, local model file was incorrect.
    */
   static boolean existsModel(String language, ModelType modelType) throws 
IOException {
-    Map<ModelType, String> modelsByLanguage = 
getAvailableModels().get(language);
+    Map<ModelType, URL> modelsByLanguage = getAvailableModels().get(language);
     if (modelsByLanguage == null) {
       return false;
     } else {
-      final String url = modelsByLanguage.get(modelType);
+      final URL url = modelsByLanguage.get(modelType);
       if (url != null) {
         final Path homeDirectory = getDownloadHome();
-        final String filename = url.substring(url.lastIndexOf("/") + 1);
+        final String extUrl = url.toExternalForm();
+        final String filename = extUrl.substring(extUrl.lastIndexOf("/") + 1);
         final Path localFile = homeDirectory.resolve(filename);
         boolean exists;
         if (Files.exists(localFile)) {
           // if this does not throw the requested model is valid!
-          validateModel(new URL(url + ".sha512"), localFile);
+          validateModel(url + ".sha512", localFile);
           exists = true;
         } else {
           exists = false;
@@ -112,9 +115,9 @@ public class DownloadUtil {
                                                       Class<T> type) throws 
IOException {
 
     if (getAvailableModels().containsKey(language)) {
-      final String url = getAvailableModels().get(language).get(modelType);
+      final URL url = getAvailableModels().get(language).get(modelType);
       if (url != null) {
-        return downloadModel(new URL(url), type);
+        return downloadModel(url, type);
       }
     }
 
@@ -156,7 +159,7 @@ public class DownloadUtil {
       try (final InputStream in = url.openStream()) {
         Files.copy(in, localFile, StandardCopyOption.REPLACE_EXISTING);
       }
-      validateModel(new URL(url + ".sha512"), localFile);
+      validateModel(url + ".sha512", localFile);
       logger.debug("Download complete.");
     } else {
       logger.debug("Model file '{}' already exists. Skipping download.", 
filename);
@@ -169,11 +172,12 @@ public class DownloadUtil {
     }
   }
 
-  public static Map<String, Map<ModelType, String>> getAvailableModels() {
+  public static Map<String, Map<ModelType, URL>> getAvailableModels() {
     if (availableModels == null) {
       try {
-        availableModels = new DownloadParser(new URL(BASE_URL + 
MODEL_URI_PATH)).getAvailableModels();
-      } catch (MalformedURLException e) {
+        DownloadParser p = new DownloadParser(new URI(BASE_URL + 
MODEL_URI_PATH).toURL());
+        availableModels = p.getAvailableModels();
+      } catch (MalformedURLException | URISyntaxException e) {
         throw new RuntimeException(e);
       }
     }
@@ -187,15 +191,21 @@ public class DownloadUtil {
    * @param downloadedModel the model file to check
    * @throws IOException thrown if the checksum could not be computed
    */
-  private static void validateModel(URL sha512, Path downloadedModel) throws 
IOException {
-    // Download SHA512 checksum file
+  private static void validateModel(String sha512, Path downloadedModel) 
throws IOException {
     String expectedChecksum;
-    try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(sha512.openStream()))) {
-      expectedChecksum = reader.readLine();
+    try {
+      // Download SHA512 checksum file
+      final URL hashSum = new URI(sha512).toURL();
+      try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(hashSum.openStream()))) {
+        expectedChecksum = reader.readLine();
 
-      if (expectedChecksum != null) {
-        expectedChecksum = expectedChecksum.split("\\s")[0].trim();
+        if (expectedChecksum != null) {
+          expectedChecksum = expectedChecksum.split("\\s")[0].trim();
+        }
       }
+    } catch (URISyntaxException use) {
+      throw new IOException("Expected SHA512 checksum could not be retrieved 
for " +
+          downloadedModel.getFileName(), use);
     }
 
     // Validate SHA512 checksum
@@ -248,7 +258,8 @@ public class DownloadUtil {
       this.indexUrl = indexUrl;
     }
 
-    Map<String, Map<ModelType, String>> getAvailableModels() {
+    Map<String, Map<ModelType, URL>> getAvailableModels()
+        throws MalformedURLException, URISyntaxException {
       final Matcher matcher = LINK_PATTERN.matcher(fetchPageIndex());
 
       final List<String> links = new ArrayList<>();
@@ -259,8 +270,9 @@ public class DownloadUtil {
       return toMap(links);
     }
 
-    private Map<String, Map<ModelType, String>> toMap(List<String> links) {
-      final Map<String, Map<ModelType, String>> result = new HashMap<>();
+    private Map<String, Map<ModelType, URL>> toMap(List<String> links)
+        throws MalformedURLException, URISyntaxException {
+      final Map<String, Map<ModelType, URL>> result = new HashMap<>();
       for (String link : links) {
         if (link.endsWith(".bin")) {
           if (link.contains("de-ud")) { // German
@@ -341,10 +353,11 @@ public class DownloadUtil {
       return result;
     }
 
-    private void addModel(String locale, String link, Map<String, 
Map<ModelType, String>> result) {
-      final Map<ModelType, String> models = result.getOrDefault(locale, new 
HashMap<>());
-      final String url = (indexUrl.toString().endsWith("/") ? indexUrl : 
indexUrl + "/") + link;
-
+    private void addModel(String locale, String link, Map<String, 
Map<ModelType, URL>> result)
+        throws URISyntaxException, MalformedURLException {
+      final Map<ModelType, URL> models = result.getOrDefault(locale, new 
HashMap<>());
+      final String combined = (indexUrl.toString().endsWith("/") ? indexUrl : 
indexUrl + "/") + link;
+      final URL url = new URI(combined).toURL();
       if (link.contains("sentence")) {
         models.put(ModelType.SENTENCE_DETECTOR, url);
       } else if (link.contains("tokens")) {
diff --git 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
index 059a07d3..8da15743 100644
--- 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
+++ 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/AbstractModelLoaderTest.java
@@ -20,6 +20,8 @@ package opennlp.tools;
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -47,11 +49,19 @@ public abstract class AbstractModelLoaderTest {
           "sv", "tr", "uk");
 
   protected static void downloadVersion15Model(String modelName) throws 
IOException {
-    downloadModel(new URL(BASE_URL_MODELS_V15 + modelName));
+    downloadModel(toModelURL(BASE_URL_MODELS_V15 + modelName));
   }
 
   protected static void downloadVersion183Model(String modelName) throws 
IOException {
-    downloadModel(new URL(BASE_URL_MODELS_V183 + modelName));
+    downloadModel(toModelURL(BASE_URL_MODELS_V183 + modelName));
+  }
+
+  private static URL toModelURL(String location) throws IOException {
+    try {
+      return new URI(location).toURL();
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
+    }
   }
 
   private static void downloadModel(URL url) throws IOException {
diff --git 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
index fccc5526..d15f2707 100644
--- 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
+++ 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import javax.net.ssl.HttpsURLConnection;
 
@@ -57,7 +59,7 @@ public @interface EnabledWhenCDNAvailable {
           socket.connect(new InetSocketAddress(host, 443), TIMEOUT_MS);
 
           // Then, try to check the HTTP status by making an HTTPS request
-          final URL url = new URL("https://"; + host);
+          final URL url = new URI("https://"; + host).toURL();
           final HttpsURLConnection connection = (HttpsURLConnection) 
url.openConnection();
           connection.setConnectTimeout(TIMEOUT_MS);
           connection.setReadTimeout(TIMEOUT_MS);
@@ -72,6 +74,9 @@ public @interface EnabledWhenCDNAvailable {
                 "Resource (CDN) reachable, but HTTP status code: " + 
statusCode);
 
           }
+        } catch (URISyntaxException e) {
+          return ConditionEvaluationResult.disabled(
+              "Resource (CDN) identifier has an invalid form.");
         } catch (IOException e) {
           return ConditionEvaluationResult.disabled(
               "Resource (CDN) unreachable.");
diff --git 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/sentdetect/AbstractSentenceDetectorTest.java
 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/sentdetect/AbstractSentenceDetectorTest.java
index 64f2a000..d258ea4e 100644
--- 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/sentdetect/AbstractSentenceDetectorTest.java
+++ 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/sentdetect/AbstractSentenceDetectorTest.java
@@ -30,10 +30,10 @@ import opennlp.tools.util.TrainingParameters;
 
 public abstract class AbstractSentenceDetectorTest {
 
-  protected static final Locale LOCALE_DUTCH = new Locale("nl");
-  protected static final Locale LOCALE_POLISH = new Locale("pl");
-  protected static final Locale LOCALE_PORTUGUESE = new Locale("pt");
-  protected static final Locale LOCALE_SPANISH = new Locale("es");
+  protected static final Locale LOCALE_DUTCH = Locale.of("nl");
+  protected static final Locale LOCALE_POLISH = Locale.of("pl");
+  protected static final Locale LOCALE_PORTUGUESE = Locale.of("pt");
+  protected static final Locale LOCALE_SPANISH = Locale.of("es");
 
   static ObjectStream<SentenceSample> createSampleStream(Locale loc) throws 
IOException {
     final String trainingResource;
diff --git 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/tokenize/TokenizerFactoryTest.java
 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/tokenize/TokenizerFactoryTest.java
index 23d2ba7a..41ff5d44 100644
--- 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/tokenize/TokenizerFactoryTest.java
+++ 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/tokenize/TokenizerFactoryTest.java
@@ -42,10 +42,10 @@ import opennlp.tools.util.TrainingParameters;
  */
 public class TokenizerFactoryTest {
 
-  private static final Locale LOCALE_DUTCH = new Locale("nl");
-  private static final Locale LOCALE_POLISH = new Locale("pl");
-  private static final Locale LOCALE_PORTUGUESE = new Locale("pt");
-  private static final Locale LOCALE_SPANISH = new Locale("es");
+  private static final Locale LOCALE_DUTCH = Locale.of("nl");
+  private static final Locale LOCALE_POLISH = Locale.of("pl");
+  private static final Locale LOCALE_PORTUGUESE = Locale.of("pt");
+  private static final Locale LOCALE_SPANISH = Locale.of("es");
 
   private static ObjectStream<TokenSample> createSampleStream() throws 
IOException {
     InputStreamFactory in = new ResourceAsStreamFactory(
diff --git 
a/opennlp-extensions/opennlp-uima/src/main/java/opennlp/uima/normalizer/NumberUtil.java
 
b/opennlp-extensions/opennlp-uima/src/main/java/opennlp/uima/normalizer/NumberUtil.java
index 6adf9030..f14df8ec 100644
--- 
a/opennlp-extensions/opennlp-uima/src/main/java/opennlp/uima/normalizer/NumberUtil.java
+++ 
b/opennlp-extensions/opennlp-uima/src/main/java/opennlp/uima/normalizer/NumberUtil.java
@@ -36,7 +36,7 @@ public final class NumberUtil {
    * @return true if the language is supported
    */
   public static boolean isLanguageSupported(String languageCode) {
-    Locale locale = new Locale(languageCode);
+    Locale locale = Locale.of(languageCode);
 
     Locale[] possibleLocales = NumberFormat.getAvailableLocales();
 
@@ -70,7 +70,7 @@ public final class NumberUtil {
       throw new IllegalArgumentException("Language " + languageCode + " is not 
supported!");
     }
 
-    Locale locale = new Locale(languageCode);
+    Locale locale = Locale.of(languageCode);
     NumberFormat numberFormat = NumberFormat.getInstance(locale);
     number = WHITESPACE_PATTERN.matcher(number).replaceAll("");
     return numberFormat.parse(number);
diff --git 
a/opennlp-extensions/opennlp-uima/src/test/java/opennlp/uima/AbstractIT.java 
b/opennlp-extensions/opennlp-uima/src/test/java/opennlp/uima/AbstractIT.java
index 4e0c45ef..9d5633dd 100644
--- a/opennlp-extensions/opennlp-uima/src/test/java/opennlp/uima/AbstractIT.java
+++ b/opennlp-extensions/opennlp-uima/src/test/java/opennlp/uima/AbstractIT.java
@@ -21,6 +21,8 @@ import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -71,7 +73,15 @@ abstract class AbstractIT extends AbstractUimaTest {
   }
 
   private static void downloadVersion15Model(String modelName) throws 
IOException {
-    downloadModel(new URL(BASE_URL_MODELS_V15 + modelName));
+    downloadModel(toModelURL(BASE_URL_MODELS_V15 + modelName));
+  }
+
+  private static URL toModelURL(String location) throws IOException {
+    try {
+      return new URI(location).toURL();
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
+    }
   }
 
   private static void downloadModel(URL url) throws IOException {
diff --git 
a/opennlp-tools/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java 
b/opennlp-tools/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
index fccc5526..d15f2707 100644
--- a/opennlp-tools/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
+++ b/opennlp-tools/src/test/java/opennlp/tools/EnabledWhenCDNAvailable.java
@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import javax.net.ssl.HttpsURLConnection;
 
@@ -57,7 +59,7 @@ public @interface EnabledWhenCDNAvailable {
           socket.connect(new InetSocketAddress(host, 443), TIMEOUT_MS);
 
           // Then, try to check the HTTP status by making an HTTPS request
-          final URL url = new URL("https://"; + host);
+          final URL url = new URI("https://"; + host).toURL();
           final HttpsURLConnection connection = (HttpsURLConnection) 
url.openConnection();
           connection.setConnectTimeout(TIMEOUT_MS);
           connection.setReadTimeout(TIMEOUT_MS);
@@ -72,6 +74,9 @@ public @interface EnabledWhenCDNAvailable {
                 "Resource (CDN) reachable, but HTTP status code: " + 
statusCode);
 
           }
+        } catch (URISyntaxException e) {
+          return ConditionEvaluationResult.disabled(
+              "Resource (CDN) identifier has an invalid form.");
         } catch (IOException e) {
           return ConditionEvaluationResult.disabled(
               "Resource (CDN) unreachable.");
diff --git 
a/opennlp-tools/src/test/java/opennlp/tools/namefind/AbstractNameFinderTest.java
 
b/opennlp-tools/src/test/java/opennlp/tools/namefind/AbstractNameFinderTest.java
index 47fa4d72..3a0cca4a 100644
--- 
a/opennlp-tools/src/test/java/opennlp/tools/namefind/AbstractNameFinderTest.java
+++ 
b/opennlp-tools/src/test/java/opennlp/tools/namefind/AbstractNameFinderTest.java
@@ -21,6 +21,8 @@ import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -112,7 +114,15 @@ abstract class AbstractNameFinderTest {
   }
 
   protected static void downloadVersion15Model(String modelName) throws 
IOException {
-    downloadModel(new URL(BASE_URL_MODELS_V15 + modelName));
+    downloadModel(toModelURL(BASE_URL_MODELS_V15 + modelName));
+  }
+
+  private static URL toModelURL(String location) throws IOException {
+    try {
+      return new URI(location).toURL();
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
+    }
   }
 
   private static void downloadModel(URL url) throws IOException {
diff --git 
a/opennlp-tools/src/test/java/opennlp/tools/util/DownloadParserTest.java 
b/opennlp-tools/src/test/java/opennlp/tools/util/DownloadParserTest.java
index b2d360bd..ac04d7d4 100644
--- a/opennlp-tools/src/test/java/opennlp/tools/util/DownloadParserTest.java
+++ b/opennlp-tools/src/test/java/opennlp/tools/util/DownloadParserTest.java
@@ -18,6 +18,8 @@
 package opennlp.tools.util;
 
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.Map;
 import java.util.stream.Stream;
@@ -32,6 +34,7 @@ import opennlp.tools.models.ModelType;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
 
 public class DownloadParserTest {
 
@@ -44,20 +47,23 @@ public class DownloadParserTest {
 
     final DownloadUtil.DownloadParser downloadParser = new 
DownloadUtil.DownloadParser(baseUrl);
 
-    Map<String, Map<ModelType, String>> result = 
downloadParser.getAvailableModels();
+    try {
+      Map<String, Map<ModelType, URL>> result = 
downloadParser.getAvailableModels();
+      assertNotNull(result);
+      assertEquals(36, result.size());
 
-    assertNotNull(result);
-    assertEquals(36, result.size());
+      final Map<ModelType, URL> availableModels = result.get(language);
+      assertNotNull(availableModels);
 
-    final Map<ModelType, String> availableModels = result.get(language);
-    assertNotNull(availableModels);
+      for (Map.Entry<ModelType, String> e : expectedModels.entrySet()) {
+        final URL url = availableModels.get(e.getKey());
+        final String expectedUrl = baseUrl + "/" + e.getValue();
 
-    for (Map.Entry<ModelType, String> e : expectedModels.entrySet()) {
-      final String url = availableModels.get(e.getKey());
-      final String expectedUrl = baseUrl + "/" + e.getValue();
-
-      assertNotNull(url, "A model for the given model type is expected");
-      assertEquals(expectedUrl, url);
+        assertNotNull(url, "A model for the given model type is expected");
+        assertEquals(expectedUrl, url.toExternalForm());
+      }
+    } catch (URISyntaxException | MalformedURLException e) {
+      fail(e);
     }
   }
 
@@ -68,12 +74,16 @@ public class DownloadParserTest {
   }
 
   @Test
-  void testInvalidUrl() throws MalformedURLException {
-    final DownloadUtil.DownloadParser downloadParser =
-        new DownloadUtil.DownloadParser(new URL("file:/this/does/not/exist"));
-    Map<String, Map<ModelType, String>> result = 
downloadParser.getAvailableModels();
-    assertNotNull(result);
-    assertEquals(0, result.size());
+  void testInvalidUrl() {
+    try {
+      final DownloadUtil.DownloadParser downloadParser =
+          new DownloadUtil.DownloadParser(new 
URI("file:/this/does/not/exist").toURL());
+      Map<String, Map<ModelType, URL>> result = 
downloadParser.getAvailableModels();
+      assertNotNull(result);
+      assertEquals(0, result.size());
+    } catch (URISyntaxException | MalformedURLException e) {
+      fail(e);
+    }
   }
 
   private URL fromClasspath(String file) {
diff --git a/pom.xml b/pom.xml
index b85d3a32..98a3df1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -221,8 +221,8 @@
 
        <properties>
                <!-- Build properties -->
-               <java.version>17</java.version>
-               <maven.version>3.3.9</maven.version>
+               <java.version>21</java.version>
+               <maven.version>3.9.0</maven.version>
                <maven.compiler.release>${java.version}</maven.compiler.release>
                <maven.compiler.target>${java.version}</maven.compiler.target>
                
@@ -485,11 +485,11 @@
                                                <configuration>
                                                <rules>
                                                        <requireJavaVersion>
-                                                               <message>Java 
17 or higher is required to compile this module</message>
+                                                               <message>Java 
21 or higher is required to compile this module</message>
                                                                
<version>[${java.version},)</version>
                                                        </requireJavaVersion>
                                                        <requireMavenVersion>
-                                                               <message>Maven 
3.3.9 or higher is required to compile this module</message>
+                                                               <message>Maven 
3.9.0 or higher is required to compile this module</message>
                                                                
<version>[${maven.version},)</version>
                                                        </requireMavenVersion>
                                                </rules>


Reply via email to