package com.eactive.eai.util; import org.json.simple.JSONObject; 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; // TOUSE // kbbank 어댑터에서 업무데이터의 암호화 필드에 대한 암복호화시 // 해당 필드에 대한 get/set 을 수행 public class JsonPathUtil { private JsonPathUtil() { } private static final ObjectMapper objectMapper = new ObjectMapper(); // --------------------------------------------------------------- // JsonNode 기반 단일 파싱 API — 연속 get/set 시 파싱 횟수 절감 // --------------------------------------------------------------- /** JSON 문자열을 JsonNode 트리로 파싱. */ public static JsonNode toTree(Object json) throws Exception { if(json == null) return null; else if(json instanceof JsonNode) return (JsonNode)json; else if(json instanceof String) return objectMapper.readTree((String)json); else if(json instanceof JSONObject) return objectMapper.readTree(((JSONObject)json).toJSONString()); else return objectMapper.readTree(json.toString()); } /** 이미 파싱된 트리에서 경로의 값을 추출. */ public static String getAt(JsonNode root, String path) { JsonNode node = getNodeAtPath(root, path, "/"); return node != null ? node.asText() : null; } /** 이미 파싱된 트리에서 경로의 값을 추출. */ public static String getAt(JsonNode root, String path, String delimeter) { JsonNode node = getNodeAtPath(root, path, delimeter); return node != null ? node.asText() : null; } /** 이미 파싱된 트리의 경로에 값을 설정 (in-place). */ public static void setAt(JsonNode root, String path, String value) { JsonNode parent = getParentNode(root, getParentPath(path)); if (parent == null) return; String key = getFieldName(path); if (parent.isArray()) { ((ArrayNode) parent).set(Integer.parseInt(key), objectMapper.convertValue(value, JsonNode.class)); } else if (parent.isObject()) { ((ObjectNode) parent).put(key, value); } } /** JsonNode 트리를 JSON 문자열로 직렬화. */ public static String fromTree(JsonNode root) throws Exception { return objectMapper.writeValueAsString(root); } // --------------------------------------------------------------- // 문자열 기반 API (내부적으로 위 메서드에 위임) // --------------------------------------------------------------- // 특정 경로의 값을 설정하는 함수 public static String setValueAtPath(String jsonString, String path, String newValue, boolean isPretty) { try { JsonNode rootNode = objectMapper.readTree(jsonString); setAt(rootNode, path, newValue); if(isPretty) return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode); else return objectMapper.writeValueAsString(rootNode); } catch (Exception e) { e.printStackTrace(); return null; } } // 특정 경로의 값을 가져오는 함수 public static String getValueAtPath(String jsonString, String path) { try { return getAt(objectMapper.readTree(jsonString), path); } catch (Exception e) { e.printStackTrace(); return null; } } // 경로에서 부모 경로를 추출하는 유틸리티 함수 private static String getParentPath(String path) { int lastSlashIndex = path.lastIndexOf('/'); return lastSlashIndex == 0 ? "/" : path.substring(0, lastSlashIndex); } // 경로에서 필드 이름 또는 배열 인덱스를 추출하는 유틸리티 함수 private static String getFieldName(String path) { int lastSlashIndex = path.lastIndexOf('/'); return path.substring(lastSlashIndex + 1); } // 특정 경로에 해당하는 부모 노드를 가져오는 유틸리티 함수 private static JsonNode getParentNode(JsonNode rootNode, String parentPath) { return getNodeAtPath(rootNode, parentPath, "/"); } // 특정 경로에 해당하는 노드를 가져오는 유틸리티 함수 private static JsonNode getNodeAtPath(JsonNode rootNode, String path, String delimeter) { String[] tokens = path.split(delimeter); JsonNode currentNode = rootNode; for (String token : tokens) { if (token.isEmpty()) continue; if (token.matches("\\d+")) { // 배열 인덱스 처리 currentNode = currentNode.get(Integer.parseInt(token)); } else { // 객체 필드 처리 currentNode = currentNode.get(token); } if (currentNode == null) { return null; // 경로가 존재하지 않음 } } return currentNode; } /** * 이미 파싱된 bodyNode에서 removeFromPath 필드를 제거하고 mergeJson을 병합. * body의 재파싱 없이 호출 가능하여 파싱 횟수를 1회 절감. * * body와 mergeJson이 모두 JSON 객체인 경우에만 병합을 수행한다. * 어느 한쪽이 JSON 객체가 아니면 mergeJson을 그대로 반환한다 (전체 교체 fallback). * * @param bodyNode 이미 파싱된 원본 JsonNode * @param removeFromPath 제거할 필드의 경로 (예: /encryptedData) * @param mergeJson 병합할 JSON 문자열 * @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환 */ public static JsonNode mergeAtRoot(JsonNode bodyNode, String removeFromPath, String mergeJson) { try { JsonNode mergeNode = objectMapper.readTree(mergeJson); if (!bodyNode.isObject() || !mergeNode.isObject()) { return bodyNode; } ObjectNode result = (ObjectNode) bodyNode.deepCopy(); // removeFromPath 필드 제거 int lastSlash = removeFromPath.lastIndexOf('/'); String fieldName = removeFromPath.substring(lastSlash + 1); if (lastSlash == 0) { result.remove(fieldName); } else { JsonNode parent = getNodeAtPath(result, removeFromPath.substring(0, lastSlash), "/"); if (parent instanceof ObjectNode) { ((ObjectNode) parent).remove(fieldName); } } // mergeJson 필드를 body에 병합 (기존 필드 덮어쓰기) result.setAll((ObjectNode) mergeNode); // return objectMapper.writeValueAsString(result); return result; } catch (Exception e) { return bodyNode; } } /** * body에서 removeFromPath 필드를 제거한 뒤, mergeJson의 필드를 body에 병합하여 반환한다. * (문자열 기반 overload — 내부적으로 body를 1회 파싱하여 위 메서드에 위임) * * 사용 예 (toPath="/" 복호화): * body = {"encryptedData":"...","requestId":"REQ001"} * removeFrom = /encryptedData * mergeJson = {"userId":"U001","name":"test"} * 결과 = {"requestId":"REQ001","userId":"U001","name":"test"} * * @param body 원본 JSON 문자열 * @param removeFromPath 제거할 필드의 경로 (예: /encryptedData) * @param mergeJson 병합할 JSON 문자열 * @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환 */ public static String mergeAtRoot(String body, String removeFromPath, String mergeJson) { try { return objectMapper.writeValueAsString(mergeAtRoot(objectMapper.readTree(body), removeFromPath, mergeJson)); } catch (Exception e) { return mergeJson; } } public static void main(String[] args) { String jsonString = "{ \"person\": { \"name\": \"John\", \"age\": 30, \"groups\": [ { \"id\": 1, \"name\": \"Group A\" }, { \"id\": 2, \"name\": \"Group B\" }, { \"id\": 3, \"name\": \"Group C\" } ] } }"; // System.out.println("Source JSON: \n" + jsonString); // 경로를 사용하여 값 가져오기 (반복되는 그룹 포함) String getPath = "/person/groups/1/name"; String secondGroupName = getValueAtPath(jsonString, getPath); // System.out.println("getPath: " + getPath + " -> " + secondGroupName); // 경로를 사용하여 값 설정하기 (반복되는 그룹 포함) 및 Pretty 출력 String putPath = "/person/groups/2/name"; String updatedJsonString = setValueAtPath(jsonString, putPath, "Group Z", true); // System.out.println("putPath: " + putPath); // System.out.println("Updated JSON (Pretty): \n" + updatedJsonString); getPath = "/person/name"; // System.out.println("getPath: " + getPath + " -> " + getValueAtPath(jsonString, getPath)); getPath = "/person/age"; // System.out.println("getPath: " + getPath + " -> " + getValueAtPath(jsonString, getPath)); } }