tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
Transformers convert matched strings to Java types using functional interfaces. The library supports three transformer types for different conversion scenarios: single capture groups, multiple capture groups, and type-based default transformation.
All transformer interfaces are functional interfaces and thread-safety depends on the implementation:
Argument.getValue(), which can be concurrentTransform a string from a single capture group to type T.
package io.cucumber.cucumberexpressions;
import org.jspecify.annotations.Nullable;
/**
* Transform string from single capture group to type T
* Functional interface for simple single-value transformations
*/
@FunctionalInterface
public interface Transformer<T> {
/**
* Transform a captured string to type T
*
* IMPORTANT: Exceptions thrown from this method are preserved and re-thrown
* when Argument.getValue() is called. This allows validation in transformers.
*
* @param arg - Captured string value (may be null)
* @return Transformed value of type T (may be null)
* @throws Throwable if transformation fails (signals validation or conversion errors)
*/
@Nullable T transform(@Nullable String arg) throws Throwable;
}Usage Examples:
import io.cucumber.cucumberexpressions.*;
// Simple string to integer transformer
Transformer<Integer> intTransformer = (String arg) -> {
return arg == null ? null : Integer.parseInt(arg);
};
// String to custom type transformer
Transformer<Color> colorTransformer = (String arg) -> {
if (arg == null) return null;
switch (arg.toLowerCase()) {
case "red": return Color.RED;
case "blue": return Color.BLUE;
case "green": return Color.GREEN;
default: throw new IllegalArgumentException("Unknown color: " + arg);
}
};
// Use in parameter type
ParameterType<Color> colorType = new ParameterType<>(
"color",
"red|blue|green",
Color.class,
colorTransformer
);
// Transformer with complex logic
Transformer<LocalDate> dateTransformer = (String arg) -> {
if (arg == null) return null;
// Support multiple date formats
try {
return LocalDate.parse(arg, DateTimeFormatter.ISO_DATE);
} catch (DateTimeParseException e1) {
try {
return LocalDate.parse(arg, DateTimeFormatter.ofPattern("MM/dd/yyyy"));
} catch (DateTimeParseException e2) {
throw new IllegalArgumentException(
"Invalid date format. Expected ISO date or MM/dd/yyyy");
}
}
};
// Transformer with validation
Transformer<Age> ageTransformer = (String arg) -> {
if (arg == null) {
throw new IllegalArgumentException("Age cannot be null");
}
int value = Integer.parseInt(arg);
if (value < 0 || value > 150) {
throw new IllegalArgumentException(
"Age must be between 0 and 150, got: " + value);
}
return new Age(value);
};
// Transformer with trimming
Transformer<String> trimTransformer = (String arg) -> {
return arg == null ? null : arg.trim();
};
// Transformer with case conversion
Transformer<String> upperTransformer = (String arg) -> {
return arg == null ? null : arg.toUpperCase();
};Transform strings from multiple capture groups to type T, useful for complex types requiring multiple input values.
package io.cucumber.cucumberexpressions;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
/**
* Transform strings from multiple capture groups to type T
* Functional interface for multi-value transformations
*/
@FunctionalInterface
@API(status = API.Status.STABLE)
public interface CaptureGroupTransformer<T> {
/**
* Transform multiple captured strings to type T
*
* The args array contains all captured groups:
* - args[0] is the first capture group
* - args[1] is the second capture group
* - etc.
*
* IMPORTANT: Exceptions thrown from this method are preserved and re-thrown
* when Argument.getValue() is called.
*
* @param args - Array of captured string values (elements may be null)
* @return Transformed value of type T (may be null)
* @throws Throwable if transformation fails (signals validation or conversion errors)
*/
@Nullable T transform(@Nullable String[] args) throws Throwable;
}Usage Examples:
import io.cucumber.cucumberexpressions.*;
import java.util.Arrays;
// Transform two captures into a Point
CaptureGroupTransformer<Point> pointTransformer = (String[] args) -> {
if (args == null || args.length < 2) {
throw new IllegalArgumentException("Point requires two coordinates");
}
int x = Integer.parseInt(args[0]);
int y = Integer.parseInt(args[1]);
return new Point(x, y);
};
// Use in parameter type with multiple capture groups
ParameterType<Point> pointType = new ParameterType<>(
"point",
"(\\d+),(\\d+)", // Two capture groups
Point.class,
pointTransformer
);
// Transform RGB values into Color
CaptureGroupTransformer<Color> rgbTransformer = (String[] args) -> {
if (args == null || args.length < 3) {
throw new IllegalArgumentException("RGB requires three values");
}
int r = Integer.parseInt(args[0]);
int g = Integer.parseInt(args[1]);
int b = Integer.parseInt(args[2]);
// Validation
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
throw new IllegalArgumentException(
"RGB values must be 0-255, got: " + Arrays.toString(args));
}
return new Color(r, g, b);
};
ParameterType<Color> rgbType = new ParameterType<>(
"rgb",
"(\\d+)/(\\d+)/(\\d+)", // Three capture groups
Color.class,
rgbTransformer
);
// Transform with validation
CaptureGroupTransformer<DateRange> dateRangeTransformer = (String[] args) -> {
if (args == null || args.length < 2) {
throw new IllegalArgumentException("DateRange requires two dates");
}
LocalDate start = LocalDate.parse(args[0]);
LocalDate end = LocalDate.parse(args[1]);
if (end.isBefore(start)) {
throw new IllegalArgumentException(
"End date must be after start date");
}
return new DateRange(start, end);
};
ParameterType<DateRange> dateRangeType = new ParameterType<>(
"dateRange",
"(\\d{4}-\\d{2}-\\d{2})\\.\\.\\.(\\d{4}-\\d{2}-\\d{2})",
DateRange.class,
dateRangeTransformer
);
// Transform coordinates with optional values
CaptureGroupTransformer<Coordinate> coordTransformer = (String[] args) -> {
if (args == null) return null;
// Latitude (required)
Double lat = args[0] != null ? Double.parseDouble(args[0]) : 0.0;
// Longitude (required)
Double lon = args[1] != null ? Double.parseDouble(args[1]) : 0.0;
// Altitude (optional)
Double alt = (args.length > 2 && args[2] != null)
? Double.parseDouble(args[2])
: null;
return new Coordinate(lat, lon, alt);
};
// Example usage in expression
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("Point at {point}");
Optional<List<Argument<?>>> match = expr.match("Point at 10,20");
Point point = (Point) match.get().get(0).getValue();
// point = Point(10, 20)Default transformer for built-in and anonymous parameter types, used when no specific parameter type matches.
package io.cucumber.cucumberexpressions;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import java.lang.reflect.Type;
/**
* Default transformer for built-in and anonymous parameter types
* Functional interface for type-based transformation
* Used for anonymous {} parameters with type hints
*/
@FunctionalInterface
@API(status = API.Status.STABLE)
public interface ParameterByTypeTransformer {
/**
* Transform string value to target type
*
* This transformer is invoked for:
* - Anonymous {} parameters with type hints
* - Built-in types when no specific transformer matches
* - Fallback transformation scenarios
*
* IMPORTANT: Exceptions thrown from this method are preserved and re-thrown
* when Argument.getValue() is called.
*
* @param fromValue - String value to transform (may be null)
* @param toValueType - Target Java type (from type hint or parameter definition)
* @return Transformed value (may be null)
* @throws Throwable if transformation fails
*/
@Nullable Object transform(@Nullable String fromValue, Type toValueType)
throws Throwable;
}Usage Examples:
import io.cucumber.cucumberexpressions.*;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.UUID;
import com.fasterxml.jackson.databind.ObjectMapper;
// Simple default transformer for common types
ParameterByTypeTransformer simpleTransformer = (fromValue, toValueType) -> {
if (fromValue == null) return null;
if (toValueType == UUID.class) {
return UUID.fromString(fromValue);
}
if (toValueType == LocalDate.class) {
return LocalDate.parse(fromValue);
}
if (toValueType == java.net.URL.class) {
return new java.net.URL(fromValue);
}
// Fallback - return as string
return fromValue;
};
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
registry.setDefaultParameterTransformer(simpleTransformer);
// JSON-based default transformer using Jackson
ObjectMapper objectMapper = new ObjectMapper();
ParameterByTypeTransformer jsonTransformer = (fromValue, toValueType) -> {
if (fromValue == null) return null;
// Try to deserialize as JSON
try {
return objectMapper.readValue(fromValue,
objectMapper.constructType(toValueType));
} catch (Exception e) {
// Fallback to string
return fromValue;
}
};
registry.setDefaultParameterTransformer(jsonTransformer);
// Use with anonymous parameters
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("User ID is {}");
// Type hint guides the transformer
Optional<List<Argument<?>>> match = expr.match(
"User ID is 550e8400-e29b-41d4-a716-446655440000",
UUID.class // Type hint
);
UUID userId = (UUID) match.get().get(0).getValue();
// Advanced: Support enums and custom types
ParameterByTypeTransformer advancedTransformer = (fromValue, toValueType) -> {
if (fromValue == null) return null;
// Handle enums
if (toValueType instanceof Class<?>) {
Class<?> clazz = (Class<?>) toValueType;
if (clazz.isEnum()) {
@SuppressWarnings("unchecked")
Class<? extends Enum> enumClass = (Class<? extends Enum>) clazz;
return Enum.valueOf(enumClass, fromValue);
}
}
// Handle primitives and wrappers
if (toValueType == Integer.class || toValueType == int.class) {
return Integer.parseInt(fromValue);
}
if (toValueType == Long.class || toValueType == long.class) {
return Long.parseLong(fromValue);
}
if (toValueType == Boolean.class || toValueType == boolean.class) {
return Boolean.parseBoolean(fromValue);
}
if (toValueType == Double.class || toValueType == double.class) {
return Double.parseDouble(fromValue);
}
if (toValueType == Float.class || toValueType == float.class) {
return Float.parseFloat(fromValue);
}
// Handle custom types with static valueOf method
if (toValueType instanceof Class<?>) {
Class<?> clazz = (Class<?>) toValueType;
try {
var method = clazz.getMethod("valueOf", String.class);
return method.invoke(null, fromValue);
} catch (Exception e) {
// No valueOf method, continue
}
// Try constructor with String parameter
try {
var constructor = clazz.getConstructor(String.class);
return constructor.newInstance(fromValue);
} catch (Exception e) {
// No string constructor, continue
}
}
// Fallback
return fromValue;
};The library includes a built-in transformer (BuiltInParameterTransformer) that handles:
This transformer is used internally for built-in parameter types and as the initial default transformer.
// The built-in transformer is automatically used for built-in types
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
// Built-in types work automatically
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("I have {int} items");
Optional<List<Argument<?>>> match = expr.match("I have 42 items");
Integer count = (Integer) match.get().get(0).getValue();
// count = 42
// Enum support in built-in transformer
enum Status { ACTIVE, INACTIVE }
Expression enumExpr = factory.createExpression("Status is {}");
Optional<List<Argument<?>>> match2 = enumExpr.match("Status is ACTIVE", Status.class);
Status status = (Status) match2.get().get(0).getValue();
// status = Status.ACTIVETransformers can throw exceptions to signal transformation failures:
import io.cucumber.cucumberexpressions.*;
// Transformer with validation
Transformer<Age> ageTransformer = (String arg) -> {
if (arg == null) {
throw new IllegalArgumentException("Age cannot be null");
}
int value = Integer.parseInt(arg);
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Age must be between 0 and 150");
}
return new Age(value);
};
ParameterType<Age> ageType = new ParameterType<>(
"age",
"\\d+",
Age.class,
ageTransformer
);
registry.defineParameterType(ageType);
Expression expr = factory.createExpression("Person is {age} years old");
// Valid age
Optional<List<Argument<?>>> match1 = expr.match("Person is 25 years old");
Age age1 = (Age) match1.get().get(0).getValue();
// Success
// Invalid age
try {
Optional<List<Argument<?>>> match2 = expr.match("Person is 200 years old");
if (match2.isPresent()) {
Age age2 = (Age) match2.get().get(0).getValue();
// getValue() will throw the IllegalArgumentException from transformer
}
} catch (IllegalArgumentException e) {
System.err.println("Validation error: " + e.getMessage());
// Handle validation error
}
// Number format exception
try {
Optional<List<Argument<?>>> match3 = expr.match("Person is abc years old");
// This won't match because "abc" doesn't match \\d+
if (match3.isPresent()) {
// Won't reach here
} else {
System.out.println("No match - pattern rejected invalid format");
}
} catch (NumberFormatException e) {
// Won't be thrown because pattern rejects non-numeric input
}Exception Handling Best Practices:
// Pattern 1: Specific exception types for different error conditions
Transformer<Email> emailTransformer = (String arg) -> {
if (arg == null || arg.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!arg.contains("@")) {
throw new IllegalArgumentException("Email must contain @");
}
if (!arg.matches("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}")) {
throw new IllegalArgumentException("Invalid email format");
}
return new Email(arg);
};
// Pattern 2: Wrapper for detailed error information
public class TransformationException extends RuntimeException {
private final String inputValue;
private final Type targetType;
public TransformationException(String message, String inputValue, Type targetType) {
super(message);
this.inputValue = inputValue;
this.targetType = targetType;
}
public String getInputValue() { return inputValue; }
public Type getTargetType() { return targetType; }
}
Transformer<CustomType> customTransformer = (String arg) -> {
try {
return CustomType.parse(arg);
} catch (Exception e) {
throw new TransformationException(
"Failed to parse as CustomType: " + e.getMessage(),
arg,
CustomType.class
);
}
};All transformer interfaces support nullable parameters and return values:
// Transformer that handles null gracefully
Transformer<String> nullSafeTransformer = (String arg) -> {
return arg == null ? "default" : arg.trim();
};
// Transformer that returns null
Transformer<Optional<String>> optionalTransformer = (String arg) -> {
return (arg == null || arg.isEmpty()) ? null : arg;
};
// CaptureGroupTransformer with null handling
CaptureGroupTransformer<Coordinate> coordinateTransformer = (String[] args) -> {
if (args == null) return null;
// Individual array elements might also be null
Double lat = args[0] != null ? Double.parseDouble(args[0]) : 0.0;
Double lon = args[1] != null ? Double.parseDouble(args[1]) : 0.0;
return new Coordinate(lat, lon);
};
// Transformer that validates against null
Transformer<NonNullValue> nonNullTransformer = (String arg) -> {
if (arg == null) {
throw new IllegalArgumentException("Value cannot be null");
}
return new NonNullValue(arg);
};Transformers can be composed for complex transformation pipelines:
import io.cucumber.cucumberexpressions.*;
import java.util.function.Function;
// Helper to compose transformers
public class TransformerUtils {
public static <T, U> Transformer<U> compose(
Transformer<T> first,
Function<T, U> second
) {
return (String arg) -> {
T intermediate = first.transform(arg);
return intermediate == null ? null : second.apply(intermediate);
};
}
public static <T> Transformer<T> withDefault(
Transformer<T> transformer,
T defaultValue
) {
return (String arg) -> {
T result = transformer.transform(arg);
return result != null ? result : defaultValue;
};
}
public static <T> Transformer<T> withValidation(
Transformer<T> transformer,
java.util.function.Predicate<T> validator,
String errorMessage
) {
return (String arg) -> {
T result = transformer.transform(arg);
if (result != null && !validator.test(result)) {
throw new IllegalArgumentException(errorMessage);
}
return result;
};
}
}
// Example: trim then parse
Transformer<Integer> trimAndParse = TransformerUtils.compose(
(String s) -> s == null ? null : s.trim(),
(String s) -> Integer.parseInt(s)
);
ParameterType<Integer> trimmedIntType = new ParameterType<>(
"trimmedInt",
"\\s*\\d+\\s*",
Integer.class,
trimAndParse
);
// Example: parse with default
Transformer<Integer> parseWithDefault = TransformerUtils.withDefault(
(String s) -> s == null || s.isEmpty() ? null : Integer.parseInt(s),
0
);
// Example: parse with validation
Transformer<Integer> parsePositive = TransformerUtils.withValidation(
(String s) -> Integer.parseInt(s),
(Integer i) -> i > 0,
"Value must be positive"
);Pattern 1: Stateful Transformer with Context
public class ContextualTransformer<T> implements Transformer<T> {
private final Function<String, T> baseTransform;
private final Map<String, Object> context;
public ContextualTransformer(Function<String, T> baseTransform, Map<String, Object> context) {
this.baseTransform = baseTransform;
this.context = context;
}
@Override
public T transform(String arg) throws Throwable {
// Can access context during transformation
Object sessionData = context.get("session");
// Use session data in transformation...
return baseTransform.apply(arg);
}
}
// Usage
Map<String, Object> context = new HashMap<>();
context.put("locale", Locale.GERMAN);
context.put("timezone", ZoneId.of("Europe/Berlin"));
ContextualTransformer<LocalDateTime> dateTimeTransformer =
new ContextualTransformer<>(
(String s) -> {
Locale locale = (Locale) context.get("locale");
// Use locale in parsing...
return LocalDateTime.parse(s);
},
context
);Pattern 2: Caching Transformer
public class CachingTransformer<T> implements Transformer<T> {
private final Transformer<T> delegate;
private final Map<String, T> cache = new ConcurrentHashMap<>();
public CachingTransformer(Transformer<T> delegate) {
this.delegate = delegate;
}
@Override
public T transform(String arg) throws Throwable {
if (arg == null) return delegate.transform(null);
return cache.computeIfAbsent(arg, k -> {
try {
return delegate.transform(k);
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
}
}
// Usage for expensive transformations
Transformer<ComplexObject> expensiveTransformer = (String s) -> {
// Expensive computation...
return ComplexObject.parseAndValidate(s);
};
CachingTransformer<ComplexObject> cachedTransformer =
new CachingTransformer<>(expensiveTransformer);Pattern 3: Logging Transformer
public class LoggingTransformer<T> implements Transformer<T> {
private final Transformer<T> delegate;
private final Logger logger;
public LoggingTransformer(Transformer<T> delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}
@Override
public T transform(String arg) throws Throwable {
logger.debug("Transforming: {}", arg);
try {
T result = delegate.transform(arg);
logger.debug("Transformed {} to {}", arg, result);
return result;
} catch (Throwable e) {
logger.error("Transformation failed for: {}", arg, e);
throw e;
}
}
}Pattern 4: Builder-Style Transformer Creation
public class TransformerBuilder<T> {
private Function<String, T> baseTransform;
private boolean trimInput = false;
private boolean caseInsensitive = false;
private T defaultValue = null;
private java.util.function.Predicate<T> validator = null;
private String validationMessage = "Validation failed";
public TransformerBuilder<T> transform(Function<String, T> transform) {
this.baseTransform = transform;
return this;
}
public TransformerBuilder<T> trim() {
this.trimInput = true;
return this;
}
public TransformerBuilder<T> caseInsensitive() {
this.caseInsensitive = true;
return this;
}
public TransformerBuilder<T> withDefault(T defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public TransformerBuilder<T> validate(java.util.function.Predicate<T> validator,
String message) {
this.validator = validator;
this.validationMessage = message;
return this;
}
public Transformer<T> build() {
return (String arg) -> {
if (arg == null || arg.isEmpty()) {
return defaultValue;
}
String processed = arg;
if (trimInput) {
processed = processed.trim();
}
if (caseInsensitive) {
processed = processed.toLowerCase();
}
T result = baseTransform.apply(processed);
if (validator != null && result != null && !validator.test(result)) {
throw new IllegalArgumentException(validationMessage);
}
return result;
};
}
}
// Usage
Transformer<Integer> transformer = new TransformerBuilder<Integer>()
.transform(Integer::parseInt)
.trim()
.withDefault(0)
.validate(i -> i > 0, "Must be positive")
.build();Transformation Cost:
Optimization Tips:
// GOOD: Reuse compiled patterns
public class PatternTransformer implements Transformer<String> {
private final Pattern pattern = Pattern.compile("[a-z]+");
@Override
public String transform(String arg) {
if (arg == null) return null;
Matcher m = pattern.matcher(arg);
return m.matches() ? arg : null;
}
}
// BAD: Compile pattern on every transformation
Transformer<String> inefficient = (String arg) -> {
Pattern pattern = Pattern.compile("[a-z]+"); // Compiled every time
Matcher m = pattern.matcher(arg);
return m.matches() ? arg : null;
};
// GOOD: Cache expensive lookups
Map<String, User> userCache = new ConcurrentHashMap<>();
Transformer<User> userTransformer = (String userId) -> {
return userCache.computeIfAbsent(userId, id -> database.findUser(id));
};
// GOOD: Early return for null
Transformer<ComplexType> optimized = (String arg) -> {
if (arg == null) return null; // Fast path
// Expensive computation only if non-null
return ComplexType.parse(arg);
};