Repository navigation
OPENNLP-1932: Regex removal (4/10): Match model finder wildcards without regular expressions - #1278
Conversation
5d7315e to
8913d04
Compare
8913d04 to
e7e5f06
Compare
This comment was marked as outdated.
This comment was marked as outdated.
1 similar comment
This comment was marked as outdated.
This comment was marked as outdated.
a363b77 to
4bebe3c
Compare
5d1749b to
d6c4422
Compare
|
I didn't want to be too pedantic, but there is a similar fix we did a few days ago on #1308 , but left it out because it would've bloated the ticket. There might be some logic we can unify there. I will make a follow-up issue to investigate it. |
|
I think this is ready for review, I have everything noted about the repeat logic - it's not much and shouldn't block this merge. |
c52e89c to
5b01eb1
Compare
rzo1
left a comment
There was a problem hiding this comment.
Thanks for the update. The switch to decoded paths and the rewritten docs and description address points 2 and 3 of my earlier review. Most of the code-level points are still open, and we've decided on point 1.
Blocking
- Remove
java.util.regex.PatternfromAbstractClassPathModelFinder. WithmatchesWildcardin place, the built-in finders no longer use regex. The rewrittenasRegexis not the old contract either ((?s)plus quoted runs), so keeping it as a deprecated method doesn't give subclasses compatibility. It only keepsPatternin production code, which #1282 would then have to suppress. 3.0.0 is a major release, so remove the regex API instead of deprecating it:- Delete
asRegex(String)(:186-209),appendQuoted(:213),matchesPattern(URL, Pattern)(:232-243), theDOT_ALLandPATTERN_MUST_NOT_BE_NULLconstants (:54-55), and thejava.util.regex.Patternimport (:34). - Delete the tests that only exercise them:
asRegexGlobsand its test, thematchesPatternassertions,LegacyFinderand its test, andasRegex(null)(GlobMatcherTest.java:221, :284-337, :346-381, :421). - Add a line to the manual/release notes: "Since 3.0,
asRegexandmatchesPatternare removed; subclasses usematchesWildcard(URL, String)." - Drop the matching suppression and comment from #1282.
- Delete
Minor
- AbstractClassPathModelFinder.java:72. The constructor still uses
Objects.requireNonNull(NPE), whilematchesWildcardthrows IAE. Please align it to IAE. - GlobMatcher.java:44-52.
- The null checks repeat those in
matchesWildcard. Validate at the protected boundary only, and use the constants there instead of inline literals. codePoints().toArray()allocates twoint[]per call, twice per jar entry. Walk the strings withcodePointAt/Character.charCountinstead.
- The null checks repeat those in
- GlobMatcher.java:22. "matches exactly one character" should say one Unicode code point, as
matchesWildcarddoes. - GlobMatcher.java. "Glob" suggests
PathMatchersyntax ({a,b},[..], escapes), while the API and docs say "wildcard". Rename itWildcardMatcher, or fold it into a private instance method ofAbstractClassPathModelFinder. - SimpleClassPathModelFinder.java:177. No need for
splitClassPathto be static. The same "split and skip empty fields" loop also appears in #1320 (FrequencyDictionaryLoader.splitColumns) and #1321 (EventFields.split). Please add one helper toStringUtiland use it in all three. - GlobMatcherTest.java:196. Once the regex tests are gone, move the remaining
newProbeFinder/matchesWildcardtests to anAbstractClassPathModelFinderTest. - GlobMatcherTest.java. Pin the linear worst case, the real improvement here: for example
assertTimeoutPreemptivelywith*a*a*a*bagainst 100kas. The old regex didn't finish on inputs like that. - DirectoryModelFinderTest.java:107. The hard-coded
4duplicates the number ofopennlp-models-*test dependencies. Assert against the number of jars copied in@BeforeAll. - Title. Please use release-notes language, e.g. "OPENNLP-1932: Model finders treat regex metacharacters in jar and resource masks literally".
|
Thanks for the review. I have the pattern ready in another unpushed PR. I'll add it to this ticket - it's manageable. I'll do another review of the other dependent tickets to isolate the Pattern dependency remainders for future tickets. |
…ixes OPENNLP-1932: Remove the regex finder API and address review (for apache apache#1278)
5b01eb1 to
de07675
Compare
|
Thanks for the review. I pushed changes for all of it. AI provided a better summary than myself:
|
|
Thanks for the update, most of my points are addressed. I tried to push the remaining small fixes directly, but GitHub doesn't allow maintainer edits on PR branches of organization-owned forks ( It does three things:
Apply it on top of your branch with 1278.patchFrom 175023a8ab52ab24e12d12fc92725334903db4b9 Mon Sep 17 00:00:00 2001
From: Richard Zowalla <rzo1@apache.org>
Date: Fri, 25 Sep 2026 21:05:52 +0200
Subject: [PATCH] OPENNLP-1932: Drop duplicated null checks and document the
breaking change
- WildcardMatcher.matches no longer repeats the null checks done by
AbstractClassPathModelFinder.matchesWildcard, its only caller.
- SimpleClassPathModelFinder.splitClassPath is no longer static and
relies on StringUtil.splitNonEmpty for argument validation.
- README: list the removal of asRegex and matchesPattern under
breaking changes in the 2.x to 3.x migration section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
README.md | 6 +++++-
.../opennlp/tools/models/WildcardMatcher.java | 7 -------
.../simple/SimpleClassPathModelFinder.java | 5 +----
.../tools/models/WildcardMatcherTest.java | 16 ----------------
.../simple/SimpleClassPathModelFinderTest.java | 6 +++---
5 files changed, 9 insertions(+), 31 deletions(-)
diff --git a/README.md b/README.md
index 5a40f38e4..c0f5e0570 100644
--- a/README.md
+++ b/README.md
@@ -120,7 +120,7 @@ For more details please check our [documentation](https://opennlp.apache.org/doc
## Migrating from 2.x to 3.x
-The 3.x release line of Apache OpenNLP introduces **no** known breaking changes but modularizes the project for better usage as a library and to support future extensibility.
+The 3.x release line of Apache OpenNLP introduces only a few breaking changes, listed below, but modularizes the project for better usage as a library and to support future extensibility.
The core API remains stable and compatible with 2.x, but the project structure has been reorganized into multiple modules.
That means, that you can continue to use the previous `opennlp-tools` artifact as a dependency. However, we strongly recommend to switch to the new modular structure
@@ -129,6 +129,10 @@ and import only the components you need, which will result in a smaller dependen
Only `opennlp-runtime` needs to be added as a dependency, and you can add additional modules (e.g. `opennlp-ml-maxent`, `opennlp-model-resolver`, etc.) as required by your project.
For users of the traditional CLI toolkit, nothing changes with the 3.x release line. CLI usage remains stable as described in the [project's dev manual](https://opennlp.apache.org/docs/).
+### Breaking changes
+
+- `AbstractClassPathModelFinder`: the protected `asRegex` and `matchesPattern` methods were removed. Custom model finders call `matchesWildcard(url, "*.bin")` instead ([OPENNLP-1932](https://issues.apache.org/jira/browse/OPENNLP-1932)).
+
### Thread safety
Starting with 3.0.0, the core `*ME` classes (`POSTaggerME`, `TokenizerME`, `SentenceDetectorME`, `ChunkerME`, `LemmatizerME`, `NameFinderME`) are thread safe and a single instance can be shared across threads. This eliminates the need to pool or recreate ME instances per thread. The legacy `ThreadSafe*ME` wrappers from 2.x still work but are now deprecated; existing code does not need to change to upgrade.
diff --git a/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/WildcardMatcher.java b/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/WildcardMatcher.java
index 0b22b1090..94db098d1 100644
--- a/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/WildcardMatcher.java
+++ b/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/WildcardMatcher.java
@@ -42,15 +42,8 @@ final class WildcardMatcher {
* @param input The text to test. Must not be {@code null}.
* @return {@code true} if {@code input} matches {@code wildcard} from start to end,
* {@code false} otherwise.
- * @throws IllegalArgumentException Thrown if {@code wildcard} or {@code input} is {@code null}.
*/
static boolean matches(String wildcard, String input) {
- if (wildcard == null) {
- throw new IllegalArgumentException("wildcard must not be null");
- }
- if (input == null) {
- throw new IllegalArgumentException("input must not be null");
- }
final int wildcardLength = wildcard.length();
final int inputLength = input.length();
int wi = 0;
diff --git a/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java b/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
index d5df05d35..9ab57839d 100644
--- a/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
+++ b/opennlp-core/opennlp-model-resolver/src/main/java/opennlp/tools/models/simple/SimpleClassPathModelFinder.java
@@ -175,10 +175,7 @@ public class SimpleClassPathModelFinder extends AbstractClassPathModelFinder imp
* @return The non-empty class path entries in order.
* @throws IllegalArgumentException Thrown if {@code classPath} is {@code null}.
*/
- static String[] splitClassPath(String classPath, boolean isWindows) {
- if (classPath == null) {
- throw new IllegalArgumentException("classPath must not be null");
- }
+ String[] splitClassPath(String classPath, boolean isWindows) {
return StringUtil.splitNonEmpty(classPath,
isWindows ? CLASSPATH_SEPARATOR_WINDOWS : CLASSPATH_SEPARATOR_UNIX);
}
diff --git a/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/WildcardMatcherTest.java b/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/WildcardMatcherTest.java
index ed7653417..92df58bb6 100644
--- a/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/WildcardMatcherTest.java
+++ b/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/WildcardMatcherTest.java
@@ -218,22 +218,6 @@ public class WildcardMatcherTest {
Assertions.assertFalse(WildcardMatcher.matches(wildcard, rejected));
}
- private static Stream<Arguments> nullInputs() {
- return Stream.of(
- Arguments.of(null, "a"),
- Arguments.of("a", null),
- Arguments.of(null, null));
- }
-
- /**
- * Checks that a null wildcard or input fails fast instead of matching.
- */
- @ParameterizedTest
- @MethodSource("nullInputs")
- void testMatchesRejectsNull(String wildcard, String input) {
- Assertions.assertThrows(IllegalArgumentException.class, () -> WildcardMatcher.matches(wildcard, input));
- }
-
/**
* Checks a wildcard that makes a backtracking regular expression take polynomial time,
* {@code .*a.*a.*a.*b} against a long run of {@code a}. The matcher keeps one backtracking
diff --git a/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/simple/SimpleClassPathModelFinderTest.java b/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/simple/SimpleClassPathModelFinderTest.java
index 48750e70f..5f4dcab60 100644
--- a/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/simple/SimpleClassPathModelFinderTest.java
+++ b/opennlp-core/opennlp-model-resolver/src/test/java/opennlp/tools/models/simple/SimpleClassPathModelFinderTest.java
@@ -69,7 +69,7 @@ public class SimpleClassPathModelFinderTest extends AbstractClassPathFinderTest
@ParameterizedTest
@MethodSource("unixClassPaths")
void testSplitClassPathUnix(String classPath, String[] expected) {
- Assertions.assertArrayEquals(expected, SimpleClassPathModelFinder.splitClassPath(classPath, false));
+ Assertions.assertArrayEquals(expected, new SimpleClassPathModelFinder().splitClassPath(classPath, false));
}
private static Stream<Arguments> windowsClassPaths() {
@@ -101,12 +101,12 @@ public class SimpleClassPathModelFinderTest extends AbstractClassPathFinderTest
@ParameterizedTest
@MethodSource("windowsClassPaths")
void testSplitClassPathWindows(String classPath, String[] expected) {
- Assertions.assertArrayEquals(expected, SimpleClassPathModelFinder.splitClassPath(classPath, true));
+ Assertions.assertArrayEquals(expected, new SimpleClassPathModelFinder().splitClassPath(classPath, true));
}
@Test
void testSplitClassPathRejectsNull() {
Assertions.assertThrows(IllegalArgumentException.class,
- () -> SimpleClassPathModelFinder.splitClassPath(null, false));
+ () -> new SimpleClassPathModelFinder().splitClassPath(null, false));
}
}
--
2.55.0
Side note for future PRs: it would make things easier if you pushed your branches to the ASF repo ( |
|
I'm +1 once @rzo1's patch is applied and the merge conflicts are resolved. |
…out regular expressions I've followed all the points made from the review, hand-reviewed with full comments public and in the upstream fork. The suggestions were all good - and the implementation was checked on every level. All of it is well tested, and approved in 2 PRs on the ai-pipestream fork. Edited AI summary below: DirectoryModelFinder and SimpleClassPathModelFinder no longer turn the search mask into a regular expression. A new package-private WildcardMatcher compares the mask with the decoded file part of each URL by code point: * matches any run of characters, ? matches exactly one code point, and every other character is literal. It keeps a single backtrack point, so the worst case is O(n*m) with no recursion and no allocation per call. The protected asRegex and matchesPattern methods of AbstractClassPathModelFinder are removed. Subclasses call matchesWildcard(url, wildcard) instead, and model-loading.xml describes the mask rules and the migration. A null jarModelPrefix now throws IllegalArgumentException, like the other argument checks. URL paths are decoded before matching, so masks use real names with spaces or non-ASCII characters, and JAR entry URIs are escaped. A class loader URL that is not a valid URI, such as one with an unescaped space, is matched on its file part as is instead of failing the scan. The class path is split with a new StringUtil.splitNonEmpty(CharSequence, char...), which splits on literal characters and skips empty fields. Tests cover the matcher, including a pathological *a*a*a*b mask over 100k characters, the finder base class, decoded paths, URLs that are not valid URIs, and splitNonEmpty against a regex split on random input. Assisted-by: Claude Code 2.1.281 Co-authored-by: Kristian Rickert <kristian@apache.org> Co-authored-by: Claude <noreply@anthropic.com> Reviewed-by: Kristian Rickert <kristian@apache.org> Signed-off-by: Kristian Rickert <kristian@apache.org>
…hange - WildcardMatcher.matches no longer repeats the null checks done by AbstractClassPathModelFinder.matchesWildcard, its only caller. - SimpleClassPathModelFinder.splitClassPath is no longer static and relies on StringUtil.splitNonEmpty for argument validation. - README: list the removal of asRegex and matchesPattern under breaking changes in the 2.x to 3.x migration section.
de07675 to
5185ace
Compare
|
Applied your patch and rebased onto main. The one conflict was |
DirectoryModelFinderandSimpleClassPathModelFinderno longer turn the search mask into a regular expression. I added the package-privateWildcardMatchercompares the mask with the decoded file part of each classpath URL.AI summary: