From 3cc66fcaa80fa82e1f4e7ceecab5b1d88823f5dd Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 25 Jun 2026 09:56:01 +0900 Subject: [PATCH] =?UTF-8?q?JacksonUtil=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eactive/eai/common/util/JacksonUtil.java | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 src/main/java/com/eactive/eai/common/util/JacksonUtil.java diff --git a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java new file mode 100644 index 0000000..13fe42c --- /dev/null +++ b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java @@ -0,0 +1,261 @@ +package com.eactive.eai.common.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.json.simple.JSONObject; + +/** + * Jackson JsonNode 범용 접근 유틸리티 + * + * "." 구분자로 필드 단계를 이동하고, "[n]" 으로 배열 인덱스에 접근하는 + * path 표현식을 지원한다. + * + * 사용 예: + * JacksonUtil.getText(root, "term_agreements[0].is_agreed") + * JacksonUtil.getBoolean(root, "term_agreements[0].is_agreed", false) + * JacksonUtil.getNode(root, "data.list[2].child[0].name") + * + * 경로 중간에 필드가 없거나, 배열 인덱스가 범위를 벗어나거나, + * 배열이 아닌 노드에 인덱스 접근을 시도하는 경우 모두 + * 예외를 던지지 않고 null / 기본값을 반환한다. + */ +public final class JacksonUtil { + + /** "fieldName" 또는 "fieldName[0]" 형태의 토큰을 분해하는 패턴 */ + private static final Pattern TOKEN_PATTERN = Pattern.compile("([^\\[\\]]*)((?:\\[\\d+\\])*)"); + private static final Pattern INDEX_PATTERN = Pattern.compile("\\[(\\d+)\\]"); + + private JacksonUtil() { + // 인스턴스화 방지 + } + + // ------------------------------------------------------------------ + // 기본 트리 탐색 + // ------------------------------------------------------------------ + + /** + * Object(JSONObject/String/JsonNode 등)를 JsonNode로 변환한다. + * KakaopayFilter.readTree()를 일반화한 버전. + */ + public static JsonNode readTree(Object jsonData, ObjectMapper objectMapper) + throws JsonMappingException, JsonProcessingException { + + if (jsonData == null) { + return null; + } + + if (jsonData instanceof JsonNode) { + return (JsonNode) jsonData; + } + + String jsonStr = null; + + if (jsonData instanceof JSONObject) { + jsonStr = ((JSONObject) jsonData).toJSONString(); + } else if (jsonData instanceof String) { + jsonStr = (String) jsonData; + } else { + // 그 외 POJO 등은 writeValueAsString을 통해 변환 + jsonStr = objectMapper.writeValueAsString(jsonData); + } + + if (jsonStr == null) { + return null; + } + + return objectMapper.readTree(jsonStr); + } + + /** + * path 표현식으로 JsonNode를 탐색한다. + * 경로가 존재하지 않으면 null을 반환한다 (MissingNode가 아닌 진짜 null). + * + * @param root 탐색을 시작할 JsonNode + * @param path 예: "term_agreements[0].is_agreed", "data.list[2].name" + */ + public static JsonNode getNode(JsonNode root, String path) { + if (root == null || path == null || path.isEmpty()) { + return null; + } + + JsonNode current = root; + + for (String rawToken : path.split("\\.")) { + if (current == null || current.isMissingNode() || current.isNull()) { + return null; + } + + String fieldName = extractFieldName(rawToken); + List indices = extractIndices(rawToken); + + // 필드명이 있으면 먼저 필드로 이동 (빈 문자열이면 현재 노드 유지 - 최상위 배열 접근용) + if (!fieldName.isEmpty()) { + if (!current.has(fieldName)) { + return null; + } + current = current.get(fieldName); + } + + // 이어지는 [n][m]... 인덱스를 순서대로 적용 + for (Integer idx : indices) { + if (current == null || !current.isArray() || idx < 0 || idx >= current.size()) { + return null; + } + current = current.get(idx); + } + } + + return current; + } + + // ------------------------------------------------------------------ + // 타입별 getter (전부 null-safe, 기본값 지원) + // ------------------------------------------------------------------ + + public static String getText(JsonNode root, String path) { + return getText(root, path, null); + } + + public static String getText(JsonNode root, String path, String defaultValue) { + JsonNode node = getNode(root, path); + return (node == null || node.isMissingNode() || node.isNull()) ? defaultValue : node.asText(defaultValue); + } + + public static boolean getBoolean(JsonNode root, String path, boolean defaultValue) { + JsonNode node = getNode(root, path); + return (node == null || node.isMissingNode() || node.isNull()) ? defaultValue : node.asBoolean(defaultValue); + } + + public static int getInt(JsonNode root, String path, int defaultValue) { + JsonNode node = getNode(root, path); + return (node == null || node.isMissingNode() || node.isNull()) ? defaultValue : node.asInt(defaultValue); + } + + public static long getLong(JsonNode root, String path, long defaultValue) { + JsonNode node = getNode(root, path); + return (node == null || node.isMissingNode() || node.isNull()) ? defaultValue : node.asLong(defaultValue); + } + + public static double getDouble(JsonNode root, String path, double defaultValue) { + JsonNode node = getNode(root, path); + return (node == null || node.isMissingNode() || node.isNull()) ? defaultValue : node.asDouble(defaultValue); + } + + /** path가 가리키는 노드가 실제로 존재하는지 (null/missing이 아닌지) */ + public static boolean exists(JsonNode root, String path) { + JsonNode node = getNode(root, path); + return node != null && !node.isMissingNode() && !node.isNull(); + } + + /** path가 가리키는 노드가 배열일 때 그 크기를 반환, 배열이 아니거나 없으면 -1 */ + public static int size(JsonNode root, String path) { + JsonNode node = getNode(root, path); + return (node != null && node.isArray()) ? node.size() : -1; + } + + /** path가 가리키는 ArrayNode를 List로 반환, 없으면 빈 리스트 */ + public static List getList(JsonNode root, String path) { + List result = new ArrayList<>(); + JsonNode node = getNode(root, path); + if (node != null && node.isArray()) { + for (JsonNode item : node) { + result.add(item); + } + } + return result; + } + + // ------------------------------------------------------------------ + // 값 설정 (필요 시 사용 - 존재하는 경로에 대해서만 동작) + // ------------------------------------------------------------------ + + /** + * path가 가리키는 위치의 텍스트 값을 변경한다. + * 부모 컨테이너(ObjectNode/ArrayNode)가 존재해야 하며, 중간 경로가 없으면 false를 반환한다. + * (자동으로 중간 경로를 생성하지는 않음) + */ + public static boolean setText(JsonNode root, String path, String value) { + return setValue(root, path, value); + } + + private static boolean setValue(JsonNode root, String path, String value) { + int lastDot = path.lastIndexOf('.'); + String parentPath = (lastDot == -1) ? "" : path.substring(0, lastDot); + String lastToken = (lastDot == -1) ? path : path.substring(lastDot + 1); + + JsonNode parent = parentPath.isEmpty() ? root : getNode(root, parentPath); + if (parent == null) { + return false; + } + + String fieldName = extractFieldName(lastToken); + List indices = extractIndices(lastToken); + + JsonNode target = parent; + if (!fieldName.isEmpty()) { + if (!target.has(fieldName)) { + return false; + } + target = target.get(fieldName); + } + + // 마지막 인덱스 전까지 이동 + for (int i = 0; i < indices.size() - 1; i++) { + int idx = indices.get(i); + if (target == null || !target.isArray() || idx < 0 || idx >= target.size()) { + return false; + } + target = target.get(idx); + } + + if (!indices.isEmpty()) { + // 배열의 특정 인덱스 값 교체 + int lastIdx = indices.get(indices.size() - 1); + if (target == null || !target.isArray() || lastIdx < 0 || lastIdx >= target.size()) { + return false; + } + ((ArrayNode) target).set(lastIdx, value); + return true; + } else { + // 객체 필드 값 교체 (target은 fieldName으로 이미 이동된 상태이므로, parent 기준 재설정 필요) + if (parent.isObject() && fieldName != null && !fieldName.isEmpty()) { + ((ObjectNode) parent).put(fieldName, value); + return true; + } + return false; + } + } + + // ------------------------------------------------------------------ + // 내부 파싱 헬퍼 + // ------------------------------------------------------------------ + + /** "term_agreements[0]" -> "term_agreements" / "[0]" -> "" */ + private static String extractFieldName(String token) { + Matcher m = TOKEN_PATTERN.matcher(token); + if (m.matches()) { + return m.group(1) == null ? "" : m.group(1); + } + return token; + } + + /** "term_agreements[0][1]" -> [0, 1] / "term_agreements" -> [] */ + private static List extractIndices(String token) { + List indices = new ArrayList<>(); + Matcher m = INDEX_PATTERN.matcher(token); + while (m.find()) { + indices.add(Integer.parseInt(m.group(1))); + } + return indices; + } +}