diff --git a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java index 143e03b..f1d0756 100644 --- a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java +++ b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java @@ -67,6 +67,92 @@ public final class JacksonUtil { return objectMapper; } + /** + * JSON 문자열 리터럴 안의 이스케이프되지 않은 제어문자(0x00~0x1F)를 JSON 이스케이프로 바꾼다. + * + * JSON 표준은 문자열 안의 제어문자를 반드시 이스케이프하도록 요구하므로, 파서는 raw 개행 등을 + * 만나면 아래 오류로 파싱을 거부한다. + * Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash + * 그런데 연동 상대 시스템들이 개행을 이스케이프하지 않고 그대로 보내는 사례가 있다. + * 파서 옵션(ALLOW_UNESCAPED_CONTROL_CHARS)으로 푸는 대신, 입력을 표준 JSON 으로 + * 정규화해서 파서는 strict 로 유지한다. + * + * 중요: 문자열 리터럴 "안" 에 있는 것만 바꾼다. JSON 은 토큰 사이의 개행/탭을 공백으로 + * 허용하므로, 구조적 공백까지 치환하면 pretty-print 된 JSON 이 오히려 깨진다. + * + * 값 자체는 보존된다. raw 개행은 \n 으로 바뀌어 파싱 후 다시 개행 문자가 된다. + * 제어문자가 데이터가 아니라 쓰레기 값(고정길이 전문의 0x00 패딩 등)이라면 + * 이 메서드에 의존하지 말고 파싱 전에 제거해야 한다. + * + * @param json 원본 JSON 문자열. null 이면 null 반환 + * @return 제어문자가 이스케이프된 JSON. 바꿀 게 없으면 원본을 그대로 반환 + */ + public static String escapeControlChars(String json) { + if (json == null) { + return null; + } + + // 빠른 경로: 제어문자가 아예 없으면 원본 그대로 (대부분의 전문이 여기 해당) + boolean found = false; + for (int i = 0; i < json.length(); i++) { + if (json.charAt(i) < 0x20) { + found = true; + break; + } + } + if (!found) { + return json; + } + + StringBuilder sb = new StringBuilder(json.length() + 16); + boolean inString = false; + boolean escaped = false; + + for (int i = 0; i < json.length(); i++) { + char c = json.charAt(i); + + if (!inString) { + // 문자열 밖: 구조적 공백(개행/탭 등)은 건드리지 않는다 + if (c == '"') { + inString = true; + } + sb.append(c); + continue; + } + + if (escaped) { + // 백슬래시 뒤 한 글자는 그대로 통과 (이스케이프된 따옴표/백슬래시, + // 유니코드 이스케이프의 선두 u 등). 이미 이스케이프된 것은 건드리지 않는다. + sb.append(c); + escaped = false; + continue; + } + if (c == '\\') { + sb.append(c); + escaped = true; + continue; + } + if (c == '"') { + sb.append(c); + inString = false; + continue; + } + if (c < 0x20) { + switch (c) { + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + case '\b': sb.append("\\b"); break; + case '\f': sb.append("\\f"); break; + default: sb.append(String.format("\\u%04x", (int) c)); break; + } + continue; + } + sb.append(c); + } + return sb.toString(); + } + private JacksonUtil() { // 인스턴스화 방지 } diff --git a/src/main/java/com/eactive/eai/message/parser/JsonReader.java b/src/main/java/com/eactive/eai/message/parser/JsonReader.java index e27d9c9..a75e2a6 100644 --- a/src/main/java/com/eactive/eai/message/parser/JsonReader.java +++ b/src/main/java/com/eactive/eai/message/parser/JsonReader.java @@ -41,7 +41,13 @@ public class JsonReader implements StandardReader { } else { jsonString = (String) obj; } - + + // 상대 시스템이 개행 등 제어문자를 이스케이프하지 않고 그대로 보내는 경우가 있다. + // 그대로 파싱하면 "Illegal unquoted character ((CTRL-CHAR, code 10))" 로 실패하므로 + // 문자열 리터럴 안의 제어문자만 표준 이스케이프로 정규화한 뒤 파싱한다. + // (파서 옵션으로 푸는 대신 입력을 표준 JSON 으로 맞추는 방식) + jsonString = JacksonUtil.escapeControlChars(jsonString); + JsonNode jsonNode = null; ObjectMapper mapper = MAPPER; JsonFactory factory = null;