Skip to content

Commit 25c9ff7

Browse files
committed
refactor(core)!: decouple regex compilation to support pluggable engines
Transform Sift into an engine-agnostic framework by introducing the `SiftEngine` SPI. This architectural shift allows developers to swap the default JDK regex engine with alternative implementations (e.g., RE2J for ReDoS protection) without breaking the default zero-dependency behavior. As a direct consequence of this decoupling: - Implemented an internal feature-tracking system (Metadata) to enable engines to fail-fast on unsupported constructs. - Fixed a Double-Checked Locking race condition in `BaseSiftPattern` caching. - Standardized null-safety behavior (returning null) for replacement convenience methods. BREAKING CHANGE: The terminal method `.sieve()` now returns a `SiftCompiledPattern` abstraction instead of a `java.util.regex.Pattern`.
1 parent 655d6ed commit 25c9ff7

23 files changed

Lines changed: 1369 additions & 399 deletions
Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
1+
/*
2+
* Copyright 2026 Mirko Dimartino
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package com.mirkoddd.sift.core;
217

318
import com.mirkoddd.sift.core.dsl.SiftContext;
419
import com.mirkoddd.sift.core.dsl.SiftPattern;
20+
import com.mirkoddd.sift.core.engine.RegexFeature;
521

22+
import java.util.Collections;
23+
import java.util.EnumSet;
624
import java.util.Set;
725

826
/**
927
* Decorator that wraps a pattern in an atomic group (?>...).
1028
*/
11-
class AtomicPattern<Ctx extends SiftContext> extends BaseSiftPattern<Ctx> implements PatternMetadata {
29+
class AtomicPattern<Ctx extends SiftContext> extends BaseSiftPattern<Ctx> {
30+
1231
private final SiftPattern<Ctx> inner;
1332

1433
AtomicPattern(SiftPattern<Ctx> inner) {
@@ -17,24 +36,33 @@ class AtomicPattern<Ctx extends SiftContext> extends BaseSiftPattern<Ctx> implem
1736

1837
@Override
1938
protected String buildRegex() {
20-
final String atomicOpen = "(?>";
21-
final String atomicClose = ")";
22-
return atomicOpen + inner.shake() + atomicClose;
39+
return "(?>" + inner.shake() + ")";
40+
}
41+
42+
@Override
43+
protected Set<RegexFeature> buildFeatures() {
44+
Set<RegexFeature> features = EnumSet.of(RegexFeature.ATOMIC_GROUP);
45+
46+
if (inner instanceof PatternMetadata) {
47+
features.addAll(((PatternMetadata) inner).getInternalUsedFeatures());
48+
}
49+
50+
return Collections.unmodifiableSet(features);
2351
}
2452

2553
@Override
2654
public Set<String> getInternalRegisteredGroups() {
2755
if (inner instanceof PatternMetadata) {
2856
return ((PatternMetadata) inner).getInternalRegisteredGroups();
2957
}
30-
return java.util.Collections.emptySet();
58+
return Collections.emptySet();
3159
}
3260

3361
@Override
3462
public Set<String> getInternalRequiredBackreferences() {
3563
if (inner instanceof PatternMetadata) {
3664
return ((PatternMetadata) inner).getInternalRequiredBackreferences();
3765
}
38-
return java.util.Collections.emptySet();
66+
return Collections.emptySet();
3967
}
4068
}

sift-core/src/main/java/com/mirkoddd/sift/core/BaseSiftPattern.java

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
1+
/*
2+
* Copyright 2026 Mirko Dimartino
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package com.mirkoddd.sift.core;
217

318
import com.mirkoddd.sift.core.dsl.SiftContext;
419
import com.mirkoddd.sift.core.dsl.SiftPattern;
20+
import com.mirkoddd.sift.core.engine.RegexFeature;
21+
import com.mirkoddd.sift.core.engine.SiftCompiledPattern;
22+
import com.mirkoddd.sift.core.engine.SiftEngine;
23+
24+
import java.util.Collections;
525
import java.util.Objects;
6-
import java.util.regex.Pattern;
26+
import java.util.Set;
727

828
/**
929
* Internal foundation for all Sift patterns.
1030
* <p>
1131
* This class centralizes the <b>Double-Checked Locking</b> logic for thread-safe caching
12-
* of both the regex string and the compiled Pattern. It also implements the
13-
* com.mirkoddd.sift.core.dsl.SiftInternalSealer interface to prevent unauthorized
14-
* external implementations.
32+
* of the generated regex string and its associated structural features.
33+
* <p>
34+
* By completely decoupling the string generation from the runtime execution engine,
35+
* this core class implements the Dependency Inversion Principle, allowing Sift
36+
* to act as a framework-agnostic regex builder.
37+
*
38+
* @param <Ctx> The specific context type for state machine validation.
1539
*/
16-
abstract class BaseSiftPattern<Ctx extends SiftContext> implements SiftPattern<Ctx> {
40+
abstract class BaseSiftPattern<Ctx extends SiftContext> implements SiftPattern<Ctx>, PatternMetadata {
1741

1842
private volatile String cachedRegex = null;
19-
private volatile Pattern cachedPattern = null;
43+
private volatile Set<RegexFeature> cachedFeatures = null;
2044

2145
@Override
2246
public final Object ___internal_lock___() {
@@ -28,30 +52,62 @@ public final String shake() {
2852
if (cachedRegex == null) {
2953
synchronized (this) {
3054
if (cachedRegex == null) {
31-
String raw = buildRegex();
32-
try {
33-
cachedPattern = Pattern.compile(raw);
34-
cachedRegex = raw;
35-
} catch (java.util.regex.PatternSyntaxException e) {
36-
throw new IllegalStateException("Sift generated an invalid regex pattern: " + raw, e);
37-
}
55+
Set<RegexFeature> computedFeatures = Collections.unmodifiableSet(buildFeatures());
56+
String computedRegex = buildRegex();
57+
this.cachedFeatures = computedFeatures;
58+
this.cachedRegex = computedRegex;
3859
}
3960
}
4061
}
4162
return cachedRegex;
4263
}
4364

4465
@Override
45-
public final Pattern sieve() {
66+
public final SiftCompiledPattern sieveWith(SiftEngine engine) {
4667
shake();
47-
return cachedPattern;
68+
return engine.compile(cachedRegex, cachedFeatures);
69+
}
70+
71+
@Override
72+
public Set<String> getInternalRegisteredGroups() {
73+
// Default implementation: empty.
74+
// Overridden by complex builders (like SiftConnector) that manage state.
75+
return Collections.emptySet();
76+
}
77+
78+
@Override
79+
public Set<String> getInternalRequiredBackreferences() {
80+
// Default implementation: empty.
81+
// Overridden by complex builders.
82+
return Collections.emptySet();
83+
}
84+
85+
@Override
86+
public Set<RegexFeature> getInternalUsedFeatures() {
87+
shake();
88+
return buildFeatures();
4889
}
4990

5091
/**
51-
* Implemented by subclasses to define the specific regex logic.
92+
* Implemented by subclasses to define the specific regex string generation logic.
93+
*
94+
* @return The raw regular expression string.
5295
*/
5396
protected abstract String buildRegex();
5497

98+
/**
99+
* Implemented by subclasses to report advanced features (e.g., lookarounds, backreferences)
100+
* used during the pattern assembly.
101+
* <p>
102+
* The default implementation returns an empty set. Complex pattern builders should
103+
* override this method to accurately reflect their structural requirements.
104+
*
105+
* @return A set of features used by this specific pattern.
106+
*/
107+
protected Set<RegexFeature> buildFeatures() {
108+
return Collections.emptySet();
109+
}
110+
55111
@Override
56112
public final SiftPattern<Ctx> preventBacktracking() {
57113
return new AtomicPattern<>(this);

sift-core/src/main/java/com/mirkoddd/sift/core/PatternAssembler.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717

1818
import com.mirkoddd.sift.core.dsl.Composable;
1919
import com.mirkoddd.sift.core.dsl.SiftPattern;
20+
import com.mirkoddd.sift.core.engine.RegexFeature;
2021

2122
import java.util.Collections;
23+
import java.util.EnumSet;
2224
import java.util.HashSet;
2325
import java.util.Set;
2426

2527
/**
2628
* Internal Buffer and String manipulator for Regex generation.
27-
* Handles the low-level string concatenation, character escaping, and class flushing.
29+
* Handles the low-level string concatenation, character escaping, class flushing,
30+
* and tracking of advanced regex features for cross-engine compatibility.
2831
*/
2932
class PatternAssembler {
3033

@@ -46,6 +49,7 @@ private enum QuantifierModifier {
4649
private final Set<String> registeredGroups = new HashSet<>();
4750
private final Set<String> requiredBackreferences = new HashSet<>();
4851
private boolean containsAbsoluteAnchor = false;
52+
private final EnumSet<RegexFeature> usedFeatures = EnumSet.noneOf(RegexFeature.class);
4953

5054
PatternAssembler() {
5155
}
@@ -56,6 +60,7 @@ private enum QuantifierModifier {
5660
mainPattern.append(flag.getSymbol());
5761
}
5862
mainPattern.append(RegexSyntax.GROUP_CLOSE);
63+
registerFeature(RegexFeature.INLINE_FLAGS);
5964
}
6065

6166
Set<String> getRegisteredGroups() {
@@ -70,6 +75,14 @@ public boolean isContainsAbsoluteAnchor() {
7075
return containsAbsoluteAnchor;
7176
}
7277

78+
Set<RegexFeature> getUsedFeatures() {
79+
return Collections.unmodifiableSet(usedFeatures);
80+
}
81+
82+
void registerFeature(RegexFeature feature) {
83+
this.usedFeatures.add(feature);
84+
}
85+
7386
void setQuantifier(String quantifier) {
7487
this.currentQuantifier = quantifier;
7588
}
@@ -125,6 +138,8 @@ void addNamedCapture(NamedCapture group) {
125138

126139
extractAndCheckGroupsAndRequirements(group.getPattern(), group.getName());
127140

141+
registerFeature(RegexFeature.NAMED_CAPTURE);
142+
128143
mainPattern.append(RegexSyntax.NAMED_GROUP_OPEN)
129144
.append(group.getName())
130145
.append(RegexSyntax.NAMED_GROUP_NAME_CLOSE)
@@ -135,6 +150,8 @@ void addNamedCapture(NamedCapture group) {
135150
void addBackreference(NamedCapture group) {
136151
requiredBackreferences.add(group.getName());
137152

153+
registerFeature(RegexFeature.BACKREFERENCE);
154+
138155
mainPattern.append(RegexSyntax.NAMED_BACKREFERENCE_OPEN)
139156
.append(group.getName())
140157
.append(RegexSyntax.NAMED_BACKREFERENCE_CLOSE);
@@ -214,6 +231,8 @@ private void extractAndCheckGroupsAndRequirements(SiftPattern<?> pattern, String
214231
}
215232

216233
requiredBackreferences.addAll(getIncomingBackreferences(pattern));
234+
235+
usedFeatures.addAll(getIncomingFeatures(pattern));
217236
}
218237

219238
private static Set<String> getIncomingGroups(SiftPattern<?> pattern) {
@@ -230,6 +249,13 @@ private static Set<String> getIncomingBackreferences(SiftPattern<?> pattern) {
230249
return Collections.emptySet();
231250
}
232251

252+
private static Set<RegexFeature> getIncomingFeatures(SiftPattern<?> pattern) {
253+
if (pattern instanceof PatternMetadata) {
254+
return ((PatternMetadata) pattern).getInternalUsedFeatures();
255+
}
256+
return Collections.emptySet();
257+
}
258+
233259
void addWordBoundary() {
234260
flush();
235261
mainPattern.append(RegexSyntax.WORD_BOUNDARY);
@@ -308,6 +334,7 @@ PatternAssembler copy() {
308334
clone.registeredGroups.addAll(this.registeredGroups);
309335
clone.requiredBackreferences.addAll(this.requiredBackreferences);
310336
clone.containsAbsoluteAnchor = this.containsAbsoluteAnchor;
337+
clone.usedFeatures.addAll(this.usedFeatures);
311338
return clone;
312339
}
313340

sift-core/src/main/java/com/mirkoddd/sift/core/PatternMetadata.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.mirkoddd.sift.core;
22

3+
import com.mirkoddd.sift.core.engine.RegexFeature;
4+
35
import java.util.Set;
46

57
/**
@@ -9,4 +11,5 @@
911
interface PatternMetadata {
1012
Set<String> getInternalRegisteredGroups();
1113
Set<String> getInternalRequiredBackreferences();
14+
Set<RegexFeature> getInternalUsedFeatures();
1215
}

sift-core/src/main/java/com/mirkoddd/sift/core/Sift.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.mirkoddd.sift.core;
1717

1818
import com.mirkoddd.sift.core.dsl.*;
19+
import com.mirkoddd.sift.core.engine.RegexFeature;
1920

2021
import java.util.Objects;
2122

@@ -111,6 +112,7 @@ public static Connector<Fragment> fromWordBoundary() {
111112
*/
112113
public static Quantifier<Root> fromPreviousMatchEnd() {
113114
PatternAssembler assembler = new PatternAssembler();
115+
assembler.registerFeature(RegexFeature.PREVIOUS_MATCH_ANCHOR);
114116
assembler.addAnchor(RegexSyntax.PREVIOUS_MATCH_END);
115117
return new SiftQuantifier<>(assembler);
116118
}
@@ -227,6 +229,7 @@ public Connector<Root> fromWordBoundary() {
227229
*/
228230
public Quantifier<Root> fromPreviousMatchEnd() {
229231
PatternAssembler assembler = new PatternAssembler(flags);
232+
assembler.registerFeature(RegexFeature.PREVIOUS_MATCH_ANCHOR);
230233
assembler.addAnchor(RegexSyntax.PREVIOUS_MATCH_END);
231234
return new SiftQuantifier<>(assembler);
232235
}

sift-core/src/main/java/com/mirkoddd/sift/core/SiftConnector.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.mirkoddd.sift.core;
1717

1818
import com.mirkoddd.sift.core.dsl.*;
19+
import com.mirkoddd.sift.core.engine.RegexFeature;
1920

2021
import java.util.Objects;
2122
import java.util.Set;
@@ -74,6 +75,7 @@ public Connector<Ctx> followedByAssertion(SiftPattern<Assertion> assertion) {
7475
Objects.requireNonNull(assertion, "Assertion cannot be null");
7576

7677
PatternAssembler next = assembler.copy();
78+
next.registerFeature(RegexFeature.LOOKAHEAD);
7779
next.addPattern(assertion);
7880
return new SiftConnector<>(next);
7981
}
@@ -113,6 +115,7 @@ public Connector<Ctx> precededByAssertion(SiftPattern<Assertion> assertion) {
113115
Objects.requireNonNull(assertion, "Assertion cannot be null");
114116

115117
PatternAssembler next = assembler.copy();
118+
next.registerFeature(RegexFeature.LOOKBEHIND);
116119
next.prependPattern(assertion);
117120
return new SiftConnector<>(next);
118121
}
@@ -176,4 +179,9 @@ public Set<String> getInternalRegisteredGroups() {
176179
public Set<String> getInternalRequiredBackreferences() {
177180
return assembler.getRequiredBackreferences();
178181
}
182+
183+
@Override
184+
public Set<RegexFeature> getInternalUsedFeatures() {
185+
return assembler.getUsedFeatures();
186+
}
179187
}

0 commit comments

Comments
 (0)