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
commit fa79a524382734d997a2754c9acfb628e3d74f3d Author: Richard Zowalla <[email protected]> AuthorDate: Thu Jul 16 15:31:54 2026 +0200 OPENNLP-1890: Using allowlist based class loading in StringInterners --- .../tools/util/jvm/CHMStringDeduplicator.java | 2 +- .../opennlp/tools/util/jvm/CHMStringInterner.java | 2 +- .../opennlp/tools/util/jvm/HMStringInterner.java | 2 +- .../opennlp/tools/util/jvm/JvmStringInterner.java | 2 +- .../opennlp/tools/util/jvm/NoOpStringInterner.java | 2 +- .../opennlp/tools/util/jvm/StringInterners.java | 12 ++-- .../tools/util/jvm/StringInternersTest.java | 74 ++++++++++++++++++++++ 7 files changed, 86 insertions(+), 10 deletions(-) diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringDeduplicator.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringDeduplicator.java index d8a0a50d9..bf0148976 100644 --- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringDeduplicator.java +++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringDeduplicator.java @@ -35,7 +35,7 @@ import opennlp.tools.commons.ThreadSafe; */ @Internal @ThreadSafe -class CHMStringDeduplicator implements StringInterner { +public class CHMStringDeduplicator implements StringInterner { private final int prob; private final Map<String, String> map; diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringInterner.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringInterner.java index 597cae26a..812c343f1 100644 --- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringInterner.java +++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/CHMStringInterner.java @@ -32,7 +32,7 @@ import opennlp.tools.commons.ThreadSafe; */ @Internal @ThreadSafe -class CHMStringInterner implements StringInterner { +public class CHMStringInterner implements StringInterner { private final Map<String, String> map; public CHMStringInterner() { map = new ConcurrentHashMap<>(); diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/HMStringInterner.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/HMStringInterner.java index 2cef86ff8..84f816863 100644 --- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/HMStringInterner.java +++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/HMStringInterner.java @@ -30,7 +30,7 @@ import opennlp.tools.commons.Internal; * https://shipilev.net/jvm/anatomy-quarks/10-string-intern/</a> */ @Internal -class HMStringInterner implements StringInterner { +public class HMStringInterner implements StringInterner { private final Map<String, String> map; diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/JvmStringInterner.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/JvmStringInterner.java index 26567b4d0..a6b040f3c 100644 --- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/JvmStringInterner.java +++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/JvmStringInterner.java @@ -31,7 +31,7 @@ import opennlp.tools.commons.ThreadSafe; */ @Internal @ThreadSafe -class JvmStringInterner implements StringInterner { +public class JvmStringInterner implements StringInterner { /** * {@inheritDoc} diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/NoOpStringInterner.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/NoOpStringInterner.java index 8398191c8..1bb8111a6 100644 --- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/NoOpStringInterner.java +++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/NoOpStringInterner.java @@ -25,7 +25,7 @@ import opennlp.tools.commons.ThreadSafe; */ @Internal @ThreadSafe -class NoOpStringInterner implements StringInterner { +public class NoOpStringInterner implements StringInterner { /** * {@inheritDoc} diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/StringInterners.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/StringInterners.java index 295def2d6..d65c99b8c 100644 --- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/StringInterners.java +++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/util/jvm/StringInterners.java @@ -17,17 +17,21 @@ package opennlp.tools.util.jvm; -import java.lang.reflect.Constructor; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import opennlp.tools.util.ext.ExtensionLoader; + /** * Provides string interning utility methods. Interning mechanism can be configured via the * system property {@code opennlp.interner.class} by specifying an implementation via its * fully qualified classname. It needs to implement {@link StringInterner}. * <p> * If not specified by the user, the default interner is {@link CHMStringInterner}. + * <p> + * The implementation is loaded via the {@link ExtensionLoader}. Custom implementations + * must provide a public no-arg constructor and reside in an allowed package, + * see {@link ExtensionLoader#registerAllowedPackage(String)}. */ public class StringInterners { @@ -40,9 +44,7 @@ public class StringInterners { CHMStringInterner.class.getCanonicalName()); try { - final Class<?> clazz = Class.forName(clazzName); - final Constructor<?> cons = clazz.getDeclaredConstructor(); - INTERNER = (StringInterner) cons.newInstance(); + INTERNER = ExtensionLoader.instantiateExtension(StringInterner.class, clazzName); LOGGER.debug("Using '{}' as String interner implementation.", clazzName); } catch (Exception e) { throw new RuntimeException("Could not load specified String interner implementation: '" diff --git a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/util/jvm/StringInternersTest.java b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/util/jvm/StringInternersTest.java new file mode 100644 index 000000000..0522ebf11 --- /dev/null +++ b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/util/jvm/StringInternersTest.java @@ -0,0 +1,74 @@ +/* + * 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.jvm; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import opennlp.tools.util.ext.ExtensionLoader; +import opennlp.tools.util.ext.ExtensionNotLoadedException; + +public class StringInternersTest { + + @Test + void testIntern() { + final String interned = StringInterners.intern("opennlp"); + Assertions.assertSame(interned, StringInterners.intern(new String("opennlp".toCharArray()))); + } + + /** + * Positive: all built-in interner implementations can be loaded via the + * ExtensionLoader, as done by {@link StringInterners} when configured through + * the {@code opennlp.interner.class} system property. + */ + @ParameterizedTest + @ValueSource(strings = { + "opennlp.tools.util.jvm.CHMStringInterner", + "opennlp.tools.util.jvm.CHMStringDeduplicator", + "opennlp.tools.util.jvm.HMStringInterner", + "opennlp.tools.util.jvm.JvmStringInterner", + "opennlp.tools.util.jvm.NoOpStringInterner"}) + void testBuiltInInternersLoadable(String clazzName) { + final StringInterner interner = + ExtensionLoader.instantiateExtension(StringInterner.class, clazzName); + Assertions.assertNotNull(interner); + } + + /** + * Negative: an interner class in a non-allowed package is rejected by + * the ExtensionLoader allowlist before Class.forName() is called. + */ + @Test + void testInternerFromBlockedPackageThrows() { + Assertions.assertThrows(ExtensionNotLoadedException.class, () -> + ExtensionLoader.instantiateExtension(StringInterner.class, "com.malicious.Interner")); + } + + /** + * Negative: an opennlp.* class that does NOT implement StringInterner is + * rejected by the isAssignableFrom type check. + */ + @Test + void testInternerOfWrongTypeThrows() { + Assertions.assertThrows(ExtensionNotLoadedException.class, () -> + ExtensionLoader.instantiateExtension(StringInterner.class, + "opennlp.tools.util.jvm.StringInterners")); + } +}
