엣지 케이스 발견
문제 데이터
"0홍길""동","0 동 ""번지""",0,0.2,2024-11-20,"0"" 한글","0"" 한글","0""\n
한글","0"" 한글","0"" 한글"\n
"1홍길""동","1 동 ""번지""",1,2.2,2024-11-19,"1"" 한글","1"" 한글","1""\n
한글","1"" 한글","1"" 한글"\n
"2홍길""동","2 동 ""번지""",2,4.2,2024-11-18,"2"" 한글","2"" 한글","2""\n
한글","2"" 한글","2"" 한글"\n
"3홍길""동","3 동 ""번지""",3,6.2,2024-11-17,"3"" 한글","3"" 한글","3""\n
한글","3"" 한글","3"" 한글"\n
"4홍길""동","4 동 ""번지""",4,8.2,2024-11-16,"4"" 한글","4"" 한글","4""\n
한글","4"" 한글","4"" 한글"\n
Plain Text
복사
문제 상황
해당 데이터에서 \n 이 buf의 buf[buf.length-1]에 딱 맞아 떨어질 경우 해당 위치를 row의 delimiter로 판단해버리는 케이스 발견. 정상적 출력의 경우 1) 과 같이 되어야 하지만 2)와 같은 형태로 출력됨. 이 때문에 이후 데이터가 모두 밀려서 출력 되어지는 현상 발생.
1)"3홍길""동","3 동 ""번지""",3,6.2,2024-11-17,"3"" 한글","3"" 한글","3""\n
한글","3"" 한글","3"" 한글"\n
2) "3홍길""동","3 동 ""번지""",3,6.2,2024-11-17,"3"" 한글","3"" 한글","3""\n
Plain Text
복사
해결 방식
1.
pos, begin, limit를 활용하여 csvConfig의 lineDelimiter의 길이를 가져와 해당 길이를 더한 만큼의 길이를 extendAndLoad() 호출.
기각 - 해당 방식의 경우 CsvConfig class 의 종속성 발생 및 pos, begin 관리 시 유지 보수성 떨어짐.
2.
해당 엣지 발생 사유 분석 - 결국 상태 inQuote, matchIndex가 지역변수로 선언되어 fill() 이후 초기화됨으로 인하여 delimiter를 인식못함.
해결 - inQuote, matchIndex 인스턴스 변수로 추출.
전체 코드
package parser3;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
public class RecordParser implements CSVParser<List<Record>> {
private final InternalBuffer2 buffer;
private final CSVConfig csvConfig;
// TODO 차후 추가 private final CSVParser<List<String>> fieldParser;
private int skipLine;
private boolean inQuote = false;
private int matchIndex = 0;
public RecordParser(Reader reader, int bufferCapacity ,CSVConfig csvConfig) {
if (bufferCapacity <= 0) throw new IllegalArgumentException("Buffer capacity cannot be less than 1.");
this.buffer = new InternalBuffer2(reader);
this.csvConfig = csvConfig;
// TODO 차후 추가 this.fieldParser = new FieldParser(buffer, csvConfig);
this.skipLine = csvConfig.getSkipLine();
}
@Override
public List<Record> parse(){
final List<String> list = new ArrayList<>();
final StringBuilder builder = new StringBuilder();
while (buffer.fill()){
normalParse(buffer,list, builder);
}
return null;
}
private void normalParse(InternalBuffer2 buffer, List<String> record, StringBuilder builder){
while (buffer.notBeginAtLimit()){
char c = buffer.getBeginAndIncrement();
if (c == csvConfig.getQuote()) {
inQuote = !inQuote;
}else if (!inQuote){
matchIndex = incrementIfMatch(csvConfig.getLineDelimiter(), c, matchIndex);
if (matchIndex == csvConfig.getLineDelimiterLength()){
parse(record, builder);
matchIndex = 0;
inQuote = false;
}
}
}
}
private void parse(List<String> record, StringBuilder builder){
int length = (matchIndex == 1 ? buffer.getBegin() + matchIndex : buffer.getBegin() + matchIndex - 1) - buffer.getPos() - 1;
record.add(builder.append(buffer.getBuf(), buffer.getPos(), length).toString());
builder.setLength(0);
buffer.setPos(buffer.getBegin());
}
/**
* lastRow의 경우는 pos 부터 limit까지 넘겨버리면 됨
* 어차피 row 단위로 하기때문에 row가 존재함은 무조건 보장됨
*
* normalParse -> return List<Record>
* boolean inQuote = false;
* int matchIndex = 0;
*
* -> while 조건
* char c = getCharAtBegin();
* if -> csvConfig.getQoute() == c
* inQuote = !inQuote
*
* if -> !inQuote && mathcIndex == 1
* fieldParser -> 넘김
* matchIndex = 0; 초기화
* buffer.newPos(buffer.getIncrementBegin());
* break;
* buffer.getIncrementBegin();
*
*/
@Override
public boolean canParse() {
if (skipLine > 0){
skipLine--;
return false;
}
return true;
}
}
Java
복사
package parser3;
import lombok.AccessLevel;
import lombok.Getter;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.util.Optional;
import java.util.function.Function;
@Getter
public class InternalBuffer2 {
@Getter(AccessLevel.NONE)
private final Reader reader;
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static final int READ_SIZE = DEFAULT_BUFFER_SIZE;
private BufferState state = BufferState.INITIAL;
private char[] buf = new char[DEFAULT_BUFFER_SIZE];
private int pos, begin, limit;
private enum BufferState {
INITIAL,
PROGRESSING,
EXTEND,
RELOCATE,
LAST_LINE;
}
public InternalBuffer2(Reader reader) {
this.reader = reader;
}
/**
* TODO isLastLine 추가
* RecordParser내에서 마지막 줄 여부 판단을 위해서 필요함
* <p>
* 상태 전이 [] 반복, () 선택, {} 종료
* INITIAL -> [PROGRESSING <-> (EXTEND | RELOCATE)] -> LAST_LINE{종료}
*/
boolean fill() {
while (true) {
switch (state) {
case INITIAL:
int cnt = read(buf, pos, READ_SIZE);
state = BufferState.PROGRESSING;
if (cnt == -1){
return false;
}
limit = cnt;
return true;
case PROGRESSING:
/**
* if -> begin >= limit 일 경우 즉, 커서가 끝까지 갔는데 아무 변화 없을때
* state = (pos == 0) ? EXTEND : RELOCATE;
* continue;
* retrun true;
*/
if (begin >= limit) {
state = pos == 0 ? BufferState.EXTEND : BufferState.RELOCATE;
continue;
}
return true;
case EXTEND:
/**
* 확장 및 read
* extend() 확장
* cnt = read(begin) -> READ_SIZE 만큼 읽기
* if -> cnt == -1 LAST_LINE
* else -> PROGRESSING and limit += cnt
* continue;
*/
extendAndLoad();
return true;
case RELOCATE:
/**
* 재배치 및 read
* relocate()
* begin = limit - pos;
* pos = 0;
* cnt = read(begin) // READ_SIZE 만큼 데이터 로드
* if -> cnt == -1 LAST_LINE
* else -> PROGRESSING
*/
relocateAndLoad();
return true;
case LAST_LINE: //TODO 라스트 삭제 예정
return false;
}
}
}
static <T, R> Optional<Function<T, R>> ifLastRowOrElse(InternalBuffer2 buffer,
Function<T, R> action,
Function<T, R> elseAction) {
if (buffer.getPos() == buffer.getLimit()){
return Optional.empty();
}
boolean isLastLine = buffer.getState() == BufferState.LAST_LINE && buffer.getPos() < buffer.getLimit();
return Optional.of(isLastLine ? action : elseAction);
}
char getCharAtBegin(){
return buf[begin];
}
int getIncrementBegin(){
return ++begin;
}
char getBeginAndIncrement(){
return buf[begin++];
}
void setPos(int newPos){
this.pos = newPos;
}
boolean notBeginAtLimit(){
return begin < limit;
}
private int read(char[] buf, int off, int len) {
try {
return reader.read(buf, off, len);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void extendAndLoad() {
// TODO max size 예외처리
char[] newBuf = new char[buf.length * 2];
System.arraycopy(buf, 0, newBuf, 0, buf.length);
buf = newBuf;
int cnt = read(buf, begin, READ_SIZE);
stateAndLimitUpdate(cnt, limit + cnt);
}
private void relocateAndLoad() {
final int copyLength = begin - pos;
System.arraycopy(buf, pos, buf, 0, copyLength);
int cnt = read(buf, copyLength, READ_SIZE - copyLength);
pos = 0;
begin = copyLength;
stateAndLimitUpdate(cnt, copyLength + cnt);
}
private void stateAndLimitUpdate(int readSize, int newLimit){
state = readSize == -1 ? BufferState.LAST_LINE : BufferState.PROGRESSING;
limit = readSize == -1 ? limit : newLimit;
}
}
Java
복사