package cucumber.runtime.table; import cucumber.api.DataTable; import cucumber.deps.com.thoughtworks.xstream.converters.ConversionException; import cucumber.deps.com.thoughtworks.xstream.converters.SingleValueConverter; import cucumber.deps.com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter; import cucumber.deps.com.thoughtworks.xstream.io.HierarchicalStreamReader; import cucumber.runtime.CucumberException; import cucumber.runtime.ParameterInfo; import cucumber.runtime.xstream.CellWriter; import cucumber.runtime.xstream.ComplexTypeWriter; import cucumber.runtime.xstream.ListOfComplexTypeReader; import cucumber.runtime.xstream.ListOfSingleValueWriter; import cucumber.runtime.xstream.LocalizedXStreams; import cucumber.runtime.xstream.MapWriter; import gherkin.formatter.model.Comment; import gherkin.formatter.model.DataTableRow; import gherkin.util.Mapper; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static cucumber.runtime.Utils.listItemType; import static cucumber.runtime.Utils.mapKeyType; import static cucumber.runtime.Utils.mapValueType; import static gherkin.util.FixJava.map; import static java.util.Arrays.asList; /** * This class converts a {@link cucumber.api.DataTable} to various other types. */ public class TableConverter { private static final List<Comment> NO_COMMENTS = Collections.emptyList(); private final LocalizedXStreams.LocalizedXStream xStream; private final ParameterInfo parameterInfo; public TableConverter(LocalizedXStreams.LocalizedXStream xStream, ParameterInfo parameterInfo) { this.xStream = xStream; this.parameterInfo = parameterInfo; } /** * This method converts a {@link cucumber.api.DataTable} to abother type. * When a Step Definition is passed a Gherkin Data Table, the runtime will use this method to convert the * {@link cucumber.api.DataTable} to the declared type before invoking the Step Definition. * <p/> * This method uses reflection to inspect the type and delegates to the appropriate {@code toXxx} method. * * @param dataTable the table to convert * @param type the type to convert to * @param transposed whether the table should be transposed first. * @return the transformed object. */ public <T> T convert(DataTable dataTable, Type type, boolean transposed) { if (transposed) { dataTable = dataTable.transpose(); } if (type == null || (type instanceof Class && ((Class) type).isAssignableFrom(DataTable.class))) { return (T) dataTable; } Type mapKeyType = mapKeyType(type); if (mapKeyType != null) { Type mapValueType = mapValueType(type); return (T) toMap(dataTable, mapKeyType, mapValueType); } Type itemType = listItemType(type); if (itemType == null) { throw new CucumberException("Not a Map or List type: " + type); } Type listItemType = listItemType(itemType); if (listItemType != null) { return (T) toLists(dataTable, listItemType); } else { SingleValueConverter singleValueConverter = xStream.getSingleValueConverter(itemType); if (singleValueConverter != null) { return (T) toList(dataTable, singleValueConverter); } else { if (itemType instanceof Class) { if (Map.class.equals(itemType)) { // Non-generic map return (T) toMaps(dataTable, String.class, String.class); } else { return (T) toListOfComplexType(dataTable, (Class) itemType); } } else { return (T) toMaps(dataTable, mapKeyType(itemType), mapValueType(itemType)); } } } } private <T> List<T> toListOfComplexType(DataTable dataTable, Class<T> itemType) { HierarchicalStreamReader reader = new ListOfComplexTypeReader(itemType, convertTopCellsToFieldNames(dataTable), dataTable.cells(1)); try { xStream.setParameterInfo(parameterInfo); return Collections.unmodifiableList((List<T>) xStream.unmarshal(reader)); } catch (AbstractReflectionConverter.UnknownFieldException e) { throw new CucumberException(e.getShortMessage()); } catch (AbstractReflectionConverter.DuplicateFieldException e) { throw new CucumberException(e.getShortMessage()); } catch (ConversionException e) { if (e.getCause() instanceof NullPointerException) { throw new CucumberException(String.format("Can't assign null value to one of the primitive fields in %s. Please use boxed types.", e.get("class"))); } else { throw new CucumberException(e); } } finally { xStream.unsetParameterInfo(); } } public <T> List<T> toList(DataTable dataTable, Type itemType) { SingleValueConverter itemConverter = xStream.getSingleValueConverter(itemType); if (itemConverter != null) { return toList(dataTable, itemConverter); } else { if (itemType instanceof Class) { return toListOfComplexType(dataTable, (Class<T>) itemType); } else { throw new CucumberException(String.format("Can't convert DataTable to List<%s>", itemType)); } } } private <T> List<T> toList(DataTable dataTable, SingleValueConverter itemConverter) { List<T> result = new ArrayList<T>(); for (List<String> row : dataTable.raw()) { for (String cell : row) { result.add((T) itemConverter.fromString(cell)); } } return Collections.unmodifiableList(result); } public <T> List<List<T>> toLists(DataTable dataTable, Type itemType) { try { xStream.setParameterInfo(parameterInfo); SingleValueConverter itemConverter = xStream.getSingleValueConverter(itemType); if (itemConverter == null) { throw new CucumberException(String.format("Can't convert DataTable to List<List<%s>>", itemType)); } List<List<T>> result = new ArrayList<List<T>>(); for (List<String> row : dataTable.raw()) { List<T> convertedRow = new ArrayList<T>(); for (String cell : row) { convertedRow.add((T) itemConverter.fromString(cell)); } result.add(Collections.unmodifiableList(convertedRow)); } return Collections.unmodifiableList(result); } finally { xStream.unsetParameterInfo(); } } public <K, V> Map<K, V> toMap(DataTable dataTable, Type keyType, Type valueType) { try { xStream.setParameterInfo(parameterInfo); SingleValueConverter keyConverter = xStream.getSingleValueConverter(keyType); SingleValueConverter valueConverter = xStream.getSingleValueConverter(valueType); if (keyConverter == null || valueConverter == null) { throw new CucumberException(String.format("Can't convert DataTable to Map<%s,%s>", keyType, valueType)); } Map<K, V> result = new LinkedHashMap<K, V>(); for (List<String> row : dataTable.raw()) { if (row.size() != 2) { throw new CucumberException("A DataTable can only be converted to a Map when there are 2 columns"); } K key = (K) keyConverter.fromString(row.get(0)); V value = (V) valueConverter.fromString(row.get(1)); result.put(key, value); } return Collections.unmodifiableMap(result); } finally { xStream.unsetParameterInfo(); } } public <K, V> List<Map<K, V>> toMaps(DataTable dataTable, Type keyType, Type valueType) { try { xStream.setParameterInfo(parameterInfo); SingleValueConverter keyConverter = xStream.getSingleValueConverter(keyType); SingleValueConverter valueConverter = xStream.getSingleValueConverter(valueType); if (keyConverter == null || valueConverter == null) { throw new CucumberException(String.format("Can't convert DataTable to List<Map<%s,%s>>", keyType, valueType)); } List<Map<K, V>> result = new ArrayList<Map<K, V>>(); List<String> keyStrings = dataTable.topCells(); List<K> keys = new ArrayList<K>(); for (String keyString : keyStrings) { keys.add((K) keyConverter.fromString(keyString)); } List<List<String>> valueRows = dataTable.cells(1); for (List<String> valueRow : valueRows) { Map<K, V> map = new LinkedHashMap<K, V>(); int i = 0; for (String cell : valueRow) { map.put(keys.get(i), (V) valueConverter.fromString(cell)); i++; } result.add(Collections.unmodifiableMap(map)); } return Collections.unmodifiableList(result); } finally { xStream.unsetParameterInfo(); } } /** * Converts a List of objects to a DataTable. * * @param objects the objects to convert * @param columnNames an explicit list of column names * @return a DataTable */ public DataTable toTable(List<?> objects, String... columnNames) { try { xStream.setParameterInfo(parameterInfo); List<String> header = null; List<List<String>> valuesList = new ArrayList<List<String>>(); for (Object object : objects) { CellWriter writer; if (isListOfSingleValue(object)) { // XStream needs an instance of ArrayList object = new ArrayList<Object>((List<Object>) object); writer = new ListOfSingleValueWriter(); } else if (isArrayOfSingleValue(object)) { // XStream needs an instance of ArrayList object = new ArrayList<Object>(asList((Object[]) object)); writer = new ListOfSingleValueWriter(); } else if (object instanceof Map) { writer = new MapWriter(asList(columnNames)); } else { writer = new ComplexTypeWriter(asList(columnNames)); } xStream.marshal(object, writer); if (header == null) { header = writer.getHeader(); } List<String> values = writer.getValues(); valuesList.add(values); } return createDataTable(header, valuesList); } finally { xStream.unsetParameterInfo(); } } private DataTable createDataTable(List<String> header, List<List<String>> valuesList) { List<DataTableRow> gherkinRows = new ArrayList<DataTableRow>(); if (header != null) { gherkinRows.add(gherkinRow(header)); } for (List<String> values : valuesList) { gherkinRows.add(gherkinRow(values)); } return new DataTable(gherkinRows, this); } private DataTableRow gherkinRow(List<String> cells) { return new DataTableRow(NO_COMMENTS, cells, 0); } private List<String> convertTopCellsToFieldNames(DataTable dataTable) { final StringConverter mapper = new CamelCaseStringConverter(); return map(dataTable.topCells(), new Mapper<String, String>() { @Override public String map(String attributeName) { return mapper.map(attributeName); } }); } private boolean isListOfSingleValue(Object object) { if (object instanceof List) { List list = (List) object; return !list.isEmpty() && xStream.getSingleValueConverter(list.get(0).getClass()) != null; } return false; } private boolean isArrayOfSingleValue(Object object) { if (object.getClass().isArray()) { Object[] array = (Object[]) object; return array.length > 0 && xStream.getSingleValueConverter(array[0].getClass()) != null; } return false; } }