Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0144431
OPENNLP-1928: Replace cap and digit patterns with ASCII range checks …
krickert Sep 5, 2026
71a79d9
OPENNLP-1928: Replace cap and digit patterns with ASCII range checks …
krickert Sep 5, 2026
b4c44c0
OPENNLP-1928: Replace capPeriod pattern with explicit length and char…
krickert Sep 5, 2026
c0a731b
OPENNLP-1928: Replace noLetters pattern with a char range scan in Tok…
krickert Sep 5, 2026
ad7142a
OPENNLP-1928: Replace typed outcome pattern with lastIndexOf and word…
krickert Sep 5, 2026
44d6b34
OPENNLP-1928: Replace tab pattern split with explicit tab splitting i…
krickert Sep 5, 2026
1e466bd
OPENNLP-1928: Replace whitespace split and alphanumeric pattern with …
krickert Sep 5, 2026
e2c1d70
OPENNLP-1928: Replace emoji pattern with a code point range scan in E…
krickert Sep 5, 2026
32b9bee
OPENNLP-1928: Replace token id split and text language pattern with e…
krickert Sep 5, 2026
99891dc
OPENNLP-1928: Replace untokenized paren patterns with explicit two-pa…
krickert Sep 5, 2026
426e49a
OPENNLP-1928: Replace checksum split and link pattern with explicit s…
krickert Sep 5, 2026
09e531c
OPENNLP-1928: Replace tag, whitespace, underline, hyphen, and alphanu…
krickert Sep 5, 2026
80e448f
OPENNLP-1928: Replace whitespace pattern with an explicit scan in ADP…
krickert Sep 5, 2026
26950fd
OPENNLP-1928: Replace metadata pattern with an explicit scan in ADSen…
krickert Sep 5, 2026
ca15512
OPENNLP-1928: Replace punctuation patterns with explicit scans in ADS…
krickert Sep 5, 2026
2b8db9e
OPENNLP-1928: Document the scan helpers that replace the patterns
krickert Sep 7, 2026
85dceea
OPENNLP-1928: Share the ASCII scan helpers and test each helper
krickert Sep 7, 2026
f0563d2
OPENNLP-1930: Scan the AD markup tags in ADSentenceStream
krickert Sep 7, 2026
a95c065
OPENNLP-1930: Scan the AD node and leaf lines in SentenceParser
krickert Sep 7, 2026
bd736fc
OPENNLP-1930: Share the AD metadata scan between the sample streams
krickert Sep 7, 2026
10d3c64
OPENNLP-1931: Add the JsonScan cursor helpers
krickert Sep 7, 2026
060a5d5
OPENNLP-1931: Scan the JSON vocabulary without a pattern
krickert Sep 7, 2026
711920e
OPENNLP-1931: Scan id2label without a pattern
krickert Sep 7, 2026
fc609c3
OPENNLP-1932: Split the class path with a character scan in SimpleCla…
krickert Sep 7, 2026
3206b32
OPENNLP-1932: Match model finder wildcards without regular expressions
krickert Sep 7, 2026
cb76e19
OPENNLP-1932: Add DirectoryModelFinderTest
krickert Sep 7, 2026
463dc90
OPENNLP-1933: Scan event context fields in the ml-commons streams
krickert Sep 7, 2026
403aaf3
OPENNLP-1933: Scan event context fields in RealBasicEventStream
krickert Sep 7, 2026
2ededf4
OPENNLP-1933: Scan Leipzig file names for the language code
krickert Sep 7, 2026
ccbcf32
OPENNLP-1933: Strip MASC identifier prefixes with a text search
krickert Sep 7, 2026
7b66936
OPENNLP-1933: Derive the dictionary file name with an endsWith check
krickert Sep 7, 2026
f2982af
OPENNLP-1933: Scan dictionary columns on TAB and space runs
krickert Sep 7, 2026
b6661e7
OPENNLP-1933: Close the line-counting stream in LeipzigSentencesStream
krickert Sep 7, 2026
426e203
OPENNLP-1933: Test that closing a MarkableFileInputStream closes the …
krickert Sep 7, 2026
998107d
OPENNLP-1933: Close the underlying file in MarkableFileInputStream
krickert Sep 7, 2026
18e184b
OPENNLP-1929: Test that BasicContextGenerator splits on the separator…
krickert Sep 7, 2026
e749057
OPENNLP-1929: Split on the literal separator in BasicContextGenerator
krickert Sep 7, 2026
d84c37f
OPENNLP-1934: Evaluate the tokenizer alphanumeric pattern as a charac…
krickert Sep 7, 2026
25bc925
OPENNLP-1935: Checkstyle guard for java.util.regex in production code
krickert Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">

<!--
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.
-->

<!--
Exceptions to the two regex checks in checkstyle.xml (ids NoRegexImport and NoRegexStringCall).
Production code is regex-free by policy; a class is listed here only when regular expressions
are its documented contract. Tests may use regular expressions freely.
-->
<suppressions>

<!-- Tests may compare a scan against the pattern it replaced. -->
<suppress id="NoRegexImport" files="[\\/]src[\\/]test[\\/]"/>
<suppress id="NoRegexStringCall" files="[\\/]src[\\/]test[\\/]"/>

<!-- The class is a regex step: the user supplies the patterns. -->
<suppress id="NoRegexImport" files="[\\/]namefind[\\/]RegexNameFinder(Factory)?\.java"/>

<!-- Head rules are regex strings defined by the AnCora treebank specification. -->
<suppress id="NoRegexImport" files="[\\/]parser[\\/]lang[\\/]es[\\/]AncoraSpanishHeadRules\.java"/>

<!-- Penn Treebank constituent markup grammar. -->
<suppress id="NoRegexImport" files="[\\/]parser[\\/]Parse\.java"/>

<!-- Name sample markup grammar. -->
<suppress id="NoRegexImport" files="[\\/]namefind[\\/]NameSample\.java"/>

<!-- The URL heuristic is an alternation of schemes, an email shape, and a TLD list. -->
<suppress id="NoRegexImport"
files="[\\/]spellcheck[\\/]normalizer[\\/]SpellCheckingCharSequenceNormalizer\.java"/>

<!--
The alphanumeric pattern is a tokenizer model parameter stored in the manifest as a regex,
so the Pattern type is part of the model contract. Matching runs through AlphaNumericCheck,
which evaluates the built-in defaults as a character set lookup.
-->
<suppress id="NoRegexImport"
files="[\\/]tokenize[\\/](TokenizerFactory|TokenizerME|TokSpanEventStream|AlphaNumericCheck|lang[\\/]Factory)\.java"/>

</suppressions>
22 changes: 22 additions & 0 deletions checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,30 @@
<property name="message" value="CRLF line endings are prohibited"/>
</module>

<!--
Production code stays free of java.util.regex; the exceptions are listed in
checkstyle-suppressions.xml with the reason for each.
-->
<module name="SuppressionFilter">
<property name="file" value="${checkstyle.suppressions.file}"/>
<property name="optional" value="false"/>
</module>

<module name="TreeWalker">
<module name="SuppressionCommentFilter"/>
<module name="IllegalImport">
<property name="id" value="NoRegexImport"/>
<property name="illegalPkgs" value="java.util.regex"/>
<message key="import.illegal"
value="java.util.regex is not used in production code; write a cursor or code-point scan (see checkstyle-suppressions.xml for the documented exceptions)."/>
</module>
<module name="RegexpSinglelineJava">
<property name="id" value="NoRegexStringCall"/>
<property name="format" value="\.(replaceAll|replaceFirst)\s*\(|\.matches\s*\(\s*&quot;|\.split\s*\(\s*&quot;\\\\"/>
<property name="ignoreComments" value="true"/>
<property name="message"
value="String.replaceAll, replaceFirst, matches, and split with a regex compile a Pattern on each call; write a cursor scan or use the StringUtil helpers."/>
</module>
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
Expand Down
102 changes: 102 additions & 0 deletions opennlp-api/src/main/java/opennlp/tools/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ public static boolean isUnicodeWhitespace(int charCode) {
return UnicodeWhitespace.isWhitespace(charCode);
}

/**
* Determines if the specified {@link Character} is one of the six ASCII whitespace
* characters, the set the regular expression class {@code \s} matches: space, tab,
* line feed, vertical tab, form feed, and carriage return. Unlike
* {@link #isWhitespace(char)}, this ignores the {@link WhitespaceMode} and rejects
* every non-ASCII space.
*
* @param charCode The character to check.
*
* @return {@code true} if {@code charCode} is one of those six characters,
* {@code false} otherwise.
*/
public static boolean isAsciiWhitespace(char charCode) {
return charCode == ' ' || charCode == '\t' || charCode == '\n' || charCode == '\u000B'
|| charCode == '\f' || charCode == '\r';
}

/**
* Splits {@code input} on runs of Unicode {@code White_Space}. Leading and trailing
* runs are ignored, so whitespace-only input yields an empty array. This is a
Expand Down Expand Up @@ -151,6 +168,91 @@ public static String[] splitOnUnicodeWhitespace(CharSequence input) {
return terms.toArray(new String[0]);
}

/**
* Splits {@code input} on runs of ASCII whitespace with the result of
* {@code String.split("\\s+")}: a leading run yields one empty first element, trailing
* empty elements are dropped, whitespace-only input yields an empty array, and empty
* input yields a single empty element. This is a character scan, not a regular
* expression.
*
* @param input The text to split. Must not be {@code null}.
* @return The elements in order.
* @throws IllegalArgumentException If {@code input} is {@code null}.
*/
public static String[] splitOnAsciiWhitespace(String input) {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
if (input.isEmpty()) {
return new String[] {""};
}
final List<String> elements = new ArrayList<>();
if (isAsciiWhitespace(input.charAt(0))) {
elements.add("");
}
int start = 0;
for (int i = 0; i < input.length(); i++) {
if (isAsciiWhitespace(input.charAt(i))) {
if (i > start) {
elements.add(input.substring(start, i));
}
while (i + 1 < input.length() && isAsciiWhitespace(input.charAt(i + 1))) {
i++;
}
start = i + 1;
}
}
if (input.length() > start) {
elements.add(input.substring(start));
}
while (!elements.isEmpty() && elements.get(elements.size() - 1).isEmpty()) {
elements.remove(elements.size() - 1);
}
return elements.toArray(new String[0]);
}

/**
* Tests whether {@code input} contains an ASCII capital letter, {@code A} to {@code Z}.
* Capitals outside ASCII do not count. This is a character scan, not a regular expression.
*
* @param input The text to check. Must not be {@code null}.
* @return {@code true} if at least one character is an ASCII capital letter.
* @throws IllegalArgumentException If {@code input} is {@code null}.
*/
public static boolean containsAsciiUpperCase(CharSequence input) {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
for (int i = 0; i < input.length(); i++) {
final char c = input.charAt(i);
if (c >= 'A' && c <= 'Z') {
return true;
}
}
return false;
}

/**
* Tests whether {@code input} contains an ASCII digit, {@code 0} to {@code 9}. Digits outside
* ASCII do not count. This is a character scan, not a regular expression.
*
* @param input The text to check. Must not be {@code null}.
* @return {@code true} if at least one character is an ASCII digit.
* @throws IllegalArgumentException If {@code input} is {@code null}.
*/
public static boolean containsAsciiDigit(CharSequence input) {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
for (int i = 0; i < input.length(); i++) {
final char c = input.charAt(i);
if (c >= '0' && c <= '9') {
return true;
}
}
return false;
}

/**
* Trims leading and trailing runs of Unicode {@code White_Space}, the same set
* {@link #splitOnUnicodeWhitespace(CharSequence)} breaks terms on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -61,23 +60,71 @@ public String getHelp() {
+ "Defaults to a WhitespaceTokenizer.";
}

private static final Pattern UNTOKENIZED_PAREN_PATTERN_1 = Pattern.compile("([^ ])([({)}])");
private static final Pattern UNTOKENIZED_PAREN_PATTERN_2 = Pattern.compile("([({)}])([^ ])");

public static Parse[] parseLine(String line, Parser parser, int numParses) {
return parseLine( line, parser, WhitespaceTokenizer.INSTANCE, numParses );
}

public static Parse[] parseLine(String line, Parser parser, Tokenizer tokenizer, int numParses) {
// fix some parens patterns
line = UNTOKENIZED_PAREN_PATTERN_1.matcher(line).replaceAll("$1 $2");
line = UNTOKENIZED_PAREN_PATTERN_2.matcher(line).replaceAll("$1 $2");
line = spaceUntokenizedParens(line);

// tokenize
String[] tokens = tokenizer.tokenize(line);
return parseLine(tokens, parser, numParses);
}

/**
* Separates round and curly brackets from adjacent text in two left-to-right passes: the
* first puts a space between a non-space character and a following bracket, the second
* between a bracket and a following non-space character. Each pass resumes after the pair
* it just spaced, so a pair overlapping that match is only seen by the second pass.
*
* @param line The untokenized line.
* @return The spaced line.
*/
static String spaceUntokenizedParens(String line) {
return insertParenSpaces(insertParenSpaces(line, false), true);
}

/**
* Inserts a space between a bracket and an adjacent non-space character, left to right.
*
* @param line The untokenized line.
* @param parenFirst {@code true} to space a bracket before a character, {@code false} after one.
* @return The spaced line.
*/
private static String insertParenSpaces(String line, boolean parenFirst) {
StringBuilder spaced = new StringBuilder(line.length() + 8);
int i = 0;
while (i < line.length()) {
char c = line.charAt(i);
if (i + 1 < line.length()) {
char next = line.charAt(i + 1);
boolean match = parenFirst
? isParen(c) && next != ' '
: c != ' ' && isParen(next);
if (match) {
spaced.append(c).append(' ').append(next);
i += 2;
continue;
}
}
spaced.append(c);
i++;
}
return spaced.toString();
}

/**
* Tests for a round or curly bracket.
*
* @param c The character.
* @return {@code true} for {@code (}, {@code )}, <code>{</code>, or <code>}</code>.
*/
private static boolean isParen(char c) {
return c == '(' || c == ')' || c == '{' || c == '}';
}

/**
* Parses the specified pre-tokenized sentence and returns the requested number of parses
* or fewer.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.cmdline.parser;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import opennlp.tools.parser.Parse;
import opennlp.tools.parser.Parser;
import opennlp.tools.tokenize.WhitespaceTokenizer;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ParserToolTest {

/*
* The first pass puts a space before a bracket that follows a non-space character, the
* second after a bracket that precedes one. Each pass resumes after the pair it spaced, so
* "((" gets its space from the second pass only, and only a space, not a tab, separates.
*/
private static Stream<Arguments> parenLines() {
return Stream.of(
Arguments.of("a(b)c", "a ( b ) c"),
Arguments.of("a (b) c", "a ( b ) c"),
Arguments.of("foo(bar){baz}", "foo ( bar ) {baz }"),
Arguments.of("((a)(b))", "( ( a ) (b ) )"),
Arguments.of("a(b(c)d)e", "a ( b ( c ) d ) e"),
Arguments.of("x((", "x ( ("),
Arguments.of("((x", "( ( x"),
Arguments.of("()", "( )"),
Arguments.of("(", "("),
Arguments.of("", ""),
Arguments.of("no parens here", "no parens here"),
Arguments.of("«quoted»", "«quoted»"),
Arguments.of("tab\there(", "tab\there ("),
Arguments.of("a (b", "a ( b"));
}

@ParameterizedTest
@MethodSource("parenLines")
void testSpaceUntokenizedParens(String line, String expected) {
assertEquals(expected, ParserTool.spaceUntokenizedParens(line));
}

@Test
void testParseLineSeparatesParensBeforeTokenizing() {
Parser echo = new Parser() {
@Override
public Parse[] parse(Parse tokens, int numParses) {
return new Parse[] {tokens};
}

@Override
public Parse parse(Parse tokens) {
return tokens;
}
};
Parse[] parses = ParserTool.parseLine("f(x)+g({y})", echo, WhitespaceTokenizer.INSTANCE, 1);
assertEquals(1, parses.length);
String[] tokens = Stream.of(parses[0].getChildren()).map(Parse::getCoveredText)
.toArray(String[]::new);
// "{y" stays joined: the second pass consumed "{" while spacing "( {"
assertArrayEquals(new String[] {"f", "(", "x", ")", "+g", "(", "{y", "}", ")"}, tokens);
}
}
Loading
Loading