diff --git a/core/src/main/java/cucumber/runtime/table/PascalCaseStringConverter.java b/core/src/main/java/cucumber/runtime/table/PascalCaseStringConverter.java new file mode 100644 index 0000000000..1455078458 --- /dev/null +++ b/core/src/main/java/cucumber/runtime/table/PascalCaseStringConverter.java @@ -0,0 +1,35 @@ +package cucumber.runtime.table; + +import java.util.regex.Pattern; + +public class PascalCaseStringConverter implements StringConverter { + + private static final String WHITESPACE = " "; + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + + @Override + public String map(String string) { + String[] splitted = normalizeSpace(string).split(WHITESPACE); + for (int i = 0; i < splitted.length; i++) { + splitted[i] = capitalize(splitted[i]); + } + return join(splitted); + } + + private String join(String[] splitted) { + StringBuilder sb = new StringBuilder(); + for (String s : splitted) { + sb.append(s); + } + return sb.toString(); + } + + private String normalizeSpace(String originalHeaderName) { + return WHITESPACE_PATTERN.matcher(originalHeaderName.trim()).replaceAll(WHITESPACE); + } + + private String capitalize(String string) { + return new StringBuilder(string.length()).append(Character.toTitleCase(string.charAt(0))).append(string.substring(1)).toString(); + } + +} diff --git a/core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java b/core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java index 31d0751df9..35b8f09441 100644 --- a/core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java +++ b/core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java @@ -1,18 +1,25 @@ package cucumber.runtime.xstream; + +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; +import cucumber.runtime.CucumberException; import cucumber.runtime.table.CamelCaseStringConverter; +import cucumber.runtime.table.PascalCaseStringConverter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Stack; import static java.util.Arrays.asList; public class ComplexTypeWriter extends CellWriter { private final List columnNames; - private final List fieldNames = new ArrayList(); - private final List fieldValues = new ArrayList(); - - private int nodeDepth = 0; + private final Map fields = new LinkedHashMap(); + private final Stack currentKey = new Stack(); public ComplexTypeWriter(List columnNames) { this.columnNames = columnNames; @@ -20,7 +27,7 @@ public ComplexTypeWriter(List columnNames) { @Override public List getHeader() { - return columnNames.isEmpty() ? fieldNames : columnNames; + return columnNames.isEmpty() ? new ArrayList(fields.keySet()) : columnNames; } @Override @@ -30,26 +37,26 @@ public List getValues() { String[] explicitFieldValues = new String[columnNames.size()]; int n = 0; for (String columnName : columnNames) { - int index = fieldNames.indexOf(converter.map(columnName)); - if (index == -1) { - explicitFieldValues[n] = ""; + final String convertedColumnName = converter.map(columnName); + if (fields.containsKey(convertedColumnName)) { + explicitFieldValues[n] = fields.get(convertedColumnName); } else { - explicitFieldValues[n] = fieldValues.get(index); + explicitFieldValues[n] = ""; } n++; } return asList(explicitFieldValues); } else { - return fieldValues; + return new ArrayList(fields.values()); } } @Override public void startNode(String name) { - if (nodeDepth == 1) { - this.fieldNames.add(name); + currentKey.push(name); + if (currentKey.size() == 2) { + fields.put(name, ""); } - nodeDepth++; } @Override @@ -58,12 +65,26 @@ public void addAttribute(String name, String value) { @Override public void setValue(String value) { - fieldValues.add(value == null ? "" : value); + // Add all simple types at level 2. nodeDepth 1 is the root node. + if(currentKey.size() < 2){ + return; + } + + if (currentKey.size() == 2) { + fields.put(currentKey.peek(), value == null ? "" : value); + return; + } + + final String clazz = currentKey.get(0); + final String field = currentKey.get(1); + if ((columnNames.isEmpty() || columnNames.contains(field))) { + throw createMissingConverterException(clazz, field); + } } @Override public void endNode() { - nodeDepth--; + currentKey.pop(); } @Override @@ -75,4 +96,39 @@ public void flush() { public void close() { throw new UnsupportedOperationException(); } + + private static CucumberException createMissingConverterException(String clazz, String field) { + PascalCaseStringConverter converter = new PascalCaseStringConverter(); + return new CucumberException(String.format( + "Don't know how to convert \"%s.%s\" into a table entry.\n" + + "Either exclude %s from the table by selecting the fields to include:\n" + + "\n" + + "DataTable.create(entries, \"Field\", \"Other Field\")\n" + + "\n" + + "Or try writing your own converter:\n" + + "\n" + + "@%s(%sConverter.class)\n" + + "%s %s;\n", + clazz, + field, + field, + XStreamConverter.class.getName(), + converter.map(field), + modifierAndTypeOfField(clazz, field), + field + )); + } + + private static String modifierAndTypeOfField(String clazz, String fieldName) { + try { + Field field = Class.forName(clazz).getDeclaredField(fieldName); + String simpleTypeName = field.getType().getSimpleName(); + String modifiers = Modifier.toString(field.getModifiers()); + return modifiers + " " + simpleTypeName; + } catch (NoSuchFieldException e) { + return "private Object"; + } catch (ClassNotFoundException e) { + return "private Object"; + } + } } diff --git a/core/src/test/java/cucumber/runtime/table/TableConverterTest.java b/core/src/test/java/cucumber/runtime/table/TableConverterTest.java index d83a33bd3c..f89d91c19d 100644 --- a/core/src/test/java/cucumber/runtime/table/TableConverterTest.java +++ b/core/src/test/java/cucumber/runtime/table/TableConverterTest.java @@ -2,12 +2,15 @@ import cucumber.api.DataTable; import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; +import cucumber.deps.com.thoughtworks.xstream.converters.SingleValueConverter; import cucumber.deps.com.thoughtworks.xstream.converters.javabean.JavaBeanConverter; +import cucumber.runtime.CucumberException; import cucumber.runtime.ParameterInfo; import org.junit.Test; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -15,7 +18,9 @@ import java.util.Map; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class TableConverterTest { @@ -184,6 +189,141 @@ public void converts_to_list_of_java_bean_and_almost_back() { assertEquals(" | birthDate | deathCal |\n | 1957-05-10 | 1979-02-02 |\n", table.toTable(converted).toString()); } + public static class BlogBean { + private String author; + private List tags; + private String post; + + public String getPost() { + return post; + } + + public void setPost(String post) { + this.post = post; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + } + + @Test + public void throws_cucumber_exception_for_complex_types() { + BlogBean blog = new BlogBean(); + blog.setAuthor("Tom Scott"); + blog.setTags(asList("Language", "Linguistics", " Mycenaean Greek")); + blog.setPost("Linear B is a syllabic script that was used for writing Mycenaean Greek..."); + try { + DataTable.create(Collections.singletonList(blog)); + fail(); + } catch (CucumberException expected) { + assertEquals("" + + "Don't know how to convert \"cucumber.runtime.table.TableConverterTest$BlogBean.tags\" into a table entry.\n" + + "Either exclude tags from the table by selecting the fields to include:\n" + + "\n" + + "DataTable.create(entries, \"Field\", \"Other Field\")\n" + + "\n" + + "Or try writing your own converter:\n" + + "\n" + + "@cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter(TagsConverter.class)\n" + + "private List tags;\n", + expected.getMessage()); + } + } + + @Test + public void converts_empty_complex_types_and_almost_back() { + DataTable table = TableParser.parse("" + + "|Author |Tags |Post |\n" + + "|Tom Scott| |Linear B is a...|\n", PARAMETER_INFO); + List converted = table.asList(BlogBean.class); + BlogBean blog = converted.get(0); + assertEquals("Tom Scott", blog.getAuthor()); + assertEquals(emptyList(), blog.getTags()); + assertEquals("Linear B is a...", blog.getPost()); + assertEquals("" + + " | author | tags | post |\n" + + " | Tom Scott | | Linear B is a... |\n", + table.toTable(converted).toString()); + } + + public static class AnnotatedBlogBean { + private String author; + @XStreamConverter(TagsConverter.class) + private List tags; + private String post; + + public String getPost() { + return post; + } + + public void setPost(String post) { + this.post = post; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + } + + public static class TagsConverter implements SingleValueConverter { + + @Override + public String toString(Object o) { + return o.toString().replace("[", "").replace("]", ""); + } + + @Override + public Object fromString(String s) { + return asList(s.split(", ")); + } + + @Override + public boolean canConvert(Class type) { + return List.class.isAssignableFrom(type); + } + } + + @Test + public void converts_annotated_complex_types_and_almost_back() { + DataTable table = TableParser.parse("" + + "|Author |Tags |Post |\n" + + "|Tom Scott|Language, Linguistics, Mycenaean Greek|Linear B is a...|\n", PARAMETER_INFO); + List converted = table.asList(AnnotatedBlogBean.class); + AnnotatedBlogBean blog = converted.get(0); + assertEquals("Tom Scott", blog.getAuthor()); + assertEquals(asList("Language", "Linguistics", "Mycenaean Greek"), blog.getTags()); + assertEquals("Linear B is a...", blog.getPost()); + assertEquals("" + + " | author | tags | post |\n" + + " | Tom Scott | Language, Linguistics, Mycenaean Greek | Linear B is a... |\n", + table.toTable(converted).toString()); + } + @Test public void converts_to_list_of_map_of_date() { DataTable table = TableParser.parse("|Birth Date|Death Cal|\n|1957-05-10|1979-02-02|\n", PARAMETER_INFO); diff --git a/core/src/test/java/cucumber/runtime/table/ToDataTableTest.java b/core/src/test/java/cucumber/runtime/table/ToDataTableTest.java index 7fdf7468a3..c6a983cf52 100644 --- a/core/src/test/java/cucumber/runtime/table/ToDataTableTest.java +++ b/core/src/test/java/cucumber/runtime/table/ToDataTableTest.java @@ -7,13 +7,19 @@ import org.junit.Before; import org.junit.Test; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; +import java.util.Set; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -39,6 +45,54 @@ public void converts_list_of_beans_to_table() { "", table.toString()); } + @Test + public void converts_only_selected_fields_of_object_to_table() throws ParseException { + RelationPojo relation = new RelationPojo(); + relation.id = 12; + relation.user = new UserPojo(0); + relation.user.credits = 1000; + relation.user.name = "Tom Scott"; + relation.user.birthDate = new SimpleDateFormat("yyyy-MM-dd").parse("1984-01-01"); + relation.created = new SimpleDateFormat("yyyy-MM-dd").parse("2000-01-01"); + relation.tags = new HashSet(Arrays.asList("A","B", "C")); + + DataTable table = tc.toTable(singletonList(relation), "id", "created"); + assertEquals("" + + " | id | created |\n" + + " | 12 | 01/01/2000 |\n" + + "", table.toString()); + } + + @Test + public void throws_exception_when_converting_complex_selected_field_to_table() throws ParseException { + RelationPojo relation = new RelationPojo(); + relation.id = 12; + relation.user = new UserPojo(0); + relation.user.credits = 1000; + relation.user.name = "Tom Scott"; + relation.user.birthDate = new SimpleDateFormat("yyyy-MM-dd").parse("1984-01-01"); + relation.created = new SimpleDateFormat("yyyy-MM-dd").parse("2000-01-01"); + relation.tags = new HashSet(Arrays.asList("A","B", "C")); + + try { + tc.toTable(singletonList(relation), "id", "created", "tags"); + fail(); + } catch (CucumberException expected){ + assertEquals("" + + "Don't know how to convert \"cucumber.runtime.table.ToDataTableTest$RelationPojo.tags\" into a table entry.\n" + + "Either exclude tags from the table by selecting the fields to include:\n" + + "\n" + + "DataTable.create(entries, \"Field\", \"Other Field\")\n" + + "\n" + + "Or try writing your own converter:\n" + + "\n" + + "@cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter(TagsConverter.class)\n" + + "public Set tags;\n", + expected.getMessage()); + } + } + + @Test public void converts_list_of_beans_with_null_to_table() { List users = tc.toList(personTableWithNull(), UserPojo.class); @@ -215,6 +269,17 @@ public void enum_value_should_be_null_when_text_omitted_for_plain_enum() { assertEquals("[yes, null]", actual.toString()); } + // No setters + public static class RelationPojo { + public Integer id; + public UserPojo user; + public Date created; + public Set tags = new HashSet(); + + public RelationPojo() { + } + } + // No setters public static class UserPojo { public Integer credits;