Skip to content

Commit 807e007

Browse files
committed
[Java] Support empty strings and null values in data tables
Empty data table cells can either be considered null or empty strings. For example table below can converted to a map as `{name=Aspiring Author, first publication=}` or `{name=Aspiring Author, first publication=null}`. ```gherkin | name | first publication | | Aspiring Author | | ``` And as demonstrated #1617 there are good reasons to default the empty table cell to null. However this does not cover all cases. There are however good use cases to use the empty string. By declaring a table transformer with a replacement string it becomes possible to explicitly disambiguate between the two scenarios. For example: ```gherkin Given some authors | name | first publication | | Aspiring Author | | | Ancient Author | [blank] | ``` ```java @DataTableType(replaceWithEmptyString = "[blank]") public Author convert(Map<String, String> entry){ return new Author( entry.get("name"), entry.get("first publication") ); } @given("some authors") public void given_some_authors(List<Author> authors){ // authors = [Author(name="Aspiring Author", firstPublication=null), Author(name="Ancient Author", firstPublication=)] } ```
1 parent d2c28f1 commit 807e007

15 files changed

+284
-63
lines changed

java/README.md

+44-1
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,47 @@ public class DataTableSteps {
160160
return objectMapper.convertValue(fromValue, objectMapper.constructType(toValueType));
161161
}
162162
}
163-
```
163+
```
164+
165+
### Empty Cells
166+
167+
Data tables in Gherkin can not represent null or the empty string unambiguously.
168+
Cucumber will interpret empty cells as `null`.
169+
170+
Empty string be represented using a replacement. For example `[empty]`.
171+
The replacement can be configured by setting the `replaceWithEmptyString`
172+
property of `DataTableType`, `DefaultDataTableCellTransformer` and
173+
`DefaultDataTableEntryTransformerBody`. By default no replacement is configured.
174+
175+
```gherkin
176+
Given some authors
177+
| name | first publication |
178+
| Aspiring Author | |
179+
| Ancient Author | [blank] |
180+
```
181+
182+
```java
183+
package com.example.app;
184+
185+
import io.cucumber.java.DataTableType;
186+
import io.cucumber.java.en.Given;
187+
188+
import java.util.Map;
189+
import java.util.List;
190+
191+
public class DataTableSteps {
192+
193+
@DataTableType(replaceWithEmptyString = "[blank]")
194+
public Author convert(Map<String, String> entry){
195+
return new Author(
196+
entry.get("name"),
197+
entry.get("first publication")
198+
);
199+
}
200+
201+
@Given("some authors")
202+
public void given_some_authors(List<Author> authors){
203+
// authors = [Author(name="Aspiring Author", firstPublication=null), Author(name="Ancient Author", firstPublication=)]
204+
}
205+
}
206+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.cucumber.java;
2+
3+
import io.cucumber.core.backend.Lookup;
4+
import io.cucumber.datatable.DataTable;
5+
6+
import java.lang.reflect.Method;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
import static java.util.stream.Collectors.toList;
12+
import static java.util.stream.Collectors.toMap;
13+
14+
public class AbstractDatatableElementTransformerDefinition extends AbstractGlueDefinition {
15+
private final String[] emptyPatterns;
16+
17+
AbstractDatatableElementTransformerDefinition(Method method, Lookup lookup, String[] emptyPatterns) {
18+
super(method, lookup);
19+
this.emptyPatterns = emptyPatterns;
20+
}
21+
22+
23+
List<String> replaceEmptyPatternsWithEmptyString(List<String> row) {
24+
return row.stream()
25+
.map(this::replaceEmptyPatternsWithEmptyString)
26+
.collect(toList());
27+
}
28+
29+
DataTable replaceEmptyPatternsWithEmptyString(DataTable table) {
30+
List<List<String>> rawWithEmptyStrings = table.cells().stream()
31+
.map(this::replaceEmptyPatternsWithEmptyString)
32+
.collect(toList());
33+
34+
return DataTable.create(rawWithEmptyStrings); //TODO: Add converter back in
35+
}
36+
37+
Map<String, String> replaceEmptyPatternsWithEmptyString(Map<String, String> fromValue) {
38+
return fromValue.entrySet().stream()
39+
.collect(toMap(
40+
entry -> replaceEmptyPatternsWithEmptyString(entry.getKey()),
41+
entry -> replaceEmptyPatternsWithEmptyString(entry.getValue()),
42+
(s, s2) -> {
43+
throw createDuplicateKeyAfterReplacement(fromValue);
44+
}
45+
));
46+
}
47+
48+
private IllegalArgumentException createDuplicateKeyAfterReplacement(Map<String, String> fromValue) {
49+
List<String> conflict = new ArrayList<>(2);
50+
for (String emptyPattern : emptyPatterns) {
51+
if (fromValue.containsKey(emptyPattern)) {
52+
conflict.add(emptyPattern);
53+
}
54+
}
55+
String msg = "After replacing %s and %s with empty strings the datatable entry contains duplicate keys: %s";
56+
return new IllegalArgumentException(String.format(msg, conflict.get(0), conflict.get(1), fromValue));
57+
}
58+
59+
String replaceEmptyPatternsWithEmptyString(String t) {
60+
for (String emptyPattern : emptyPatterns) {
61+
if (t.equals(emptyPattern)) {
62+
return "";
63+
}
64+
}
65+
return t;
66+
}
67+
}

java/src/main/java/io/cucumber/java/DataTableType.java

+12
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,16 @@
2727
@API(status = API.Status.STABLE)
2828
public @interface DataTableType {
2929

30+
/**
31+
* Replace these strings in the Datatable with empty strings.
32+
* <p>
33+
* A data table can only represent absent and non-empty strings. By replacing
34+
* a known value (for example [empty]) a data table can also represent
35+
* empty strings.
36+
* <p>
37+
* It is not recommended to use multiple replacements in the same table.
38+
*
39+
* @return strings to be replaced with empty strings.
40+
*/
41+
String[] replaceWithEmptyString() default {};
3042
}

java/src/main/java/io/cucumber/java/DefaultDataTableCellTransformer.java

+13
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,17 @@
2424
@API(status = API.Status.STABLE)
2525
public @interface DefaultDataTableCellTransformer {
2626

27+
/**
28+
* Replace these strings in the Datatable with empty strings.
29+
* <p>
30+
* A data table can only represent absent and non-empty strings. By replacing
31+
* a known value (for example [empty]) a data table can also represent
32+
* empty strings.
33+
* <p>
34+
* It is not recommended to use multiple replacements in the same table.
35+
*
36+
* @return strings to be replaced with empty strings.
37+
*/
38+
String[] replaceWithEmptyString() default {};
39+
2740
}

java/src/main/java/io/cucumber/java/DefaultDataTableEntryTransformer.java

+14
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,18 @@
3333
* @return true if conversion should be be applied, true by default.
3434
*/
3535
boolean headersToProperties() default true;
36+
37+
/**
38+
* Replace these strings in the Datatable with empty strings.
39+
* <p>
40+
* A data table can only represent absent and non-empty strings. By replacing
41+
* a known value (for example [empty]) a data table can also represent
42+
* empty strings.
43+
* <p>
44+
* It is not recommended to use multiple replacements in the same table.
45+
*
46+
* @return strings to be replaced with empty strings.
47+
*/
48+
String[] replaceWithEmptyString() default {};
49+
3650
}

java/src/main/java/io/cucumber/java/GlueAdaptor.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,19 @@ void addDefinition(Method method, Annotation annotation) {
4545
boolean preferForRegexMatch = parameterType.preferForRegexMatch();
4646
glue.addParameterType(new JavaParameterTypeDefinition(name, pattern, method, useForSnippets, preferForRegexMatch, lookup));
4747
} else if (annotationType.equals(DataTableType.class)) {
48-
glue.addDataTableType(new JavaDataTableTypeDefinition(method, lookup));
48+
DataTableType dataTableType = (DataTableType) annotation;
49+
glue.addDataTableType(new JavaDataTableTypeDefinition(method, lookup, dataTableType.replaceWithEmptyString()));
4950
} else if (annotationType.equals(DefaultParameterTransformer.class)) {
5051
glue.addDefaultParameterTransformer(new JavaDefaultParameterTransformerDefinition(method, lookup));
5152
} else if (annotationType.equals(DefaultDataTableEntryTransformer.class)) {
5253
DefaultDataTableEntryTransformer transformer = (DefaultDataTableEntryTransformer) annotation;
5354
boolean headersToProperties = transformer.headersToProperties();
54-
glue.addDefaultDataTableEntryTransformer(new JavaDefaultDataTableEntryTransformerDefinition(method, lookup, headersToProperties));
55+
String[] replaceWithEmptyString = transformer.replaceWithEmptyString();
56+
glue.addDefaultDataTableEntryTransformer(new JavaDefaultDataTableEntryTransformerDefinition(method, lookup, headersToProperties, replaceWithEmptyString));
5557
} else if (annotationType.equals(DefaultDataTableCellTransformer.class)) {
56-
glue.addDefaultDataTableCellTransformer(new JavaDefaultDataTableCellTransformerDefinition(method, lookup));
58+
DefaultDataTableCellTransformer cellTransformer = (DefaultDataTableCellTransformer) annotation;
59+
String[] emptyPatterns = cellTransformer.replaceWithEmptyString();
60+
glue.addDefaultDataTableCellTransformer(new JavaDefaultDataTableCellTransformerDefinition(method, lookup, emptyPatterns));
5761
} else if (annotationType.equals(DocStringType.class)){
5862
DocStringType docStringType = (DocStringType) annotation;
5963
String contentType = docStringType.contentType();

java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java

+7-12
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
import io.cucumber.core.backend.Lookup;
55
import io.cucumber.datatable.DataTable;
66
import io.cucumber.datatable.DataTableType;
7-
import io.cucumber.datatable.TableCellTransformer;
8-
import io.cucumber.datatable.TableEntryTransformer;
9-
import io.cucumber.datatable.TableRowTransformer;
10-
import io.cucumber.datatable.TableTransformer;
117

128
import java.lang.reflect.Method;
139
import java.lang.reflect.ParameterizedType;
@@ -17,12 +13,12 @@
1713

1814
import static io.cucumber.java.InvalidMethodSignatureException.builder;
1915

20-
class JavaDataTableTypeDefinition extends AbstractGlueDefinition implements DataTableTypeDefinition {
16+
class JavaDataTableTypeDefinition extends AbstractDatatableElementTransformerDefinition implements DataTableTypeDefinition {
2117

2218
private final DataTableType dataTableType;
2319

24-
JavaDataTableTypeDefinition(Method method, Lookup lookup) {
25-
super(method, lookup);
20+
JavaDataTableTypeDefinition(Method method, Lookup lookup, String[] emptyPatterns) {
21+
super(method, lookup, emptyPatterns);
2622
this.dataTableType = createDataTableType(method);
2723
}
2824

@@ -75,33 +71,32 @@ private DataTableType createDataTableType(Method method) {
7571
if (DataTable.class.equals(parameterType)) {
7672
return new DataTableType(
7773
returnType,
78-
(TableTransformer<Object>) this::execute
74+
(DataTable table) -> execute(replaceEmptyPatternsWithEmptyString(table))
7975
);
8076
}
8177

8278
if (List.class.equals(parameterType)) {
8379
return new DataTableType(
8480
returnType,
85-
(TableRowTransformer<Object>) this::execute
81+
(List<String> row) -> execute(replaceEmptyPatternsWithEmptyString(row))
8682
);
8783
}
8884

8985
if (Map.class.equals(parameterType)) {
9086
return new DataTableType(
9187
returnType,
92-
(TableEntryTransformer<Object>) this::execute
88+
(Map<String, String> entry) -> execute(replaceEmptyPatternsWithEmptyString(entry))
9389
);
9490
}
9591

9692
if (String.class.equals(parameterType)) {
9793
return new DataTableType(
9894
returnType,
99-
(TableCellTransformer<Object>) this::execute
95+
(String cell) -> execute(replaceEmptyPatternsWithEmptyString(cell))
10096
);
10197
}
10298

10399
throw createInvalidSignatureException(method);
104-
105100
}
106101

107102
@Override

java/src/main/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinition.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99

1010
import static io.cucumber.java.InvalidMethodSignatureException.builder;
1111

12-
class JavaDefaultDataTableCellTransformerDefinition extends AbstractGlueDefinition implements DefaultDataTableCellTransformerDefinition {
12+
class JavaDefaultDataTableCellTransformerDefinition extends AbstractDatatableElementTransformerDefinition implements DefaultDataTableCellTransformerDefinition {
1313

1414
private final TableCellByTypeTransformer transformer;
1515

16-
JavaDefaultDataTableCellTransformerDefinition(Method method, Lookup lookup) {
17-
super(requireValidMethod(method), lookup);
18-
this.transformer = this::execute;
16+
JavaDefaultDataTableCellTransformerDefinition(Method method, Lookup lookup, String[] emptyPatterns) {
17+
super(requireValidMethod(method), lookup, emptyPatterns);
18+
this.transformer = (cellValue, toValueType) ->
19+
execute(replaceEmptyPatternsWithEmptyString(cellValue), toValueType);
1920
}
2021

2122
private static Method requireValidMethod(Method method) {

java/src/main/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinition.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@
1212

1313
import static io.cucumber.java.InvalidMethodSignatureException.builder;
1414

15-
class JavaDefaultDataTableEntryTransformerDefinition extends AbstractGlueDefinition implements DefaultDataTableEntryTransformerDefinition {
15+
class JavaDefaultDataTableEntryTransformerDefinition extends AbstractDatatableElementTransformerDefinition implements DefaultDataTableEntryTransformerDefinition {
1616

1717
private final TableEntryByTypeTransformer transformer;
1818
private final boolean headersToProperties;
1919

2020
JavaDefaultDataTableEntryTransformerDefinition(Method method, Lookup lookup) {
21-
super(requireValidMethod(method), lookup);
22-
this.headersToProperties = false;
23-
this.transformer = this::execute;
21+
this(method, lookup, false, new String[0]);
2422
}
2523

26-
JavaDefaultDataTableEntryTransformerDefinition(Method method, Lookup lookup, boolean headersToProperties) {
27-
super(requireValidMethod(method), lookup);
24+
JavaDefaultDataTableEntryTransformerDefinition(Method method, Lookup lookup, boolean headersToProperties, String[] emptyPatterns) {
25+
super(requireValidMethod(method), lookup, emptyPatterns);
2826
this.headersToProperties = headersToProperties;
29-
this.transformer = this::execute;
27+
this.transformer = (entryValue, toValueType, cellTransformer) ->
28+
execute(replaceEmptyPatternsWithEmptyString(entryValue), toValueType, cellTransformer);
3029
}
3130

3231
private static Method requireValidMethod(Method method) {

0 commit comments

Comments
 (0)