or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/io.cucumber/cucumber-expressions@19.0.x

docs

index.md
tile.json

tessl/maven-io-cucumber--cucumber-expressions

tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0

Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps

utilities.mddocs/reference/

Utilities and Extension Points

This document covers utility classes and service provider interfaces (SPI) for advanced usage and customization of the cucumber-expressions library.

Capabilities

KeyboardFriendlyDecimalFormatSymbols (Experimental)

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:

OriginalReplacementReason
U+2212 (minus sign)- (hyphen-minus)More accessible on keyboards
Period decimal → Space thousandsPeriod decimal → Comma thousandsComma is more keyboard-accessible
Comma decimal → Space thousandsComma decimal → Period thousandsPeriod 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 type

Use Cases:

  1. Number Parsing: Used internally by built-in parameter types for locale-specific number parsing
  2. Custom Number Types: Use in custom parameter types that parse numbers
  3. Input Validation: Validate user input with keyboard-accessible formats
  4. Cross-Platform Compatibility: Ensure consistent behavior across different platforms

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.

PatternCompiler (SPI)

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.AndroidPatternCompiler

Or for multiple implementations (first found will be used):

com.example.AndroidPatternCompiler
com.example.CachingPatternCompiler

The 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:

  1. Platform Compatibility: Disable unsupported regex flags on Android or other platforms
  2. Custom Regex Engines: Integrate alternative regex engines (e.g., RE2, PCRE)
  3. Performance Optimization: Add caching or other optimizations to pattern compilation
  4. Security: Add validation or sanitization before pattern compilation
  5. Monitoring: Add logging or metrics collection for pattern compilation
  6. Resource Limits: Enforce limits on pattern complexity or compilation time

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:

  1. Test Thoroughly: Custom pattern compilers affect all regex operations in the library
  2. Document Limitations: Clearly document which flags or features are unsupported
  3. Fallback Gracefully: Provide sensible defaults when unsupported features are used
  4. Performance: Consider caching compiled patterns if compilation is expensive
  5. Thread Safety: Ensure your implementation is thread-safe (ServiceLoader may invoke from multiple threads)
  6. Error Handling: Preserve PatternSyntaxException for invalid patterns
  7. Flag Preservation: Only modify flags that are actually unsupported
  8. Logging: Add appropriate logging for debugging issues
  9. Metrics: Consider collecting metrics on pattern compilation for monitoring
  10. Security: Validate patterns to prevent ReDoS or other security issues

Related Documentation

  • Expressions - Uses PatternCompiler for regex compilation
  • Parameter Types - Uses KeyboardFriendlyDecimalFormatSymbols for number parsing