You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opennlp.apache.org by rz...@apache.org on 2026/09/18 11:12:49 UTC

(opennlp) branch main updated: OPENNLP-1973: Regex removal (prep): Add a CompatibilityMode switch for the earlier output (#1312)

This is an automated email from the ASF dual-hosted git repository.

rzo1 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 c47eeafef OPENNLP-1973: Regex removal (prep): Add a CompatibilityMode switch for the earlier output (#1312)
c47eeafef is described below

commit c47eeafef4bf129690bcb7c0e9c4c267b0f91844
Author: Richard Zowalla <13...@users.noreply.github.com>
AuthorDate: Fri Sep 18 13:12:44 2026 +0200

    OPENNLP-1973: Regex removal (prep): Add a CompatibilityMode switch for the earlier output (#1312)
    
    * OPENNLP-1973: Add a CompatibilityMode switch for the earlier output
    
    CompatibilityMode holds LEGACY or CURRENT, resolved from the system
    property opennlp.compat.mode and overridable with setActive and reset.
    Classes whose output was corrected consult it so models trained with an
    earlier release keep their input. Built like the whitespace mode switch
    and scheduled for removal in 4.0.
    
    * OPENNLP-1973: Document that an invalid mode property fails class initialization
    
    ---------
    
    Co-authored-by: Kristian Rickert <kr...@apache.org>
---
 .../java/opennlp/tools/util/CompatibilityMode.java | 134 +++++++++++++++++++++
 .../opennlp/tools/util/CompatibilityModeTest.java  | 126 +++++++++++++++++++
 2 files changed, 260 insertions(+)

diff --git a/opennlp-api/src/main/java/opennlp/tools/util/CompatibilityMode.java b/opennlp-api/src/main/java/opennlp/tools/util/CompatibilityMode.java
new file mode 100644
index 000000000..ccfd90b57
--- /dev/null
+++ b/opennlp-api/src/main/java/opennlp/tools/util/CompatibilityMode.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Selects, for the classes that corrected their output in 3.0.0, between the corrected output
+ * and the output of the 1.x/2.x releases. A model trained under the old output may depend on
+ * it until it is retrained. Each class that consults the mode documents what differs.
+ * <p>
+ * Resolved from the {@value #MODE_PROPERTY} system property when this class is initialized
+ * and shared process-wide, so a model is trained and decoded under one mode. Tests and
+ * embedders may override the mode via {@link #setActive(CompatibilityMode)} and
+ * {@link #reset()}. The mode is independent of {@link WhitespaceMode}.
+ *
+ * @since 3.0.0
+ */
+public enum CompatibilityMode {
+
+  /**
+   * The output of OpenNLP 1.x/2.x. Restores byte-identical output for models trained with
+   * those releases in the classes that consult the mode.
+   */
+  LEGACY,
+
+  /**
+   * The corrected output. The default from 3.0 onward.
+   */
+  CURRENT;
+
+  /**
+   * System property that selects the active {@link CompatibilityMode} at startup. Accepts
+   * {@code LEGACY} or {@code CURRENT}, case-insensitive; unset or blank resolves to
+   * {@link #CURRENT}, any other value raises an {@link IllegalArgumentException} when the
+   * mode is resolved. The property is first resolved during class initialization, so an
+   * invalid value at startup surfaces as an {@link ExceptionInInitializerError} on first use.
+   */
+  public static final String MODE_PROPERTY = "opennlp.compat.mode";
+
+  private static final Logger logger = LoggerFactory.getLogger(CompatibilityMode.class);
+  private static final AtomicBoolean LEGACY_WARNED = new AtomicBoolean();
+
+  private static volatile CompatibilityMode active = fromProperty();
+
+  /**
+   * Returns the active {@link CompatibilityMode}: the value resolved from the
+   * {@value #MODE_PROPERTY} system property when this class was initialized, or the value
+   * most recently passed to {@link #setActive(CompatibilityMode)}.
+   *
+   * @return The active {@link CompatibilityMode}.
+   */
+  public static CompatibilityMode current() {
+    return active;
+  }
+
+  /**
+   * Overrides the active {@link CompatibilityMode} for the whole process, taking precedence
+   * over the {@value #MODE_PROPERTY} system property. Intended for tests and embedders;
+   * callers pinning a mode temporarily should call {@link #reset()} afterward.
+   *
+   * @param mode The {@link CompatibilityMode} to activate. Must not be {@code null}.
+   *
+   * @throws IllegalArgumentException Thrown if {@code mode} is {@code null}.
+   */
+  public static void setActive(CompatibilityMode mode) {
+    if (mode == null) {
+      throw new IllegalArgumentException("mode must not be null");
+    }
+    active = mode;
+    warnIfLegacy(mode);
+  }
+
+  /**
+   * Discards any override set via {@link #setActive(CompatibilityMode)} and re-resolves the
+   * active mode from the {@value #MODE_PROPERTY} system property.
+   *
+   * @throws IllegalArgumentException Thrown if the property holds a value other than
+   *     {@code LEGACY} or {@code CURRENT} (case-insensitive); the previous mode is retained.
+   */
+  public static void reset() {
+    active = fromProperty();
+  }
+
+  /**
+   * Resolves the mode from the {@value #MODE_PROPERTY} system property; unset or blank
+   * resolves to {@link #CURRENT}. Warns once per process when {@link #LEGACY} is selected.
+   */
+  private static CompatibilityMode fromProperty() {
+    String value = System.getProperty(MODE_PROPERTY);
+    CompatibilityMode mode;
+    if (value == null || value.isBlank()) {
+      mode = CURRENT;
+    } else {
+      try {
+        mode = CompatibilityMode.valueOf(value.trim().toUpperCase(Locale.ROOT));
+      } catch (IllegalArgumentException e) {
+        throw new IllegalArgumentException("Invalid value '" + value + "' for system property '"
+            + MODE_PROPERTY + "': expected LEGACY or CURRENT", e);
+      }
+    }
+    warnIfLegacy(mode);
+    return mode;
+  }
+
+  /**
+   * Logs the legacy-mode removal warning, once per process.
+   */
+  private static void warnIfLegacy(CompatibilityMode mode) {
+    if (mode == LEGACY && LEGACY_WARNED.compareAndSet(false, true)) {
+      logger.warn("Using the legacy (pre-3.0) output of the classes that consult " + MODE_PROPERTY
+          + ". This compatibility mode is scheduled for removal in 4.0.");
+    }
+  }
+}
diff --git a/opennlp-api/src/test/java/opennlp/tools/util/CompatibilityModeTest.java b/opennlp-api/src/test/java/opennlp/tools/util/CompatibilityModeTest.java
new file mode 100644
index 000000000..5ba98af0c
--- /dev/null
+++ b/opennlp-api/src/test/java/opennlp/tools/util/CompatibilityModeTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for the {@link CompatibilityMode} class.
+ */
+public class CompatibilityModeTest {
+
+  /**
+   * Initializes {@link CompatibilityMode} while the property is unset, so tests that set an
+   * invalid value exercise {@link CompatibilityMode#reset()} rather than class initialization.
+   */
+  @BeforeAll
+  static void initializeWithCleanProperty() {
+    System.clearProperty(CompatibilityMode.MODE_PROPERTY);
+    CompatibilityMode.reset();
+  }
+
+  /**
+   * Restores property resolution after each test, so no mode or property state leaks.
+   */
+  @AfterEach
+  void resetCompatibilityMode() {
+    System.clearProperty(CompatibilityMode.MODE_PROPERTY);
+    CompatibilityMode.reset();
+  }
+
+  @Test
+  void testDefaultsToCurrent() {
+    CompatibilityMode.reset();
+    assertEquals(CompatibilityMode.CURRENT, CompatibilityMode.current());
+  }
+
+  @Test
+  void testBlankPropertyDefaultsToCurrent() {
+    System.setProperty(CompatibilityMode.MODE_PROPERTY, "   ");
+    CompatibilityMode.reset();
+    assertEquals(CompatibilityMode.CURRENT, CompatibilityMode.current());
+  }
+
+  @ParameterizedTest
+  @CsvSource({"LEGACY, LEGACY", "legacy, LEGACY", " Legacy , LEGACY", "CURRENT, CURRENT",
+      "current, CURRENT"})
+  void testPropertyResolvesCaseInsensitively(String value, CompatibilityMode expected) {
+    System.setProperty(CompatibilityMode.MODE_PROPERTY, value);
+    CompatibilityMode.reset();
+    assertEquals(expected, CompatibilityMode.current());
+  }
+
+  @ParameterizedTest
+  @CsvSource({"sloppy", "UNICODE", "LEGACY CURRENT"})
+  void testInvalidPropertyValueThrows(String value) {
+    CompatibilityMode before = CompatibilityMode.current();
+    System.setProperty(CompatibilityMode.MODE_PROPERTY, value);
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, CompatibilityMode::reset);
+    assertTrue(e.getMessage().contains(CompatibilityMode.MODE_PROPERTY));
+    assertTrue(e.getMessage().contains(value));
+    assertEquals(before, CompatibilityMode.current());
+  }
+
+  @Test
+  void testCurrentCachesUntilReset() {
+    CompatibilityMode.reset();
+    assertEquals(CompatibilityMode.CURRENT, CompatibilityMode.current());
+
+    System.setProperty(CompatibilityMode.MODE_PROPERTY, "LEGACY");
+    assertEquals(CompatibilityMode.CURRENT, CompatibilityMode.current());
+
+    CompatibilityMode.reset();
+    assertEquals(CompatibilityMode.LEGACY, CompatibilityMode.current());
+  }
+
+  @Test
+  void testSetActiveOverridesProperty() {
+    System.setProperty(CompatibilityMode.MODE_PROPERTY, "CURRENT");
+    CompatibilityMode.reset();
+    CompatibilityMode.setActive(CompatibilityMode.LEGACY);
+    assertEquals(CompatibilityMode.LEGACY, CompatibilityMode.current());
+  }
+
+  @Test
+  void testSetActiveRejectsNull() {
+    assertThrows(IllegalArgumentException.class, () -> CompatibilityMode.setActive(null));
+  }
+
+  @Test
+  void testIndependentOfWhitespaceMode() {
+    WhitespaceMode before = WhitespaceMode.current();
+    try {
+      CompatibilityMode.setActive(CompatibilityMode.LEGACY);
+      assertEquals(before, WhitespaceMode.current());
+      WhitespaceMode.setActive(WhitespaceMode.LEGACY);
+      CompatibilityMode.setActive(CompatibilityMode.CURRENT);
+      assertEquals(WhitespaceMode.LEGACY, WhitespaceMode.current());
+    } finally {
+      WhitespaceMode.reset();
+    }
+  }
+}