Skip to content

Fix issue in ComplexTypeWriter #1042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
}

}
86 changes: 71 additions & 15 deletions core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
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<String> columnNames;
private final List<String> fieldNames = new ArrayList<String>();
private final List<String> fieldValues = new ArrayList<String>();

private int nodeDepth = 0;
private final Map<String, String> fields = new LinkedHashMap<String, String>();
private final Stack<String> currentKey = new Stack<String>();

public ComplexTypeWriter(List<String> columnNames) {
this.columnNames = columnNames;
}

@Override
public List<String> getHeader() {
return columnNames.isEmpty() ? fieldNames : columnNames;
return columnNames.isEmpty() ? new ArrayList<String>(fields.keySet()) : columnNames;
}

@Override
Expand All @@ -30,26 +37,26 @@ public List<String> 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<String>(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
Expand All @@ -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
Expand All @@ -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";
}
}
}
140 changes: 140 additions & 0 deletions core/src/test/java/cucumber/runtime/table/TableConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@

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;
import java.util.Locale;
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 {

Expand Down Expand Up @@ -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<String> 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<String> getTags() {
return tags;
}

public void setTags(List<String> 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<BlogBean> 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<String> 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<String> getTags() {
return tags;
}

public void setTags(List<String> 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<AnnotatedBlogBean> 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);
Expand Down
Loading