Skip to content

Commit 9fcf26a

Browse files
mpkorstanjesagacity
authored andcommitted
Fixed complex type writer and added tests. (#1)
* Fixed complex type writer and added tests. * [Core] Add explicit error message to ComplexTypeWriter When dealing with nested complex types, the ComplexTypeWriter would produce unbalanced and misaligned tables. This could be resolved by adding an `@XStreamConverter` to the complex field but this was not obvious. By adding an explicit exception this is resolved. For ease of use this exception is not thrown when acomplex field is empty or not included in the table .
1 parent fd25cdc commit 9fcf26a

File tree

4 files changed

+303
-10
lines changed

4 files changed

+303
-10
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package cucumber.runtime.table;
2+
3+
import java.util.regex.Pattern;
4+
5+
public class PascalCaseStringConverter implements StringConverter {
6+
7+
private static final String WHITESPACE = " ";
8+
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
9+
10+
@Override
11+
public String map(String string) {
12+
String[] splitted = normalizeSpace(string).split(WHITESPACE);
13+
for (int i = 0; i < splitted.length; i++) {
14+
splitted[i] = capitalize(splitted[i]);
15+
}
16+
return join(splitted);
17+
}
18+
19+
private String join(String[] splitted) {
20+
StringBuilder sb = new StringBuilder();
21+
for (String s : splitted) {
22+
sb.append(s);
23+
}
24+
return sb.toString();
25+
}
26+
27+
private String normalizeSpace(String originalHeaderName) {
28+
return WHITESPACE_PATTERN.matcher(originalHeaderName.trim()).replaceAll(WHITESPACE);
29+
}
30+
31+
private String capitalize(String string) {
32+
return new StringBuilder(string.length()).append(Character.toTitleCase(string.charAt(0))).append(string.substring(1)).toString();
33+
}
34+
35+
}

core/src/main/java/cucumber/runtime/xstream/ComplexTypeWriter.java

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
package cucumber.runtime.xstream;
22

3+
4+
import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter;
5+
import cucumber.runtime.CucumberException;
36
import cucumber.runtime.table.CamelCaseStringConverter;
7+
import cucumber.runtime.table.PascalCaseStringConverter;
48

9+
import java.lang.reflect.Field;
10+
import java.lang.reflect.Modifier;
511
import java.util.ArrayList;
612
import java.util.LinkedHashMap;
713
import java.util.List;
814
import java.util.Map;
15+
import java.util.Stack;
916

1017
import static java.util.Arrays.asList;
1118

1219
public class ComplexTypeWriter extends CellWriter {
1320
private final List<String> columnNames;
14-
private Map<String, String> fields = new LinkedHashMap<String, String>();
15-
private String currentKey;
16-
17-
private int nodeDepth = 0;
21+
private final Map<String, String> fields = new LinkedHashMap<String, String>();
22+
private final Stack<String> currentKey = new Stack<String>();
1823

1924
public ComplexTypeWriter(List<String> columnNames) {
2025
this.columnNames = columnNames;
@@ -48,10 +53,10 @@ public List<String> getValues() {
4853

4954
@Override
5055
public void startNode(String name) {
51-
if (nodeDepth == 1) {
52-
currentKey = name;
56+
currentKey.push(name);
57+
if (currentKey.size() == 2) {
58+
fields.put(name, "");
5359
}
54-
nodeDepth++;
5560
}
5661

5762
@Override
@@ -60,13 +65,26 @@ public void addAttribute(String name, String value) {
6065

6166
@Override
6267
public void setValue(String value) {
63-
fields.put(currentKey, value == null ? "" : value);
68+
// Add all simple types at level 2. nodeDepth 1 is the root node.
69+
if(currentKey.size() < 2){
70+
return;
71+
}
72+
73+
if (currentKey.size() == 2) {
74+
fields.put(currentKey.peek(), value == null ? "" : value);
75+
return;
76+
}
77+
78+
final String clazz = currentKey.get(0);
79+
final String field = currentKey.get(1);
80+
if ((columnNames.isEmpty() || columnNames.contains(field))) {
81+
throw createMissingConverterException(clazz, field);
82+
}
6483
}
6584

6685
@Override
6786
public void endNode() {
68-
nodeDepth--;
69-
currentKey = null;
87+
currentKey.pop();
7088
}
7189

7290
@Override
@@ -78,4 +96,39 @@ public void flush() {
7896
public void close() {
7997
throw new UnsupportedOperationException();
8098
}
99+
100+
private static CucumberException createMissingConverterException(String clazz, String field) {
101+
PascalCaseStringConverter converter = new PascalCaseStringConverter();
102+
return new CucumberException(String.format(
103+
"Don't know how to convert \"%s.%s\" into a table entry.\n" +
104+
"Either exclude %s from the table by selecting the fields to include:\n" +
105+
"\n" +
106+
"DataTable.create(entries, \"Field\", \"Other Field\")\n" +
107+
"\n" +
108+
"Or try writing your own converter:\n" +
109+
"\n" +
110+
"@%s(%sConverter.class)\n" +
111+
"%s %s;\n",
112+
clazz,
113+
field,
114+
field,
115+
XStreamConverter.class.getName(),
116+
converter.map(field),
117+
modifierAndTypeOfField(clazz, field),
118+
field
119+
));
120+
}
121+
122+
private static String modifierAndTypeOfField(String clazz, String fieldName) {
123+
try {
124+
Field field = Class.forName(clazz).getDeclaredField(fieldName);
125+
String simpleTypeName = field.getType().getSimpleName();
126+
String modifiers = Modifier.toString(field.getModifiers());
127+
return modifiers + " " + simpleTypeName;
128+
} catch (NoSuchFieldException e) {
129+
return "private Object";
130+
} catch (ClassNotFoundException e) {
131+
return "private Object";
132+
}
133+
}
81134
}

core/src/test/java/cucumber/runtime/table/TableConverterTest.java

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@
22

33
import cucumber.api.DataTable;
44
import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter;
5+
import cucumber.deps.com.thoughtworks.xstream.converters.SingleValueConverter;
56
import cucumber.deps.com.thoughtworks.xstream.converters.javabean.JavaBeanConverter;
7+
import cucumber.runtime.CucumberException;
68
import cucumber.runtime.ParameterInfo;
79
import org.junit.Test;
810

911
import java.util.Arrays;
1012
import java.util.Calendar;
13+
import java.util.Collections;
1114
import java.util.Date;
1215
import java.util.HashMap;
1316
import java.util.List;
1417
import java.util.Locale;
1518
import java.util.Map;
1619

1720
import static java.util.Arrays.asList;
21+
import static java.util.Collections.emptyList;
1822
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.fail;
1924

2025
public class TableConverterTest {
2126

@@ -184,6 +189,141 @@ public void converts_to_list_of_java_bean_and_almost_back() {
184189
assertEquals(" | birthDate | deathCal |\n | 1957-05-10 | 1979-02-02 |\n", table.toTable(converted).toString());
185190
}
186191

192+
public static class BlogBean {
193+
private String author;
194+
private List<String> tags;
195+
private String post;
196+
197+
public String getPost() {
198+
return post;
199+
}
200+
201+
public void setPost(String post) {
202+
this.post = post;
203+
}
204+
205+
public String getAuthor() {
206+
return author;
207+
}
208+
209+
public void setAuthor(String author) {
210+
this.author = author;
211+
}
212+
213+
public List<String> getTags() {
214+
return tags;
215+
}
216+
217+
public void setTags(List<String> tags) {
218+
this.tags = tags;
219+
}
220+
}
221+
222+
@Test
223+
public void throws_cucumber_exception_for_complex_types() {
224+
BlogBean blog = new BlogBean();
225+
blog.setAuthor("Tom Scott");
226+
blog.setTags(asList("Language", "Linguistics", " Mycenaean Greek"));
227+
blog.setPost("Linear B is a syllabic script that was used for writing Mycenaean Greek...");
228+
try {
229+
DataTable.create(Collections.singletonList(blog));
230+
fail();
231+
} catch (CucumberException expected) {
232+
assertEquals("" +
233+
"Don't know how to convert \"cucumber.runtime.table.TableConverterTest$BlogBean.tags\" into a table entry.\n" +
234+
"Either exclude tags from the table by selecting the fields to include:\n" +
235+
"\n" +
236+
"DataTable.create(entries, \"Field\", \"Other Field\")\n" +
237+
"\n" +
238+
"Or try writing your own converter:\n" +
239+
"\n" +
240+
"@cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter(TagsConverter.class)\n" +
241+
"private List tags;\n",
242+
expected.getMessage());
243+
}
244+
}
245+
246+
@Test
247+
public void converts_empty_complex_types_and_almost_back() {
248+
DataTable table = TableParser.parse("" +
249+
"|Author |Tags |Post |\n" +
250+
"|Tom Scott| |Linear B is a...|\n", PARAMETER_INFO);
251+
List<BlogBean> converted = table.asList(BlogBean.class);
252+
BlogBean blog = converted.get(0);
253+
assertEquals("Tom Scott", blog.getAuthor());
254+
assertEquals(emptyList(), blog.getTags());
255+
assertEquals("Linear B is a...", blog.getPost());
256+
assertEquals("" +
257+
" | author | tags | post |\n" +
258+
" | Tom Scott | | Linear B is a... |\n",
259+
table.toTable(converted).toString());
260+
}
261+
262+
public static class AnnotatedBlogBean {
263+
private String author;
264+
@XStreamConverter(TagsConverter.class)
265+
private List<String> tags;
266+
private String post;
267+
268+
public String getPost() {
269+
return post;
270+
}
271+
272+
public void setPost(String post) {
273+
this.post = post;
274+
}
275+
276+
public String getAuthor() {
277+
return author;
278+
}
279+
280+
public void setAuthor(String author) {
281+
this.author = author;
282+
}
283+
284+
public List<String> getTags() {
285+
return tags;
286+
}
287+
288+
public void setTags(List<String> tags) {
289+
this.tags = tags;
290+
}
291+
}
292+
293+
public static class TagsConverter implements SingleValueConverter {
294+
295+
@Override
296+
public String toString(Object o) {
297+
return o.toString().replace("[", "").replace("]", "");
298+
}
299+
300+
@Override
301+
public Object fromString(String s) {
302+
return asList(s.split(", "));
303+
}
304+
305+
@Override
306+
public boolean canConvert(Class type) {
307+
return List.class.isAssignableFrom(type);
308+
}
309+
}
310+
311+
@Test
312+
public void converts_annotated_complex_types_and_almost_back() {
313+
DataTable table = TableParser.parse("" +
314+
"|Author |Tags |Post |\n" +
315+
"|Tom Scott|Language, Linguistics, Mycenaean Greek|Linear B is a...|\n", PARAMETER_INFO);
316+
List<AnnotatedBlogBean> converted = table.asList(AnnotatedBlogBean.class);
317+
AnnotatedBlogBean blog = converted.get(0);
318+
assertEquals("Tom Scott", blog.getAuthor());
319+
assertEquals(asList("Language", "Linguistics", "Mycenaean Greek"), blog.getTags());
320+
assertEquals("Linear B is a...", blog.getPost());
321+
assertEquals("" +
322+
" | author | tags | post |\n" +
323+
" | Tom Scott | Language, Linguistics, Mycenaean Greek | Linear B is a... |\n",
324+
table.toTable(converted).toString());
325+
}
326+
187327
@Test
188328
public void converts_to_list_of_map_of_date() {
189329
DataTable table = TableParser.parse("|Birth Date|Death Cal|\n|1957-05-10|1979-02-02|\n", PARAMETER_INFO);

0 commit comments

Comments
 (0)