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
The following commit(s) were added to refs/heads/main by this push:
new 7a560bd7a OPENNLP-1935: Regex removal (10/10): Forbid java.util.regex
in production code (#1348)
7a560bd7a is described below
commit 7a560bd7a89478ea6d5c3768bf5c5c41d67052b4
Author: Kristian Rickert <[email protected]>
AuthorDate: Sat Oct 10 04:45:40 2026 -0500
OPENNLP-1935: Regex removal (10/10): Forbid java.util.regex in production
code (#1348)
Add a forbiddenapis check for production classes in every module. Reject
java.util.regex and regex-based String and Scanner methods at
process-classes, before tests run. Exclude test classes and point error
messages to the StringUtil replacements.
Add SuppressForbidden for documented APIs that accept user-supplied
regular expressions. Require a reason for each exception and apply it
to RegexNameFinder and RegexNameFinderFactory.
Document the rule, dev/forbidden-regex.txt and allowed exceptions in
CONTRIBUTING.md. The build will fail until the remaining production
regex uses are removed by the related PRs.
* OPENNLP-1935: Share one non-empty split loop in StringUtil
EventFields, MascIdentifiers, MecabDictionary and HunspellDictionary each
had the same
split loop with a different character test. StringUtil has it as one
private loop behind
splitNonEmpty, splitOnUnicodeWhitespace and the new mode-aware
splitOnWhitespace, and the
four classes call those.
* OPENNLP-1935: Name the StringUtil helpers in the forbidden list message
* OPENNLP-1935: Allow the user-supplied tokenizer pattern and head-rule tags
Mark the tokenizer alphanumeric-pattern API and the AnCora head-rule tag
matching with
@SuppressForbidden, since both regular expressions are public,
user-supplied contracts.
---
.github/CONTRIBUTING.md | 14 ++++++
dev/forbidden-regex.txt | 30 ++++++++++++
.../opennlp/tools/commons/SuppressForbidden.java | 51 ++++++++++++++++++++
.../main/java/opennlp/tools/util/StringUtil.java | 56 +++++++++++++---------
.../tools/util/StringUtilSplitNonEmptyTest.java | 7 ++-
.../java/opennlp/tools/util/StringUtilTest.java | 40 ++++++++++++++++
.../opennlp/tools/namefind/RegexNameFinder.java | 8 ++++
.../tools/namefind/RegexNameFinderFactory.java | 6 +++
.../parser/lang/es/AncoraSpanishHeadRules.java | 13 +++--
.../tools/stemmer/hunspell/HunspellDictionary.java | 29 ++---------
.../opennlp/tools/tokenize/TokSpanEventStream.java | 4 ++
.../opennlp/tools/tokenize/TokenizerFactory.java | 7 +++
.../java/opennlp/tools/tokenize/TokenizerME.java | 3 ++
.../java/opennlp/tools/tokenize/lang/Factory.java | 34 +++++++++----
pom.xml | 19 ++++++++
15 files changed, 260 insertions(+), 61 deletions(-)
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 577eb1615..0b02ac4ca 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -9,3 +9,17 @@ To make the process smooth for the project *committers* (those
who review and ac
Please check out the [How to get
involved](http://opennlp.apache.org/get-involved.html) to understand how
contributions are made.
A detailed list of coding standards can be found at [Apache OpenNLP Code
Conventions](http://opennlp.apache.org/code-conventions.html) which also
contains a list of coding guidelines that you should follow.
For pull requests, there is a [check list](PULL_REQUEST_TEMPLATE.md) with
criteria for acceptable contributions.
+
+## Regular expressions in production code
+
+OpenNLP production code scans text with code-point loops and the helpers in
`opennlp.tools.util.StringUtil`
+(for example `splitOnUnicodeWhitespace` and `split(CharSequence, char)`)
instead of `java.util.regex`.
+The build enforces this with forbiddenapis: `dev/forbidden-regex.txt` lists
the forbidden signatures,
+and the `forbidden-regex-main` execution in the root `pom.xml` checks
production classes in the `process-classes`
+phase (tests are not checked).
+An exception exists only for a member with a documented public contract that
is a user-supplied regular expression,
+such as the patterns given to `RegexNameFinder`;
+mark that member (not the class) with
`@opennlp.tools.commons.SuppressForbidden("reason")`.
+A private pattern, or one that is an implementation detail, is converted to a
scan, not annotated:
+the check's report is the list of what remains to convert.
+See OPENNLP-1935 for the background.
diff --git a/dev/forbidden-regex.txt b/dev/forbidden-regex.txt
new file mode 100644
index 000000000..3eecc2937
--- /dev/null
+++ b/dev/forbidden-regex.txt
@@ -0,0 +1,30 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# forbiddenapis signatures for the forbidden-regex-main execution in the root
pom.xml.
+# Production code scans text with code-point loops and the
opennlp.tools.util.StringUtil
+# helpers instead of java.util.regex (OPENNLP-1935). A member with a public
contract that is
+# a user-supplied pattern (RegexNameFinder, the tokenizer factories) is marked
with
+# @opennlp.tools.commons.SuppressForbidden and is exempt. Only production
classes are
+# checked; tests may use regular expressions.
+
+@defaultMessage Use a code-point scan or a StringUtil helper
(splitOnUnicodeWhitespace, splitOnWhitespace, split(CharSequence, char),
splitNonEmpty) instead of java.util.regex; a member with a user-supplied
pattern as its public contract is marked
@opennlp.tools.commons.SuppressForbidden (OPENNLP-1935)
+java.util.regex.**
+java.lang.String#split(java.lang.String)
+java.lang.String#split(java.lang.String,int)
+java.lang.String#matches(java.lang.String)
+java.lang.String#replaceAll(java.lang.String,java.lang.String)
+java.lang.String#replaceFirst(java.lang.String,java.lang.String)
+java.util.Scanner#useDelimiter(java.lang.String)
diff --git
a/opennlp-api/src/main/java/opennlp/tools/commons/SuppressForbidden.java
b/opennlp-api/src/main/java/opennlp/tools/commons/SuppressForbidden.java
new file mode 100644
index 000000000..8bde26a2a
--- /dev/null
+++ b/opennlp-api/src/main/java/opennlp/tools/commons/SuppressForbidden.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package opennlp.tools.commons;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a member with a public contract that is a user-supplied regular
expression,
+ * for example the patterns given to {@code RegexNameFinder} or the
alphanumeric
+ * {@link java.util.regex.Pattern} accepted by the tokenizer factories.
+ * <p>
+ * Production code scans text with code-point loops and the
+ * {@code opennlp.tools.util.StringUtil} helpers instead of {@code
java.util.regex}.
+ * The build enforces this rule with forbiddenapis, using the signatures in
+ * {@code dev/forbidden-regex.txt} (OPENNLP-1935). An annotated member is
exempt
+ * from that check. Apply it to the narrowest member (field, constructor, or
method)
+ * that needs it rather than to the enclosing class, so a regular expression
added to
+ * another member of the same class is still reported.
+ * <p>
+ * The annotation is kept in class files and read by the build only; it has no
+ * effect at runtime.
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,
ElementType.CONSTRUCTOR})
+public @interface SuppressForbidden {
+
+ /**
+ * @return The public contract that requires the regular expression.
+ */
+ String value();
+}
diff --git a/opennlp-api/src/main/java/opennlp/tools/util/StringUtil.java
b/opennlp-api/src/main/java/opennlp/tools/util/StringUtil.java
index 6b7839d10..c84d98bde 100644
--- a/opennlp-api/src/main/java/opennlp/tools/util/StringUtil.java
+++ b/opennlp-api/src/main/java/opennlp/tools/util/StringUtil.java
@@ -21,6 +21,7 @@ import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.function.IntPredicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -130,26 +131,22 @@ public class StringUtil {
*/
public static String[] splitOnUnicodeWhitespace(CharSequence input) {
ParamChecks.requireNonNullArg(input, "input");
- final List<String> terms = new ArrayList<>();
- final int n = input.length();
- int start = -1;
- int i = 0;
- while (i < n) {
- final int cp = Character.codePointAt(input, i);
- if (isUnicodeWhitespace(cp)) {
- if (start >= 0) {
- terms.add(input.subSequence(start, i).toString());
- start = -1;
- }
- } else if (start < 0) {
- start = i;
- }
- i += Character.charCount(cp);
- }
- if (start >= 0) {
- terms.add(input.subSequence(start, n).toString());
- }
- return terms.toArray(new String[0]);
+ return splitNonEmpty(input, StringUtil::isUnicodeWhitespace);
+ }
+
+ /**
+ * Splits {@code input} on runs of whitespace under the active {@link
WhitespaceMode}, see
+ * {@link #isWhitespace(char)}. Leading and trailing runs are ignored, so
whitespace-only
+ * input yields an empty array. Use {@link
#splitOnUnicodeWhitespace(CharSequence)} where
+ * the Unicode definition should apply regardless of the mode.
+ *
+ * @param input The text to split. Must not be {@code null}.
+ * @return The non-whitespace fields in order.
+ * @throws IllegalArgumentException If {@code input} is {@code null}.
+ */
+ public static String[] splitOnWhitespace(CharSequence input) {
+ ParamChecks.requireNonNullArg(input, "input");
+ return splitNonEmpty(input, StringUtil::isWhitespace);
}
/**
@@ -282,11 +279,25 @@ public class StringUtil {
throw new IllegalArgumentException("separators must not contain a
surrogate");
}
}
+ return splitNonEmpty(input, c -> c <= Character.MAX_VALUE &&
isAnyOf((char) c, separators));
+ }
+
+ /**
+ * Splits {@code input} on runs of the code points that {@code separator}
accepts and
+ * drops the empty fields that leading, trailing or adjacent separators
would produce. The
+ * one loop behind every non-empty split of this class.
+ *
+ * @param input The text to split, not {@code null}.
+ * @param separator Accepts a code point that separates fields.
+ * @return The non-empty fields in order.
+ */
+ private static String[] splitNonEmpty(CharSequence input, IntPredicate
separator) {
final List<String> fields = new ArrayList<>();
final int length = input.length();
int start = -1;
- for (int i = 0; i < length; i++) {
- if (isAnyOf(input.charAt(i), separators)) {
+ for (int i = 0; i < length; ) {
+ final int codePoint = Character.codePointAt(input, i);
+ if (separator.test(codePoint)) {
if (start >= 0) {
fields.add(input.subSequence(start, i).toString());
start = -1;
@@ -294,6 +305,7 @@ public class StringUtil {
} else if (start < 0) {
start = i;
}
+ i += Character.charCount(codePoint);
}
if (start >= 0) {
fields.add(input.subSequence(start, length).toString());
diff --git
a/opennlp-api/src/test/java/opennlp/tools/util/StringUtilSplitNonEmptyTest.java
b/opennlp-api/src/test/java/opennlp/tools/util/StringUtilSplitNonEmptyTest.java
index e8c538ffd..c9228ba16 100644
---
a/opennlp-api/src/test/java/opennlp/tools/util/StringUtilSplitNonEmptyTest.java
+++
b/opennlp-api/src/test/java/opennlp/tools/util/StringUtilSplitNonEmptyTest.java
@@ -36,6 +36,8 @@ public class StringUtilSplitNonEmptyTest {
private static final String SMILEY = "\uD83D\uDE00";
+ private static final String SUPPLEMENTARY_WITH_SPACE_LOW_BITS =
"\uD800\uDC20";
+
private static Stream<Arguments> cases() {
return Stream.of(
Arguments.of("", new char[] {':'}, new String[0]),
@@ -54,6 +56,8 @@ public class StringUtilSplitNonEmptyTest {
Arguments.of("a\\sb", new char[] {' '}, new String[] {"a\\sb"}),
// only the listed separators split, other whitespace stays in the
field
Arguments.of("a\u00A0b\u2003c", new char[] {' '}, new String[]
{"a\u00A0b\u2003c"}),
+ Arguments.of(SUPPLEMENTARY_WITH_SPACE_LOW_BITS + " x", new char[] {'
'},
+ new String[] {SUPPLEMENTARY_WITH_SPACE_LOW_BITS, "x"}),
Arguments.of(SMILEY + ":" + SMILEY, new char[] {':'}, new String[]
{SMILEY, SMILEY}));
}
@@ -82,7 +86,8 @@ public class StringUtilSplitNonEmptyTest {
*/
@Test
void testMatchesRegexSplitWithoutEmptyFields() {
- final String[] pieces = {":", ";", " ", "\t", ".", "|", "a", "b", "xy",
SMILEY, "0"};
+ final String[] pieces = {":", ";", " ", "\t", ".", "|", "a", "b", "xy",
SMILEY,
+ SUPPLEMENTARY_WITH_SPACE_LOW_BITS, "0"};
final char[][] separatorSets = {{':'}, {';'}, {' ', '\t'}, {'.', '|'},
{':', ';', ' '}};
final Pattern[] patterns = new Pattern[separatorSets.length];
for (int s = 0; s < separatorSets.length; s++) {
diff --git a/opennlp-api/src/test/java/opennlp/tools/util/StringUtilTest.java
b/opennlp-api/src/test/java/opennlp/tools/util/StringUtilTest.java
index 1aa05bb8b..0cd9256fb 100644
--- a/opennlp-api/src/test/java/opennlp/tools/util/StringUtilTest.java
+++ b/opennlp-api/src/test/java/opennlp/tools/util/StringUtilTest.java
@@ -556,6 +556,46 @@ public class StringUtilTest {
StringUtil.splitOnUnicodeWhitespace(withInfo));
}
+ @Test
+ void testSplitOnWhitespaceNullThrows() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
StringUtil.splitOnWhitespace(null));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", " \t\n ", "\u00A0\u3000"})
+ void testSplitOnWhitespaceBlankYieldsEmpty(String input) {
+ Assertions.assertArrayEquals(new String[0],
StringUtil.splitOnWhitespace(input));
+ }
+
+ @Test
+ void testSplitOnWhitespaceCollapsesRunsAndIgnoresEnds() {
+ Assertions.assertArrayEquals(new String[] {"a", "b", "c"},
+ StringUtil.splitOnWhitespace(" a \t\u00A0b\u3000\r\nc "));
+ Assertions.assertArrayEquals(new String[] {"a\u200Bb", "\uD83D\uDE00"},
+ StringUtil.splitOnWhitespace("a\u200Bb \uD83D\uDE00"));
+ }
+
+ @Test
+ void testSplitOnWhitespaceFollowsTheWhitespaceMode() {
+ // U+0085 has the White_Space property but is not Character.isWhitespace;
U+001C is the
+ // reverse. The mode-aware split follows the active mode, the Unicode
split does not.
+ final String withNel = "a\u0085b";
+ final String withInfo = "a\u001Cb";
+ Assertions.assertArrayEquals(new String[] {"a", "b"},
StringUtil.splitOnWhitespace(withNel));
+ Assertions.assertArrayEquals(new String[] {"a\u001Cb"},
StringUtil.splitOnWhitespace(withInfo));
+
+ WhitespaceMode.setActive(WhitespaceMode.LEGACY);
+ Assertions.assertArrayEquals(new String[] {"a\u0085b"},
StringUtil.splitOnWhitespace(withNel));
+ Assertions.assertArrayEquals(new String[] {"a", "b"},
StringUtil.splitOnWhitespace(withInfo));
+ }
+
+ @Test
+ void testSplitOnWhitespaceAgreesWithTheUnicodeSplitInCurrentMode() {
+ final String input = " t0\u2003t1\u0085t2 \u3000 t3\uD801\uDC12 ";
+ Assertions.assertArrayEquals(StringUtil.splitOnUnicodeWhitespace(input),
+ StringUtil.splitOnWhitespace(input));
+ }
+
@Test
void testSplitOnUnicodeWhitespaceAllWhiteSpaceCodePointsAsMixedSeparators() {
// Build "t0 <ws0> t1 <ws1> ... tN" using every White_Space code point.
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
index 205e4052f..5e6a66f94 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
@@ -24,6 +24,7 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.util.ParamChecks;
import opennlp.tools.util.Span;
@@ -32,6 +33,7 @@ import opennlp.tools.util.Span;
*/
public final class RegexNameFinder implements TokenNameFinder {
+ @SuppressForbidden("user-supplied name finder patterns")
private Pattern[] mPatterns;
private String sType;
private Map<String, Pattern[]> regexMap;
@@ -56,6 +58,7 @@ public final class RegexNameFinder implements TokenNameFinder
{
*
* @throws IllegalArgumentException Thrown if {@code patterns} were {@code
null} or empty.
*/
+ @SuppressForbidden("user-supplied name finder patterns")
public RegexNameFinder(Pattern[] patterns, String type) {
if (patterns == null || patterns.length == 0) {
throw new IllegalArgumentException("patterns must not be null or
empty!");
@@ -65,6 +68,7 @@ public final class RegexNameFinder implements TokenNameFinder
{
sType = type;
}
+ @SuppressForbidden("user-supplied name finder patterns")
@Override
public Span[] find(String[] tokens) {
Map<Integer, Integer> sentencePosTokenMap = new HashMap<>();
@@ -131,10 +135,12 @@ public final class RegexNameFinder implements
TokenNameFinder {
* @param text The text to use.
* @return An array of {@link Span} instances representing the annotations.
*/
+ @SuppressForbidden("user-supplied name finder patterns")
public Span[] find(String text) {
return getAnnotations(text);
}
+ @SuppressForbidden("user-supplied name finder patterns")
private Span[] getAnnotations(String text) {
Collection<Span> annotations = new LinkedList<>();
if (regexMap != null) {
@@ -168,6 +174,7 @@ public final class RegexNameFinder implements
TokenNameFinder {
/**
* @return Retrieves the {@link Pattern matching patterns} used.
*/
+ @SuppressForbidden("user-supplied name finder patterns")
public Pattern[] getMatchingPatterns() {
return mPatterns;
}
@@ -175,6 +182,7 @@ public final class RegexNameFinder implements
TokenNameFinder {
/**
* @param mPatterns The {@link Pattern matching patterns} to be set.
*/
+ @SuppressForbidden("user-supplied name finder patterns")
public void setMatchingPatterns(Pattern[] mPatterns) {
this.mPatterns = mPatterns;
}
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
index 7a6097a5a..3f8e798ec 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.util.ParamChecks;
/**
@@ -89,6 +90,7 @@ public class RegexNameFinderFactory {
USA_PHONE_NUM {
@Override
+ @SuppressForbidden("default patterns of the regex name finder")
public Map<String, Pattern[]> getRegexMap() {
Pattern[] p = new Pattern[1];
// p[0] = Pattern.compile("([\\+(]?(\\d){2,}[)]?[- \\.]?(\\d){2,}[-
\\.]?(\\d){2,}[- \\.]?
@@ -108,6 +110,7 @@ public class RegexNameFinderFactory {
},
EMAIL {
@Override
+ @SuppressForbidden("default patterns of the regex name finder")
public Map<String, Pattern[]> getRegexMap() {
Pattern[] p = new Pattern[1];
// Every quantifier is bounded by a constant, which removes both the
exponential
@@ -134,6 +137,7 @@ public class RegexNameFinderFactory {
},
URL {
@Override
+ @SuppressForbidden("default patterns of the regex name finder")
public Map<String, Pattern[]> getRegexMap() {
Pattern[] p = new Pattern[1];
// Flattened to single-level groups and every quantifier bounded by a
constant.
@@ -161,6 +165,7 @@ public class RegexNameFinderFactory {
},
MGRS {
@Override
+ @SuppressForbidden("default patterns of the regex name finder")
public Map<String, Pattern[]> getRegexMap() {
Pattern[] p = new Pattern[1];
p[0] =
Pattern.compile("\\d{1,2}[A-Za-z]\\s*[A-Za-z]{2}\\s*\\d{1,5}\\s*\\d{1,5}",
@@ -177,6 +182,7 @@ public class RegexNameFinderFactory {
},
DEGREES_MIN_SEC_LAT_LON {
@Override
+ @SuppressForbidden("default patterns of the regex name finder")
public Map<String, Pattern[]> getRegexMap() {
Pattern[] p = new Pattern[1];
p[0] =
Pattern.compile("([-|\\+]?\\d{1,3}[d|D|\\u00B0|\\s](\\s*\\d{1,2}['|\\u2019|\\s])"
+
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/parser/lang/es/AncoraSpanishHeadRules.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/parser/lang/es/AncoraSpanishHeadRules.java
index 61e38595e..68c246af1 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/parser/lang/es/AncoraSpanishHeadRules.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/parser/lang/es/AncoraSpanishHeadRules.java
@@ -37,6 +37,7 @@ import java.util.Stack;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.parser.Constituent;
import opennlp.tools.parser.GapLabeler;
import opennlp.tools.parser.HeadRules;
@@ -85,6 +86,7 @@ public class AncoraSpanishHeadRules implements HeadRules,
GapLabeler, Serializab
private static class HeadRule {
public final boolean leftToRight;
public final String[] tags;
+ @SuppressForbidden("head-rule tags are regular expressions from a
user-supplied rules file")
public final Pattern[] tagPatterns;
public HeadRule(boolean l2r, String[] tags) {
@@ -119,21 +121,23 @@ public class AncoraSpanishHeadRules implements HeadRules,
GapLabeler, Serializab
}
}
- // Tag patterns are regexes (e.g. "AQA.*", "GRUP\\.A"), not literals, so
they must be
- // matched with Pattern/matches() semantics - they are precompiled once here
(and once
- // per HeadRule, see HeadRule.tagPatterns) instead of via String.matches(),
which would
- // otherwise recompile the same regex on every single constituent comparison.
+ // Tags are regular expressions (e.g. "AQA.*", "GRUP\\.A") matched against
the whole
+ // constituent type; they are compiled once, here and per HeadRule.
private static final String[] TAGS1 =
{"AQA.*", "AQC.*", "GRUP\\.A", "S\\.A", "NC.*S.*", "NP.*", "NC.*P.*",
"GRUP\\.NOM"};
+ @SuppressForbidden("head-rule tags are regular expressions from a
user-supplied rules file")
private static final Pattern[] TAGS1_PATTERNS = compile(TAGS1);
private static final String[] TAGS2 = {"\\$", "GRUP\\.A", "SA"};
+ @SuppressForbidden("head-rule tags are regular expressions from a
user-supplied rules file")
private static final Pattern[] TAGS2_PATTERNS = compile(TAGS2);
private static final String[] TAGS3 =
{"AQ0.*", "AQ[AC].*", "AO.*", "GRUP\\.A", "S\\.A", "RG", "RN",
"GRUP\\.NOM"};
+ @SuppressForbidden("head-rule tags are regular expressions from a
user-supplied rules file")
private static final Pattern[] TAGS3_PATTERNS = compile(TAGS3);
+ @SuppressForbidden("head-rule tags are regular expressions from a
user-supplied rules file")
private static Pattern[] compile(String[] tags) {
Pattern[] patterns = new Pattern[tags.length];
for (int i = 0; i < tags.length; i++) {
@@ -170,6 +174,7 @@ public class AncoraSpanishHeadRules implements HeadRules,
GapLabeler, Serializab
}
@Override
+ @SuppressForbidden("head-rule tags are regular expressions from a
user-supplied rules file")
public Parse getHead(Parse[] constituents, String type) {
if (Parser.TOK_NODE.equals(constituents[0].getType())) {
return null;
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/hunspell/HunspellDictionary.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/hunspell/HunspellDictionary.java
index 294103598..493c8751a 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/hunspell/HunspellDictionary.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/hunspell/HunspellDictionary.java
@@ -2295,7 +2295,7 @@ public final class HunspellDictionary {
final String trimmed = trim(line);
if (trimmed.startsWith(SET_PREFIX) ||
trimmed.startsWith(SET_TAB_PREFIX)) {
// the encoding is the first field after the directive; later fields
are ignored
- final String name = split(trimmed.substring(SET_PREFIX.length()))[0];
+ final String name =
StringUtil.splitOnWhitespace(trimmed.substring(SET_PREFIX.length()))[0];
try {
return Charset.forName(name);
} catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
@@ -2382,7 +2382,7 @@ public final class HunspellDictionary {
final String[] lines = splitLines(StringUtil.stripByteOrderMark(content));
final String[][] fieldsByLine = new String[lines.length][];
for (int i = 0; i < lines.length; i++) {
- fieldsByLine[i] = split(lines[i]);
+ fieldsByLine[i] = StringUtil.splitOnWhitespace(lines[i]);
}
result.flagMode = readFlagMode(fieldsByLine);
result.flagAliases.addAll(readFlagAliases(fieldsByLine, result.flagMode));
@@ -2768,7 +2768,8 @@ public final class HunspellDictionary {
flags = parseAliasedFlags(flagRun, affix.flagMode, affix.flagAliases,
i + 1).clone();
}
if (morphology >= 0) {
- affix.entryMorphology.put(flags,
parseMorphology(split(line.substring(morphology)),
+ affix.entryMorphology.put(flags,
+
parseMorphology(StringUtil.splitOnWhitespace(line.substring(morphology)),
affix.morphologyAliases, i + 1));
}
entries.computeIfAbsent(removeIgnored(word.replace("\\/", "/"),
affix.ignoredCharacters),
@@ -3130,26 +3131,4 @@ public final class HunspellDictionary {
}
return lines.toArray(new String[0]);
}
-
- /**
- * Splits a line on whitespace with a single character scan.
- *
- * @param line The line to split.
- * @return The whitespace-separated fields, without empty ones. Never {@code
null}.
- */
- private static String[] split(String line) {
- final List<String> parts = new ArrayList<>();
- int start = -1;
- for (int i = 0; i <= line.length(); i++) {
- if (i == line.length() || StringUtil.isWhitespace(line.charAt(i))) {
- if (start >= 0) {
- parts.add(line.substring(start, i));
- start = -1;
- }
- } else if (start < 0) {
- start = i;
- }
- }
- return parts.toArray(new String[0]);
- }
}
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokSpanEventStream.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokSpanEventStream.java
index 36f4a98de..b5e297aff 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokSpanEventStream.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokSpanEventStream.java
@@ -26,6 +26,7 @@ import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.ml.model.Event;
import opennlp.tools.tokenize.lang.Factory;
import opennlp.tools.util.AbstractEventStream;
@@ -44,6 +45,7 @@ public class TokSpanEventStream extends
AbstractEventStream<TokenSample> {
private final boolean skipAlphaNumerics;
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
private final Pattern alphaNumeric;
/**
@@ -56,6 +58,7 @@ public class TokSpanEventStream extends
AbstractEventStream<TokenSample> {
* {@link Factory#DEFAULT_ALPHANUMERIC}.
* @param cg A {@link TokenContextGenerator} which should be used for the
event stream {@code d}.
*/
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
public TokSpanEventStream(ObjectStream<TokenSample> tokenSamples, boolean
skipAlphaNumerics,
Pattern alphaNumeric, TokenContextGenerator cg) {
super(tokenSamples);
@@ -95,6 +98,7 @@ public class TokSpanEventStream extends
AbstractEventStream<TokenSample> {
* @return An {@link Iterator} for text {@link Event events} representing
the {@code tokenSample}.
*/
@Override
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
protected Iterator<Event> createEvents(TokenSample tokenSample) {
List<Event> events = new ArrayList<>(50);
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerFactory.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerFactory.java
index 2cbb9461d..eabc986cb 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerFactory.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerFactory.java
@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.dictionary.Dictionary;
import opennlp.tools.tokenize.lang.Factory;
import opennlp.tools.util.BaseToolFactory;
@@ -38,6 +39,7 @@ public class TokenizerFactory extends BaseToolFactory {
private String languageCode;
private Dictionary abbreviationDictionary;
private Boolean useAlphaNumericOptimization = false;
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
private Pattern alphaNumericPattern;
private static final String ABBREVIATIONS_ENTRY_NAME =
"abbreviations.dictionary";
@@ -62,6 +64,7 @@ public class TokenizerFactory extends BaseToolFactory {
* (default is: {@code "^[A-Za-z0-9]+$"},
provided by
* {@link Factory#DEFAULT_ALPHANUMERIC}.
*/
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
public TokenizerFactory(String languageCode, Dictionary
abbreviationDictionary,
boolean useAlphaNumericOptimization, Pattern
alphaNumericPattern) {
this.init(languageCode, abbreviationDictionary,
@@ -76,6 +79,7 @@ public class TokenizerFactory extends BaseToolFactory {
* (default is: {@code "^[A-Za-z0-9]+$"},
provided by
* {@link Factory#DEFAULT_ALPHANUMERIC}.
*/
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
protected void init(String languageCode, Dictionary abbreviationDictionary,
boolean useAlphaNumericOptimization, Pattern alphaNumericPattern) {
this.languageCode = languageCode;
@@ -111,6 +115,7 @@ public class TokenizerFactory extends BaseToolFactory {
}
@Override
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
public Map<String, String> createManifestEntries() {
Map<String, String> manifestEntries = super.createManifestEntries();
@@ -140,6 +145,7 @@ public class TokenizerFactory extends BaseToolFactory {
*
* @throws InvalidFormatException Thrown if one of the input parameters
doesn't comply the expected format.
*/
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
public static TokenizerFactory create(String subclassName, String
languageCode,
Dictionary abbreviationDictionary,
boolean useAlphaNumericOptimization,
@@ -166,6 +172,7 @@ public class TokenizerFactory extends BaseToolFactory {
/**
* @return Retrieves the (user-)specified alphanumeric {@link Pattern} or a
default.
*/
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
public Pattern getAlphaNumericPattern() {
if (this.alphaNumericPattern == null) {
if (this.artifactProvider != null) {
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerME.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerME.java
index 859d7b14a..256fea895 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerME.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/TokenizerME.java
@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.commons.ThreadSafe;
import opennlp.tools.dictionary.Dictionary;
import opennlp.tools.ml.ArrayMath;
@@ -102,6 +103,7 @@ public class TokenizerME extends AbstractTokenizer
implements Probabilistic {
*/
public static final String NO_SPLIT = "F";
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
private final Pattern alphanumeric;
/*
@@ -200,6 +202,7 @@ public class TokenizerME extends AbstractTokenizer
implements Probabilistic {
* @return A {@link Span} array containing individual tokens as elements.
*/
@Override
+ @SuppressForbidden("user-supplied alphanumeric pattern stored in the
tokenizer model")
public Span[] tokenizePos(String d) {
Span[] tokens = (keepNewLines ? WHITESPACE_KEEPING_NEW_LINES :
WHITESPACE).tokenizePos(d);
List<Span> localTokens = new ArrayList<>();
diff --git
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/lang/Factory.java
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/lang/Factory.java
index ebb236ccc..99c53d8f3 100644
---
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/lang/Factory.java
+++
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/tokenize/lang/Factory.java
@@ -20,37 +20,52 @@ package opennlp.tools.tokenize.lang;
import java.util.Set;
import java.util.regex.Pattern;
+import opennlp.tools.commons.SuppressForbidden;
import opennlp.tools.tokenize.DefaultTokenContextGenerator;
import opennlp.tools.tokenize.TokenContextGenerator;
public class Factory {
- public static final Pattern DEFAULT_ALPHANUMERIC =
Pattern.compile("^[A-Za-z0-9]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ public static final Pattern DEFAULT_ALPHANUMERIC = compile("^[A-Za-z0-9]+$");
- private static final Pattern PORTUGUESE =
Pattern.compile("^[0-9a-záãâàéêíóõôúüçA-ZÁÃÂÀÉÊÍÓÕÔÚÜÇ]+$");
- private static final Pattern FRENCH =
Pattern.compile("^[a-zA-Z0-9àâäèéêëîïôœùûüÿçÀÂÄÈÉÊËÎÏÔŒÙÛÜŸÇ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern PORTUGUESE =
compile("^[0-9a-záãâàéêíóõôúüçA-ZÁÃÂÀÉÊÍÓÕÔÚÜÇ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern FRENCH =
compile("^[a-zA-Z0-9àâäèéêëîïôœùûüÿçÀÂÄÈÉÊËÎÏÔŒÙÛÜŸÇ]+$");
// From: https://www.sttmedia.com/characterfrequency-dutch
- private static final Pattern DUTCH =
Pattern.compile("^[A-Za-z0-9äöüëèéïijÄÖÜËÉÈÏIJ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern DUTCH =
compile("^[A-Za-z0-9äöüëèéïijÄÖÜËÉÈÏIJ]+$");
// Note: The extra é and É are included to cover German "Lehnwörter" such as
"Café"
- private static final Pattern GERMAN =
Pattern.compile("^[A-Za-z0-9äéöüÄÉÖÜß]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern GERMAN = compile("^[A-Za-z0-9äéöüÄÉÖÜß]+$");
// From: https://en.wikipedia.org/wiki/Polish_alphabet
// https://pl.wikipedia.org/wiki/Alfabet_polski
- private static final Pattern POLISH =
Pattern.compile("^[A-Za-z0-9żźćńółęąśŻŹĆĄŚĘŁÓŃ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern POLISH =
compile("^[A-Za-z0-9żźćńółęąśŻŹĆĄŚĘŁÓŃ]+$");
// From: https://it.wikipedia.org/wiki/Alfabeto_italiano
- private static final Pattern ITALIAN =
Pattern.compile("^[0-9a-zàèéìîíòóùüA-ZÀÈÉÌÎÍÒÓÙÜ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern ITALIAN =
compile("^[0-9a-zàèéìîíòóùüA-ZÀÈÉÌÎÍÒÓÙÜ]+$");
// From: https://en.wikiversity.org/wiki/Alphabet/Spanish_alphabet &
//
https://en.wikipedia.org/wiki/Spanish_orthography#Alphabet_in_Spanish &
// https://www.fundeu.es/consulta/tilde-en-la-y-y-griega-o-ye-24786/
- private static final Pattern SPANISH =
Pattern.compile("^[0-9a-záéíóúüýñA-ZÁÉÍÓÚÝÑ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern SPANISH =
compile("^[0-9a-záéíóúüýñA-ZÁÉÍÓÚÝÑ]+$");
// From: https://en.wikipedia.org/wiki/Catalan_orthography#Spelling_patterns
- private static final Pattern CATALAN =
Pattern.compile("^[0-9a-zàèéíïòóúüçA-ZÀÈÉÍÏÒÓÚÜÇ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static final Pattern CATALAN =
compile("^[0-9a-zàèéíïòóúüçA-ZÀÈÉÍÏÒÓÚÜÇ]+$");
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
+ private static Pattern compile(String regex) {
+ return Pattern.compile(regex);
+ }
+
/**
* Gets the alphanumeric pattern for a language.
*
@@ -58,6 +73,7 @@ public class Factory {
* {@link #DEFAULT_ALPHANUMERIC} pattern will be
returned.
* @return The alphanumeric {@link Pattern} for the language, or the default
pattern.
*/
+ @SuppressForbidden("default alphanumeric patterns of the tokenizer model")
public Pattern getAlphanumeric(String languageCode) {
// For reference: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
if ("es".equals(languageCode) || "spa".equals(languageCode)) {
diff --git a/pom.xml b/pom.xml
index f7a98a098..157d0776a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -447,6 +447,25 @@ org.xml.sax.helpers.XMLReaderFactory#createXMLReader(**)
<goal>testCheck</goal>
</goals>
</execution>
+ <execution>
+ <!-- Production classes
only: no java.util.regex outside members marked
+
@opennlp.tools.commons.SuppressForbidden (OPENNLP-1935). -->
+
<id>forbidden-regex-main</id>
+
<phase>process-classes</phase>
+ <goals>
+
<goal>check</goal>
+ </goals>
+ <configuration>
+
<bundledSignatures combine.self="override" />
+ <signatures
combine.self="override" />
+
<signaturesFiles>
+
<signaturesFile>${maven.multiModuleProjectDirectory}/dev/forbidden-regex.txt</signaturesFile>
+
</signaturesFiles>
+
<suppressAnnotations>
+
<suppressAnnotation>opennlp.tools.commons.SuppressForbidden</suppressAnnotation>
+
</suppressAnnotations>
+ </configuration>
+ </execution>
</executions>
</plugin>
</plugins>