Skip to content

Commit 163849b

Browse files
PujolDavidcowtowncoder
authored andcommitted
Fix #469: Add a way to distinguish between null and empty (#471)
1 parent 029030b commit 163849b

File tree

3 files changed

+194
-5
lines changed

3 files changed

+194
-5
lines changed

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvParser.java

+32-4
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,28 @@ public enum Feature
148148
INSERT_NULLS_FOR_MISSING_COLUMNS(false),
149149

150150
/**
151-
* Feature that enables coercing an empty {@link String} to `null`
151+
* Feature that enables coercing an empty {@link String} to `null`.
152+
*<p>
153+
* Note that if this setting is enabled, {@link #EMPTY_UNQUOTED_STRING_AS_NULL}
154+
* has no effect.
152155
*
153-
* Feature is disabled by default
156+
* Feature is disabled by default for backwards compatibility.
154157
*/
155-
EMPTY_STRING_AS_NULL(false)
158+
EMPTY_STRING_AS_NULL(false),
159+
160+
/**
161+
* Feature that enables coercing an empty un-quoted {@link String} to `null`.
162+
* This feature allow differentiating between an empty quoted {@link String} and an empty un-quoted {@link String}.
163+
*<p>
164+
* Note that this feature is only considered if
165+
* {@link #EMPTY_STRING_AS_NULL}
166+
* is disabled.
167+
*<p>
168+
* Feature is disabled by default for backwards compatibility.
169+
*
170+
* @since 2.18
171+
*/
172+
EMPTY_UNQUOTED_STRING_AS_NULL(false),
156173
;
157174

158175
final boolean _defaultState;
@@ -326,6 +343,11 @@ private Feature(boolean defaultState) {
326343
*/
327344
protected boolean _cfgEmptyStringAsNull;
328345

346+
/**
347+
* @since 2.18
348+
*/
349+
protected boolean _cfgEmptyUnquotedStringAsNull;
350+
329351
/*
330352
/**********************************************************************
331353
/* State
@@ -426,6 +448,7 @@ public CsvParser(IOContext ctxt, int stdFeatures, int csvFeatures,
426448
_reader = new CsvDecoder(this, ctxt, reader, _schema, _textBuffer,
427449
stdFeatures, csvFeatures);
428450
_cfgEmptyStringAsNull = CsvParser.Feature.EMPTY_STRING_AS_NULL.enabledIn(csvFeatures);
451+
_cfgEmptyUnquotedStringAsNull = Feature.EMPTY_UNQUOTED_STRING_AS_NULL.enabledIn(csvFeatures);
429452
}
430453

431454
@Override
@@ -537,6 +560,7 @@ public JsonParser overrideFormatFeatures(int values, int mask) {
537560
_formatFeatures = newF;
538561
_reader.overrideFormatFeatures(newF);
539562
_cfgEmptyStringAsNull = CsvParser.Feature.EMPTY_STRING_AS_NULL.enabledIn(_formatFeatures);
563+
_cfgEmptyUnquotedStringAsNull = Feature.EMPTY_UNQUOTED_STRING_AS_NULL.enabledIn(_formatFeatures);
540564
}
541565
return this;
542566
}
@@ -555,6 +579,7 @@ public JsonParser enable(Feature f)
555579
{
556580
_formatFeatures |= f.getMask();
557581
_cfgEmptyStringAsNull = CsvParser.Feature.EMPTY_STRING_AS_NULL.enabledIn(_formatFeatures);
582+
_cfgEmptyUnquotedStringAsNull = Feature.EMPTY_UNQUOTED_STRING_AS_NULL.enabledIn(_formatFeatures);
558583
return this;
559584
}
560585

@@ -566,6 +591,7 @@ public JsonParser disable(Feature f)
566591
{
567592
_formatFeatures &= ~f.getMask();
568593
_cfgEmptyStringAsNull = CsvParser.Feature.EMPTY_STRING_AS_NULL.enabledIn(_formatFeatures);
594+
_cfgEmptyUnquotedStringAsNull = Feature.EMPTY_UNQUOTED_STRING_AS_NULL.enabledIn(_formatFeatures);
569595
return this;
570596
}
571597

@@ -1434,7 +1460,6 @@ protected void _startArray(CsvSchema.Column column)
14341460
_arraySeparator = sep;
14351461
}
14361462

1437-
14381463
/**
14391464
* Helper method called to check whether specified String value should be considered
14401465
* "null" value, if so configured.
@@ -1450,6 +1475,9 @@ protected boolean _isNullValue(String value) {
14501475
if (_cfgEmptyStringAsNull && value.isEmpty()) {
14511476
return true;
14521477
}
1478+
if (_cfgEmptyUnquotedStringAsNull && value.isEmpty() && !_reader.isCurrentTokenQuoted()) {
1479+
return true;
1480+
}
14531481
return false;
14541482
}
14551483
}

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvDecoder.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ public class CsvDecoder
153153
*/
154154
protected int _currInputRowStart = 0;
155155

156+
/**
157+
* Flag that indicates whether the current token has been quoted or not.
158+
*
159+
* @since 2.18
160+
*/
161+
protected boolean _currInputQuoted = false;
162+
156163
// // // Location info at point when current token was started
157164

158165
/**
@@ -405,6 +412,16 @@ public final int getCurrentColumn() {
405412
}
406413
return ptr - _currInputRowStart + 1; // 1-based
407414
}
415+
416+
/**
417+
* Tell if the current token has been quoted or not.
418+
* @return True if the current token has been quoted, false otherwise
419+
*
420+
* @since 2.18
421+
*/
422+
public final boolean isCurrentTokenQuoted() {
423+
return _currInputQuoted;
424+
}
408425

409426
/*
410427
/**********************************************************************
@@ -673,7 +690,8 @@ public String nextString() throws IOException
673690
return "";
674691
}
675692
// two modes: quoted, unquoted
676-
if (i == _quoteChar) { // offline quoted case (longer)
693+
_currInputQuoted = i == _quoteChar; // Keep track of quoting
694+
if (_currInputQuoted) { // offline quoted case (longer)
677695
return _nextQuotedString();
678696
}
679697
if (i == _separatorChar) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.fasterxml.jackson.dataformat.csv.deser;
2+
3+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
4+
import com.fasterxml.jackson.databind.MappingIterator;
5+
import com.fasterxml.jackson.databind.ObjectReader;
6+
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
7+
import com.fasterxml.jackson.dataformat.csv.CsvParser;
8+
import com.fasterxml.jackson.dataformat.csv.ModuleTestBase;
9+
10+
import java.io.IOException;
11+
12+
/**
13+
* Tests for {@code CsvParser.Feature.EMPTY_UNQUOTED_STRING_AS_NULL}
14+
*/
15+
public class EmptyUnquotedStringAsNullTest
16+
extends ModuleTestBase
17+
{
18+
@JsonPropertyOrder({"firstName", "middleName", "lastName"})
19+
static class TestUser {
20+
public String firstName, middleName, lastName;
21+
}
22+
23+
/*
24+
/**********************************************************
25+
/* Test methods
26+
/**********************************************************
27+
*/
28+
29+
private final CsvMapper MAPPER = mapperForCsv();
30+
31+
public void testDefaultParseAsEmptyString() throws IOException {
32+
// setup test data
33+
TestUser expectedTestUser = new TestUser();
34+
expectedTestUser.firstName = "Grace";
35+
expectedTestUser.middleName = "";
36+
expectedTestUser.lastName = "Hopper";
37+
ObjectReader objectReader = MAPPER.readerFor(TestUser.class).with(MAPPER.schemaFor(TestUser.class));
38+
String csv = "Grace,,Hopper";
39+
40+
// execute
41+
TestUser actualTestUser = objectReader.readValue(csv);
42+
43+
// test
44+
assertNotNull(actualTestUser);
45+
assertEquals(expectedTestUser.firstName, actualTestUser.firstName);
46+
assertEquals(expectedTestUser.middleName, actualTestUser.middleName);
47+
assertEquals(expectedTestUser.lastName, actualTestUser.lastName);
48+
}
49+
50+
public void testSimpleParseEmptyUnquotedStringAsNull() throws IOException {
51+
// setup test data
52+
TestUser expectedTestUser = new TestUser();
53+
expectedTestUser.firstName = "Grace";
54+
expectedTestUser.lastName = "Hopper";
55+
56+
ObjectReader objectReader = MAPPER
57+
.readerFor(TestUser.class)
58+
.with(MAPPER.schemaFor(TestUser.class))
59+
.with(CsvParser.Feature.EMPTY_UNQUOTED_STRING_AS_NULL);
60+
String csv = "Grace,,Hopper";
61+
62+
// execute
63+
TestUser actualTestUser = objectReader.readValue(csv);
64+
65+
// test
66+
assertNotNull(actualTestUser);
67+
assertEquals(expectedTestUser.firstName, actualTestUser.firstName);
68+
assertNull("The column that contains an empty String should be deserialized as null ", actualTestUser.middleName);
69+
assertEquals(expectedTestUser.lastName, actualTestUser.lastName);
70+
}
71+
72+
public void testSimpleParseEmptyQuotedStringAsNonNull() throws IOException {
73+
// setup test data
74+
TestUser expectedTestUser = new TestUser();
75+
expectedTestUser.firstName = "Grace";
76+
expectedTestUser.middleName = "";
77+
expectedTestUser.lastName = "Hopper";
78+
79+
ObjectReader objectReader = MAPPER
80+
.readerFor(TestUser.class)
81+
.with(MAPPER.schemaFor(TestUser.class))
82+
.with(CsvParser.Feature.EMPTY_UNQUOTED_STRING_AS_NULL);
83+
String csv = "Grace,\"\",Hopper";
84+
85+
// execute
86+
TestUser actualTestUser = objectReader.readValue(csv);
87+
88+
// test
89+
assertNotNull(actualTestUser);
90+
assertEquals(expectedTestUser.firstName, actualTestUser.firstName);
91+
assertEquals(expectedTestUser.middleName, actualTestUser.middleName);
92+
assertEquals(expectedTestUser.lastName, actualTestUser.lastName);
93+
}
94+
95+
// [dataformats-text#222]
96+
public void testEmptyUnquotedStringAsNullNonPojo() throws Exception
97+
{
98+
String csv = "Grace,,Hopper";
99+
100+
ObjectReader r = MAPPER.reader()
101+
.with(CsvParser.Feature.EMPTY_UNQUOTED_STRING_AS_NULL)
102+
.with(CsvParser.Feature.WRAP_AS_ARRAY);
103+
104+
try (MappingIterator<Object[]> it1 = r.forType(Object[].class).readValues(csv)) {
105+
Object[] array1 = it1.next();
106+
assertEquals(3, array1.length);
107+
assertEquals("Grace", array1[0]);
108+
assertNull(array1[1]);
109+
assertEquals("Hopper", array1[2]);
110+
}
111+
try (MappingIterator<String[]> it2 = r.forType(String[].class).readValues(csv)) {
112+
String[] array2 = it2.next();
113+
assertEquals(3, array2.length);
114+
assertEquals("Grace", array2[0]);
115+
assertNull(array2[1]);
116+
assertEquals("Hopper", array2[2]);
117+
}
118+
}
119+
120+
public void testEmptyQuotedStringAsNonNullNonPojo() throws Exception
121+
{
122+
String csv = "Grace,\"\",Hopper";
123+
124+
ObjectReader r = MAPPER.reader()
125+
.with(CsvParser.Feature.EMPTY_UNQUOTED_STRING_AS_NULL)
126+
.with(CsvParser.Feature.WRAP_AS_ARRAY);
127+
128+
try (MappingIterator<Object[]> it1 = r.forType(Object[].class).readValues(csv)) {
129+
Object[] array1 = it1.next();
130+
assertEquals(3, array1.length);
131+
assertEquals("Grace", array1[0]);
132+
assertEquals("", array1[1]);
133+
assertEquals("Hopper", array1[2]);
134+
}
135+
try (MappingIterator<String[]> it2 = r.forType(String[].class).readValues(csv)) {
136+
String[] array2 = it2.next();
137+
assertEquals(3, array2.length);
138+
assertEquals("Grace", array2[0]);
139+
assertEquals("", array2[1]);
140+
assertEquals("Hopper", array2[2]);
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)