Skip to content

Commit b995fa2

Browse files
committed
Replace uniVocity CSV library with jackson-csv #702 review comments update -fixing csvConfig
1 parent 4307c54 commit b995fa2

File tree

7 files changed

+149
-7
lines changed

7 files changed

+149
-7
lines changed

core/src/main/java/org/jsmart/zerocode/core/db/DbCsvLoader.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.slf4j.LoggerFactory;
1616

1717

18-
1918
/**
2019
* Data loading in the database from a CSV external source
2120
*/

core/src/main/java/org/jsmart/zerocode/core/db/DbSqlExecutor.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package org.jsmart.zerocode.core.db;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4-
import com.fasterxml.jackson.dataformat.csv.CsvParser;
54
import com.google.inject.Inject;
65
import com.google.inject.name.Named;
76

8-
97
import org.apache.commons.dbutils.DbUtils;
108
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
119
import org.slf4j.Logger;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.jsmart.zerocode.core.di.provider;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
6+
7+
import java.io.IOException;
8+
import java.util.Collections;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
import java.util.Objects;
12+
13+
/**
14+
* /
15+
* Configuration class for parsing CSV files with Jackson's CSV module,
16+
* using a custom deserializer to maintain compatibility with non-standard formats previously handled by uniVocity.
17+
*
18+
*/
19+
public class CsvParserConfig {
20+
21+
public static class CustomStringDeserializer extends StdDeserializer<String> {
22+
// Immutable map of replacement rules: pattern → replacement
23+
private static final Map<String, String> REPLACEMENTS = createReplacements();
24+
25+
/**
26+
* Creates an immutable map of replacement rules for escape patterns.
27+
* @return A map containing patterns and their replacements.
28+
*/
29+
private static Map<String, String> createReplacements() {
30+
Map<String, String> map = new HashMap<>();
31+
map.put("\\'", "'"); // Backslash-escaped single quote
32+
map.put("''", "'"); // Double single quote (single-quoted CSV)
33+
map.put("\\\\", "\\"); // Double backslash to preserve literal backslash
34+
return Collections.unmodifiableMap(map);
35+
}
36+
37+
public CustomStringDeserializer() {
38+
super(String.class);
39+
}
40+
41+
/**
42+
* Deserializes a String value from the CSV parser, applying custom escape pattern replacements.
43+
* <p>
44+
* The method processes the input string to handle non-standard escape patterns required
45+
* for the expected output (e.g., <code>["a'c", "d\"f", "x\y"]</code>). It uses a stream-based
46+
* approach to apply replacements only when the pattern is present, ensuring efficiency.
47+
* <p>
48+
* Without this deserializer, Jackson's default CSV parser may:
49+
* <ul>
50+
* <li>Strip literal backslashes (e.g., <code>x\y</code> becomes <code>xy</code>).</li>
51+
* <li>Misinterpret single-quote escaping (e.g., <code>\'</code> or <code>''</code>).</li>
52+
* </ul>
53+
* <p>
54+
* This implementation ensures compatibility with the previous CSV parsing library's behavior
55+
* and handles inputs like <code>"a'c","d""f","x\y"</code> or <code>"a\'c","d\"f","x\y"</code>.
56+
*
57+
* @return The processed string with escape patterns replaced, or null if the input is null.
58+
* @throws IOException If an I/O error occurs during parsing.
59+
*/
60+
@Override
61+
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
62+
final String value = p.getText();
63+
if (Objects.isNull(value)) {
64+
return null;
65+
}
66+
return REPLACEMENTS.entrySet().stream()
67+
.filter(entry -> value.contains(entry.getKey()))
68+
.reduce(value, (current, entry) -> current.replace(entry.getKey(), entry.getValue()), (v1, v2) -> v1);
69+
}
70+
}
71+
72+
}

core/src/main/java/org/jsmart/zerocode/core/di/provider/CsvParserProvider.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
import com.fasterxml.jackson.databind.ObjectReader;
5+
import com.fasterxml.jackson.databind.module.SimpleModule;
56
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
67
import com.fasterxml.jackson.dataformat.csv.CsvParser;
78
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
@@ -20,7 +21,12 @@ public class CsvParserProvider implements Provider<JacksonCsvParserAdapter> {
2021

2122
static {
2223
final CsvSchema schema = createCsvSchema();
23-
final ObjectReader mapper = new CsvMapper()
24+
final CsvMapper csvMapper = new CsvMapper();
25+
csvMapper.enable(CsvParser.Feature.TRIM_SPACES);
26+
csvMapper.enable(CsvParser.Feature.ALLOW_TRAILING_COMMA);
27+
csvMapper.registerModule(new SimpleModule()
28+
.addDeserializer(String.class, new CsvParserConfig.CustomStringDeserializer()));
29+
final ObjectReader mapper = csvMapper
2430
.enable(CsvParser.Feature.TRIM_SPACES)
2531
.readerFor(String[].class)
2632
.with(schema);
@@ -45,14 +51,13 @@ private String sanitizeLine(final String line) {
4551
if (StringUtils.isNotBlank(line) && !line.contains(CARRIAGE_RETURN)) {
4652
return line;
4753
}
48-
return line.replace(CARRIAGE_RETURN, StringUtils.EMPTY);
54+
return line.replace(CARRIAGE_RETURN, StringUtils.SPACE);
4955
}
5056

5157
private static CsvSchema createCsvSchema() {
5258
return CsvSchema.builder()
5359
.setColumnSeparator(',')
5460
.setQuoteChar('\'')
55-
.setEscapeChar('\'')
5661
.setNullValue("")
5762
.setLineSeparator(LINE_SEPARATOR)
5863
.build();

core/src/main/java/org/jsmart/zerocode/core/di/provider/JacksonCsvParserAdapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.jsmart.zerocode.core.di.provider;
22

33
import com.fasterxml.jackson.databind.ObjectReader;
4+
import org.apache.commons.lang3.StringUtils;
45

56
import java.io.IOException;
67
import java.io.StringReader;
@@ -13,6 +14,10 @@ public JacksonCsvParserAdapter(final ObjectReader mapper) {
1314
}
1415
@Override
1516
public String[] parseLine(final String line) throws IOException {
17+
if (StringUtils.isEmpty(line)) return null ;
18+
if(line.trim().isEmpty()) {
19+
return new String[]{ null };
20+
}
1621
return mapper.readValue(new StringReader(line));
1722
}
1823
}

core/src/test/java/org/jsmart/zerocode/core/db/DbCsvLoaderTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import com.google.inject.Inject;
1818

19-
2019
@RunWith(JukitoRunner.class)
2120
public class DbCsvLoaderTest extends DbTestBase{
2221

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.jsmart.zerocode.core.di;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.core.Is.is;
5+
import static org.junit.Assert.assertNull;
6+
7+
import org.jsmart.zerocode.core.di.main.ApplicationMainModule;
8+
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
9+
import org.jukito.JukitoRunner;
10+
import org.jukito.TestModule;
11+
import org.junit.Test;
12+
import org.junit.runner.RunWith;
13+
14+
import com.google.inject.Inject;
15+
16+
17+
@RunWith(JukitoRunner.class)
18+
public class CsvParserTest {
19+
20+
public static class JukitoModule extends TestModule {
21+
@Override
22+
protected void configureTest() {
23+
ApplicationMainModule applicationMainModule = new ApplicationMainModule("config_hosts_test.properties");
24+
install(applicationMainModule);
25+
}
26+
}
27+
28+
@Inject
29+
CsvParserProvider parser;
30+
31+
@Test
32+
public void testCsvparseSpaces() {
33+
assertThat(parser.parseLine(" abc ,\t de f , ghi "), is(new String[] { "abc", "de f", "ghi" }));
34+
}
35+
36+
@Test
37+
public void testCsvParseEmptyCell() {
38+
assertThat(parser.parseLine(",, , \t ,"), is(new String[] { null, null, null, null, null }));
39+
}
40+
41+
@Test
42+
public void testCsvparseEmptyLine() {
43+
assertNull(parser.parseLine(""));
44+
assertThat(parser.parseLine(" "), is(new String[] { null }));
45+
assertThat(parser.parseLine(" \t "), is(new String[] { null }));
46+
}
47+
48+
@Test
49+
public void testCsvParseQuotesAndOtherChars() {
50+
assertThat(parser.parseLine("a'c, d\"f, x\\y"), is(new String[] { "a'c", "d\"f", "x\\y" }));
51+
assertThat(parser.parseLine("euro\u20AC, año, naïf"), is(new String[] { "euro\u20AC", "año", "naïf" }));
52+
}
53+
54+
@Test
55+
public void testCsvParseWindowsEndings() { // assume that lines where already splitted by \n
56+
assertThat(parser.parseLine("abc, def\r"), is(new String[] { "abc", "def" }));
57+
assertThat(parser.parseLine("abc, def \r"), is(new String[] { "abc", "def" }));
58+
// empty lines
59+
assertThat(parser.parseLine("\r"), is(new String[] { null })); // should be null?, see testCsvparseEmptyLine
60+
assertThat(parser.parseLine(" \r"), is(new String[] { null }));
61+
assertThat(parser.parseLine(" \t \r"), is(new String[] { null }));
62+
}
63+
64+
}

0 commit comments

Comments
 (0)