tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
This document covers utility classes and service provider interfaces (SPI) for advanced usage and customization of the cucumber-expressions library.
Utility class that provides localized decimal format symbols with keyboard-friendly alternatives. This is used internally for number parsing but exposed as a public experimental API.
package io.cucumber.cucumberexpressions;
import org.apiguardian.api.API;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
/**
* A set of localized decimal symbols that can be written on a regular keyboard
* Experimental API - subject to change
* Stateless utility class with static factory method
*/
@API(status = API.Status.EXPERIMENTAL)
public final class KeyboardFriendlyDecimalFormatSymbols {
/**
* Get decimal format symbols for a locale with keyboard-friendly substitutions
* Thread-safe static factory method
*
* Substitutions made:
* - Replaces Unicode minus sign (U+2212) with hyphen-minus (-) ASCII 45
* - Uses comma (,) as thousands separator when period (.) is decimal separator
* - Uses period (.) as thousands separator when comma (,) is decimal separator
* - Ensures symbols are all typeable on standard keyboards
*
* @param locale - Locale for number formatting
* @return DecimalFormatSymbols with keyboard-friendly symbols
*/
public static DecimalFormatSymbols getInstance(Locale locale);
}Usage Example:
import io.cucumber.cucumberexpressions.KeyboardFriendlyDecimalFormatSymbols;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
// Get keyboard-friendly symbols for English locale
DecimalFormatSymbols symbols = KeyboardFriendlyDecimalFormatSymbols.getInstance(Locale.ENGLISH);
// Symbols use keyboard-accessible characters:
// - Minus sign: - (ASCII 45, hyphen-minus)
// - Decimal separator: . (period)
// - Thousands separator: , (comma)
System.out.println("Decimal separator: " + symbols.getDecimalSeparator());
// Output: .
System.out.println("Grouping separator: " + symbols.getGroupingSeparator());
// Output: ,
System.out.println("Minus sign: " + (int) symbols.getMinusSign());
// Output: 45 (ASCII code for hyphen-minus)
// French locale
DecimalFormatSymbols frenchSymbols = KeyboardFriendlyDecimalFormatSymbols.getInstance(Locale.FRENCH);
// - Decimal separator: , (comma)
// - Thousands separator: . (period)
System.out.println("French decimal: " + frenchSymbols.getDecimalSeparator());
// Output: ,
System.out.println("French grouping: " + frenchSymbols.getGroupingSeparator());
// Output: .
// German locale
DecimalFormatSymbols germanSymbols = KeyboardFriendlyDecimalFormatSymbols.getInstance(Locale.GERMAN);
// - Decimal separator: , (comma)
// - Thousands separator: . (period)Keyboard-Friendly Substitutions:
The utility makes these replacements for better keyboard accessibility:
| Original | Replacement | Reason |
|---|---|---|
| U+2212 (minus sign) | - (hyphen-minus) | More accessible on keyboards |
| Period decimal → Space thousands | Period decimal → Comma thousands | Comma is more keyboard-accessible |
| Comma decimal → Space thousands | Comma decimal → Period thousands | Period is more keyboard-accessible |
Comparison with Standard Symbols:
import java.text.DecimalFormatSymbols;
import java.util.Locale;
// Standard symbols
DecimalFormatSymbols standard = DecimalFormatSymbols.getInstance(Locale.ENGLISH);
// Keyboard-friendly symbols
DecimalFormatSymbols keyboardFriendly =
KeyboardFriendlyDecimalFormatSymbols.getInstance(Locale.ENGLISH);
// Minus sign comparison
char standardMinus = standard.getMinusSign();
char keyboardMinus = keyboardFriendly.getMinusSign();
// standardMinus might be U+2212 (Unicode minus)
// keyboardMinus is always '-' (ASCII 45)
// Both are functionally equivalent for parsing
// but keyboard-friendly version is easier to typeUse Cases:
Note: This utility is marked experimental and may change in future versions. It's primarily used internally by the number parsing system but exposed for advanced customization needs.
Service Provider Interface for custom regular expression pattern compilation. This allows clients to provide alternative pattern compilation strategies for platforms where certain Java regex features are not available (e.g., Pattern.UNICODE_CHARACTER_CLASS on Android).
package io.cucumber.cucumberexpressions;
import org.apiguardian.api.API;
import java.util.regex.Pattern;
/**
* Abstracts creation of new Pattern instances
* Service Provider Interface for custom pattern compilation
* Functional interface for pattern compilation strategies
*
* The library uses Java's ServiceLoader to discover implementations.
* If no custom implementation is provided, DefaultPatternCompiler is used.
*/
@API(status = API.Status.STABLE)
@FunctionalInterface
public interface PatternCompiler {
/**
* Compile a regular expression pattern with flags
*
* @param regexp - Regular expression string to compile
* @param flags - Additional flags (e.g., Pattern.UNICODE_CHARACTER_CLASS)
* May include: Pattern.CASE_INSENSITIVE, Pattern.MULTILINE, etc.
* @return Compiled Pattern instance
* @throws java.util.regex.PatternSyntaxException if regexp syntax is invalid
*/
Pattern compile(String regexp, int flags);
}Usage - Implementing Custom PatternCompiler:
import io.cucumber.cucumberexpressions.PatternCompiler;
import java.util.regex.Pattern;
/**
* Custom implementation for platforms with limited regex support
* Example: Android platforms that don't support UNICODE_CHARACTER_CLASS flag
*/
public class AndroidPatternCompiler implements PatternCompiler {
@Override
public Pattern compile(String regexp, int flags) {
// Remove flags not supported on Android
// Android doesn't support Pattern.UNICODE_CHARACTER_CLASS (0x100)
int supportedFlags = flags & ~Pattern.UNICODE_CHARACTER_CLASS;
return Pattern.compile(regexp, supportedFlags);
}
}
/**
* Performance-optimized implementation with pattern caching
*/
public class CachingPatternCompiler implements PatternCompiler {
private final Map<String, Pattern> cache = new ConcurrentHashMap<>();
@Override
public Pattern compile(String regexp, int flags) {
String key = regexp + ":" + flags;
return cache.computeIfAbsent(key, k -> Pattern.compile(regexp, flags));
}
}
/**
* Security-hardened implementation with validation
*/
public class SecurePatternCompiler implements PatternCompiler {
private static final int MAX_PATTERN_LENGTH = 1000;
private static final Pattern DANGEROUS_PATTERNS =
Pattern.compile(".*\\(\\?\\{.*|.*\\(\\?\\<.*");
@Override
public Pattern compile(String regexp, int flags) {
// Validate pattern length
if (regexp.length() > MAX_PATTERN_LENGTH) {
throw new IllegalArgumentException(
"Pattern too long: " + regexp.length() + " characters");
}
// Check for potentially dangerous patterns
if (DANGEROUS_PATTERNS.matcher(regexp).matches()) {
throw new IllegalArgumentException(
"Pattern contains potentially dangerous constructs");
}
return Pattern.compile(regexp, flags);
}
}Usage - Service Provider Configuration:
To use a custom PatternCompiler, create a service provider configuration file:
File: META-INF/services/io.cucumber.cucumberexpressions.PatternCompiler
Content:
com.example.AndroidPatternCompilerOr for multiple implementations (first found will be used):
com.example.AndroidPatternCompiler
com.example.CachingPatternCompilerThe library will automatically load and use your custom implementation via Java's ServiceLoader mechanism.
Default Implementation:
The library provides DefaultPatternCompiler as the default implementation:
/**
* Default pattern compiler implementation
* Used when no custom implementation is provided via ServiceLoader
*/
public class DefaultPatternCompiler implements PatternCompiler {
@Override
public Pattern compile(String regexp, int flags) {
return Pattern.compile(regexp, flags);
}
}Use Cases:
Module Configuration:
The io.cucumber.cucumberexpressions module declares:
module io.cucumber.cucumberexpressions {
// ...
uses io.cucumber.cucumberexpressions.PatternCompiler;
}This enables service loading of custom implementations.
Integration Example:
// The library automatically discovers and uses custom PatternCompiler
// No code changes needed in your application - just provide the service file
import io.cucumber.cucumberexpressions.*;
import java.util.Locale;
// Normal usage - custom PatternCompiler is used automatically
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("I have {int} cucumbers");
// The expression uses your custom PatternCompiler internally
// All regex compilation goes through your implementation
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");Testing Custom PatternCompiler:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class PatternCompilerTest {
@Test
public void testAndroidPatternCompiler() {
PatternCompiler compiler = new AndroidPatternCompiler();
// Test that unsupported flags are removed
Pattern pattern = compiler.compile("\\d+", Pattern.UNICODE_CHARACTER_CLASS);
assertNotNull(pattern);
// Test normal compilation still works
Pattern pattern2 = compiler.compile("[a-z]+", 0);
assertTrue(pattern2.matcher("hello").matches());
}
@Test
public void testCachingPatternCompiler() {
CachingPatternCompiler compiler = new CachingPatternCompiler();
// Compile same pattern twice
Pattern p1 = compiler.compile("\\d+", 0);
Pattern p2 = compiler.compile("\\d+", 0);
// Should return cached instance
assertSame(p1, p2);
}
@Test
public void testSecurePatternCompiler() {
SecurePatternCompiler compiler = new SecurePatternCompiler();
// Test that dangerous patterns are rejected
assertThrows(
IllegalArgumentException.class,
() -> compiler.compile("(?{malicious_code})", 0)
);
// Test that long patterns are rejected
String longPattern = "a".repeat(2000);
assertThrows(
IllegalArgumentException.class,
() -> compiler.compile(longPattern, 0)
);
// Test that normal patterns work
Pattern safe = compiler.compile("\\d+", 0);
assertTrue(safe.matcher("123").matches());
}
}Advanced Usage - Delegating Pattern Compiler:
/**
* Base class for pattern compilers that delegate to default behavior
* with additional processing
*/
public abstract class DelegatingPatternCompiler implements PatternCompiler {
protected final PatternCompiler delegate;
public DelegatingPatternCompiler() {
this.delegate = new DefaultPatternCompiler();
}
public DelegatingPatternCompiler(PatternCompiler delegate) {
this.delegate = delegate;
}
@Override
public Pattern compile(String regexp, int flags) {
// Pre-process
beforeCompile(regexp, flags);
// Delegate
Pattern pattern = delegate.compile(regexp, flags);
// Post-process
afterCompile(pattern, regexp, flags);
return pattern;
}
protected void beforeCompile(String regexp, int flags) {
// Override to add pre-processing
}
protected void afterCompile(Pattern pattern, String regexp, int flags) {
// Override to add post-processing
}
}
/**
* Example: Logging pattern compiler
*/
public class LoggingPatternCompiler extends DelegatingPatternCompiler {
private static final Logger LOG = LoggerFactory.getLogger(LoggingPatternCompiler.class);
@Override
protected void beforeCompile(String regexp, int flags) {
LOG.debug("Compiling pattern: {} with flags: {}", regexp, flags);
}
@Override
protected void afterCompile(Pattern pattern, String regexp, int flags) {
LOG.debug("Compiled pattern: {}", pattern.pattern());
}
}
/**
* Example: Metrics-collecting pattern compiler
*/
public class MetricsPatternCompiler extends DelegatingPatternCompiler {
private final AtomicLong compilationCount = new AtomicLong();
private final AtomicLong compilationTimeNs = new AtomicLong();
@Override
public Pattern compile(String regexp, int flags) {
long startNs = System.nanoTime();
try {
return super.compile(regexp, flags);
} finally {
long durationNs = System.nanoTime() - startNs;
compilationCount.incrementAndGet();
compilationTimeNs.addAndGet(durationNs);
}
}
public long getCompilationCount() {
return compilationCount.get();
}
public long getAverageCompilationTimeNs() {
long count = compilationCount.get();
return count > 0 ? compilationTimeNs.get() / count : 0;
}
}Best Practices: