diff --git a/libre2-core/src/main/java/com/axonops/libre2/api/Matcher.java b/libre2-core/src/main/java/com/axonops/libre2/api/Matcher.java index 4d7feff..cb297a9 100644 --- a/libre2-core/src/main/java/com/axonops/libre2/api/Matcher.java +++ b/libre2-core/src/main/java/com/axonops/libre2/api/Matcher.java @@ -16,7 +16,6 @@ package com.axonops.libre2.api; -import com.axonops.libre2.jni.RE2NativeJNI; import com.axonops.libre2.metrics.RE2MetricsRegistry; import com.axonops.libre2.metrics.MetricNames; @@ -73,7 +72,7 @@ public boolean matches() { long startNanos = System.nanoTime(); - boolean result = RE2NativeJNI.fullMatch(pattern.getNativeHandle(), input); + boolean result = pattern.jni.fullMatch(pattern.getNativeHandle(), input); long durationNanos = System.nanoTime() - startNanos; metrics.recordTimer(MetricNames.MATCHING_FULL_MATCH_LATENCY, durationNanos); @@ -87,7 +86,7 @@ public boolean find() { long startNanos = System.nanoTime(); - boolean result = RE2NativeJNI.partialMatch(pattern.getNativeHandle(), input); + boolean result = pattern.jni.partialMatch(pattern.getNativeHandle(), input); long durationNanos = System.nanoTime() - startNanos; metrics.recordTimer(MetricNames.MATCHING_PARTIAL_MATCH_LATENCY, durationNanos); diff --git a/libre2-core/src/main/java/com/axonops/libre2/api/Pattern.java b/libre2-core/src/main/java/com/axonops/libre2/api/Pattern.java index d9ff87d..2d71e2e 100644 --- a/libre2-core/src/main/java/com/axonops/libre2/api/Pattern.java +++ b/libre2-core/src/main/java/com/axonops/libre2/api/Pattern.java @@ -18,8 +18,9 @@ import com.axonops.libre2.cache.PatternCache; import com.axonops.libre2.cache.RE2Config; +import com.axonops.libre2.jni.RE2Native; +import com.axonops.libre2.jni.IRE2Native; import com.axonops.libre2.jni.RE2LibraryLoader; -import com.axonops.libre2.jni.RE2NativeJNI; import com.axonops.libre2.metrics.MetricNames; import com.axonops.libre2.metrics.RE2MetricsRegistry; import com.axonops.libre2.util.PatternHasher; @@ -77,18 +78,22 @@ public static PatternCache getGlobalCache() { private static final int maxMatchersPerPattern = RE2Config.DEFAULT.maxMatchersPerPattern(); private final long nativeMemoryBytes; + // JniAdapter for all JNI calls - allows mocking in tests + final IRE2Native jni; + Pattern(String patternString, boolean caseSensitive, long nativeHandle) { - this(patternString, caseSensitive, nativeHandle, false); + this(patternString, caseSensitive, nativeHandle, false, RE2Native.INSTANCE); } - Pattern(String patternString, boolean caseSensitive, long nativeHandle, boolean fromCache) { + Pattern(String patternString, boolean caseSensitive, long nativeHandle, boolean fromCache, IRE2Native jni) { this.patternString = Objects.requireNonNull(patternString); this.caseSensitive = caseSensitive; this.nativeHandle = nativeHandle; this.fromCache = fromCache; + this.jni = jni; - // Query native memory size - this.nativeMemoryBytes = RE2NativeJNI.patternMemory(nativeHandle); + // Query native memory size using adapter + this.nativeMemoryBytes = jni.patternMemory(nativeHandle); logger.trace("RE2: Pattern created - length: {}, caseSensitive: {}, fromCache: {}, nativeBytes: {}", patternString.length(), caseSensitive, fromCache, nativeMemoryBytes); @@ -128,7 +133,7 @@ public static Pattern compileWithoutCache(String pattern) { */ public static Pattern compileWithoutCache(String pattern, boolean caseSensitive) { // Compile with fromCache=false so it can actually be closed - return doCompile(pattern, caseSensitive, false); + return doCompile(pattern, caseSensitive, false, RE2Native.INSTANCE); } /** @@ -136,13 +141,21 @@ public static Pattern compileWithoutCache(String pattern, boolean caseSensitive) */ private static Pattern compileUncached(String pattern, boolean caseSensitive) { // Compile with fromCache=true so users can't close it (cache manages it) - return doCompile(pattern, caseSensitive, true); + return doCompile(pattern, caseSensitive, true, RE2Native.INSTANCE); + } + + /** + * Package-private compile method for test injection of mock JniAdapter. + * Bypasses cache for full control in unit tests. + */ + static Pattern compileForTesting(String pattern, boolean caseSensitive, IRE2Native jni) { + return doCompile(pattern, caseSensitive, false, jni); } /** * Actual compilation logic. */ - private static Pattern doCompile(String pattern, boolean caseSensitive, boolean fromCache) { + private static Pattern doCompile(String pattern, boolean caseSensitive, boolean fromCache, IRE2Native jni) { RE2MetricsRegistry metrics = cache.getConfig().metricsRegistry(); String hash = PatternHasher.hash(pattern); @@ -160,10 +173,10 @@ private static Pattern doCompile(String pattern, boolean caseSensitive, boolean boolean compilationSuccessful = false; try { - handle = RE2NativeJNI.compile(pattern, caseSensitive); + handle = jni.compile(pattern, caseSensitive); - if (handle == 0 || !RE2NativeJNI.patternOk(handle)) { - String error = RE2NativeJNI.getError(); + if (handle == 0 || !jni.patternOk(handle)) { + String error = jni.getError(); // Compilation failed - record error metrics.incrementCounter(MetricNames.ERRORS_COMPILATION_FAILED); @@ -177,7 +190,7 @@ private static Pattern doCompile(String pattern, boolean caseSensitive, boolean metrics.recordTimer(MetricNames.PATTERNS_COMPILATION_LATENCY, durationNanos); metrics.incrementCounter(MetricNames.PATTERNS_COMPILED); - Pattern compiled = new Pattern(pattern, caseSensitive, handle, fromCache); + Pattern compiled = new Pattern(pattern, caseSensitive, handle, fromCache, jni); logger.trace("RE2: Pattern compiled - hash: {}, length: {}, caseSensitive: {}, fromCache: {}, nativeBytes: {}, timeNs: {}", hash, pattern.length(), caseSensitive, fromCache, compiled.nativeMemoryBytes, durationNanos); @@ -194,7 +207,7 @@ private static Pattern doCompile(String pattern, boolean caseSensitive, boolean // Free handle if allocated if (handle != 0) { try { - RE2NativeJNI.freePattern(handle); + jni.freePattern(handle); } catch (Exception e) { // Silently ignore - best effort cleanup } @@ -269,7 +282,7 @@ public boolean matches(long address, int length) { } long startNanos = System.nanoTime(); - boolean result = RE2NativeJNI.fullMatchDirect(nativeHandle, address, length); + boolean result = jni.fullMatchDirect(nativeHandle, address, length); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (Zero-Copy) @@ -332,7 +345,7 @@ public boolean find(long address, int length) { } long startNanos = System.nanoTime(); - boolean result = RE2NativeJNI.partialMatchDirect(nativeHandle, address, length); + boolean result = jni.partialMatchDirect(nativeHandle, address, length); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (Zero-Copy) @@ -398,7 +411,7 @@ public MatchResult match(String input) { long startNanos = System.nanoTime(); - String[] groups = RE2NativeJNI.extractGroups(nativeHandle, input); + String[] groups = jni.extractGroups(nativeHandle, input); if (groups == null) { // No match - still track metrics (operation was attempted) @@ -487,7 +500,7 @@ public MatchResult find(String input) { long startNanos = System.nanoTime(); // RE2 extractGroups does UNANCHORED match, so it finds first occurrence - String[] groups = RE2NativeJNI.extractGroups(nativeHandle, input); + String[] groups = jni.extractGroups(nativeHandle, input); long durationNanos = System.nanoTime() - startNanos; @@ -556,7 +569,7 @@ public java.util.List findAll(String input) { long startNanos = System.nanoTime(); - String[][] allMatches = RE2NativeJNI.findAllMatches(nativeHandle, input); + String[][] allMatches = jni.findAllMatches(nativeHandle, input); long durationNanos = System.nanoTime() - startNanos; int matchCount = (allMatches != null) ? allMatches.length : 0; @@ -633,7 +646,7 @@ public MatchResult[] matchAllWithGroups(String[] inputs) { MatchResult[] results = new MatchResult[inputs.length]; for (int i = 0; i < inputs.length; i++) { - String[] groups = RE2NativeJNI.extractGroups(nativeHandle, inputs[i]); + String[] groups = jni.extractGroups(nativeHandle, inputs[i]); if (groups != null && groups.length > 0) { results[i] = new MatchResult(inputs[i], groups, namedGroupMap); } else { @@ -699,7 +712,7 @@ public MatchResult match(long address, int length) { long startNanos = System.nanoTime(); - String[] groups = RE2NativeJNI.extractGroupsDirect(nativeHandle, address, length); + String[] groups = jni.extractGroupsDirect(nativeHandle, address, length); long durationNanos = System.nanoTime() - startNanos; @@ -758,7 +771,7 @@ public MatchResult match(ByteBuffer buffer) { * Helper: Get named groups map for this pattern (lazy-loaded and cached). */ private Map getNamedGroupsMap() { - String[] namedGroupsArray = RE2NativeJNI.getNamedGroups(nativeHandle); + String[] namedGroupsArray = jni.getNamedGroups(nativeHandle); if (namedGroupsArray == null || namedGroupsArray.length == 0) { return Collections.emptyMap(); @@ -798,7 +811,7 @@ public MatchResult matchWithGroups(long address, int length) { } long startNanos = System.nanoTime(); - String[] groups = RE2NativeJNI.extractGroupsDirect(nativeHandle, address, length); + String[] groups = jni.extractGroupsDirect(nativeHandle, address, length); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (Zero-Copy) @@ -862,7 +875,7 @@ public MatchResult findWithGroups(long address, int length) { } long startNanos = System.nanoTime(); - String[] groups = RE2NativeJNI.extractGroupsDirect(nativeHandle, address, length); + String[] groups = jni.extractGroupsDirect(nativeHandle, address, length); long durationNanos = System.nanoTime() - startNanos; RE2MetricsRegistry metrics = cache.getConfig().metricsRegistry(); @@ -925,7 +938,7 @@ public java.util.List findAllWithGroups(long address, int length) { } long startNanos = System.nanoTime(); - String[][] allMatches = RE2NativeJNI.findAllMatchesDirect(nativeHandle, address, length); + String[][] allMatches = jni.findAllMatchesDirect(nativeHandle, address, length); long durationNanos = System.nanoTime() - startNanos; int matchCount = (allMatches != null) ? allMatches.length : 0; @@ -1021,7 +1034,7 @@ public String replaceFirst(String input, String replacement) { long startNanos = System.nanoTime(); - String result = RE2NativeJNI.replaceFirst(nativeHandle, input, replacement); + String result = jni.replaceFirst(nativeHandle, input, replacement); long durationNanos = System.nanoTime() - startNanos; @@ -1083,7 +1096,7 @@ public String replaceAll(String input, String replacement) { long startNanos = System.nanoTime(); - String result = RE2NativeJNI.replaceAll(nativeHandle, input, replacement); + String result = jni.replaceAll(nativeHandle, input, replacement); long durationNanos = System.nanoTime() - startNanos; @@ -1138,7 +1151,7 @@ public String[] replaceAll(String[] inputs, String replacement) { long startNanos = System.nanoTime(); - String[] results = RE2NativeJNI.replaceAllBulk(nativeHandle, inputs, replacement); + String[] results = jni.replaceAllBulk(nativeHandle, inputs, replacement); long durationNanos = System.nanoTime() - startNanos; long perItemNanos = durationNanos / inputs.length; @@ -1208,7 +1221,7 @@ public String replaceFirst(long address, int length, String replacement) { long startNanos = System.nanoTime(); - String result = RE2NativeJNI.replaceFirstDirect(nativeHandle, address, length, replacement); + String result = jni.replaceFirstDirect(nativeHandle, address, length, replacement); long durationNanos = System.nanoTime() - startNanos; @@ -1272,7 +1285,7 @@ public String replaceAll(long address, int length, String replacement) { long startNanos = System.nanoTime(); - String result = RE2NativeJNI.replaceAllDirect(nativeHandle, address, length, replacement); + String result = jni.replaceAllDirect(nativeHandle, address, length, replacement); long durationNanos = System.nanoTime() - startNanos; @@ -1347,7 +1360,7 @@ public String[] replaceAll(long[] addresses, int[] lengths, String replacement) long startNanos = System.nanoTime(); - String[] results = RE2NativeJNI.replaceAllDirectBulk(nativeHandle, addresses, lengths, replacement); + String[] results = jni.replaceAllDirectBulk(nativeHandle, addresses, lengths, replacement); long durationNanos = System.nanoTime() - startNanos; long perItemNanos = durationNanos / addresses.length; @@ -1450,7 +1463,7 @@ public long getNativeMemoryBytes() { */ public int[] getProgramFanout() { checkNotClosed(); - return RE2NativeJNI.programFanout(nativeHandle); + return jni.programFanout(nativeHandle); } /** @@ -1475,7 +1488,7 @@ public int[] getProgramFanout() { * @since 1.2.0 */ public static String quoteMeta(String text) { - return RE2NativeJNI.quoteMeta(text); + return RE2Native.INSTANCE.quoteMeta(text); } long getNativeHandle() { @@ -1532,7 +1545,7 @@ public boolean isValid() { return false; } try { - return RE2NativeJNI.patternOk(nativeHandle); + return jni.patternOk(nativeHandle); } catch (Exception e) { logger.warn("RE2: Exception while validating pattern", e); return false; @@ -1606,7 +1619,7 @@ public void forceClose(boolean releaseRegardless) { // CRITICAL: Always track freed, even if freePattern throws try { - RE2NativeJNI.freePattern(nativeHandle); + jni.freePattern(nativeHandle); } catch (Exception e) { logger.error("RE2: Error freeing pattern native handle", e); } finally { @@ -1771,7 +1784,7 @@ public boolean[] matchAll(String[] inputs) { } long startNanos = System.nanoTime(); - boolean[] results = RE2NativeJNI.fullMatchBulk(nativeHandle, inputs); + boolean[] results = jni.fullMatchBulk(nativeHandle, inputs); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (String Bulk) @@ -1828,7 +1841,7 @@ public boolean[] findAll(String[] inputs) { } long startNanos = System.nanoTime(); - boolean[] results = RE2NativeJNI.partialMatchBulk(nativeHandle, inputs); + boolean[] results = jni.partialMatchBulk(nativeHandle, inputs); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (String Bulk) @@ -1922,7 +1935,7 @@ public boolean[] matchAll(long[] addresses, int[] lengths) { } long startNanos = System.nanoTime(); - boolean[] results = RE2NativeJNI.fullMatchDirectBulk(nativeHandle, addresses, lengths); + boolean[] results = jni.fullMatchDirectBulk(nativeHandle, addresses, lengths); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (Bulk Zero-Copy) @@ -1972,7 +1985,7 @@ public boolean[] findAll(long[] addresses, int[] lengths) { } long startNanos = System.nanoTime(); - boolean[] results = RE2NativeJNI.partialMatchDirectBulk(nativeHandle, addresses, lengths); + boolean[] results = jni.partialMatchDirectBulk(nativeHandle, addresses, lengths); long durationNanos = System.nanoTime() - startNanos; // Track metrics - GLOBAL (ALL) + SPECIFIC (Bulk Zero-Copy) @@ -2129,7 +2142,7 @@ public String[] extractGroups(long address, int length) { throw new IllegalArgumentException("Length must not be negative: " + length); } - return RE2NativeJNI.extractGroupsDirect(nativeHandle, address, length); + return jni.extractGroupsDirect(nativeHandle, address, length); } /** @@ -2154,7 +2167,7 @@ public String[][] findAllMatches(long address, int length) { throw new IllegalArgumentException("Length must not be negative: " + length); } - return RE2NativeJNI.findAllMatchesDirect(nativeHandle, address, length); + return jni.findAllMatchesDirect(nativeHandle, address, length); } // ========== ByteBuffer API (Automatic Zero-Copy Routing) ========== @@ -2322,7 +2335,7 @@ private String[] extractGroupsFromByteBuffer(ByteBuffer buffer) { byte[] bytes = new byte[buffer.remaining()]; buffer.duplicate().get(bytes); String text = new String(bytes, StandardCharsets.UTF_8); - return RE2NativeJNI.extractGroups(nativeHandle, text); + return jni.extractGroups(nativeHandle, text); } /** @@ -2332,7 +2345,7 @@ private String[][] findAllMatchesFromByteBuffer(ByteBuffer buffer) { byte[] bytes = new byte[buffer.remaining()]; buffer.duplicate().get(bytes); String text = new String(bytes, StandardCharsets.UTF_8); - return RE2NativeJNI.findAllMatches(nativeHandle, text); + return jni.findAllMatches(nativeHandle, text); } /** diff --git a/libre2-core/src/main/java/com/axonops/libre2/api/RE2.java b/libre2-core/src/main/java/com/axonops/libre2/api/RE2.java index 958af58..f68dc20 100644 --- a/libre2-core/src/main/java/com/axonops/libre2/api/RE2.java +++ b/libre2-core/src/main/java/com/axonops/libre2/api/RE2.java @@ -16,8 +16,6 @@ package com.axonops.libre2.api; -import com.axonops.libre2.jni.RE2NativeJNI; - /** * Main entry point for RE2 regex operations. * diff --git a/libre2-core/src/main/java/com/axonops/libre2/jni/IRE2Native.java b/libre2-core/src/main/java/com/axonops/libre2/jni/IRE2Native.java new file mode 100644 index 0000000..d70831a --- /dev/null +++ b/libre2-core/src/main/java/com/axonops/libre2/jni/IRE2Native.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 AxonOps + * + * Licensed 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 com.axonops.libre2.jni; + +/** + * Adapter interface for RE2 JNI operations. + * Enables mocking for unit tests while maintaining production performance. + * + *

Production implementation (DirectJniAdapter) delegates directly to RE2NativeJNI. + * Test implementations can mock native calls to verify correct parameters and behavior.

+ * + *

Internal API: Not part of public API contract. Used internally by Pattern/Matcher/RE2. + * Public visibility required for cross-package access from api package.

+ */ +public interface IRE2Native { + + // Pattern lifecycle + long compile(String pattern, boolean caseSensitive); + void freePattern(long handle); + boolean patternOk(long handle); + String getError(); + String getPattern(long handle); + int numCapturingGroups(long handle); + long patternMemory(long handle); + + // Matching operations + boolean fullMatch(long handle, String text); + boolean partialMatch(long handle, String text); + boolean fullMatchDirect(long handle, long address, int length); + boolean partialMatchDirect(long handle, long address, int length); + + // Bulk operations + boolean[] fullMatchBulk(long handle, String[] texts); + boolean[] partialMatchBulk(long handle, String[] texts); + boolean[] fullMatchDirectBulk(long handle, long[] addresses, int[] lengths); + boolean[] partialMatchDirectBulk(long handle, long[] addresses, int[] lengths); + + // Capture groups + String[] extractGroups(long handle, String text); + String[][] extractGroupsBulk(long handle, String[] texts); + String[] extractGroupsDirect(long handle, long address, int length); + String[][] findAllMatches(long handle, String text); + String[][] findAllMatchesDirect(long handle, long address, int length); + String[] getNamedGroups(long handle); + + // Replace operations + String replaceFirst(long handle, String text, String replacement); + String replaceAll(long handle, String text, String replacement); + String[] replaceAllBulk(long handle, String[] texts, String replacement); + String replaceFirstDirect(long handle, long address, int length, String replacement); + String replaceAllDirect(long handle, long address, int length, String replacement); + String[] replaceAllDirectBulk(long handle, long[] addresses, int[] lengths, String replacement); + + // Utility methods + String quoteMeta(String text); + int[] programFanout(long handle); +} diff --git a/libre2-core/src/main/java/com/axonops/libre2/jni/RE2Native.java b/libre2-core/src/main/java/com/axonops/libre2/jni/RE2Native.java new file mode 100644 index 0000000..eb8de75 --- /dev/null +++ b/libre2-core/src/main/java/com/axonops/libre2/jni/RE2Native.java @@ -0,0 +1,183 @@ +/* + * Copyright 2025 AxonOps + * + * Licensed 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 com.axonops.libre2.jni; + +/** + * Production JNI adapter - delegates to package-private RE2NativeJNI. + * + *

Singleton instance used by all Pattern/Matcher/RE2 instances in production. + * Tests can inject mock JniAdapter instead.

+ * + *

Internal API: Not part of public API contract. Accessed via Pattern injection. + * Public visibility required for cross-package access from api package.

+ */ +public final class RE2Native implements IRE2Native { + + /** + * Singleton instance - used in production. + * Public so Pattern can access it from api package. + */ + public static final RE2Native INSTANCE = new RE2Native(); + + private RE2Native() { + } + + @Override + public long compile(String pattern, boolean caseSensitive) { + return RE2NativeJNI.compile(pattern, caseSensitive); + } + + @Override + public void freePattern(long handle) { + RE2NativeJNI.freePattern(handle); + } + + @Override + public boolean patternOk(long handle) { + return RE2NativeJNI.patternOk(handle); + } + + @Override + public String getError() { + return RE2NativeJNI.getError(); + } + + @Override + public String getPattern(long handle) { + return RE2NativeJNI.getPattern(handle); + } + + @Override + public int numCapturingGroups(long handle) { + return RE2NativeJNI.numCapturingGroups(handle); + } + + @Override + public long patternMemory(long handle) { + return RE2NativeJNI.patternMemory(handle); + } + + @Override + public boolean fullMatch(long handle, String text) { + return RE2NativeJNI.fullMatch(handle, text); + } + + @Override + public boolean partialMatch(long handle, String text) { + return RE2NativeJNI.partialMatch(handle, text); + } + + @Override + public boolean fullMatchDirect(long handle, long address, int length) { + return RE2NativeJNI.fullMatchDirect(handle, address, length); + } + + @Override + public boolean partialMatchDirect(long handle, long address, int length) { + return RE2NativeJNI.partialMatchDirect(handle, address, length); + } + + @Override + public boolean[] fullMatchBulk(long handle, String[] texts) { + return RE2NativeJNI.fullMatchBulk(handle, texts); + } + + @Override + public boolean[] partialMatchBulk(long handle, String[] texts) { + return RE2NativeJNI.partialMatchBulk(handle, texts); + } + + @Override + public boolean[] fullMatchDirectBulk(long handle, long[] addresses, int[] lengths) { + return RE2NativeJNI.fullMatchDirectBulk(handle, addresses, lengths); + } + + @Override + public boolean[] partialMatchDirectBulk(long handle, long[] addresses, int[] lengths) { + return RE2NativeJNI.partialMatchDirectBulk(handle, addresses, lengths); + } + + @Override + public String[] extractGroups(long handle, String text) { + return RE2NativeJNI.extractGroups(handle, text); + } + + @Override + public String[][] extractGroupsBulk(long handle, String[] texts) { + return RE2NativeJNI.extractGroupsBulk(handle, texts); + } + + @Override + public String[] extractGroupsDirect(long handle, long address, int length) { + return RE2NativeJNI.extractGroupsDirect(handle, address, length); + } + + @Override + public String[][] findAllMatches(long handle, String text) { + return RE2NativeJNI.findAllMatches(handle, text); + } + + @Override + public String[][] findAllMatchesDirect(long handle, long address, int length) { + return RE2NativeJNI.findAllMatchesDirect(handle, address, length); + } + + @Override + public String[] getNamedGroups(long handle) { + return RE2NativeJNI.getNamedGroups(handle); + } + + @Override + public String replaceFirst(long handle, String text, String replacement) { + return RE2NativeJNI.replaceFirst(handle, text, replacement); + } + + @Override + public String replaceAll(long handle, String text, String replacement) { + return RE2NativeJNI.replaceAll(handle, text, replacement); + } + + @Override + public String[] replaceAllBulk(long handle, String[] texts, String replacement) { + return RE2NativeJNI.replaceAllBulk(handle, texts, replacement); + } + + @Override + public String replaceFirstDirect(long handle, long address, int length, String replacement) { + return RE2NativeJNI.replaceFirstDirect(handle, address, length, replacement); + } + + @Override + public String replaceAllDirect(long handle, long address, int length, String replacement) { + return RE2NativeJNI.replaceAllDirect(handle, address, length, replacement); + } + + @Override + public String[] replaceAllDirectBulk(long handle, long[] addresses, int[] lengths, String replacement) { + return RE2NativeJNI.replaceAllDirectBulk(handle, addresses, lengths, replacement); + } + + @Override + public String quoteMeta(String text) { + return RE2NativeJNI.quoteMeta(text); + } + + @Override + public int[] programFanout(long handle) { + return RE2NativeJNI.programFanout(handle); + } +} diff --git a/libre2-core/src/main/java/com/axonops/libre2/jni/RE2NativeJNI.java b/libre2-core/src/main/java/com/axonops/libre2/jni/RE2NativeJNI.java index 1d622fa..2fa2f7f 100644 --- a/libre2-core/src/main/java/com/axonops/libre2/jni/RE2NativeJNI.java +++ b/libre2-core/src/main/java/com/axonops/libre2/jni/RE2NativeJNI.java @@ -22,8 +22,16 @@ *

Maps directly to the C functions in re2_jni.cpp. * All methods are native calls executing off-heap.

* - *

This class uses JNI for maximum performance, avoiding the overhead - * of JNA marshalling on every call.

+ *

PACKAGE-PRIVATE DESIGN: All methods are package-private to enforce + * abstraction. External code must use Pattern/Matcher/RE2 API. Direct JNI access + * is only available to DirectJniAdapter within this package.

+ * + *

This design enables: + *

    + *
  • Mockability - DirectJniAdapter implements JniAdapter interface
  • + *
  • Encapsulation - No direct JNI calls from API classes
  • + *
  • Testability - Tests can inject mock JniAdapter
  • + *
* *

Zero-Copy Direct Memory API

*

This class provides two categories of methods:

@@ -32,23 +40,18 @@ *
  • Direct methods (*Direct suffix) - Accept memory addresses for zero-copy operation
  • * * - *

    The Direct methods are designed for use with Chronicle Bytes or other off-heap memory - * systems that can provide stable native memory addresses via {@code addressForRead()}.

    - * *

    CRITICAL SAFETY

    *
      *
    • All long handles MUST be freed via {@link #freePattern(long)}
    • *
    • Never call methods with 0 handles (will return error/false)
    • *
    • All strings are UTF-8 encoded
    • *
    • For Direct methods: The memory at the provided address MUST remain valid - * for the duration of the call. Do NOT release the backing memory (e.g., - * Chronicle Bytes) until the method returns.
    • + * for the duration of the call. *
    * * @since 1.0.0 - * @see com.axonops.libre2.jni.RE2DirectMemory */ -public final class RE2NativeJNI { +final class RE2NativeJNI { private RE2NativeJNI() { // Utility class - prevent instantiation @@ -61,7 +64,7 @@ private RE2NativeJNI() { * @param caseSensitive true for case-sensitive, false for case-insensitive * @return native handle to compiled pattern, or 0 on error (MUST be freed) */ - public static native long compile(String pattern, boolean caseSensitive); + static native long compile(String pattern, boolean caseSensitive); /** * Frees a compiled pattern. @@ -69,7 +72,7 @@ private RE2NativeJNI() { * * @param handle native handle from compile() */ - public static native void freePattern(long handle); + static native void freePattern(long handle); /** * Tests if text fully matches the pattern. @@ -78,7 +81,7 @@ private RE2NativeJNI() { * @param text text to match (UTF-8) * @return true if matches, false if no match or error */ - public static native boolean fullMatch(long handle, String text); + static native boolean fullMatch(long handle, String text); /** * Tests if pattern matches anywhere in text. @@ -87,14 +90,14 @@ private RE2NativeJNI() { * @param text text to search (UTF-8) * @return true if matches, false if no match or error */ - public static native boolean partialMatch(long handle, String text); + static native boolean partialMatch(long handle, String text); /** * Gets the last error message. * * @return error message, or null if no error */ - public static native String getError(); + static native String getError(); /** * Gets the pattern string from a compiled pattern. @@ -102,7 +105,7 @@ private RE2NativeJNI() { * @param handle compiled pattern handle * @return pattern string, or null if invalid */ - public static native String getPattern(long handle); + static native String getPattern(long handle); /** * Gets the number of capturing groups. @@ -110,7 +113,7 @@ private RE2NativeJNI() { * @param handle compiled pattern handle * @return number of capturing groups, or -1 on error */ - public static native int numCapturingGroups(long handle); + static native int numCapturingGroups(long handle); /** * Checks if pattern is valid. @@ -118,7 +121,7 @@ private RE2NativeJNI() { * @param handle compiled pattern handle * @return true if valid, false if invalid/null */ - public static native boolean patternOk(long handle); + static native boolean patternOk(long handle); /** * Gets the native memory size of a compiled pattern. @@ -129,7 +132,7 @@ private RE2NativeJNI() { * @param handle compiled pattern handle * @return size in bytes, or 0 if handle is 0 */ - public static native long patternMemory(long handle); + static native long patternMemory(long handle); // ========== Bulk Matching Operations ========== @@ -141,7 +144,7 @@ private RE2NativeJNI() { * @param texts array of strings to match * @return boolean array (parallel to texts) indicating matches, or null on error */ - public static native boolean[] fullMatchBulk(long handle, String[] texts); + static native boolean[] fullMatchBulk(long handle, String[] texts); /** * Performs partial match on multiple strings in single JNI call. @@ -151,7 +154,7 @@ private RE2NativeJNI() { * @param texts array of strings to match * @return boolean array (parallel to texts) indicating matches, or null on error */ - public static native boolean[] partialMatchBulk(long handle, String[] texts); + static native boolean[] partialMatchBulk(long handle, String[] texts); // ========== Capture Group Operations ========== @@ -163,7 +166,7 @@ private RE2NativeJNI() { * @param text text to match * @return string array of groups, or null if no match */ - public static native String[] extractGroups(long handle, String text); + static native String[] extractGroups(long handle, String text); /** * Extracts capture groups from multiple strings in single JNI call. @@ -172,7 +175,7 @@ private RE2NativeJNI() { * @param texts array of strings to match * @return array of string arrays (groups per input), or null on error */ - public static native String[][] extractGroupsBulk(long handle, String[] texts); + static native String[][] extractGroupsBulk(long handle, String[] texts); /** * Finds all non-overlapping matches in text with capture groups. @@ -182,7 +185,7 @@ private RE2NativeJNI() { * @param text text to search * @return array of match data (flattened: [match1_groups..., match2_groups...]), or null on error */ - public static native String[][] findAllMatches(long handle, String text); + static native String[][] findAllMatches(long handle, String text); /** * Gets map of named capturing groups to their indices. @@ -191,7 +194,7 @@ private RE2NativeJNI() { * @param handle compiled pattern handle * @return flattened name-index pairs, or null if no named groups */ - public static native String[] getNamedGroups(long handle); + static native String[] getNamedGroups(long handle); // ========== Replace Operations ========== @@ -204,7 +207,7 @@ private RE2NativeJNI() { * @param replacement replacement string (supports $1, $2 backreferences) * @return text with first match replaced, or original text if no match */ - public static native String replaceFirst(long handle, String text, String replacement); + static native String replaceFirst(long handle, String text, String replacement); /** * Replaces all non-overlapping matches with replacement string. @@ -215,7 +218,7 @@ private RE2NativeJNI() { * @param replacement replacement string (supports $1, $2 backreferences) * @return text with all matches replaced, or original text if no matches */ - public static native String replaceAll(long handle, String text, String replacement); + static native String replaceAll(long handle, String text, String replacement); /** * Replaces all matches in multiple strings in single JNI call. @@ -225,7 +228,7 @@ private RE2NativeJNI() { * @param replacement replacement string (supports $1, $2 backreferences) * @return array of replaced strings (parallel to texts), or null on error */ - public static native String[] replaceAllBulk(long handle, String[] texts, String replacement); + static native String[] replaceAllBulk(long handle, String[] texts, String replacement); /** * Replaces first match using zero-copy memory access (off-heap memory). @@ -237,7 +240,7 @@ private RE2NativeJNI() { * @param replacement replacement string (supports $1, $2 backreferences) * @return text with first match replaced */ - public static native String replaceFirstDirect(long handle, long textAddress, int textLength, String replacement); + static native String replaceFirstDirect(long handle, long textAddress, int textLength, String replacement); /** * Replaces all matches using zero-copy memory access (off-heap memory). @@ -249,7 +252,7 @@ private RE2NativeJNI() { * @param replacement replacement string (supports $1, $2 backreferences) * @return text with all matches replaced */ - public static native String replaceAllDirect(long handle, long textAddress, int textLength, String replacement); + static native String replaceAllDirect(long handle, long textAddress, int textLength, String replacement); /** * Replaces all matches in multiple off-heap buffers (bulk zero-copy operation). @@ -261,7 +264,7 @@ private RE2NativeJNI() { * @param replacement replacement string (supports $1, $2 backreferences) * @return array of strings with all matches replaced (parallel to inputs) */ - public static native String[] replaceAllDirectBulk(long handle, long[] textAddresses, int[] textLengths, String replacement); + static native String[] replaceAllDirectBulk(long handle, long[] textAddresses, int[] textLengths, String replacement); // ========== Utility Operations ========== @@ -272,7 +275,7 @@ private RE2NativeJNI() { * @param text text to escape * @return escaped text safe for use in regex patterns */ - public static native String quoteMeta(String text); + static native String quoteMeta(String text); /** * Gets pattern complexity histogram (DFA branching factor). @@ -281,7 +284,7 @@ private RE2NativeJNI() { * @param handle compiled pattern handle * @return histogram array, or null on error */ - public static native int[] programFanout(long handle); + static native int[] programFanout(long handle); // ========== Zero-Copy Direct Memory Operations ========== // @@ -325,7 +328,7 @@ private RE2NativeJNI() { * @throws IllegalArgumentException if handle is 0 or textAddress is 0 * @since 1.1.0 */ - public static native boolean fullMatchDirect(long handle, long textAddress, int textLength); + static native boolean fullMatchDirect(long handle, long textAddress, int textLength); /** * Tests if pattern matches anywhere in text using direct memory access (zero-copy). @@ -356,7 +359,7 @@ private RE2NativeJNI() { * @throws IllegalArgumentException if handle is 0 or textAddress is 0 * @since 1.1.0 */ - public static native boolean partialMatchDirect(long handle, long textAddress, int textLength); + static native boolean partialMatchDirect(long handle, long textAddress, int textLength); /** * Performs full match on multiple memory regions in a single JNI call (zero-copy bulk). @@ -380,7 +383,7 @@ private RE2NativeJNI() { * @throws IllegalArgumentException if arrays are null or have different lengths * @since 1.1.0 */ - public static native boolean[] fullMatchDirectBulk(long handle, long[] textAddresses, int[] textLengths); + static native boolean[] fullMatchDirectBulk(long handle, long[] textAddresses, int[] textLengths); /** * Performs partial match on multiple memory regions in a single JNI call (zero-copy bulk). @@ -404,7 +407,7 @@ private RE2NativeJNI() { * @throws IllegalArgumentException if arrays are null or have different lengths * @since 1.1.0 */ - public static native boolean[] partialMatchDirectBulk(long handle, long[] textAddresses, int[] textLengths); + static native boolean[] partialMatchDirectBulk(long handle, long[] textAddresses, int[] textLengths); /** * Extracts capture groups from text using direct memory access (zero-copy). @@ -422,7 +425,7 @@ private RE2NativeJNI() { * @return String array where [0] = full match, [1+] = capturing groups, or null if no match * @since 1.1.0 */ - public static native String[] extractGroupsDirect(long handle, long textAddress, int textLength); + static native String[] extractGroupsDirect(long handle, long textAddress, int textLength); /** * Finds all non-overlapping matches in text using direct memory access (zero-copy). @@ -440,6 +443,6 @@ private RE2NativeJNI() { * @return array of match results with capture groups, or null if no matches * @since 1.1.0 */ - public static native String[][] findAllMatchesDirect(long handle, long textAddress, int textLength); + static native String[][] findAllMatchesDirect(long handle, long textAddress, int textLength); }