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

matching.mddocs/reference/

Matching and Arguments

The matching system extracts typed arguments from matched text with position information and group hierarchies. It provides detailed information about matched values, their types, and their locations in the source text.

Thread Safety

  • Argument instances: Immutable value objects, thread-safe.
  • Group instances: Immutable value objects, thread-safe.
  • Match operations: Thread-safe on Expression instances.

Capabilities

Argument Class

Represents a matched argument with value and type information.

package io.cucumber.cucumberexpressions;

import java.lang.reflect.Type;
import org.jspecify.annotations.Nullable;

/**
 * Matched argument with value and type information
 * Immutable value object - thread-safe
 */
public final class Argument<T> {
    /**
     * Get the matched group
     * Contains position and text information about the match
     * @return Group representing the matched text
     */
    public Group getGroup();
    
    /**
     * Get the transformed value
     * Returns the result of applying the parameter type's transformer
     * May be null if the transformer returns null or the match was empty
     * 
     * IMPORTANT: Transformer exceptions are thrown from this method
     * If the transformer throws an exception, calling getValue() will re-throw it
     * 
     * @return Transformed value of type T
     * @throws RuntimeException if transformer threw an exception during transformation
     */
    @Nullable
    public T getValue();
    
    /**
     * Get the Java type of the transformed value
     * @return Java Type (e.g., Integer.class, String.class)
     */
    public Type getType();
    
    /**
     * Get the parameter type definition
     * @return ParameterType used for this argument
     */
    public ParameterType<T> getParameterType();
}

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);

// Match and extract arguments
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");

if (match.isPresent()) {
    List<Argument<?>> arguments = match.get();
    Argument<?> arg = arguments.get(0);
    
    // Get the transformed value
    Integer value = (Integer) arg.getValue();
    // value = 42
    
    // Get the matched group (text and position)
    Group group = arg.getGroup();
    String matchedText = group.getValue();
    // matchedText = "42"
    int start = group.getStart();
    // start = 7
    int end = group.getEnd();
    // end = 9
    
    // Get type information
    Type type = arg.getType();
    // type = Integer.class
    
    ParameterType<?> paramType = arg.getParameterType();
    String paramName = paramType.getName();
    // paramName = "int"
}

Multiple Arguments:

Expression expr = factory.createExpression("Transfer {int} from account {int} to account {int}");
Optional<List<Argument<?>>> match = expr.match("Transfer 100 from account 12345 to account 67890");

if (match.isPresent()) {
    List<Argument<?>> arguments = match.get();
    
    Integer amount = (Integer) arguments.get(0).getValue();
    // amount = 100
    
    Integer fromAccount = (Integer) arguments.get(1).getValue();
    // fromAccount = 12345
    
    Integer toAccount = (Integer) arguments.get(2).getValue();
    // toAccount = 67890
    
    // Access all argument positions
    for (int i = 0; i < arguments.size(); i++) {
        Argument<?> arg = arguments.get(i);
        Group group = arg.getGroup();
        System.out.printf("Argument %d: value=%s, position=%d-%d%n",
            i, arg.getValue(), group.getStart(), group.getEnd());
    }
}

Custom Types:

// Define custom parameter type
ParameterType<Color> colorType = new ParameterType<>(
    "color",
    "red|blue|green",
    Color.class,
    (String s) -> new Color(s)
);
registry.defineParameterType(colorType);

Expression expr = factory.createExpression("I have a {color} ball");
Optional<List<Argument<?>>> match = expr.match("I have a red ball");

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    
    // Get typed value
    Color color = (Color) arg.getValue();
    // color = Color.RED
    
    // Get parameter type info
    ParameterType<?> paramType = arg.getParameterType();
    // paramType.getName() = "color"
    // paramType.getType() = Color.class
    
    // Get raw matched text
    String rawText = arg.getGroup().getValue();
    // rawText = "red"
}

Transformer Exception Handling:

// Transformer that validates and may throw exceptions
ParameterType<Age> ageType = new ParameterType<>(
    "age",
    "\\d+",
    Age.class,
    (String s) -> {
        int value = Integer.parseInt(s);
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Age must be 0-150, got: " + value);
        }
        return new Age(value);
    }
);
registry.defineParameterType(ageType);

Expression expr = factory.createExpression("Person is {age} years old");
Optional<List<Argument<?>>> match = expr.match("Person is 200 years old");

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    try {
        Age age = (Age) arg.getValue();
        // This throws IllegalArgumentException from transformer
    } catch (IllegalArgumentException e) {
        System.err.println("Validation error: " + e.getMessage());
        // Handle validation error...
    }
}

Group Class

Represents a matched text group with position information and hierarchical structure.

package io.cucumber.cucumberexpressions;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;

/**
 * Matched text group with position information
 * Immutable value object with hierarchical structure
 */
public final class Group {
    /**
     * Parse a Pattern into Groups
     * Useful for analyzing regular expression structure
     * Static factory method for regex pattern analysis
     * @param expression - Pattern to parse
     * @return Collection of root-level groups
     */
    public static Collection<Group> parse(Pattern expression);
    
    /**
     * Get the matched value
     * Returns null for non-capturing groups
     * @return Matched text value
     */
    @Nullable
    public String getValue();
    
    /**
     * Get start index of match in source text
     * @return Zero-based start index (inclusive)
     */
    public int getStart();
    
    /**
     * Get end index of match in source text
     * End index is exclusive (points to character after last matched character)
     * @return Zero-based end index (exclusive)
     */
    public int getEnd();
    
    /**
     * Get child groups (capture groups within this group)
     * Returns empty Optional if no children
     * @return Optional containing list of child groups
     */
    public Optional<List<Group>> getChildren();
    
    /**
     * Get all values from children or self
     * Flattens the group hierarchy into a list
     * First element is always the complete match, followed by capture groups
     * @return List of all matched values (nulls preserved for non-capturing groups)
     */
    public List<@Nullable String> getValues();
}

Usage Examples:

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

// Basic group information
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    Group group = arg.getGroup();
    
    // Get matched text
    String value = group.getValue();
    // value = "42"
    
    // Get position
    int start = group.getStart();
    int end = group.getEnd();
    // Text from index 7 to 9: "42"
    
    // Extract from source text
    String sourceText = "I have 42 cucumbers";
    String extracted = sourceText.substring(start, end);
    // extracted = "42"
}

Group Hierarchy:

import java.util.regex.Pattern;
import java.util.Collection;

// Parse a pattern with capture groups
Pattern pattern = Pattern.compile("(\\d+)/(\\d+)/(\\d+)");
Collection<Group> groups = Group.parse(pattern);

// Groups contain capture group structure
for (Group group : groups) {
    System.out.println("Group: " + group.getValue());
    
    Optional<List<Group>> children = group.getChildren();
    if (children.isPresent()) {
        for (Group child : children.get()) {
            System.out.println("  Child: " + child.getValue());
        }
    }
}

Values Extraction:

// Define parameter type with multiple capture groups
ParameterType<Point> pointType = new ParameterType<>(
    "point",
    "(\\d+),(\\d+)",
    Point.class,
    (String[] args) -> new Point(
        Integer.parseInt(args[0]),
        Integer.parseInt(args[1])
    )
);
registry.defineParameterType(pointType);

Expression expr = factory.createExpression("Point at {point}");
Optional<List<Argument<?>>> match = expr.match("Point at 10,20");

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    Group group = arg.getGroup();
    
    // Get all values (flattened)
    List<String> values = group.getValues();
    // values = ["10,20", "10", "20"]
    // First value is the complete match, subsequent are capture groups
    
    // Get child groups
    Optional<List<Group>> children = group.getChildren();
    if (children.isPresent()) {
        Group child1 = children.get().get(0);
        // child1.getValue() = "10"
        // child1.getStart() = position of "10"
        // child1.getEnd() = position after "10"
        
        Group child2 = children.get().get(1);
        // child2.getValue() = "20"
        // child2.getStart() = position of "20"
        // child2.getEnd() = position after "20"
    }
}

Match Result Handling

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

// Successful match
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");

if (match.isPresent()) {
    List<Argument<?>> arguments = match.get();
    // Process arguments
} else {
    // No match
}

// Failed match
Optional<List<Argument<?>>> noMatch = expr.match("I have many cucumbers");
// noMatch.isEmpty() = true

// Alternative handling
match.ifPresent(arguments -> {
    // Process arguments
    Integer count = (Integer) arguments.get(0).getValue();
});

// With default
List<Argument<?>> arguments = match.orElse(List.of());

// Pattern matching (Java 17+)
switch (match) {
    case Optional<List<Argument<?>>> opt when opt.isPresent() -> {
        List<Argument<?>> args = opt.get();
        // Process match
    }
    default -> {
        // No match
    }
}

Position-Based Processing

Use group position information for text highlighting, error reporting, or replacement:

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

String sourceText = "I have 42 cucumbers in my belly";
Expression expr = factory.createExpression("I have {int} cucumbers");
Optional<List<Argument<?>>> match = expr.match(sourceText);

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    Group group = arg.getGroup();
    
    int start = group.getStart();
    int end = group.getEnd();
    
    // Highlight matched text
    String before = sourceText.substring(0, start);
    String matched = sourceText.substring(start, end);
    String after = sourceText.substring(end);
    String highlighted = before + "[" + matched + "]" + after;
    // highlighted = "I have [42] cucumbers in my belly"
    
    // Replace matched text
    String replacement = "100";
    String replaced = before + replacement + after;
    // replaced = "I have 100 cucumbers in my belly"
    
    // Error reporting with caret indicators
    System.err.println("Error at position " + start + "-" + end);
    System.err.println(sourceText);
    System.err.println(" ".repeat(start) + "^".repeat(end - start));
    // Output:
    // Error at position 7-9
    // I have 42 cucumbers in my belly
    //        ^^
    
    // Syntax highlighting (conceptual)
    List<HighlightSpan> spans = new ArrayList<>();
    spans.add(new HighlightSpan(start, end, "parameter"));
}

Complex Matching Scenarios

Multiple Parameter Types:

Expression expr = factory.createExpression(
    "User {string} has {int} items worth {float} dollars"
);

Optional<List<Argument<?>>> match = expr.match(
    "User \"John Doe\" has 5 items worth 99.99 dollars"
);

if (match.isPresent()) {
    List<Argument<?>> arguments = match.get();
    
    String username = (String) arguments.get(0).getValue();
    // username = "John Doe" (quotes removed by {string} type)
    
    Integer itemCount = (Integer) arguments.get(1).getValue();
    // itemCount = 5
    
    Float totalValue = (Float) arguments.get(2).getValue();
    // totalValue = 99.99f
    
    // Get position of each argument for UI highlighting
    for (int i = 0; i < arguments.size(); i++) {
        Argument<?> arg = arguments.get(i);
        Group group = arg.getGroup();
        System.out.printf("Argument %d: value=%s, type=%s, position=%d-%d, text='%s'%n",
            i, 
            arg.getValue(),
            arg.getType().getTypeName(),
            group.getStart(), 
            group.getEnd(),
            group.getValue());
    }
    // Output:
    // Argument 0: value=John Doe, type=java.lang.String, position=5-15, text='"John Doe"'
    // Argument 1: value=5, type=java.lang.Integer, position=20-21, text='5'
    // Argument 2: value=99.99, type=java.lang.Float, position=34-39, text='99.99'
}

Optional and Alternative Text:

// Optional text
Expression expr1 = factory.createExpression("I have {int} cucumber(s)");
Optional<List<Argument<?>>> match1 = expr1.match("I have 1 cucumber");
Optional<List<Argument<?>>> match2 = expr1.match("I have 42 cucumbers");
// Both match, same argument extraction
// match1.get().get(0).getValue() = 1
// match2.get().get(0).getValue() = 42

// Alternative text
Expression expr2 = factory.createExpression("I have {int} in my belly/stomach");
Optional<List<Argument<?>>> match3 = expr2.match("I have 42 in my belly");
Optional<List<Argument<?>>> match4 = expr2.match("I have 42 in my stomach");
// Both match, same argument extraction
// match3.get().get(0).getValue() = 42
// match4.get().get(0).getValue() = 42

// The matched text position will differ
Group group3 = match3.get().get(0).getGroup();
Group group4 = match4.get().get(0).getGroup();
// Both groups have same value ("42") but may be at different positions

Regular Expression Matching:

Regular expression capture groups are automatically typed based on matching parameter types:

// Regular expression with capture groups
Expression regexExpr = factory.createExpression("^I have (\\d+) cucumbers$");
Optional<List<Argument<?>>> match = regexExpr.match("I have 42 cucumbers");

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    
    // Value is automatically transformed to Integer (not String!)
    // The \d+ pattern matches the built-in {int} parameter type
    Integer captured = (Integer) arg.getValue();
    // captured = 42 (Integer object, not String "42")
    
    Group group = arg.getGroup();
    // group.getValue() = "42" (raw matched text as String)
    // group.getStart() = 7
    // group.getEnd() = 9
    
    // Note: Group.getValue() returns the raw matched text as String
    // while Argument.getValue() returns the transformed typed value
}

Understanding Regex Type Transformation:

// Built-in types are automatically detected in regex patterns
Expression intExpr = factory.createExpression("^Count: (\\d+)$");
Optional<List<Argument<?>>> intMatch = intExpr.match("Count: 123");
if (intMatch.isPresent()) {
    Object value = intMatch.get().get(0).getValue();
    // value is Integer 123, not String "123"
}

Expression floatExpr = factory.createExpression("^Price: (-?[0-9]*\\.?[0-9]+)$");
Optional<List<Argument<?>>> floatMatch = floatExpr.match("Price: 99.99");
if (floatMatch.isPresent()) {
    Object value = floatMatch.get().get(0).getValue();
    // value may be Float depending on pattern match
}

// To ensure String values from regex, use patterns that don't match parameter types
Expression stringExpr = factory.createExpression("^Name: (.+)$");
Optional<List<Argument<?>>> stringMatch = stringExpr.match("Name: John");
if (stringMatch.isPresent()) {
    Object value = stringMatch.get().get(0).getValue();
    // value is String "John" (pattern .+ doesn't match any specific type)
}

Type Hints in Matching

// Use type hints for explicit type conversion with anonymous parameters
Expression expr = factory.createExpression("Value is {}");

// Without type hint - uses default transformer
Optional<List<Argument<?>>> match1 = expr.match("Value is 42");
Object value1 = match1.get().get(0).getValue();
// value1 type depends on default transformer (typically String)

// With type hint for Integer
Optional<List<Argument<?>>> match2 = expr.match("Value is 42", Integer.class);
Integer value2 = (Integer) match2.get().get(0).getValue();
// value2 = 42 (explicitly converted to Integer)

// With type hint for custom type
Optional<List<Argument<?>>> match3 = expr.match("Value is 42", CustomId.class);
CustomId value3 = (CustomId) match3.get().get(0).getValue();
// Uses default transformer to convert to CustomId

// Multiple anonymous parameters with different type hints
Expression expr2 = factory.createExpression("Data: {} and {} and {}");
Optional<List<Argument<?>>> match4 = expr2.match(
    "Data: 42 and hello and 3.14",
    Integer.class, String.class, Double.class
);
// First argument: Integer 42
// Second argument: String "hello"
// Third argument: Double 3.14

Null Value Handling

// Transformers can return null
Transformer<String> nullableTransformer = (String arg) -> {
    return arg == null || arg.isEmpty() ? null : arg;
};

ParameterType<String> nullableType = new ParameterType<>(
    "nullable",
    ".*",
    String.class,
    nullableTransformer
);
registry.defineParameterType(nullableType);

Expression expr = factory.createExpression("Value is {nullable}");
Optional<List<Argument<?>>> match = expr.match("Value is ");

if (match.isPresent()) {
    Argument<?> arg = match.get().get(0);
    String value = (String) arg.getValue();
    // value might be null if transformer returned null
    
    if (value == null) {
        System.out.println("Transformer returned null");
    }
}

Advanced Usage Patterns

Pattern 1: Argument Extraction Helper

public class ArgumentExtractor {
    public static <T> T extractValue(Optional<List<Argument<?>>> match, 
                                      int index, Class<T> expectedType) {
        if (match.isEmpty()) {
            throw new IllegalArgumentException("No match found");
        }
        
        List<Argument<?>> args = match.get();
        if (index >= args.size()) {
            throw new IndexOutOfBoundsException("Argument index out of range: " + index);
        }
        
        Object value = args.get(index).getValue();
        if (!expectedType.isInstance(value)) {
            throw new ClassCastException(
                "Expected " + expectedType + " but got " + value.getClass()
            );
        }
        
        return expectedType.cast(value);
    }
    
    // Usage
    Optional<List<Argument<?>>> match = expr.match("I have 42 cucumbers");
    Integer count = ArgumentExtractor.extractValue(match, 0, Integer.class);
}

Pattern 2: Position-Based Highlighting

public class SyntaxHighlighter {
    public String highlightArguments(String text, List<Argument<?>> arguments) {
        StringBuilder result = new StringBuilder();
        int lastEnd = 0;
        
        for (Argument<?> arg : arguments) {
            Group group = arg.getGroup();
            
            // Append text before argument
            result.append(text, lastEnd, group.getStart());
            
            // Append highlighted argument
            result.append("<span class='parameter' data-type='")
                  .append(arg.getParameterType().getName())
                  .append("'>")
                  .append(text, group.getStart(), group.getEnd())
                  .append("</span>");
            
            lastEnd = group.getEnd();
        }
        
        // Append remaining text
        result.append(text.substring(lastEnd));
        
        return result.toString();
    }
}

Pattern 3: Validation with Position Information

public class ArgumentValidator {
    public static class ValidationError {
        public final int argumentIndex;
        public final int start;
        public final int end;
        public final String message;
        
        public ValidationError(int argumentIndex, int start, int end, String message) {
            this.argumentIndex = argumentIndex;
            this.start = start;
            this.end = end;
            this.message = message;
        }
    }
    
    public List<ValidationError> validate(Optional<List<Argument<?>>> match) {
        if (match.isEmpty()) {
            return List.of();
        }
        
        List<ValidationError> errors = new ArrayList<>();
        List<Argument<?>> arguments = match.get();
        
        for (int i = 0; i < arguments.size(); i++) {
            Argument<?> arg = arguments.get(i);
            Group group = arg.getGroup();
            
            try {
                Object value = arg.getValue();
                // Perform custom validation
                if (value instanceof Integer && (Integer) value < 0) {
                    errors.add(new ValidationError(
                        i,
                        group.getStart(),
                        group.getEnd(),
                        "Value must be positive"
                    ));
                }
            } catch (Exception e) {
                errors.add(new ValidationError(
                    i,
                    group.getStart(),
                    group.getEnd(),
                    "Transformation error: " + e.getMessage()
                ));
            }
        }
        
        return errors;
    }
}

Pattern 4: Diff-Based Text Replacement

public class TextReplacer {
    public String replaceArguments(String text, 
                                   Optional<List<Argument<?>>> match,
                                   Function<Argument<?>, String> replacer) {
        if (match.isEmpty()) {
            return text;
        }
        
        List<Argument<?>> arguments = match.get();
        StringBuilder result = new StringBuilder();
        int lastEnd = 0;
        
        for (Argument<?> arg : arguments) {
            Group group = arg.getGroup();
            
            // Copy text before this argument
            result.append(text, lastEnd, group.getStart());
            
            // Apply replacement
            String replacement = replacer.apply(arg);
            result.append(replacement);
            
            lastEnd = group.getEnd();
        }
        
        // Copy remaining text
        result.append(text.substring(lastEnd));
        
        return result.toString();
    }
    
    // Usage
    String replaced = replacer.replaceArguments(
        "I have 42 cucumbers and 10 tomatoes",
        match,
        arg -> {
            Integer value = (Integer) arg.getValue();
            return String.valueOf(value * 2);  // Double all numbers
        }
    );
    // Result: "I have 84 cucumbers and 20 tomatoes"
}

Edge Cases and Special Scenarios

Empty Matches:

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

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

Overlapping Groups (Not Supported):

// Cucumber Expressions don't support overlapping parameter groups
// This is not possible:
// "I have {int} {word}" matching "I have 42cucumbers"
// Parameters must be separated by literal text

Zero-Width Matches:

// Parameter types with zero-width patterns
ParameterType<Marker> markerType = new ParameterType<>(
    "marker",
    "",
    Marker.class,
    (String s) -> new Marker()
);
// Use case: position markers, boundaries

Unicode Positions:

// Positions are based on Java char indices (UTF-16 code units)
Expression expr = factory.createExpression("Emoji {int} test");
Optional<List<Argument<?>>> match = expr.match("Emoji 42 test");

if (match.isPresent()) {
    Group group = match.get().get(0).getGroup();
    int start = group.getStart();
    // start = position in Java char indices
    
    // For proper Unicode handling with surrogate pairs:
    String text = "Emoji 42 test";
    int codePointStart = text.codePointCount(0, start);
}

Performance Considerations

Matching Performance:

  • Match operations: O(text length)
  • Argument extraction: O(number of arguments)
  • Position calculations: O(1)
  • getValue() transformation: Depends on transformer (typically < 1µs)

Memory Usage:

  • Each Argument: ~100 bytes + value object
  • Each Group: ~50 bytes
  • Match with 5 arguments: ~750 bytes total

Optimization Tips:

// GOOD: Extract values once
List<Argument<?>> args = match.get();
Integer value = (Integer) args.get(0).getValue();
// Reuse value multiple times

// AVOID: Repeated getValue() calls if transformer is expensive
for (int i = 0; i < 1000; i++) {
    Integer value = (Integer) match.get().get(0).getValue();
    // getValue() is called 1000 times (transformer runs 1000 times if not cached)
}

Related Documentation

  • Expressions - Create expressions and perform matching
  • Parameter Types - Define types for argument conversion
  • Transformers - Transform matched text to typed values
  • Expression Generation - Generate expressions from examples