ParameterConverters.java
package org.jbehave.core.steps;
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.Validate.isTrue;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.jbehave.core.annotations.AsJson;
import org.jbehave.core.annotations.AsParameters;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.i18n.LocalizedKeywords;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.ResourceLoader;
import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.ExamplesTableFactory;
import org.jbehave.core.model.TableParsers;
import org.jbehave.core.model.TableTransformers;
import org.jbehave.core.model.Verbatim;
/**
* <p>
* Facade responsible for converting parameter values to Java objects. It allows
* the registration of several {@link ParameterConverter} instances, and the
* first one that is found to matches the appropriate parameter type is used.
* </p>
* <p>
* Converters for several Java types are provided out-of-the-box:
* <ul>
* <li>{@link ParameterConverters.NumberConverter NumberConverter}</li>
* <li>{@link ParameterConverters.NumberListConverter NumberListConverter}</li>
* <li>{@link ParameterConverters.StringConverter StringConverter}</li>
* <li>{@link ParameterConverters.StringListConverter StringListConverter}</li>
* <li>{@link ParameterConverters.DateConverter DateConverter}</li>
* <li>{@link ParameterConverters.ExamplesTableConverter ExamplesTableConverter}</li>
* <li>{@link ParameterConverters.ExamplesTableParametersConverter ExamplesTableParametersConverter}</li>
* <li>{@link ParameterConverters.MethodReturningConverter MethodReturningConverter}</li>
* <li>{@link ParameterConverters.JsonConverter JsonConverter}</li>
* </ul>
* </p>
*/
public class ParameterConverters {
public static final StepMonitor DEFAULT_STEP_MONITOR = new SilentStepMonitor();
public static final Locale DEFAULT_NUMBER_FORMAT_LOCAL = Locale.ENGLISH;
public static final String DEFAULT_COLLECTION_SEPARATOR = ",";
public static final boolean DEFAULT_THREAD_SAFETY = true;
private static final String DEFAULT_TRUE_VALUE = "true";
private static final String DEFAULT_FALSE_VALUE = "false";
private final StepMonitor monitor;
private final List<ParameterConverter> converters;
private final boolean threadSafe;
private String escapedCollectionSeparator;
/**
* Creates a ParameterConverters using the default resource loader and table transformers,
* a SilentStepMonitor, English as Locale and "," as collection separator.
*/
public ParameterConverters() {
this(new LoadFromClasspath(), new TableTransformers());
}
/**
* Creates a ParameterConverters using the given table transformers.
*
* @param tableTransformers the table transformers
*/
public ParameterConverters(TableTransformers tableTransformers) {
this(new LoadFromClasspath(), tableTransformers);
}
/**
* Creates a ParameterConverters of ParameterConverters using the given resource loader.
*
* @param resourceLoader the resource loader
*/
public ParameterConverters(ResourceLoader resourceLoader) {
this(resourceLoader, new TableTransformers());
}
/**
* Creates a ParameterConverters given resource loader and table transformers.
*
* @param resourceLoader the resource loader
* @param tableTransformers the table transformers
*/
public ParameterConverters(ResourceLoader resourceLoader, TableTransformers tableTransformers) {
this(DEFAULT_STEP_MONITOR, resourceLoader, new ParameterControls(), tableTransformers);
}
/**
* Creates a ParameterConverters using given StepMonitor, resource loader and table transformers.
*
* @param monitor the StepMonitor to use
* @param resourceLoader the resource loader
* @param parameterControls the parameter controls
* @param tableTransformers the table transformers
*/
public ParameterConverters(StepMonitor monitor, ResourceLoader resourceLoader, ParameterControls parameterControls,
TableTransformers tableTransformers) {
this(monitor, resourceLoader, parameterControls, tableTransformers, DEFAULT_NUMBER_FORMAT_LOCAL,
DEFAULT_COLLECTION_SEPARATOR, DEFAULT_THREAD_SAFETY);
}
/**
* Creates a ParameterConverters using given StepMonitor, keywords, resource loader and table transformers.
*
* @param monitor the StepMonitor to use
* @param keywords the keywords to use
* @param resourceLoader the resource loader
* @param parameterControls the parameter controls
* @param tableTransformers the table transformers
*/
public ParameterConverters(StepMonitor monitor, Keywords keywords, ResourceLoader resourceLoader,
ParameterControls parameterControls, TableTransformers tableTransformers) {
this(monitor, keywords, resourceLoader, parameterControls, tableTransformers, DEFAULT_NUMBER_FORMAT_LOCAL,
DEFAULT_COLLECTION_SEPARATOR, DEFAULT_THREAD_SAFETY);
}
/**
* Create a ParameterConverters with given thread-safety
*
* @param resourceLoader the resource loader
* @param parameterControls the parameter controls
* @param tableTransformers the table transformers
* @param threadSafe the boolean flag to determine if access to {@link ParameterConverter} should be
* thread-safe
*/
public ParameterConverters(ResourceLoader resourceLoader, ParameterControls parameterControls,
TableTransformers tableTransformers, boolean threadSafe) {
this(DEFAULT_STEP_MONITOR, resourceLoader, parameterControls, tableTransformers, DEFAULT_NUMBER_FORMAT_LOCAL,
DEFAULT_COLLECTION_SEPARATOR, threadSafe);
}
/**
* Creates a ParameterConverters for the given StepMonitor, Locale, list
* separator and thread-safety. When selecting a collectionSeparator, please make
* sure that this character doesn't have a special meaning in your Locale
* (for instance "," is used as decimal separator in some Locale)
*
* @param monitor the StepMonitor reporting the conversions
* @param resourceLoader the resource loader
* @param parameterControls the parameter controls
* @param tableTransformers the table transformers
* @param locale the Locale to use when reading numbers
* @param collectionSeparator the String to use as collection separator
* @param threadSafe the boolean flag to determine if modification of {@link ParameterConverter} should be
* thread-safe
*/
public ParameterConverters(StepMonitor monitor, ResourceLoader resourceLoader, ParameterControls parameterControls,
TableTransformers tableTransformers, Locale locale, String collectionSeparator, boolean threadSafe) {
this(monitor, new LocalizedKeywords(), resourceLoader, parameterControls, tableTransformers, locale,
collectionSeparator, threadSafe);
}
/**
* Creates a ParameterConverters for the given StepMonitor, keywords, Locale, list
* separator and thread-safety. When selecting a collectionSeparator, please make
* sure that this character doesn't have a special meaning in your Locale
* (for instance "," is used as decimal separator in some Locale)
*
* @param monitor the StepMonitor reporting the conversions
* @param resourceLoader the resource loader
* @param keywords the keywords
* @param parameterControls the parameter controls
* @param tableTransformers the table transformers
* @param locale the Locale to use when reading numbers
* @param collectionSeparator the String to use as collection separator
* @param threadSafe the boolean flag to determine if modification of {@link ParameterConverter} should be
* thread-safe
*/
public ParameterConverters(StepMonitor monitor, Keywords keywords, ResourceLoader resourceLoader,
ParameterControls parameterControls, TableTransformers tableTransformers, Locale locale,
String collectionSeparator, boolean threadSafe) {
this(monitor, new ArrayList<>(), threadSafe);
this.addConverters(
defaultConverters(keywords, resourceLoader, parameterControls, new TableParsers(keywords, this),
tableTransformers, locale, collectionSeparator));
}
/**
* Creates a ParameterConverters for the given StepMonitor, keywords, Locale, list
* separator and thread-safety. When selecting a collectionSeparator, please make
* sure that this character doesn't have a special meaning in your Locale
* (for instance "," is used as decimal separator in some Locale)
*
* @param monitor the StepMonitor reporting the conversions
* @param resourceLoader the resource loader
* @param keywords the keywords
* @param parameterControls the parameter controls
* @param tableParsers the table parsers
* @param tableTransformers the table transformers
* @param locale the Locale to use when reading numbers
* @param collectionSeparator the String to use as collection separator
* @param threadSafe the boolean flag to determine if modification of {@link ParameterConverter} should be
* thread-safe
*/
public ParameterConverters(StepMonitor monitor, Keywords keywords, ResourceLoader resourceLoader,
ParameterControls parameterControls, TableParsers tableParsers, TableTransformers tableTransformers,
Locale locale, String collectionSeparator, boolean threadSafe) {
this(monitor, new ArrayList<>(), threadSafe);
this.addConverters(
defaultConverters(keywords, resourceLoader, parameterControls, tableParsers, tableTransformers, locale,
collectionSeparator));
}
private ParameterConverters(StepMonitor monitor, List<ParameterConverter> converters, boolean threadSafe) {
this.monitor = monitor;
this.threadSafe = threadSafe;
this.converters = threadSafe ? new CopyOnWriteArrayList<>(converters)
: new ArrayList<>(converters);
}
protected ParameterConverter[] defaultConverters(Keywords keywords, ResourceLoader resourceLoader,
ParameterControls parameterControls, TableParsers tableParsers, TableTransformers tableTransformers,
Locale locale, String collectionSeparator) {
this.escapedCollectionSeparator = escapeRegexPunctuation(collectionSeparator);
ExamplesTableFactory tableFactory = new ExamplesTableFactory(keywords, resourceLoader, this, parameterControls,
tableParsers, tableTransformers);
JsonFactory jsonFactory = new JsonFactory();
return new ParameterConverter[] {
new BooleanConverter(),
new NumberConverter(NumberFormat.getInstance(locale)),
new StringConverter(),
new StringListConverter(escapedCollectionSeparator),
new DateConverter(),
new EnumConverter(),
new ExamplesTableConverter(tableFactory),
new ExamplesTableParametersConverter(tableFactory),
new JsonConverter(jsonFactory),
new FunctionalParameterConverter<>(String.class, Path.class, Paths::get),
new FunctionalParameterConverter<>(String.class, Currency.class, Currency::getInstance),
new FunctionalParameterConverter<>(String.class, Pattern.class, Pattern::compile),
new FunctionalParameterConverter<>(String.class, File.class, File::new),
new FunctionalParameterConverter<>(String.class, Verbatim.class, Verbatim::new),
// java.time.* converters
new FunctionalParameterConverter<>(String.class, Duration.class, Duration::parse),
new FunctionalParameterConverter<>(String.class, Instant.class, Instant::parse),
new FunctionalParameterConverter<>(String.class, LocalDate.class, LocalDate::parse),
new FunctionalParameterConverter<>(String.class, LocalDateTime.class, LocalDateTime::parse),
new FunctionalParameterConverter<>(String.class, LocalTime.class, LocalTime::parse),
new FunctionalParameterConverter<>(String.class, MonthDay.class, MonthDay::parse),
new FunctionalParameterConverter<>(String.class, OffsetDateTime.class, OffsetDateTime::parse),
new FunctionalParameterConverter<>(String.class, OffsetTime.class, OffsetTime::parse),
new FunctionalParameterConverter<>(String.class, Period.class, Period::parse),
new FunctionalParameterConverter<>(String.class, Year.class, Year::parse),
new FunctionalParameterConverter<>(String.class, YearMonth.class, YearMonth::parse),
new FunctionalParameterConverter<>(String.class, ZonedDateTime.class, ZonedDateTime::parse),
new FunctionalParameterConverter<>(String.class, ZoneId.class, ZoneId::of),
new FunctionalParameterConverter<>(String.class, ZoneOffset.class, ZoneOffset::of),
// Converters for Optional types
new FunctionalParameterConverter<>(String.class, OptionalDouble.class,
value -> OptionalDouble.of((double) this.convert(value, double.class))
),
new FunctionalParameterConverter<>(String.class, OptionalInt.class,
value -> OptionalInt.of((int) this.convert(value, int.class))
),
new FunctionalParameterConverter<>(String.class, OptionalLong.class,
value -> OptionalLong.of((long) this.convert(value, long.class))
)
};
}
// TODO : This is a duplicate from RegExpPrefixCapturing
private String escapeRegexPunctuation(String matchThis) {
return matchThis.replaceAll("([\\[\\]\\{\\}\\?\\^\\.\\*\\(\\)\\+\\\\])", "\\\\$1");
}
public ParameterConverters addConverters(ParameterConverter... converters) {
return addConverters(asList(converters));
}
public ParameterConverters addConverters(List<? extends ParameterConverter> converters) {
this.converters.addAll(0, converters);
return this;
}
private static boolean isChainComplete(Queue<ParameterConverter> convertersChain) {
return !convertersChain.isEmpty() && isBaseType(convertersChain.peek().getSourceType());
}
private static Object applyConverters(Object value, Type basicType, Queue<ParameterConverter> convertersChain) {
Object identity = convertersChain.peek().convertValue(value, basicType);
return convertersChain.stream().skip(1).reduce(identity,
(v, c) -> c.convertValue(v, c.getTargetType()), (l, r) -> l);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public Object convert(String value, Type type) {
Queue<ParameterConverter> converters = findConverters(type);
if (isChainComplete(converters)) {
Object converted = applyConverters(value, type, converters);
Queue<Class<?>> classes = converters.stream().map(ParameterConverter::getClass)
.collect(Collectors.toCollection(LinkedList::new));
monitor.convertedValueOfType(value, type, converted, classes);
return converted;
}
if (isAssignableFromRawType(Optional.class, type)) {
Type elementType = argumentType(type);
return Optional.of(convert(value, elementType));
}
if (isAssignableFromRawType(Collection.class, type)) {
Type elementType = argumentType(type);
Collection collection = createCollection(rawClass(type));
if (collection != null) {
Queue<ParameterConverter> typeConverters = findConverters(elementType);
if (!typeConverters.isEmpty()) {
Type sourceType = typeConverters.peek().getSourceType();
if (isBaseType(sourceType)) {
fillCollection(value, escapedCollectionSeparator, typeConverters, elementType, collection);
} else if (isAssignableFrom(Parameters.class, sourceType)) {
ExamplesTable table = (ExamplesTable) findBaseConverter(ExamplesTable.class).convertValue(value,
String.class);
fillCollection(table.getRowsAsParameters(), typeConverters, elementType, collection);
}
return collection;
}
}
}
if (type instanceof Class) {
Class clazz = (Class) type;
if (clazz.isArray()) {
String[] elements = parseElements(value, escapedCollectionSeparator);
Class elementType = clazz.getComponentType();
ParameterConverter elementConverter = findBaseConverter(elementType);
Object array = createArray(elementType, elements.length);
if (elementConverter != null && array != null) {
fillArray(elements, elementConverter, elementType, array);
return array;
}
}
}
throw new ParameterConversionFailed("No parameter converter for " + type);
}
private ParameterConverter findBaseConverter(Type type) {
for (ParameterConverter converter : converters) {
if (converter.canConvertFrom(String.class) && converter.canConvertTo(type)) {
return converter;
}
}
return null;
}
private Queue<ParameterConverter> findConverters(Type type) {
LinkedList<ParameterConverter> convertersChain = new LinkedList<>();
putConverters(type, convertersChain);
return convertersChain;
}
private void putConverters(Type type, LinkedList<ParameterConverter> container) {
for (ParameterConverter converter : converters) {
if (converter.canConvertTo(type)) {
container.addFirst(converter);
Type sourceType = converter.getSourceType();
if (isBaseType(sourceType)) {
break;
}
putConverters(sourceType, container);
}
}
}
private static boolean isBaseType(Type type) {
return String.class.isAssignableFrom((Class<?>) type);
}
private static boolean isAssignableFrom(Class<?> clazz, Type type) {
return type instanceof Class<?> && clazz.isAssignableFrom((Class<?>) type);
}
private static boolean isAssignableFromRawType(Class<?> clazz, Type type) {
return type instanceof ParameterizedType && isAssignableFrom(clazz, ((ParameterizedType) type).getRawType());
}
private static Class<?> rawClass(Type type) {
return (Class<?>) ((ParameterizedType) type).getRawType();
}
private static Class<?> argumentClass(Type type) {
if (type instanceof ParameterizedType) {
Type typeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
return typeArgument instanceof ParameterizedType ? rawClass(typeArgument) : (Class<?>) typeArgument;
} else {
return (Class<?>) type;
}
}
private static Type argumentType(Type type) {
return ((ParameterizedType) type).getActualTypeArguments()[0];
}
private static boolean isAnnotationPresent(Type type, Class<? extends Annotation> annotationClass) {
if (type instanceof ParameterizedType) {
return rawClass(type).isAnnotationPresent(annotationClass) || argumentClass(type).isAnnotationPresent(
annotationClass);
}
return type instanceof Class && ((Class<?>) type).isAnnotationPresent(annotationClass);
}
private static String[] parseElements(String value, String elementSeparator) {
String[] elements = value.trim().isEmpty() ? new String[0] : value.split(elementSeparator);
Arrays.setAll(elements, i -> elements[i].trim());
return elements;
}
private static void fillCollection(String value, String elementSeparator, Queue<ParameterConverter> convertersChain,
Type elementType, Collection convertedValues) {
fillCollection(asList(parseElements(value, elementSeparator)), convertersChain, elementType, convertedValues);
}
private static void fillCollection(Collection elements, Queue<ParameterConverter> convertersChain,
Type elementType, Collection convertedValues) {
for (Object element : elements) {
Object convertedValue = applyConverters(element, elementType, convertersChain);
convertedValues.add(convertedValue);
}
}
private static <T> void fillArray(String[] elements, ParameterConverter<String, T> elementConverter,
Type elementType, Object convertedValues) {
for (int i = 0; i < elements.length; i++) {
T convertedValue = elementConverter.convertValue(elements[i], elementType);
Array.set(convertedValues, i, convertedValue);
}
}
@SuppressWarnings("unchecked")
private static <E> Collection<E> createCollection(Class<?> collectionType) {
if (collectionType.isInterface()) {
if (Set.class == collectionType) {
return new HashSet<>();
} else if (List.class == collectionType) {
return new ArrayList<>();
} else if (SortedSet.class == collectionType || NavigableSet.class == collectionType) {
return new TreeSet<>();
}
}
try {
return (Collection<E>) collectionType.getConstructor().newInstance();
} catch (@SuppressWarnings("unused") Throwable t) {
// Could not instantiate Collection type, swallowing exception quietly
}
return null;
}
private static Object createArray(Class<?> elementType, int length) {
try {
return Array.newInstance(elementType, length);
} catch (Throwable e) {
// Could not instantiate array, swallowing exception quietly
}
return null;
}
public ParameterConverters newInstanceAdding(ParameterConverter converter) {
List<ParameterConverter> convertersForNewInstance = new ArrayList<>(converters);
convertersForNewInstance.add(converter);
return new ParameterConverters(monitor, convertersForNewInstance, threadSafe);
}
/**
* A parameter converter for generic type of source input and target output.
* The converters can be chained to allow for the target of one converter
* can be used as the source for another.
*
* @param <T> the target converted output
* @param <S> the source input value
*/
public interface ParameterConverter<S, T> {
/**
* Return {@code true} if the converter can convert to the desired target type.
* @param type the type descriptor that describes the requested result type
* @return {@code true} if that conversion can be performed
*/
boolean canConvertTo(Type type);
/**
* Return {@code true} if the converter can convert from the desired target type.
* @param type the type descriptor that describes the source type
* @return {@code true} if that conversion can be performed
*/
boolean canConvertFrom(Type type);
/**
* Convert the value from one type to another, for example from a {@code boolean} to a {@code String}.
* @param value the value to be converted
* @param type the type descriptor that supplies extra information about the requested result type
* @return the converted value
*/
T convertValue(S value, Type type);
Type getSourceType();
Type getTargetType();
}
@SuppressWarnings("serial")
public static class ParameterConversionFailed extends RuntimeException {
public ParameterConversionFailed(String message) {
super(message);
}
public ParameterConversionFailed(String message, Throwable cause) {
super(message, cause);
}
}
public abstract static class FromStringParameterConverter<T> extends AbstractParameterConverter<String, T> {
public FromStringParameterConverter() {
}
public FromStringParameterConverter(Type targetType) {
super(String.class, targetType);
}
}
public abstract static class AbstractParameterConverter<S, T> implements ParameterConverter<S, T> {
private final Type sourceType;
private final Type targetType;
public AbstractParameterConverter() {
Map<TypeVariable<?>, Type> types = TypeUtils.getTypeArguments(getClass(), ParameterConverter.class);
TypeVariable<?>[] typeVariables = ParameterConverter.class.getTypeParameters();
this.sourceType = types.get(typeVariables[0]);
this.targetType = types.get(typeVariables[1]);
}
public AbstractParameterConverter(Type sourceType, Type targetType) {
this.sourceType = sourceType;
this.targetType = targetType;
}
@Override
public boolean canConvertTo(Type type) {
return isAssignable(targetType, type);
}
@Override
public boolean canConvertFrom(Type type) {
return isAssignable(sourceType, type);
}
public Type getSourceType() {
return sourceType;
}
public Type getTargetType() {
return targetType;
}
private static boolean isAssignable(Type from, Type to) {
if (from instanceof Class<?>) {
return isAssignableFrom((Class<?>) from, to);
}
return from.equals(to);
}
}
public static class FunctionalParameterConverter<S, T> extends AbstractParameterConverter<S, T> {
private final Function<S, T> converterFunction;
public FunctionalParameterConverter(Class<S> sourceType, Class<T> targetType,
Function<S, T> converterFunction) {
super(sourceType, targetType);
this.converterFunction = converterFunction;
}
protected FunctionalParameterConverter(Function<S, T> converterFunction) {
this.converterFunction = converterFunction;
}
@Override
public T convertValue(S value, Type type) {
return converterFunction.apply(value);
}
}
public abstract static class AbstractListParameterConverter<T> extends FromStringParameterConverter<List<T>> {
private final String valueSeparator;
private final Queue<ParameterConverter> elementConverters;
public AbstractListParameterConverter(String valueSeparator, FromStringParameterConverter<T> elementConverter) {
this.valueSeparator = valueSeparator;
this.elementConverters = new LinkedList<>();
this.elementConverters.add(elementConverter);
}
@Override
public boolean canConvertTo(Type type) {
return isAssignableFromRawType(List.class, type) && elementConverters.peek().canConvertTo(
argumentType(type));
}
@Override
public List<T> convertValue(String value, Type type) {
Type elementType = argumentType(type);
List<T> convertedValues = new ArrayList<>();
fillCollection(value, valueSeparator, elementConverters, elementType, convertedValues);
return convertedValues;
}
}
/**
* Converts values to numbers, supporting any subclass of {@link Number} (including generic Number type), and it
* unboxed counterpart, using a {@link NumberFormat} to parse to a {@link Number} and to convert it to a specific
* number type:
* <ul>
* <li>Byte, byte: {@link Number#byteValue()}</li>
* <li>Short, short: {@link Number#shortValue()}</li>
* <li>Integer, int: {@link Number#intValue()}</li>
* <li>Float, float: {@link Number#floatValue()}</li>
* <li>Long, long: {@link Number#longValue()}</li>
* <li>Double, double: {@link Number#doubleValue()}</li>
* <li>BigInteger: {@link BigInteger#valueOf(long)}</li>
* <li>BigDecimal: {@link BigDecimal#valueOf(double)}</li>
* </ul>
* If no number format is provided, it defaults to {@link NumberFormat#getInstance()}.
* <p>The localized instance {@link NumberFormat#getInstance(Locale)} can be used to convert numbers in specific
* locales.</p>
*/
public static class NumberConverter extends FromStringParameterConverter<Number> {
private static List<Class<?>> primitiveTypes = asList(byte.class, short.class, int.class, float.class,
long.class, double.class);
private final NumberFormat numberFormat;
private ThreadLocal<NumberFormat> threadLocalNumberFormat = new ThreadLocal<>();
public NumberConverter() {
this(NumberFormat.getInstance(DEFAULT_NUMBER_FORMAT_LOCAL));
}
public NumberConverter(NumberFormat numberFormat) {
synchronized (this) {
this.numberFormat = numberFormat;
this.threadLocalNumberFormat.set((NumberFormat) this.numberFormat.clone());
}
}
@Override
public boolean canConvertTo(Type type) {
return super.canConvertTo(type) || primitiveTypes.contains(type);
}
@Override
public Number convertValue(String value, Type type) {
try {
Number n = numberFormat().parse(value);
if (type == Byte.class || type == byte.class) {
return n.byteValue();
} else if (type == Short.class || type == short.class) {
return n.shortValue();
} else if (type == Integer.class || type == int.class) {
return n.intValue();
} else if (type == Float.class || type == float.class) {
return n.floatValue();
} else if (type == Long.class || type == long.class) {
return n.longValue();
} else if (type == Double.class || type == double.class) {
return n.doubleValue();
} else if (type == BigInteger.class) {
return BigInteger.valueOf(n.longValue());
} else if (type == BigDecimal.class) {
return new BigDecimal(canonicalize(value));
} else if (type == AtomicInteger.class) {
return new AtomicInteger(Integer.parseInt(value));
} else if (type == AtomicLong.class) {
return new AtomicLong(Long.parseLong(value));
} else {
return n;
}
} catch (NumberFormatException | ParseException e) {
throw new ParameterConversionFailed(value, e);
}
}
/**
* Return NumberFormat instance with preferred locale threadsafe
*
* @return A threadlocal version of original NumberFormat instance
*/
private NumberFormat numberFormat() {
if (threadLocalNumberFormat.get() == null) {
synchronized (this) {
threadLocalNumberFormat.set((NumberFormat) numberFormat.clone());
}
}
return threadLocalNumberFormat.get();
}
/**
* Canonicalize a number representation to a format suitable for the {@link BigDecimal#BigDecimal(String)}
* constructor, taking into account the settings of the currently configured DecimalFormat.
*
* @param value a localized number value
* @return A canonicalized string value suitable for consumption by BigDecimal
*/
private String canonicalize(String value) {
char decimalPointSeparator = '.'; // default
char minusSign = '-'; // default
String rxNotDigits = "[\\.,]";
StringBuilder builder = new StringBuilder(value.length());
// override defaults according to numberFormat's settings
if (numberFormat() instanceof DecimalFormat) {
DecimalFormatSymbols decimalFormatSymbols = ((DecimalFormat) numberFormat()).getDecimalFormatSymbols();
minusSign = decimalFormatSymbols.getMinusSign();
decimalPointSeparator = decimalFormatSymbols.getDecimalSeparator();
}
value = value.trim();
int decimalPointPosition = value.lastIndexOf(decimalPointSeparator);
int firstDecimalPointPosition = value.indexOf(decimalPointSeparator);
if (firstDecimalPointPosition != decimalPointPosition) {
throw new NumberFormatException("Invalid format, more than one decimal point has been found.");
}
if (decimalPointPosition != -1) {
String sf = value.substring(0, decimalPointPosition).replaceAll(rxNotDigits, "");
String dp = value.substring(decimalPointPosition + 1).replaceAll(rxNotDigits, "");
builder.append(sf);
builder.append('.'); // fixed "." for BigDecimal constructor
builder.append(dp);
} else {
builder.append(value.replaceAll(rxNotDigits, ""));
}
boolean isNegative = value.charAt(0) == minusSign;
if (isNegative) {
builder.setCharAt(0, '-'); // fixed "-" for BigDecimal constructor
}
return builder.toString();
}
}
/**
* Converts value to list of numbers. Splits value to a list, using an
* injectable value separator (defaulting to ",") and converts each element
* of list via the {@link NumberConverter}, using the {@link NumberFormat}
* provided (defaulting to {@link NumberFormat#getInstance()}
* ).
*/
public static class NumberListConverter extends AbstractListParameterConverter<Number> {
public NumberListConverter() {
this(NumberFormat.getInstance(DEFAULT_NUMBER_FORMAT_LOCAL), DEFAULT_COLLECTION_SEPARATOR);
}
/**
* Creates converter for a list of numbers using the specified number format and value separator
*
* @param numberFormat Specific NumberFormat to use.
* @param valueSeparator A regexp to use as list separate
*/
public NumberListConverter(NumberFormat numberFormat, String valueSeparator) {
super(valueSeparator, new NumberConverter(numberFormat));
}
}
public static class StringConverter extends FromStringParameterConverter<String> {
private static final String NEWLINES_PATTERN = "(\n)|(\r\n)";
private static final String SYSTEM_NEWLINE = System.getProperty("line.separator");
@Override
public String convertValue(String value, Type type) {
return value != null ? value.replaceAll(NEWLINES_PATTERN, SYSTEM_NEWLINE) : null;
}
}
/**
* Converts value to list of String. Splits value to a list, using an
* injectable value separator (defaults to ",") and trimming each element of
* the list.
*/
public static class StringListConverter extends AbstractListParameterConverter<String> {
public StringListConverter() {
this(DEFAULT_COLLECTION_SEPARATOR);
}
/**
* Creates converter for a list of strings using the value separator
*
* @param valueSeparator A regexp to use as list separator
*/
public StringListConverter(String valueSeparator) {
super(valueSeparator, new StringConverter());
}
@Override
public List<String> convertValue(String value, Type type) {
if (value.trim().isEmpty()) {
return Collections.emptyList();
}
return super.convertValue(value, type);
}
}
/**
* Parses value to a {@link Date} using an injectable {@link DateFormat}
* (defaults to <b>new SimpleDateFormat("dd/MM/yyyy")</b>)
*/
public static class DateConverter extends FromStringParameterConverter<Date> {
public static final DateFormat DEFAULT_FORMAT = new SimpleDateFormat("dd/MM/yyyy");
private final DateFormat dateFormat;
public DateConverter() {
this(DEFAULT_FORMAT);
}
public DateConverter(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public Date convertValue(String value, Type type) {
try {
return dateFormat.parse(value);
} catch (ParseException e) {
throw new ParameterConversionFailed("Failed to convert value " + value + " with date format "
+ (dateFormat instanceof SimpleDateFormat ? ((SimpleDateFormat) dateFormat).toPattern()
: dateFormat), e);
}
}
}
public static class BooleanConverter extends FromStringParameterConverter<Boolean> {
private final String trueValue;
private final String falseValue;
public BooleanConverter() {
this(DEFAULT_TRUE_VALUE, DEFAULT_FALSE_VALUE);
}
public BooleanConverter(String trueValue, String falseValue) {
this.trueValue = trueValue;
this.falseValue = falseValue;
}
@Override
public boolean canConvertTo(Type type) {
return super.canConvertTo(type) || isAssignableFrom(Boolean.TYPE, type);
}
@Override
public Boolean convertValue(String value, Type type) {
try {
return BooleanUtils.toBoolean(value, trueValue, falseValue);
} catch (IllegalArgumentException e) {
return false;
}
}
}
public static class BooleanListConverter extends AbstractListParameterConverter<Boolean> {
public BooleanListConverter() {
this(DEFAULT_COLLECTION_SEPARATOR, DEFAULT_TRUE_VALUE, DEFAULT_FALSE_VALUE);
}
public BooleanListConverter(String valueSeparator) {
this(valueSeparator, DEFAULT_TRUE_VALUE, DEFAULT_FALSE_VALUE);
}
public BooleanListConverter(String valueSeparator, String trueValue, String falseValue) {
super(valueSeparator, new BooleanConverter(trueValue, falseValue));
}
}
/**
* Parses value to any {@link Enum}
*/
public static class EnumConverter extends FromStringParameterConverter<Enum<?>> {
@Override
public boolean canConvertTo(Type type) {
return type instanceof Class<?> && ((Class<?>) type).isEnum();
}
@Override
public Enum<?> convertValue(String value, Type type) {
String typeClass = ((Class<?>) type).getName();
Class<?> enumClass = (Class<?>) type;
Method valueOfMethod = null;
try {
valueOfMethod = enumClass.getMethod("valueOf", String.class);
valueOfMethod.setAccessible(true);
return (Enum<?>) valueOfMethod.invoke(enumClass, new Object[] { value });
} catch (Exception e) {
throw new ParameterConversionFailed("Failed to convert " + value + " for Enum " + typeClass, e);
}
}
}
/**
* <p>An {@link EnumConverter} allowing stories prose to be more natural. Before performing the actual conversion,
* it ransforms values to upper-case, with any non-alphanumeric character replaced by an underscore ('_').</p>
* <p><b>Example</b>: assuming we have defined the step "{@code Given I am on the $page}" which is mapped to the
* method {@code iAmOnPage(PageEnum page)}, we can then write in a scenario:
* <pre>{@code
* Given I am on the login page
* }</pre>
* instead of:
* <pre>{@code
* Given I am on the LOGIN_PAGE
* }</pre>
* </p>
* <p><b>Warning</b>. This <i>requires</i> enum constants to follow the
* <a href="https://google-styleguide.googlecode.com/svn/trunk/javaguide.html#s5.2.4-constant-names">
* standard conventions for constant names</a>, i.e. all uppercase letters, with words separated by underscores.</p>
*/
public static class FluentEnumConverter extends EnumConverter {
@Override
public Enum<?> convertValue(String value, Type type) {
return super.convertValue(value.replaceAll("\\W", "_").toUpperCase(), type);
}
}
/**
* Parses value to list of the same {@link Enum}, using an injectable value
* separator (defaults to ",") and trimming each element of the list.
*/
public static class EnumListConverter extends AbstractListParameterConverter<Enum<?>> {
public EnumListConverter() {
this(DEFAULT_COLLECTION_SEPARATOR);
}
public EnumListConverter(String valueSeparator) {
super(valueSeparator, new EnumConverter());
}
}
/**
* Converts value to {@link ExamplesTable} using a
* {@link ExamplesTableFactory}.
*/
public static class ExamplesTableConverter extends FunctionalParameterConverter<String, ExamplesTable> {
public ExamplesTableConverter(ExamplesTableFactory factory) {
super(factory::createExamplesTable);
}
}
/**
* Converts ExamplesTable to list of parameters, mapped to annotated custom
* types.
*/
public static class ExamplesTableParametersConverter extends FromStringParameterConverter<Object> {
private final ExamplesTableFactory factory;
public ExamplesTableParametersConverter(ExamplesTableFactory factory) {
this.factory = factory;
}
@Override
public boolean canConvertTo(Type type) {
return isExamplesTableParameters(type);
}
@Override
public Object convertValue(String value, Type type) {
List<?> rows = factory.createExamplesTable(value).getRowsAs(argumentClass(type));
if (isAssignableFromRawType(List.class, type)) {
return rows;
}
int rowCount = rows.size();
isTrue(rowCount == 1,
"Exactly one row is expected in ExamplesTable in order to convert it to %s, but found %d row(s)",
type, rowCount);
return rows.get(0);
}
public static boolean isExamplesTableParameters(Type type) {
return isAnnotationPresent(type, AsParameters.class);
}
}
public static class JsonConverter extends FromStringParameterConverter<Object> {
private final JsonFactory factory;
public JsonConverter() {
this(new JsonFactory());
}
public JsonConverter(final JsonFactory factory) {
this.factory = factory;
}
@Override
public boolean canConvertTo(final Type type) {
return isAnnotationPresent(type, AsJson.class);
}
@Override
public Object convertValue(final String value, final Type type) {
return factory.createJson(value, type);
}
}
public static class JsonFactory {
private Keywords keywords;
private final ResourceLoader resourceLoader;
public JsonFactory() {
this(new LocalizedKeywords());
}
public JsonFactory(final Keywords keywords) {
this(keywords, new LoadFromClasspath());
}
public JsonFactory(final ResourceLoader resourceLoader) {
this(new LocalizedKeywords(), resourceLoader);
}
public JsonFactory(final Keywords keywords, final ResourceLoader resourceLoader) {
this.keywords = keywords;
this.resourceLoader = resourceLoader;
}
public JsonFactory(final Configuration configuration) {
this.keywords = configuration.keywords();
this.resourceLoader = configuration.storyLoader();
}
public Object createJson(final String input, final Type type) {
String jsonAsString;
if (isBlank(input) || isJson(input)) {
jsonAsString = input;
} else {
jsonAsString = resourceLoader.loadResourceAsText(input);
}
return new Gson().fromJson(jsonAsString, type);
}
protected boolean isJson(final String input) {
return (input.startsWith("[") && input.endsWith("]")) || (input.startsWith("{") && input.endsWith("}"));
}
public void useKeywords(final Keywords keywords) {
this.keywords = keywords;
}
public Keywords keywords() {
return this.keywords;
}
}
/**
* Invokes method on instance to return value.
*/
public static class MethodReturningConverter extends FromStringParameterConverter<Object> {
private Method method;
private Class<?> stepsType;
private InjectableStepsFactory stepsFactory;
public MethodReturningConverter(Method method, Object instance) {
this(method, instance.getClass(), new InstanceStepsFactory(new MostUsefulConfiguration(), instance));
}
public MethodReturningConverter(Method method, Class<?> stepsType, InjectableStepsFactory stepsFactory) {
this.method = method;
this.stepsType = stepsType;
this.stepsFactory = stepsFactory;
}
@Override
public boolean canConvertTo(Type type) {
return isAssignableFrom(method.getReturnType(), type);
}
@Override
public Object convertValue(String value, Type type) {
try {
Object instance = instance();
return method.invoke(instance, value);
} catch (Exception e) {
throw new ParameterConversionFailed("Failed to invoke method " + method + " with value " + value
+ " in " + type, e);
}
}
private Object instance() {
return stepsFactory.createInstanceOfType(stepsType);
}
}
}