You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opennlp.apache.org by kr...@apache.org on 2026/08/22 01:01:02 UTC
(opennlp) 08/15: relation: Reject whitespace-bearing triggers and judge blankness by the project predicate
This is an automated email from the ASF dual-hosted git repository.
krickert pushed a commit to branch OPENNLP-1920-relation-extraction
in repository https://gitbox.apache.org/repos/asf/opennlp.git
commit ee2f5bbe4c8c74cb5e691f12a6b20f953ab58c37
Author: Kristian Rickert <kr...@gmail.com>
AuthorDate: Fri Jul 17 00:22:33 2026 -0400
relation: Reject whitespace-bearing triggers and judge blankness by the project predicate
A trigger is compared against a single pivot token, so a trigger containing any
whitespace is a rule that can never match; construction now rejects it, closing
the half of the never-matching defect the case-mapping fix left open. Type,
path, and trigger blankness are judged under the project whitespace definition,
which covers the no-break spaces the JDK blank check misses, so a path spelled
from them can no longer construct a zero-step dead rule, and the steps of every
constructible pattern are guaranteed non-empty. New tests pin the
supplementary-plane cased trigger pair and the negative side of the annotator's
mapping seam: the JDK-lowercased spelling of the dotted capital I matches
nothing.
---
.../opennlp/tools/relation/RelationPattern.java | 49 ++++++++++++++++------
.../tools/relation/RelationAnnotatorTest.java | 18 ++++++++
.../tools/relation/RelationPatternTest.java | 47 +++++++++++++++++----
3 files changed, 93 insertions(+), 21 deletions(-)
diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/relation/RelationPattern.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/relation/RelationPattern.java
index 6e202c011..3b2f58f1a 100644
--- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/relation/RelationPattern.java
+++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/relation/RelationPattern.java
@@ -48,31 +48,43 @@ import opennlp.tools.util.StringUtil;
* @param path The path shape as described above. Must not be {@code null} or blank, and
* every up step must come before the first down step.
* @param trigger The required pivot form, or {@code null} for any pivot. Must not be
- * blank, and must be unchanged by {@link StringUtil#toLowerCase(CharSequence)}.
+ * blank or contain whitespace, since it is matched against a single
+ * token, and must be unchanged by
+ * {@link StringUtil#toLowerCase(CharSequence)}.
*
* @since 3.0.0
*/
public record RelationPattern(String type, String path, String trigger) {
/**
- * Validates the rule.
+ * Validates the rule. Blankness is judged under the project whitespace definition,
+ * which unlike the JDK's includes no-break spaces, so a value spelled entirely from
+ * them cannot construct a rule that could never match.
*
* @throws IllegalArgumentException Thrown if {@code type} or {@code path} is
- * {@code null} or blank, {@code path} is malformed, or {@code trigger} is blank
- * or not already lowercased under
+ * {@code null} or blank, {@code path} is malformed, or {@code trigger} is
+ * blank, contains whitespace, or is not already lowercased under
* {@link StringUtil#toLowerCase(CharSequence)}.
*/
public RelationPattern {
- if (type == null || type.isBlank()) {
+ if (type == null || blank(type)) {
throw new IllegalArgumentException("type must not be null or blank");
}
- if (path == null || path.isBlank()) {
+ if (path == null || blank(path)) {
throw new IllegalArgumentException("path must not be null or blank");
}
if (trigger != null) {
- if (trigger.isBlank()) {
+ if (trigger.isEmpty()) {
throw new IllegalArgumentException("trigger must not be blank");
}
+ for (int i = 0; i < trigger.length(); ) {
+ final int cp = trigger.codePointAt(i);
+ if (StringUtil.isWhitespace(cp)) {
+ throw new IllegalArgumentException("trigger must not contain whitespace,"
+ + " since it is matched against a single token: " + trigger);
+ }
+ i += Character.charCount(cp);
+ }
if (!StringUtil.toLowerCase(trigger).equals(trigger)) {
throw new IllegalArgumentException("trigger must be lowercased, but was: " + trigger);
}
@@ -94,16 +106,29 @@ public record RelationPattern(String type, String path, String trigger) {
/**
* Splits the path into its steps.
*
- * @return The steps in order. Never {@code null}. Empty only when the path consists
- * solely of characters that are whitespace to
- * {@link StringUtil#isWhitespace(char)} but not to {@link String#isBlank()},
- * for example the no-break space U+00A0; such a pattern never matches, since
- * every path between two distinct entity heads has at least one step.
+ * @return The steps in order. Never {@code null} and never empty: the constructor
+ * judges blankness by the same whitespace definition this split separates
+ * on, so every constructible path holds at least one step.
*/
public List<String> steps() {
return splitSteps(path);
}
+ /**
+ * Reports whether a value is blank under the project whitespace definition, which
+ * unlike the JDK's includes no-break spaces.
+ */
+ private static boolean blank(String value) {
+ for (int i = 0; i < value.length(); ) {
+ final int cp = value.codePointAt(i);
+ if (!StringUtil.isWhitespace(cp)) {
+ return false;
+ }
+ i += Character.charCount(cp);
+ }
+ return true;
+ }
+
/**
* Splits a path into whitespace-free steps with a single left-to-right character
* scan. Every character that {@link StringUtil#isWhitespace(char)} accepts acts as a
diff --git a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationAnnotatorTest.java b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationAnnotatorTest.java
index 374201638..23d997a47 100644
--- a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationAnnotatorTest.java
+++ b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationAnnotatorTest.java
@@ -366,6 +366,24 @@ public class RelationAnnotatorTest {
relations.get(0).span().getStart(), relations.get(0).span().getEnd()).toString());
}
+ /**
+ * The negative side of the mapping seam: the JDK's locale-independent lowercasing of
+ * the dotted capital I expands to an {@code i} followed by the combining dot above
+ * (U+0307), a spelling the project mapping never produces for the pivot, so a
+ * trigger written that way matches nothing. This pins that the annotator compares
+ * through the project mapping alone, and that a trigger prepared with
+ * {@link String#toLowerCase()} does not silently work by accident.
+ */
+ @Test
+ void testJdkLowercasedTriggerSpellingDoesNotMatchThePivot() {
+ final RelationAnnotator annotator = new RelationAnnotator(List.of(
+ new RelationPattern("located_in", ">appos >nmod", "i\u0307stanbul")));
+
+ final Document document = annotator.annotate(dottedCapitalPivotDocument());
+
+ Assertions.assertTrue(document.get(RelationAnnotator.RELATIONS).isEmpty());
+ }
+
/**
* Verifies that annotating a document that already carries a relations layer fails:
* documents reject duplicate layers, so the second annotation pass throws.
diff --git a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationPatternTest.java b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationPatternTest.java
index 07291edec..c65bb0d25 100644
--- a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationPatternTest.java
+++ b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/relation/RelationPatternTest.java
@@ -195,9 +195,11 @@ public class RelationPatternTest {
*/
@Test
void testTriggerCheckFollowsTheProjectCaseMapping() {
+ // the Deseret pair is supplementary-plane: capital long I, U+10400, lowercases to
+ // U+10428, so acceptance must follow the mapping across surrogate pairs as well
for (final String candidate : new String[] {"founded", "Founded",
DOTTED_CAPITAL_ISTANBUL, "istanbul", JDK_LOWERCASED_ISTANBUL, "STRASSE",
- SHARP_S_STRASSE}) {
+ SHARP_S_STRASSE, "\uD801\uDC00", "\uD801\uDC28"}) {
final String message = "trigger acceptance for " + candidate
+ " must follow StringUtil.toLowerCase";
if (StringUtil.toLowerCase(candidate).equals(candidate)) {
@@ -211,15 +213,42 @@ public class RelationPatternTest {
}
/**
- * Documents the boundary between the blank guard and the whitespace scan: the blank
- * check only recognizes the whitespace characters the JDK does, while the step scan
- * also treats space separators such as {@code U+00A0} as whitespace. A path holding
- * only a no-break space therefore passes construction but splits into zero steps, and
- * such a pattern can never match because every computed path has at least one step.
+ * Verifies that blankness is judged by the same whitespace definition the step scan
+ * separates on: a path holding only a no-break space would split into zero steps and
+ * could never match, so it is rejected at construction instead of building a dead
+ * rule, and the same holds for a type spelled from nothing but no-break spaces.
*/
@Test
- void testNoBreakSpaceOnlyPathConstructsWithZeroSteps() {
- final RelationPattern pattern = new RelationPattern("t", "\u00A0", null);
- Assertions.assertEquals(List.of(), pattern.steps());
+ void testNoBreakSpaceOnlyValuesAreRejectedAsBlank() {
+ final IllegalArgumentException path = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> new RelationPattern("t", "\u00A0", null));
+ Assertions.assertEquals("path must not be null or blank", path.getMessage());
+
+ final IllegalArgumentException type = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> new RelationPattern("\u00A0", "<nsubj", null));
+ Assertions.assertEquals("type must not be null or blank", type.getMessage());
+ }
+
+ /**
+ * Verifies that a trigger containing whitespace is rejected at construction: the
+ * trigger is compared against a single pivot token, which the tokenizer guarantees
+ * carries no whitespace, so a multi-word or padded trigger is a rule that could
+ * never match. The whitespace scan follows the project predicate, so a no-break
+ * space inside or around the trigger is caught the same way as a plain space.
+ */
+ @Test
+ void testTriggerContainingWhitespaceIsRejected() {
+ final IllegalArgumentException spaced = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> new RelationPattern("t", "<nsubj", "new york"));
+ Assertions.assertEquals("trigger must not contain whitespace, since it is matched"
+ + " against a single token: new york", spaced.getMessage());
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> new RelationPattern("t", "<nsubj", "founded\u00A0"));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> new RelationPattern("t", "<nsubj", "\u00A0"));
}
}