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

expressions.mddocs/reference/

Expression System

The expression system provides core pattern matching functionality, supporting both Cucumber Expression syntax and Regular Expressions. The ExpressionFactory automatically determines which expression type to create based on string format using simple heuristics.

Thread Safety

  • Expression instances: Immutable and thread-safe after creation. Safe for concurrent match operations.
  • ExpressionFactory: Thread-safe for reading after construction with a fully configured registry.

Capabilities

Expression Interface

Core interface for matching text against patterns and extracting typed arguments.

package io.cucumber.cucumberexpressions;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

/**
 * Core interface for matching text against patterns
 * Implementations are immutable and thread-safe
 */
public interface Expression {
    /**
     * Match text against the expression and extract arguments
     * Thread-safe operation - safe for concurrent calls on same instance
     * 
     * @param text - Text to match
     * @param typeHints - Optional type hints for argument conversion (used with anonymous {} parameters)
     * @return Optional containing list of matched arguments, or empty if no match
     */
    Optional<List<Argument<?>>> match(String text, Type... typeHints);
    
    /**
     * Get the compiled regular expression pattern
     * @return Pattern representing the expression
     */
    Pattern getRegexp();
    
    /**
     * Get the source expression string
     * @return Original expression string
     */
    String getSource();
}

Implementations:

  • CucumberExpression - Cucumber Expression syntax with parameter types (e.g., "I have {int} cucumbers")
  • RegularExpression - Standard Java regex with parameter type extraction (e.g., "^I have (\\d+) cucumbers$")

Both implementations are package-private and should be created via ExpressionFactory.

ExpressionFactory

Factory class that creates Expression instances from strings using heuristics to determine the appropriate type.

package io.cucumber.cucumberexpressions;

/**
 * Creates a CucumberExpression or RegularExpression from a String
 * using heuristics. This is particularly useful for languages that don't have a
 * literal syntax for regular expressions.
 *
 * A string that starts with ^ and/or ends with $ is considered a regular expression.
 * Everything else is considered a Cucumber expression.
 * 
 * Thread-safe for reading after construction with configured registry.
 */
public final class ExpressionFactory {
    /**
     * Create factory with parameter type registry
     * The registry should be fully configured before creating the factory
     * 
     * @param parameterTypeRegistry - Registry containing parameter types
     */
    public ExpressionFactory(ParameterTypeRegistry parameterTypeRegistry);
    
    /**
     * Create an Expression from a string
     * Heuristics:
     * - Strings starting with ^ or ending with $ become RegularExpression
     * - All other strings become CucumberExpression
     * 
     * Thread-safe operation
     *
     * @param expressionString - Expression string to parse
     * @return Expression instance (CucumberExpression or RegularExpression)
     * @throws CucumberExpressionException for invalid expression syntax
     * @throws UndefinedParameterTypeException if expression references undefined parameter type
     * @throws PatternSyntaxException if regular expression syntax is invalid
     */
    public Expression createExpression(String expressionString);
}

Usage Examples:

import io.cucumber.cucumberexpressions.*;
import java.util.Locale;
import java.util.List;
import java.util.Optional;

// Setup
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory factory = new ExpressionFactory(registry);

// Cucumber Expression (default)
Expression cucumberExpr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match1 = cucumberExpr.match("I have 42 cucumbers");
// match1 contains [Argument(42)]

// Regular Expression (with anchors)
Expression regexExpr = factory.createExpression("^I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match2 = regexExpr.match("I have 42 cucumbers");
// match2 contains [Argument(Integer: 42)] - automatically typed based on built-in parameter types

// Empty string creates Cucumber Expression
Expression emptyExpr = factory.createExpression("");
Optional<List<Argument<?>>> match3 = emptyExpr.match("");
// match3 is present with empty argument list

// Expression with no parameters
Expression noParamsExpr = factory.createExpression("Click the button");
Optional<List<Argument<?>>> match4 = noParamsExpr.match("Click the button");
// match4 is present with empty argument list

Cucumber Expression Syntax

Cucumber Expressions use a simpler, more readable syntax compared to regular expressions:

Built-in Parameter Types:

TypeSyntaxDescriptionJava TypeExample Match
Integer{int}Matches integersInteger42, -19
Byte{byte}Matches integers, converts to byteByte127, -128
Short{short}Matches integers, converts to shortShort1000, -1000
Long{long}Matches integers, converts to longLong1234567890
Float{float}Matches floatsFloat3.6, -9.2
Double{double}Matches floats, converts to doubleDouble3.14159
BigInteger{biginteger}Matches integers, converts to BigIntegerjava.math.BigInteger999999999999
BigDecimal{bigdecimal}Matches decimals, converts to BigDecimaljava.math.BigDecimal99.99
Word{word}Matches words without whitespaceStringhello, user123
String{string}Matches quoted stringsString"hello world", 'foo'
Anonymous{}Matches anythingObjectAny text

Number Format Details:

  • Integer types match: -?\d+ (optional minus, digits)
  • Float types match locale-specific formats with thousands separators
  • Numbers use locale-specific decimal and thousands separators (see ParameterTypeRegistry locale)

Optional Text:

Use parentheses for optional text:

I have {int} cucumber(s)

Matches both:

  • "I have 1 cucumber"
  • "I have 42 cucumbers"

Important: The entire parenthesized section is optional, not just the contents.

Alternative Text:

Use forward slash for alternatives:

I have {int} cucumber(s) in my belly/stomach

Matches both:

  • "I have 42 cucumbers in my belly"
  • "I have 42 cucumbers in my stomach"

Nested Alternatives:

I have {int} cucumber(s) in my belly/stomach/tummy

Matches:

  • "I have 42 cucumbers in my belly"
  • "I have 42 cucumbers in my stomach"
  • "I have 42 cucumbers in my tummy"

Combining Optional and Alternative:

I (am/was) {int} years old

Matches:

  • "I am 25 years old"
  • "I was 25 years old"
  • "I 25 years old" (entire parenthesized section optional)

Escaping Special Characters:

Escape special characters with backslash:

I have {int} \\{what} cucumber(s)

Matches: "I have 42 {what} cucumbers"

Special characters that need escaping:

  • \ (backslash)
  • { and } (braces)
  • ( and ) (parentheses)
  • / (forward slash, when used for alternatives)

Regular Expression Support

Regular expressions are created when the expression string starts with ^ or ends with $:

// These create RegularExpression:
Expression expr1 = factory.createExpression("^I have (\\d+) cucumbers$");
Expression expr2 = factory.createExpression("^I have (\\d+) cucumbers");
Expression expr3 = factory.createExpression("I have (\\d+) cucumbers$");

// This creates CucumberExpression:
Expression expr4 = factory.createExpression("I have {int} cucumbers");

// This creates CucumberExpression (anchors not at start/end):
Expression expr5 = factory.createExpression("I have {int}$ cucumbers");
// The $ is treated as literal text character

Automatic Type Transformation for Regex:

When using regular expressions with capture groups, the library automatically transforms captured text based on matching built-in parameter types. This means that regex capture groups don't always return String values - they are typed based on the pattern.

ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory factory = new ExpressionFactory(registry);

// Regex with \d+ pattern matches the built-in {int} parameter type
Expression expr1 = factory.createExpression("^I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match1 = expr1.match("I have 42 cucumbers");

if (match1.isPresent()) {
    Object value = match1.get().get(0).getValue();
    // value is Integer 42, NOT String "42"
    // The \d+ pattern matches the built-in {int} type, triggering automatic transformation
    Integer count = (Integer) value;
}

// Custom parameter types also work with regex
ParameterType<String> colorType = new ParameterType<>(
    "color",
    "red|blue|green",
    String.class,
    (String s) -> s.toUpperCase()
);
registry.defineParameterType(colorType);

// Use in regular expression
Expression expr2 = factory.createExpression("^I have a (red|blue|green) ball$");
Optional<List<Argument<?>>> match2 = expr2.match("I have a red ball");

if (match2.isPresent()) {
    String color = (String) match2.get().get(0).getValue();
    // color = "RED" (transformed by the color parameter type)
}

Important Notes on Regex Type Transformation:

  • Capture groups that match built-in parameter type patterns (like \d+ for integers) are automatically transformed
  • The transformation uses the parameter type's transformer function
  • If multiple parameter types match, the one marked with preferForRegexpMatch=true is used
  • If no parameter type matches, the value remains as a String
  • Only the content of capture groups is transformed; non-captured groups return no arguments

Ambiguity in Regex:

When multiple parameter types match the same regex pattern, use preferForRegexpMatch:

ParameterType<Integer> preferredType = new ParameterType<>(
    "myint",
    "\\d+",
    Integer.class,
    Integer::parseInt,
    true,  // useForSnippets
    true   // preferForRegexpMatch - marks as preferred
);

Expression Properties

Both CucumberExpression and RegularExpression provide:

Expression expr = factory.createExpression("I have {int} cucumbers");

// Get source string
String source = expr.getSource();
// "I have {int} cucumbers"

// Get compiled pattern
Pattern pattern = expr.getRegexp();
// Pattern representing the expression (internal regex representation)

// Pattern is useful for:
// - Debugging expression structure
// - Understanding generated regex
// - Integration with regex tools

System.out.println(pattern.pattern());
// Prints the internal regex pattern used for matching

Localized Number Parsing

Number parameter types support localized formatting via the ParameterTypeRegistry's locale:

// English locale: comma as thousands separator, period as decimal
ParameterTypeRegistry englishRegistry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory englishFactory = new ExpressionFactory(englishRegistry);
Expression englishExpr = englishFactory.createExpression("I have {float} dollars");
Optional<List<Argument<?>>> match1 = englishExpr.match("I have 1,234.56 dollars");
// Parses as 1234.56f

// French locale: period as thousands separator, comma as decimal
ParameterTypeRegistry frenchRegistry = new ParameterTypeRegistry(Locale.FRENCH);
ExpressionFactory frenchFactory = new ExpressionFactory(frenchRegistry);
Expression frenchExpr = frenchFactory.createExpression("I have {float} euros");
Optional<List<Argument<?>>> match2 = frenchExpr.match("I have 1.234,56 euros");
// Parses as 1234.56f

// German locale
ParameterTypeRegistry germanRegistry = new ParameterTypeRegistry(Locale.GERMAN);
ExpressionFactory germanFactory = new ExpressionFactory(germanRegistry);
Expression germanExpr = germanFactory.createExpression("Preis ist {float} Euro");
Optional<List<Argument<?>>> match3 = germanExpr.match("Preis ist 1.234,56 Euro");
// Parses as 1234.56f

Keyboard-Friendly Substitutions:

The library uses keyboard-friendly symbols for better accessibility:

  • Minus sign is always hyphen-minus - (ASCII 45), not Unicode minus sign (U+2212)
  • If decimal separator is period ., thousands separator is comma ,
  • If decimal separator is comma ,, thousands separator is period .
  • Scientific notation supported with appropriate exponent separator (e or E)

Scientific Notation:

Expression expr = factory.createExpression("Value is {double}");
Optional<List<Argument<?>>> match1 = expr.match("Value is 1.23e5");
// Parses as 123000.0

Optional<List<Argument<?>>> match2 = expr.match("Value is 1.23E-5");
// Parses as 0.0000123

Type Hints

The match method accepts optional type hints to assist with type conversion:

/**
 * Match text with optional type hints
 */
Optional<List<Argument<?>>> match(String text, Type... typeHints);

Type hints help the transformer system when converting anonymous parameters or when multiple type conversions are possible.

Expression expr = factory.createExpression("I have {} items");

// Without type hint - uses default transformer
Optional<List<Argument<?>>> match1 = expr.match("I have 42 items");
// Returns Argument with value as String "42" (default behavior)

// With type hint - explicit conversion
Optional<List<Argument<?>>> match2 = expr.match("I have 42 items", Integer.class);
// Returns Argument with value as Integer 42 (if default transformer supports it)

// Multiple anonymous parameters with type hints
Expression expr2 = factory.createExpression("Transfer {} from {} to {}");
Optional<List<Argument<?>>> match3 = expr2.match(
    "Transfer 100 from Alice to Bob",
    Integer.class, String.class, String.class
);
// First argument: Integer 100
// Second argument: String "Alice"
// Third argument: String "Bob"

Type Hint Ordering:

Type hints are matched to anonymous parameters in order of appearance:

Expression expr = factory.createExpression("Value {} and {} and {}");
Optional<List<Argument<?>>> match = expr.match(
    "Value 42 and hello and 3.14",
    Integer.class, String.class, Double.class
);
// Argument 0: Integer 42
// Argument 1: String "hello"
// Argument 2: Double 3.14

Error Handling

CucumberExpressionException:

Thrown for various syntax errors in Cucumber Expressions.

Note: Anchors (^ and $) in Cucumber Expressions are treated as literal text characters rather than throwing exceptions. If you need regex anchors, use Regular Expression mode by placing anchors at the start (^) or end ($) of the expression:

// Anchors treated as literal text in Cucumber Expression
Expression expr1 = factory.createExpression("I have {int} cucumbers^");
// Matches text ending with literal "^" character: "I have 42 cucumbers^"

// To use regex anchors, create a Regular Expression
Expression expr2 = factory.createExpression("^I have {int} cucumbers$");
// Creates RegularExpression with proper anchors (must match entire string)

UndefinedParameterTypeException:

Thrown when an expression references a parameter type that hasn't been registered:

try {
    Expression expr = factory.createExpression("I have a {color} ball");
    // Throws: UndefinedParameterTypeException if "color" not registered
} catch (UndefinedParameterTypeException e) {
    String missingType = e.getUndefinedParameterTypeName();
    // missingType = "color"
    
    // Register the missing type and retry
    ParameterType<Color> colorType = new ParameterType<>(
        missingType,
        "red|blue|green",
        Color.class,
        Color::new
    );
    registry.defineParameterType(colorType);
    
    // Now create expression again
    Expression expr = factory.createExpression("I have a {color} ball");
}

PatternSyntaxException:

Thrown when regular expression syntax is invalid:

import java.util.regex.PatternSyntaxException;

try {
    Expression expr = factory.createExpression("^I have (\\d+ cucumbers$");
    // Missing closing parenthesis - throws PatternSyntaxException
} catch (PatternSyntaxException e) {
    System.err.println("Regex syntax error: " + e.getMessage());
    System.err.println("Error at index: " + e.getIndex());
    System.err.println("Description: " + e.getDescription());
    // Handle regex syntax error
}

AmbiguousParameterTypeException:

Thrown when multiple parameter types match in a regular expression:

// Register two types with overlapping patterns
ParameterType<Integer> type1 = new ParameterType<>(
    "count", "\\d+", Integer.class, Integer::parseInt
);
ParameterType<Long> type2 = new ParameterType<>(
    "longcount", "\\d+", Long.class, Long::parseLong
);
registry.defineParameterType(type1);
registry.defineParameterType(type2);

try {
    Expression expr = factory.createExpression("^Count: (\\d+)$");
    // Throws: AmbiguousParameterTypeException
} catch (AmbiguousParameterTypeException e) {
    // Get conflicting types
    SortedSet<ParameterType<?>> conflicts = e.getParameterTypes();
    
    // Get suggestions
    List<GeneratedExpression> suggestions = e.getGeneratedExpressions();
    
    // Handle ambiguity...
}

Performance Considerations

Expression Creation:

  • Expression creation involves regex compilation, which is moderately expensive
  • Compilation time: O(pattern length)
  • Memory: Each expression stores a compiled Pattern (typically < 1KB)

Reuse Pattern:

// GOOD: Create once, reuse many times
Expression expr = factory.createExpression("I have {int} cucumbers");
for (String text : manyTexts) {
    Optional<List<Argument<?>>> match = expr.match(text);
    // Fast matching operation
}

// BAD: Creating expression in loop
for (String text : manyTexts) {
    Expression expr = factory.createExpression("I have {int} cucumbers");
    Optional<List<Argument<?>>> match = expr.match(text);
    // Wastes time on repeated compilation
}

Caching Expressions:

public class ExpressionCache {
    private final ExpressionFactory factory;
    private final Map<String, Expression> cache = new ConcurrentHashMap<>();
    
    public ExpressionCache(ExpressionFactory factory) {
        this.factory = factory;
    }
    
    public Expression getExpression(String expressionString) {
        return cache.computeIfAbsent(expressionString, factory::createExpression);
    }
}

Matching Performance:

  • Match operations are generally fast: O(text length)
  • Complex patterns with many alternations may be slower
  • Optional sections add minimal overhead
  • Parameter transformation adds small overhead (typically < 1µs per parameter)

Edge Cases and Special Scenarios

Empty Expressions:

Expression expr = factory.createExpression("");
Optional<List<Argument<?>>> match1 = expr.match("");
// match1.isPresent() = true, arguments = []

Optional<List<Argument<?>>> match2 = expr.match("any text");
// match2.isPresent() = false

Whitespace Handling:

// Whitespace in expression is significant
Expression expr1 = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match1 = expr1.match("I have 42 cucumbers");
// Matches

Optional<List<Argument<?>>> match2 = expr1.match("I have  42  cucumbers");
// Does NOT match (extra spaces)

// To match variable whitespace, use regex
Expression expr2 = factory.createExpression("^I\\s+have\\s+(\\d+)\\s+cucumbers$");
Optional<List<Argument<?>>> match3 = expr2.match("I  have  42  cucumbers");
// Matches

Case Sensitivity:

// Cucumber Expressions are case-sensitive by default
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match1 = expr.match("I have 42 cucumbers");
// Matches

Optional<List<Argument<?>>> match2 = expr.match("I HAVE 42 CUCUMBERS");
// Does NOT match

// For case-insensitive matching, use regex with flag
Expression expr2 = factory.createExpression("^(?i)I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match3 = expr2.match("I HAVE 42 CUCUMBERS");
// Matches

Unicode Support:

// Unicode characters supported in expressions
Expression expr1 = factory.createExpression("I have {int} 🥒");
Optional<List<Argument<?>>> match1 = expr1.match("I have 42 🥒");
// Matches

// Unicode in parameter values
ParameterType<String> emojiType = new ParameterType<>(
    "emoji",
    "[\\p{So}]+",
    String.class,
    (String s) -> s
);
registry.defineParameterType(emojiType);

Expression expr2 = factory.createExpression("Emoji is {emoji}");
Optional<List<Argument<?>>> match2 = expr2.match("Emoji is 🥒🌶️🥕");
// Matches

Special Characters in Text:

// Expressions can contain special regex characters as literal text
Expression expr = factory.createExpression("Price is {float} $");
Optional<List<Argument<?>>> match1 = expr.match("Price is 99.99 $");
// Matches ($ is literal in Cucumber Expression)

// But $ at end triggers regex mode
Expression expr2 = factory.createExpression("Price is {float}$");
// This becomes a RegularExpression due to $ at end

Empty Parameter Matches:

// Anonymous parameters can match empty strings
Expression expr = factory.createExpression("Value is {}");
Optional<List<Argument<?>>> match = expr.match("Value is ");
// match.isPresent() = false (doesn't match trailing space alone)

// String parameter requires quotes
Expression expr2 = factory.createExpression("Value is {string}");
Optional<List<Argument<?>>> match2 = expr2.match("Value is \"\"");
// match2.isPresent() = true, value = "" (empty string)

Advanced Usage Patterns

Combining Multiple Expressions:

public class MultiExpressionMatcher {
    private final List<Expression> expressions;
    
    public MultiExpressionMatcher(ExpressionFactory factory, String... patterns) {
        this.expressions = Arrays.stream(patterns)
            .map(factory::createExpression)
            .collect(Collectors.toList());
    }
    
    public Optional<MatchResult> matchFirst(String text) {
        for (int i = 0; i < expressions.size(); i++) {
            Optional<List<Argument<?>>> match = expressions.get(i).match(text);
            if (match.isPresent()) {
                return Optional.of(new MatchResult(i, match.get()));
            }
        }
        return Optional.empty();
    }
}

Dynamic Expression Construction:

public Expression buildExpression(String prefix, List<String> paramTypes, String suffix) {
    StringBuilder sb = new StringBuilder(prefix);
    for (int i = 0; i < paramTypes.size(); i++) {
        if (i > 0) sb.append(" and ");
        sb.append("{").append(paramTypes.get(i)).append("}");
    }
    sb.append(suffix);
    return factory.createExpression(sb.toString());
}

// Usage
Expression expr = buildExpression(
    "Transfer ",
    Arrays.asList("int", "string", "string"),
    " completed"
);
// Creates: "Transfer {int} and {string} and {string} completed"

Expression Validation:

public boolean isValidExpression(String expressionString) {
    try {
        factory.createExpression(expressionString);
        return true;
    } catch (CucumberExpressionException | PatternSyntaxException e) {
        return false;
    }
}

public List<String> validateExpressions(List<String> expressions) {
    List<String> errors = new ArrayList<>();
    for (String expr : expressions) {
        try {
            factory.createExpression(expr);
        } catch (UndefinedParameterTypeException e) {
            errors.add(expr + ": undefined type " + e.getUndefinedParameterTypeName());
        } catch (CucumberExpressionException | PatternSyntaxException e) {
            errors.add(expr + ": " + e.getMessage());
        }
    }
    return errors;
}

Related Documentation

  • Parameter Types - Define custom parameter types
  • Matching - Extract and process matched arguments
  • Transformers - Convert matched strings to typed values
  • Exceptions - Handle expression errors