Skip to content

Commit 818fec0

Browse files
authored
Merge pull request #711 from rkampani/feature/701-replaceUniVocityJar
Replace uniVocity CSV library with jackson-csv #702
2 parents c4442db + 954afda commit 818fec0

File tree

16 files changed

+256
-46
lines changed

16 files changed

+256
-46
lines changed

core/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,6 @@
6262
<groupId>com.fasterxml.jackson.dataformat</groupId>
6363
<artifactId>jackson-dataformat-yaml</artifactId>
6464
</dependency>
65-
<dependency>
66-
<groupId>com.univocity</groupId>
67-
<artifactId>univocity-parsers</artifactId>
68-
</dependency>
6965
<dependency>
7066
<groupId>com.google.code.gson</groupId>
7167
<artifactId>gson</artifactId>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@
1010

1111
import org.apache.commons.dbutils.QueryRunner;
1212
import org.apache.commons.lang3.StringUtils;
13+
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
1314
import org.slf4j.Logger;
1415
import org.slf4j.LoggerFactory;
1516

16-
import com.univocity.parsers.csv.CsvParser;
1717

1818
/**
1919
* Data loading in the database from a CSV external source
2020
*/
2121
class DbCsvLoader {
2222
private static final Logger LOGGER = LoggerFactory.getLogger(DbCsvLoader.class);
2323
private Connection conn;
24-
private CsvParser csvParser;
24+
private CsvParserProvider csvParser;
2525

26-
public DbCsvLoader(Connection conn, CsvParser csvParser) {
26+
public DbCsvLoader(Connection conn, CsvParserProvider csvParser) {
2727
this.conn = conn;
2828
this.csvParser = csvParser;
2929
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.google.inject.Inject;
55
import com.google.inject.name.Named;
6-
import com.univocity.parsers.csv.CsvParser;
76

87
import org.apache.commons.dbutils.DbUtils;
8+
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
99
import org.slf4j.Logger;
1010
import org.slf4j.LoggerFactory;
1111
import java.sql.Connection;
@@ -36,7 +36,7 @@ public class DbSqlExecutor {
3636
@Named("db.driver.password") private String password;
3737

3838
@Inject
39-
private CsvParser csvParser;
39+
private CsvParserProvider csvParser;
4040

4141
/**
4242
* The LOADCSV operation inserts the content of a CSV file into a table,

core/src/main/java/org/jsmart/zerocode/core/di/module/CsvParserModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import com.google.inject.Binder;
44
import com.google.inject.Module;
5-
import com.univocity.parsers.csv.CsvParser;
65
import jakarta.inject.Singleton;
6+
import org.jsmart.zerocode.core.di.provider.CsvParserInterface;
77
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
88

99
public class CsvParserModule implements Module {
1010

1111
@Override
1212
public void configure(Binder binder) {
13-
binder.bind(CsvParser.class).toProvider(CsvParserProvider.class).in(Singleton.class);
13+
binder.bind(CsvParserInterface.class).toProvider(CsvParserProvider.class).in(Singleton.class);
1414
}
1515
}
1616

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+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.jsmart.zerocode.core.di.provider;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.util.List;
6+
7+
/**
8+
* Defines a contract for parsing a single line of CSV data into a string array.
9+
* Implementations should handle CSV lines with a specified format, such as
10+
* comma-separated fields with optional quoting and escaping.
11+
*/
12+
public interface CsvParserInterface {
13+
/**
14+
* Parses a single line of CSV data into an array of fields.
15+
*
16+
* @param line the CSV line to parse, which may include quoted fields and escaped characters
17+
* @return an array of strings representing the parsed fields; may be empty if the line is invalid
18+
* @throws IOException if an error occurs during parsing, such as malformed CSV data
19+
*/
20+
String[] parseLine(final String line) throws IOException;
21+
}
Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,66 @@
11
package org.jsmart.zerocode.core.di.provider;
22

3-
import com.univocity.parsers.csv.CsvParser;
4-
import com.univocity.parsers.csv.CsvParserSettings;
3+
4+
import com.fasterxml.jackson.databind.ObjectReader;
5+
import com.fasterxml.jackson.databind.module.SimpleModule;
6+
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
7+
import com.fasterxml.jackson.dataformat.csv.CsvParser;
8+
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
59
import jakarta.inject.Provider;
10+
import org.apache.commons.lang3.StringUtils;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.io.IOException;
615

7-
public class CsvParserProvider implements Provider<CsvParser> {
16+
public class CsvParserProvider implements Provider<JacksonCsvParserAdapter> {
17+
private static final Logger logger = LoggerFactory.getLogger(CsvParserProvider.class);
18+
private static final JacksonCsvParserAdapter instance;
819
public static final String LINE_SEPARATOR = "\n";
20+
private static final String CARRIAGE_RETURN = "\r";
21+
22+
static {
23+
final CsvSchema schema = createCsvSchema();
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
30+
.enable(CsvParser.Feature.TRIM_SPACES)
31+
.readerFor(String[].class)
32+
.with(schema);
33+
instance = new JacksonCsvParserAdapter(mapper);
34+
}
935

1036
@Override
11-
public CsvParser get() {
12-
CsvParserSettings settings = createCsvSettings();
13-
return new CsvParser(settings);
37+
public JacksonCsvParserAdapter get() {
38+
return instance;
39+
}
40+
41+
public String[] parseLine(final String line) {
42+
try {
43+
return instance.parseLine(sanitizeLine(line));
44+
} catch (final IOException e) {
45+
logger.warn("Failed to parse line: {}", line, e);
46+
return new String[0];
47+
}
48+
}
49+
50+
private String sanitizeLine(final String line) {
51+
if (StringUtils.isNotBlank(line) && !line.contains(CARRIAGE_RETURN)) {
52+
return line;
53+
}
54+
return line.replace(CARRIAGE_RETURN, StringUtils.SPACE);
1455
}
1556

16-
private CsvParserSettings createCsvSettings() {
17-
CsvParserSettings settings = new CsvParserSettings();
18-
settings.getFormat().setDelimiter(",");
19-
settings.getFormat().setQuote('\'');
20-
settings.getFormat().setQuoteEscape('\'');
21-
settings.setEmptyValue("");
22-
settings.getFormat().setLineSeparator("\n");
23-
settings.setAutoConfigurationEnabled(false);
24-
return settings;
57+
private static CsvSchema createCsvSchema() {
58+
return CsvSchema.builder()
59+
.setColumnSeparator(',')
60+
.setQuoteChar('\'')
61+
.setNullValue("")
62+
.setLineSeparator(LINE_SEPARATOR)
63+
.build();
2564
}
2665

2766
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.jsmart.zerocode.core.di.provider;
2+
3+
import com.fasterxml.jackson.databind.ObjectReader;
4+
import org.apache.commons.lang3.StringUtils;
5+
6+
import java.io.IOException;
7+
import java.io.StringReader;
8+
9+
public class JacksonCsvParserAdapter implements CsvParserInterface {
10+
11+
private final ObjectReader mapper;
12+
public JacksonCsvParserAdapter(final ObjectReader mapper) {
13+
this.mapper = mapper;
14+
}
15+
@Override
16+
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+
}
21+
return mapper.readValue(new StringReader(line));
22+
}
23+
}

core/src/main/java/org/jsmart/zerocode/core/engine/preprocessor/ZeroCodeParameterizedProcessorImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.google.inject.Inject;
55
import com.google.inject.Singleton;
6-
import com.univocity.parsers.csv.CsvParser;
76
import org.apache.commons.collections4.CollectionUtils;
87
import org.apache.commons.text.StringSubstitutor;
8+
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
99
import org.jsmart.zerocode.core.domain.ScenarioSpec;
1010
import org.jsmart.zerocode.core.utils.TokenUtils;
1111
import org.slf4j.Logger;
@@ -55,10 +55,10 @@ public class ZeroCodeParameterizedProcessorImpl implements ZeroCodeParameterized
5555

5656
private final ObjectMapper objectMapper;
5757

58-
private final CsvParser csvParser;
58+
private final CsvParserProvider csvParser;
5959

6060
@Inject
61-
public ZeroCodeParameterizedProcessorImpl(ObjectMapper objectMapper, CsvParser csvParser) {
61+
public ZeroCodeParameterizedProcessorImpl(ObjectMapper objectMapper, CsvParserProvider csvParser) {
6262
this.objectMapper = objectMapper;
6363
this.csvParser = csvParser;
6464
}

core/src/main/java/org/jsmart/zerocode/core/runner/ZeroCodeMultiStepsScenarioRunnerImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import com.google.inject.Inject;
77
import com.google.inject.Singleton;
88
import com.google.inject.name.Named;
9-
import com.univocity.parsers.csv.CsvParser;
9+
1010
import static java.util.Optional.ofNullable;
1111
import static org.apache.commons.collections4.CollectionUtils.isEmpty;
1212
import static org.jsmart.zerocode.core.constants.ZerocodeConstants.KAFKA_TOPIC;
1313

14+
import org.jsmart.zerocode.core.di.provider.CsvParserProvider;
1415
import org.jsmart.zerocode.core.domain.Parameterized;
1516
import org.jsmart.zerocode.core.domain.ScenarioSpec;
1617
import org.jsmart.zerocode.core.domain.Step;
@@ -72,8 +73,7 @@ public class ZeroCodeMultiStepsScenarioRunnerImpl implements ZeroCodeMultiStepsS
7273
private ApiServiceExecutor apiExecutor;
7374

7475
@Inject
75-
private CsvParser csvParser;
76-
76+
private CsvParserProvider csvParser;
7777
@Inject
7878
private ApiTypeUtils apiTypeUtils;
7979

0 commit comments

Comments
 (0)