mawiesne commented on code in PR #1138:
URL: https://github.com/apache/opennlp/pull/1138#discussion_r3593274227


##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/FullCaseFoldCharSequenceNormalizer.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.util.normalizer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link CharSequenceNormalizer} that applies Unicode full case folding for 
case-insensitive
+ * matching, as defined by the Default Case Algorithms in
+ * <a 
href="https://www.unicode.org/versions/latest/core-spec/chapter-3/";>Section 
3.13 of the
+ * Unicode Standard</a>, using the bundled {@code CaseFolding.txt} data of the 
Unicode Character
+ * Database.
+ *
+ * <p>Unlike {@link CaseFoldCharSequenceNormalizer}, which lower cases with
+ * {@link java.util.Locale#ROOT}, this applies the full case foldings (the 
{@code C} common and
+ * {@code F} full status mappings), including the expanding folds that plain 
lower casing does not
+ * perform: the sharp s (U+00DF) to {@code ss}, the Latin ligatures (for 
example U+FB00 to
+ * {@code ff}), and the Greek and Armenian multi-character folds. It is 
therefore an expanding,
+ * offset-changing transform, so it is offset-aware: {@link 
#normalizeAligned(CharSequence)} reports
+ * the {@link Alignment} from the folded text back to the input. A single 
cursor pass with no regular
+ * expression.</p>
+ *
+ * <p>The {@code S} simple and {@code T} Turkic status mappings are excluded, 
so the Turkish and
+ * Azerbaijani dotless-i rule is not applied here. Input is expected in NFC: 
the fold matches
+ * precomposed code points, so decomposed sequences pass through unchanged.</p>
+ */
+public final class FullCaseFoldCharSequenceNormalizer implements 
OffsetAwareNormalizer {
+
+  private static final long serialVersionUID = 4520210518330612934L;
+
+  private static final String RESOURCE = "CaseFolding.txt";
+
+  /**
+   * Maps a source code point to its full case folding (one or more code 
points), for the C and F
+   * status rows of {@code CaseFolding.txt}. Loaded once when this class 
initializes, which
+   * happens on first use.
+   */
+  private static final Map<Integer, String> FOLDINGS = 
Map.copyOf(initFoldings());
+
+  private static final FullCaseFoldCharSequenceNormalizer INSTANCE =
+      new FullCaseFoldCharSequenceNormalizer();
+
+  /** Creates the singleton; use {@link #getInstance()}. */
+  private FullCaseFoldCharSequenceNormalizer() {
+  }
+
+  /** {@return the shared, stateless instance} */
+  public static FullCaseFoldCharSequenceNormalizer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public CharSequence normalize(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substitute(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public AlignedText normalizeAligned(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substituteAligned(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@return the folding table parsed from the bundled {@code 
CaseFolding.txt} resource}
+   *
+   * @throws IllegalStateException if the resource is missing.
+   * @throws UncheckedIOException if the resource cannot be read.
+   */
+  private static Map<Integer, String> initFoldings() {
+    try (InputStream in = 
FullCaseFoldCharSequenceNormalizer.class.getResourceAsStream(RESOURCE)) {
+      if (in == null) {
+        throw new IllegalStateException("Missing case folding data resource: " 
+ RESOURCE);
+      }
+      return parse(in);
+    } catch (IOException e) {
+      throw new UncheckedIOException("Unable to read case folding data 
resource " + RESOURCE, e);
+    }
+  }
+
+  /**
+   * Parses the full-folding mappings, the {@code C} (common) and {@code F} 
(full) status rows of
+   * {@code CaseFolding.txt}. The {@code S} (simple) and {@code T} (Turkic) 
status rows are
+   * deliberately skipped so the fold stays language-neutral and full rather 
than simple.
+   * Package-private so the malformed-data handling can be exercised without 
the bundled resource.
+   *
+   * @param in the stream to parse, in {@code CaseFolding.txt} format. Must 
not be {@code null}.
+   * @return the mapping from source code point to its full case folding.
+   * @throws IOException if the stream cannot be read.
+   * @throws IllegalArgumentException if the data is malformed.
+   */
+  static Map<Integer, String> parse(InputStream in) throws IOException {
+    final Map<Integer, String> map = new HashMap<>();
+    try (BufferedReader reader =
+             new BufferedReader(new InputStreamReader(in, 
StandardCharsets.UTF_8))) {
+      String line;
+      int lineNumber = 0;
+      while ((line = reader.readLine()) != null) {
+        lineNumber++;
+        final int hash = line.indexOf('#');
+        final String content = (hash < 0 ? line : line.substring(0, 
hash)).strip();
+        if (content.isEmpty()) {
+          continue;
+        }
+        final String[] fields = content.split(";");

Review Comment:
   Please declare `";"` as a constant in this class.



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/FullCaseFoldCharSequenceNormalizer.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.util.normalizer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link CharSequenceNormalizer} that applies Unicode full case folding for 
case-insensitive
+ * matching, as defined by the Default Case Algorithms in
+ * <a 
href="https://www.unicode.org/versions/latest/core-spec/chapter-3/";>Section 
3.13 of the
+ * Unicode Standard</a>, using the bundled {@code CaseFolding.txt} data of the 
Unicode Character
+ * Database.
+ *
+ * <p>Unlike {@link CaseFoldCharSequenceNormalizer}, which lower cases with
+ * {@link java.util.Locale#ROOT}, this applies the full case foldings (the 
{@code C} common and
+ * {@code F} full status mappings), including the expanding folds that plain 
lower casing does not
+ * perform: the sharp s (U+00DF) to {@code ss}, the Latin ligatures (for 
example U+FB00 to
+ * {@code ff}), and the Greek and Armenian multi-character folds. It is 
therefore an expanding,
+ * offset-changing transform, so it is offset-aware: {@link 
#normalizeAligned(CharSequence)} reports
+ * the {@link Alignment} from the folded text back to the input. A single 
cursor pass with no regular
+ * expression.</p>
+ *
+ * <p>The {@code S} simple and {@code T} Turkic status mappings are excluded, 
so the Turkish and
+ * Azerbaijani dotless-i rule is not applied here. Input is expected in NFC: 
the fold matches
+ * precomposed code points, so decomposed sequences pass through unchanged.</p>
+ */
+public final class FullCaseFoldCharSequenceNormalizer implements 
OffsetAwareNormalizer {
+
+  private static final long serialVersionUID = 4520210518330612934L;
+
+  private static final String RESOURCE = "CaseFolding.txt";
+
+  /**
+   * Maps a source code point to its full case folding (one or more code 
points), for the C and F
+   * status rows of {@code CaseFolding.txt}. Loaded once when this class 
initializes, which
+   * happens on first use.
+   */
+  private static final Map<Integer, String> FOLDINGS = 
Map.copyOf(initFoldings());
+
+  private static final FullCaseFoldCharSequenceNormalizer INSTANCE =
+      new FullCaseFoldCharSequenceNormalizer();
+
+  /** Creates the singleton; use {@link #getInstance()}. */
+  private FullCaseFoldCharSequenceNormalizer() {
+  }
+
+  /** {@return the shared, stateless instance} */
+  public static FullCaseFoldCharSequenceNormalizer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public CharSequence normalize(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substitute(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public AlignedText normalizeAligned(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substituteAligned(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@return the folding table parsed from the bundled {@code 
CaseFolding.txt} resource}
+   *
+   * @throws IllegalStateException if the resource is missing.
+   * @throws UncheckedIOException if the resource cannot be read.
+   */
+  private static Map<Integer, String> initFoldings() {
+    try (InputStream in = 
FullCaseFoldCharSequenceNormalizer.class.getResourceAsStream(RESOURCE)) {
+      if (in == null) {
+        throw new IllegalStateException("Missing case folding data resource: " 
+ RESOURCE);
+      }
+      return parse(in);
+    } catch (IOException e) {
+      throw new UncheckedIOException("Unable to read case folding data 
resource " + RESOURCE, e);
+    }
+  }
+
+  /**
+   * Parses the full-folding mappings, the {@code C} (common) and {@code F} 
(full) status rows of
+   * {@code CaseFolding.txt}. The {@code S} (simple) and {@code T} (Turkic) 
status rows are
+   * deliberately skipped so the fold stays language-neutral and full rather 
than simple.
+   * Package-private so the malformed-data handling can be exercised without 
the bundled resource.
+   *
+   * @param in the stream to parse, in {@code CaseFolding.txt} format. Must 
not be {@code null}.
+   * @return the mapping from source code point to its full case folding.
+   * @throws IOException if the stream cannot be read.
+   * @throws IllegalArgumentException if the data is malformed.
+   */
+  static Map<Integer, String> parse(InputStream in) throws IOException {
+    final Map<Integer, String> map = new HashMap<>();
+    try (BufferedReader reader =
+             new BufferedReader(new InputStreamReader(in, 
StandardCharsets.UTF_8))) {
+      String line;
+      int lineNumber = 0;
+      while ((line = reader.readLine()) != null) {
+        lineNumber++;
+        final int hash = line.indexOf('#');
+        final String content = (hash < 0 ? line : line.substring(0, 
hash)).strip();
+        if (content.isEmpty()) {
+          continue;
+        }
+        final String[] fields = content.split(";");
+        if (fields.length < 3) {
+          throw new IllegalArgumentException("Malformed case folding data in " 
+ RESOURCE
+              + " at line " + lineNumber + ": " + content);
+        }
+        final String status = fields[1].strip();
+        if ("S".equals(status) || "T".equals(status)) {
+          // Simple and Turkic mappings are recognized but deliberately not 
part of the full fold.
+          continue;
+        }
+        if (!"C".equals(status) && !"F".equals(status)) {

Review Comment:
   Please declare "C" and "F" as a constants in this class with a proper and 
meaningful constant name.



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/TextNormalizer.java:
##########
@@ -155,18 +171,14 @@ public CharSequenceNormalizer build() {
     /**
      * {@return an offset-aware composition of the rungs added so far}
      *
-     * <p>Every rung must be an {@link OffsetAwareNormalizer}. Each 
per-code-point fold is one;
-     * the folds that delegate to {@link java.text.Normalizer} or to JDK case 
mapping (NFC, NFKC,
-     * accent folding, confusable folding, and case folding) cannot report 
their per-character edits
-     * and so are rejected here. The returned normalizer's
+     * <p>Every rung must be an {@link OffsetAwareNormalizer}. NFC, NFKC, 
accent folding, confusable

Review Comment:
   Please clarify and sharpen the Javadoc: Can we please avoid the term "rung" 
here? What is a better alternative in the context of this Javadoc snippet 
and/or class?



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/TextNormalizer.java:
##########
@@ -70,6 +70,7 @@ public static final class Builder {
 
     private final List<CharSequenceNormalizer> steps = new ArrayList<>();
 
+    /** Creates an empty builder; use {@link TextNormalizer#builder()}. */

Review Comment:
   This is badly written Javadoc. Please clarify and sharpen the text.



##########
opennlp-docs/src/docbkx/normalizer.xml:
##########
@@ -245,9 +245,11 @@ Span hit = aligned.toOriginalSpan(5, 14);   // "the-match" 
in the normalized tex
                        for it with a plain <code>instanceof</code>, the same 
pattern the name finder uses for
                        <code>OffsetMappingNameFinder</code>. Every 
per-code-point fold implements it: whitespace, the
                        line-break-preserving whitespace rung, dashes, 
invisible-control stripping, quotes, digits,
-                       ellipsis, bullets, and the German umlaut 
transliteration. The folds that route through
+                       ellipsis, bullets, the German umlaut transliteration, 
and Unicode full case folding
+                               (<code>fullCaseFold()</code>, whose expansions 
come from a bundled table with known lengths, so

Review Comment:
   Please check and fix the indentation in line 249 and 250 to align properly 
with the surrounding text.



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/FullCaseFoldCharSequenceNormalizer.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.util.normalizer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link CharSequenceNormalizer} that applies Unicode full case folding for 
case-insensitive
+ * matching, as defined by the Default Case Algorithms in
+ * <a 
href="https://www.unicode.org/versions/latest/core-spec/chapter-3/";>Section 
3.13 of the
+ * Unicode Standard</a>, using the bundled {@code CaseFolding.txt} data of the 
Unicode Character
+ * Database.
+ *
+ * <p>Unlike {@link CaseFoldCharSequenceNormalizer}, which lower cases with
+ * {@link java.util.Locale#ROOT}, this applies the full case foldings (the 
{@code C} common and
+ * {@code F} full status mappings), including the expanding folds that plain 
lower casing does not
+ * perform: the sharp s (U+00DF) to {@code ss}, the Latin ligatures (for 
example U+FB00 to
+ * {@code ff}), and the Greek and Armenian multi-character folds. It is 
therefore an expanding,
+ * offset-changing transform, so it is offset-aware: {@link 
#normalizeAligned(CharSequence)} reports
+ * the {@link Alignment} from the folded text back to the input. A single 
cursor pass with no regular
+ * expression.</p>
+ *
+ * <p>The {@code S} simple and {@code T} Turkic status mappings are excluded, 
so the Turkish and
+ * Azerbaijani dotless-i rule is not applied here. Input is expected in NFC: 
the fold matches
+ * precomposed code points, so decomposed sequences pass through unchanged.</p>
+ */
+public final class FullCaseFoldCharSequenceNormalizer implements 
OffsetAwareNormalizer {
+
+  private static final long serialVersionUID = 4520210518330612934L;
+
+  private static final String RESOURCE = "CaseFolding.txt";
+
+  /**
+   * Maps a source code point to its full case folding (one or more code 
points), for the C and F
+   * status rows of {@code CaseFolding.txt}. Loaded once when this class 
initializes, which
+   * happens on first use.
+   */
+  private static final Map<Integer, String> FOLDINGS = 
Map.copyOf(initFoldings());
+
+  private static final FullCaseFoldCharSequenceNormalizer INSTANCE =
+      new FullCaseFoldCharSequenceNormalizer();
+
+  /** Creates the singleton; use {@link #getInstance()}. */
+  private FullCaseFoldCharSequenceNormalizer() {
+  }
+
+  /** {@return the shared, stateless instance} */
+  public static FullCaseFoldCharSequenceNormalizer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public CharSequence normalize(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substitute(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public AlignedText normalizeAligned(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substituteAligned(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@return the folding table parsed from the bundled {@code 
CaseFolding.txt} resource}
+   *
+   * @throws IllegalStateException if the resource is missing.
+   * @throws UncheckedIOException if the resource cannot be read.
+   */
+  private static Map<Integer, String> initFoldings() {
+    try (InputStream in = 
FullCaseFoldCharSequenceNormalizer.class.getResourceAsStream(RESOURCE)) {
+      if (in == null) {
+        throw new IllegalStateException("Missing case folding data resource: " 
+ RESOURCE);
+      }
+      return parse(in);
+    } catch (IOException e) {
+      throw new UncheckedIOException("Unable to read case folding data 
resource " + RESOURCE, e);
+    }
+  }
+
+  /**
+   * Parses the full-folding mappings, the {@code C} (common) and {@code F} 
(full) status rows of
+   * {@code CaseFolding.txt}. The {@code S} (simple) and {@code T} (Turkic) 
status rows are
+   * deliberately skipped so the fold stays language-neutral and full rather 
than simple.
+   * Package-private so the malformed-data handling can be exercised without 
the bundled resource.
+   *
+   * @param in the stream to parse, in {@code CaseFolding.txt} format. Must 
not be {@code null}.
+   * @return the mapping from source code point to its full case folding.
+   * @throws IOException if the stream cannot be read.
+   * @throws IllegalArgumentException if the data is malformed.
+   */
+  static Map<Integer, String> parse(InputStream in) throws IOException {
+    final Map<Integer, String> map = new HashMap<>();
+    try (BufferedReader reader =
+             new BufferedReader(new InputStreamReader(in, 
StandardCharsets.UTF_8))) {
+      String line;
+      int lineNumber = 0;
+      while ((line = reader.readLine()) != null) {
+        lineNumber++;
+        final int hash = line.indexOf('#');
+        final String content = (hash < 0 ? line : line.substring(0, 
hash)).strip();
+        if (content.isEmpty()) {
+          continue;
+        }
+        final String[] fields = content.split(";");
+        if (fields.length < 3) {
+          throw new IllegalArgumentException("Malformed case folding data in " 
+ RESOURCE
+              + " at line " + lineNumber + ": " + content);
+        }
+        final String status = fields[1].strip();
+        if ("S".equals(status) || "T".equals(status)) {
+          // Simple and Turkic mappings are recognized but deliberately not 
part of the full fold.
+          continue;
+        }
+        if (!"C".equals(status) && !"F".equals(status)) {
+          // An unrecognized status is not a known-and-skipped case (S/T 
above); treat it as
+          // corruption rather than silently dropping data.
+          throw new IllegalArgumentException("Malformed case folding data in " 
+ RESOURCE
+              + " at line " + lineNumber + ": unrecognized status '" + status 
+ "' in: " + content);
+        }
+        try {
+          final int source = Integer.parseInt(fields[0].strip(), 16);
+          final StringBuilder target = new StringBuilder();
+          for (final String hex : fields[2].strip().split(" ")) {

Review Comment:
   Is it a good idea to have a hard-coded whitespace character here? Shouldn't 
his better be "\s+" regex which covers all forms of whitespaces? Please check 
in this context here. 
   
   In both cases: Declare a constant for the split symbol / regex expr. so that 
repeated creation of objects is avoided.



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/TermAnalyzer.java:
##########
@@ -217,6 +216,7 @@ public static final class Builder {
     private Lemmatizer lemmatizer;
     private WordTokenizer tokenizer = new WordTokenizer();
 
+    /** Creates an empty builder; use {@link TermAnalyzer#builder()}. */

Review Comment:
   This is badly written Javadoc. Please clarify and sharpen the text.



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/TermAnalyzer.java:
##########
@@ -188,9 +189,7 @@ String apply(Dimension dimension, String input, String 
posTag) {
         }
         final String[] lemmas = lemmatizer.lemmatize(new String[] {input}, new 
String[] {posTag});
         if (lemmas == null || lemmas.length == 0 || lemmas[0] == null) {
-          // A contract-violating Lemmatizer must fail loud here: a null 
cached under LEMMA would
-          // read as "absent" in Term.at's lazy cache and recompute through 
normalized() forever,
-          // surfacing as a StackOverflowError far from the cause.
+          // A contract-violating Lemmatizer must fail loud here rather than 
caching a null lemma.

Review Comment:
   Please carefully reconsider this shortening. It seems odd to a human 
reviewer and likely unrelated to this PR!



##########
opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/util/normalizer/TermAnalyzerTest.java:
##########
@@ -152,10 +164,7 @@ public List<List<String>> lemmatize(List<String> tokens, 
List<String> tags) {
 
   @Test
   void testLemmatizerReturningNullFailsLoudlyInsteadOfOverflowing() {
-    // A contract-violating Lemmatizer that returns a null lemma must surface 
as a clear
-    // IllegalStateException. Before this guard the null was cached under 
LEMMA, read as "absent"
-    // by Term.at's lazy cache, and recomputed through normalized() forever, 
surfacing as a
-    // StackOverflowError far from the cause.
+    // Pins that a Lemmatizer returning a null lemma fails with a clear 
IllegalStateException.

Review Comment:
   Why was this longer comment reduced here? Is it a behavior change or what? 
Could we better keep the previous form if it is still correct and helpful?



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/TermAnalyzer.java:
##########
@@ -188,9 +189,7 @@ String apply(Dimension dimension, String input, String 
posTag) {
         }
         final String[] lemmas = lemmatizer.lemmatize(new String[] {input}, new 
String[] {posTag});
         if (lemmas == null || lemmas.length == 0 || lemmas[0] == null) {
-          // A contract-violating Lemmatizer must fail loud here: a null 
cached under LEMMA would
-          // read as "absent" in Term.at's lazy cache and recompute through 
normalized() forever,
-          // surfacing as a StackOverflowError far from the cause.
+          // A contract-violating Lemmatizer must fail loud here rather than 
caching a null lemma.

Review Comment:
   Why was this longer comment reduced here? Is it a behavior change or what? 
Could we better keep the previous form if it is still correct and helpful?



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/FullCaseFoldCharSequenceNormalizer.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.util.normalizer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link CharSequenceNormalizer} that applies Unicode full case folding for 
case-insensitive
+ * matching, as defined by the Default Case Algorithms in
+ * <a 
href="https://www.unicode.org/versions/latest/core-spec/chapter-3/";>Section 
3.13 of the
+ * Unicode Standard</a>, using the bundled {@code CaseFolding.txt} data of the 
Unicode Character
+ * Database.
+ *
+ * <p>Unlike {@link CaseFoldCharSequenceNormalizer}, which lower cases with
+ * {@link java.util.Locale#ROOT}, this applies the full case foldings (the 
{@code C} common and
+ * {@code F} full status mappings), including the expanding folds that plain 
lower casing does not
+ * perform: the sharp s (U+00DF) to {@code ss}, the Latin ligatures (for 
example U+FB00 to
+ * {@code ff}), and the Greek and Armenian multi-character folds. It is 
therefore an expanding,
+ * offset-changing transform, so it is offset-aware: {@link 
#normalizeAligned(CharSequence)} reports
+ * the {@link Alignment} from the folded text back to the input. A single 
cursor pass with no regular
+ * expression.</p>
+ *
+ * <p>The {@code S} simple and {@code T} Turkic status mappings are excluded, 
so the Turkish and
+ * Azerbaijani dotless-i rule is not applied here. Input is expected in NFC: 
the fold matches
+ * precomposed code points, so decomposed sequences pass through unchanged.</p>
+ */
+public final class FullCaseFoldCharSequenceNormalizer implements 
OffsetAwareNormalizer {
+
+  private static final long serialVersionUID = 4520210518330612934L;
+
+  private static final String RESOURCE = "CaseFolding.txt";
+
+  /**
+   * Maps a source code point to its full case folding (one or more code 
points), for the C and F
+   * status rows of {@code CaseFolding.txt}. Loaded once when this class 
initializes, which
+   * happens on first use.
+   */
+  private static final Map<Integer, String> FOLDINGS = 
Map.copyOf(initFoldings());
+
+  private static final FullCaseFoldCharSequenceNormalizer INSTANCE =
+      new FullCaseFoldCharSequenceNormalizer();
+
+  /** Creates the singleton; use {@link #getInstance()}. */
+  private FullCaseFoldCharSequenceNormalizer() {
+  }
+
+  /** {@return the shared, stateless instance} */
+  public static FullCaseFoldCharSequenceNormalizer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public CharSequence normalize(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substitute(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public AlignedText normalizeAligned(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substituteAligned(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@return the folding table parsed from the bundled {@code 
CaseFolding.txt} resource}
+   *
+   * @throws IllegalStateException if the resource is missing.
+   * @throws UncheckedIOException if the resource cannot be read.
+   */
+  private static Map<Integer, String> initFoldings() {
+    try (InputStream in = 
FullCaseFoldCharSequenceNormalizer.class.getResourceAsStream(RESOURCE)) {
+      if (in == null) {
+        throw new IllegalStateException("Missing case folding data resource: " 
+ RESOURCE);
+      }
+      return parse(in);
+    } catch (IOException e) {
+      throw new UncheckedIOException("Unable to read case folding data 
resource " + RESOURCE, e);
+    }
+  }
+
+  /**
+   * Parses the full-folding mappings, the {@code C} (common) and {@code F} 
(full) status rows of
+   * {@code CaseFolding.txt}. The {@code S} (simple) and {@code T} (Turkic) 
status rows are
+   * deliberately skipped so the fold stays language-neutral and full rather 
than simple.
+   * Package-private so the malformed-data handling can be exercised without 
the bundled resource.
+   *
+   * @param in the stream to parse, in {@code CaseFolding.txt} format. Must 
not be {@code null}.
+   * @return the mapping from source code point to its full case folding.
+   * @throws IOException if the stream cannot be read.
+   * @throws IllegalArgumentException if the data is malformed.
+   */
+  static Map<Integer, String> parse(InputStream in) throws IOException {
+    final Map<Integer, String> map = new HashMap<>();
+    try (BufferedReader reader =
+             new BufferedReader(new InputStreamReader(in, 
StandardCharsets.UTF_8))) {
+      String line;
+      int lineNumber = 0;
+      while ((line = reader.readLine()) != null) {
+        lineNumber++;
+        final int hash = line.indexOf('#');
+        final String content = (hash < 0 ? line : line.substring(0, 
hash)).strip();
+        if (content.isEmpty()) {
+          continue;
+        }
+        final String[] fields = content.split(";");
+        if (fields.length < 3) {
+          throw new IllegalArgumentException("Malformed case folding data in " 
+ RESOURCE
+              + " at line " + lineNumber + ": " + content);
+        }
+        final String status = fields[1].strip();
+        if ("S".equals(status) || "T".equals(status)) {

Review Comment:
   Please declare "S" and "T" as a constants in this class with a proper and 
meaningful constant name.



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/normalizer/FullCaseFoldCharSequenceNormalizer.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.util.normalizer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link CharSequenceNormalizer} that applies Unicode full case folding for 
case-insensitive
+ * matching, as defined by the Default Case Algorithms in
+ * <a 
href="https://www.unicode.org/versions/latest/core-spec/chapter-3/";>Section 
3.13 of the
+ * Unicode Standard</a>, using the bundled {@code CaseFolding.txt} data of the 
Unicode Character
+ * Database.
+ *
+ * <p>Unlike {@link CaseFoldCharSequenceNormalizer}, which lower cases with
+ * {@link java.util.Locale#ROOT}, this applies the full case foldings (the 
{@code C} common and
+ * {@code F} full status mappings), including the expanding folds that plain 
lower casing does not
+ * perform: the sharp s (U+00DF) to {@code ss}, the Latin ligatures (for 
example U+FB00 to
+ * {@code ff}), and the Greek and Armenian multi-character folds. It is 
therefore an expanding,
+ * offset-changing transform, so it is offset-aware: {@link 
#normalizeAligned(CharSequence)} reports
+ * the {@link Alignment} from the folded text back to the input. A single 
cursor pass with no regular
+ * expression.</p>
+ *
+ * <p>The {@code S} simple and {@code T} Turkic status mappings are excluded, 
so the Turkish and
+ * Azerbaijani dotless-i rule is not applied here. Input is expected in NFC: 
the fold matches
+ * precomposed code points, so decomposed sequences pass through unchanged.</p>
+ */
+public final class FullCaseFoldCharSequenceNormalizer implements 
OffsetAwareNormalizer {
+
+  private static final long serialVersionUID = 4520210518330612934L;
+
+  private static final String RESOURCE = "CaseFolding.txt";
+
+  /**
+   * Maps a source code point to its full case folding (one or more code 
points), for the C and F
+   * status rows of {@code CaseFolding.txt}. Loaded once when this class 
initializes, which
+   * happens on first use.
+   */
+  private static final Map<Integer, String> FOLDINGS = 
Map.copyOf(initFoldings());
+
+  private static final FullCaseFoldCharSequenceNormalizer INSTANCE =
+      new FullCaseFoldCharSequenceNormalizer();
+
+  /** Creates the singleton; use {@link #getInstance()}. */
+  private FullCaseFoldCharSequenceNormalizer() {
+  }
+
+  /** {@return the shared, stateless instance} */
+  public static FullCaseFoldCharSequenceNormalizer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public CharSequence normalize(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substitute(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws IllegalArgumentException if {@code text} is {@code null}.
+   */
+  @Override
+  public AlignedText normalizeAligned(CharSequence text) {
+    if (text == null) {
+      throw new IllegalArgumentException("text must not be null");
+    }
+    return CharClass.substituteAligned(text, FOLDINGS::get);
+  }
+
+  /**
+   * {@return the folding table parsed from the bundled {@code 
CaseFolding.txt} resource}
+   *
+   * @throws IllegalStateException if the resource is missing.
+   * @throws UncheckedIOException if the resource cannot be read.
+   */
+  private static Map<Integer, String> initFoldings() {
+    try (InputStream in = 
FullCaseFoldCharSequenceNormalizer.class.getResourceAsStream(RESOURCE)) {
+      if (in == null) {
+        throw new IllegalStateException("Missing case folding data resource: " 
+ RESOURCE);
+      }
+      return parse(in);
+    } catch (IOException e) {
+      throw new UncheckedIOException("Unable to read case folding data 
resource " + RESOURCE, e);
+    }
+  }
+
+  /**
+   * Parses the full-folding mappings, the {@code C} (common) and {@code F} 
(full) status rows of
+   * {@code CaseFolding.txt}. The {@code S} (simple) and {@code T} (Turkic) 
status rows are
+   * deliberately skipped so the fold stays language-neutral and full rather 
than simple.
+   * Package-private so the malformed-data handling can be exercised without 
the bundled resource.
+   *
+   * @param in the stream to parse, in {@code CaseFolding.txt} format. Must 
not be {@code null}.
+   * @return the mapping from source code point to its full case folding.
+   * @throws IOException if the stream cannot be read.
+   * @throws IllegalArgumentException if the data is malformed.
+   */
+  static Map<Integer, String> parse(InputStream in) throws IOException {
+    final Map<Integer, String> map = new HashMap<>();
+    try (BufferedReader reader =
+             new BufferedReader(new InputStreamReader(in, 
StandardCharsets.UTF_8))) {
+      String line;
+      int lineNumber = 0;
+      while ((line = reader.readLine()) != null) {
+        lineNumber++;
+        final int hash = line.indexOf('#');
+        final String content = (hash < 0 ? line : line.substring(0, 
hash)).strip();
+        if (content.isEmpty()) {
+          continue;
+        }
+        final String[] fields = content.split(";");

Review Comment:
   Please check other open PRs for similar occasions of repeated String object 
creation in loops. This is not preferable for performance / object allocation 
reasons!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to