Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public EgovEscapableDelimitedLineTokenizer() {
*/
public EgovEscapableDelimitedLineTokenizer(String delimiter) {
Assert.state(!delimiter.equals(DEFAULT_QUOTE_CHARACTER), "[" + DEFAULT_QUOTE_CHARACTER + "] is not allowed as delimiter for tokenizers.");
this.delimiter = delimiter;
initDelimiter(delimiter);
setQuoteCharacter(DEFAULT_QUOTE_CHARACTER);
}

Expand All @@ -93,6 +93,10 @@ public EgovEscapableDelimitedLineTokenizer(String delimiter) {
* @param delimiter : delimiter로 사용 할 문자열
*/
public void setDelimiter(String delimiter) {
initDelimiter(delimiter);
}

private void initDelimiter(String delimiter) {
this.delimiter = delimiter;
// regex 정규식에서 특수문자로 인식하는 것에 대한 처리
this.regexDelimiter = getRegexDelimiter(delimiter);
Expand Down Expand Up @@ -133,7 +137,7 @@ public List<String> doTokenize(String line) {
int closeIndex = 0;
boolean flagEndQuote = true;
String incompleteToken = "";
String[] arrLine = line.split(this.regexDelimiter);
String[] arrLine = line.split(this.regexDelimiter, -1);

for (int ii = 0; ii < arrLine.length; ii++) {
if (this.escape) {
Expand All @@ -153,9 +157,10 @@ public List<String> doTokenize(String line) {
tokens.add(arrLine[ii]);
}
} else {
closeIndex = findCloseIndex(arrLine[ii], quoteCharacter, quoteIndex + 1);
// 이어붙이는 셀에서는 닫는 따옴표가 0번에 올 수 있으므로 셀 처음부터 찾는다.
closeIndex = findCloseIndex(arrLine[ii], quoteCharacter, 0);
incompleteToken += this.delimiter + arrLine[ii];
if (closeIndex > 0) {
if (closeIndex >= 0) {
tokens.add(incompleteToken);
flagEndQuote = true;
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.egovframe.rte.bat.core.item.file.transform;

import org.egovframe.rte.bat.core.item.file.mapping.EgovObjectMapper;
import org.egovframe.rte.bat.mapper.EmpVO2;
import org.junit.jupiter.api.Test;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;

import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* {@link EgovEscapableDelimitedLineTokenizer}가 CSV 후행 빈 컬럼을 보존하는지,
* 따옴표 필드가 구분자로 끝나는 행을 정확히 자르는지,
* 그리고 생성자만으로 구분자가 초기화되는지 검증한다.
*/
class EgovEscapableDelimitedLineTokenizerEmptyColumnTest {

private final EgovEscapableDelimitedLineTokenizer tokenizer = createCommaTokenizer();

private EgovEscapableDelimitedLineTokenizer createCommaTokenizer() {
EgovEscapableDelimitedLineTokenizer tokenizer = new EgovEscapableDelimitedLineTokenizer();
tokenizer.setDelimiter(",");
return tokenizer;
}

@Test
void doTokenize_trailingEmptyColumns_keepsColumns() {
Object[][] cases = {
{"a,b,c,d", List.of("a", "b", "c", "d")},
{"a,b,,", List.of("a", "b", "", "")},
{"a,,,", List.of("a", "", "", "")},
{",,,", List.of("", "", "", "")},
{"a,b,c,", List.of("a", "b", "c", "")},
{"a,b,,d", List.of("a", "b", "", "d")}
};

for (Object[] testCase : cases) {
String line = (String) testCase[0];
@SuppressWarnings("unchecked")
List<String> expected = (List<String>) testCase[1];
assertEquals(expected, tokenizer.doTokenize(line), line + "의 빈 컬럼이 유지되어야 한다");
}
}

@Test
void doTokenize_trailingEmptyColumns_matchesSpringBatchTokenizer() throws Exception {
DelimitedLineTokenizer springTokenizer = new DelimitedLineTokenizer();
springTokenizer.setNames("col1", "col2", "col3", "col4");
springTokenizer.afterPropertiesSet();

List<String> lines = List.of("a,b,c,d", "a,b,,", "a,,,", ",,,", "a,b,c,", "a,b,,d");

for (String line : lines) {
List<String> expected = Arrays.asList(springTokenizer.tokenize(line).getValues());
List<String> actual = tokenizer.doTokenize(line);

assertEquals(expected.size(), actual.size(), line + "의 토큰 수가 Spring Batch 표준과 같아야 한다");
assertEquals(expected, actual, line + "의 토큰 값이 Spring Batch 표준과 같아야 한다");
}
}

@Test
void doTokenize_trailingEmptyColumn_mapsWithEgovObjectMapper() {
EgovObjectMapper<EmpVO2> mapper = new EgovObjectMapper<>();
mapper.setType(EmpVO2.class);
mapper.setNames(new String[]{"empNo", "empName", "job", "mgr"});
mapper.afterPropertiesSet();

List<String> tokens = tokenizer.doTokenize("a,b,c,");
EmpVO2 vo = assertDoesNotThrow(() -> mapper.mapObject(tokens),
"EgovObjectMapper는 names 4개와 토큰 4개가 일치하면 IncorrectTokenCountException을 던지면 안 된다");

assertEquals("a", vo.getEmpNo());
assertEquals("b", vo.getEmpName());
assertEquals("c", vo.getJob());
assertEquals("", vo.getMgr());
}

@Test
void doTokenize_constructorInitializedDelimiter_doesNotThrow() {
EgovEscapableDelimitedLineTokenizer commaTokenizer = new EgovEscapableDelimitedLineTokenizer();
assertEquals(List.of("a", "b", "c"), commaTokenizer.doTokenize("a,b,c"),
"기본 생성자 직후에도 콤마 구분자로 토큰화되어야 한다");

EgovEscapableDelimitedLineTokenizer pipeTokenizer = new EgovEscapableDelimitedLineTokenizer("|");
assertEquals(List.of("a", "b", "c"), pipeTokenizer.doTokenize("a|b|c"),
"구분자 생성자 직후에도 지정 구분자로 토큰화되어야 한다");
}

@Test
void doTokenize_escapeColumns_keepsEscapedDelimiterAndQuote() {
assertEquals(List.of("\"customer5,c6\"", "90"), tokenizer.doTokenize("\"customer5,c6\",90"),
"따옴표 안의 구분자는 컬럼을 나누면 안 된다");
assertEquals(List.of("\"customer8 is \"\"Tom\"\"\"", "70"), tokenizer.doTokenize("\"customer8 is \"\"Tom\"\"\",70"),
"따옴표 안의 이중 따옴표는 컬럼을 나누면 안 된다");
}

@Test
void doTokenize_quotedTrailingDelimiter_keepsFollowingColumns() {
Object[][] cases = {
{"a,\"b,\",", List.of("a", "\"b,\"", "")},
{"\"b,\",", List.of("\"b,\"", "")},
{"\"a,b,\",", List.of("\"a,b,\"", "")},
// 따옴표 필드가 구분자로 끝나도 뒤 컬럼을 삼키지 않는다.
{"\"b,\",c", List.of("\"b,\"", "c")}
};

for (Object[] testCase : cases) {
String line = (String) testCase[0];
@SuppressWarnings("unchecked")
List<String> expected = (List<String>) testCase[1];
assertEquals(expected, tokenizer.doTokenize(line),
line + "의 따옴표 필드와 뒤 컬럼이 정확히 보존되어야 한다");
}
}
}