diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java index 2d1956f..714f99b 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -9,7 +9,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; -import com.eactive.eai.adapter.AdapterPropManager; +import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.Logger; import com.eactive.eai.message.StandardItem; import com.eactive.eai.message.StandardMessage; @@ -62,6 +62,12 @@ import com.eactive.eai.message.StandardMessage; * {{/foreach}} * * + * + * [callProp 중첩 Properties 참조] + * callProp 의 값이 Properties 인 경우 점(.) 으로 체인 탐색이 가능하다. + * callProp.put("OUTBOUND", subProps) // subProps.setProperty("IF_ID", "SVC001") + * → 템플릿에서 ${callprop.OUTBOUND.IF_ID} 로 참조 + * 중간 값이 Properties 가 아닌 경우 전체 keyPath 를 키로 폴백 조회한다. */ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler { @@ -91,7 +97,7 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle StandardMessage resStandardMessage) throws Exception { String templateKey = inboundAdapterGroupName + ".template"; - String template = AdapterPropManager.getInstance().getProperty(PROP_GROUP, templateKey); + String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey); if (StringUtils.isBlank(template)) { if (logger.isWarn()) { @@ -135,20 +141,48 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle /** * 변수 표현식을 해석해 값을 반환한다. - * - "callprop.키" → callProp.getProperty("키") - * - 그 외 → StandardMessage.findItemValue(expr) + * - "callprop.키" → callProp 에서 직접 조회 + * - "callprop.부모.자식" → callProp.get("부모") 가 Properties 이면 중첩 탐색 + * - 그 외 → StandardMessage.findItemValue(expr) */ private String resolveScalar(String expr, StandardMessage msg, Properties callProp) { if (expr.startsWith(CALLPROP_PREFIX)) { - String key = expr.substring(CALLPROP_PREFIX.length()); - if (callProp != null) { - return StringUtils.defaultString(callProp.getProperty(key)); - } - return ""; + if (callProp == null) return ""; + String keyPath = expr.substring(CALLPROP_PREFIX.length()); + return StringUtils.defaultString(resolveCallProp(callProp, keyPath)); } return StringUtils.defaultString(msg.findItemValue(expr)); } + /** + * Properties 체인을 점(.) 구분자로 재귀 탐색한다. + * - 첫 세그먼트의 값이 Properties 이면 나머지 경로로 재귀 + * - String 이면 반환, 그 외 Object 이면 toString() 반환 + * - 첫 세그먼트 값이 Properties 가 아닌 경우 전체 keyPath 로 폴백 조회 + */ + private String resolveCallProp(Properties props, String keyPath) { + int dotIdx = keyPath.indexOf('.'); + if (dotIdx < 0) { + Object val = props.get(keyPath); + if (val == null) return null; + if (val instanceof String) return (String) val; + return val.toString(); + } + + String first = keyPath.substring(0, dotIdx); + String rest = keyPath.substring(dotIdx + 1); + Object val = props.get(first); + if (val instanceof Properties) { + return resolveCallProp((Properties) val, rest); + } + + // 중간 값이 Properties 가 아닌 경우: 전체 keyPath 를 키로 폴백 + Object direct = props.get(keyPath); + if (direct == null) return null; + if (direct instanceof String) return (String) direct; + return direct.toString(); + } + /** * GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환, * 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원) diff --git a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java index 7326022..fc9a769 100644 --- a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java +++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.context.ApplicationContext; -import com.eactive.eai.adapter.AdapterPropManager; +import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.message.StandardItem; import com.eactive.eai.message.StandardMessage; @@ -26,7 +26,7 @@ import com.eactive.eai.message.StandardType; * * - render() : package-private 이므로 같은 패키지에서 직접 호출 * - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에 - * mock ApplicationContext 를 리플렉션으로 주입하여 AdapterPropManager 격리 + * mock ApplicationContext 를 리플렉션으로 주입하여 PropManager 격리 */ class TemplateAdapterErrorMsgHandlerTest { @@ -210,6 +210,69 @@ class TemplateAdapterErrorMsgHandlerTest { assertEquals("", handler.render("${callprop.ADAPTER_NAME}", msg, null)); } + + @Test + @DisplayName("callProp 값이 Properties 인 경우 중첩 탐색") + void callProp_중첩_Properties() { + Properties sub = new Properties(); + sub.setProperty("IF_ID", "SVC001"); + sub.setProperty("SVC_NM", "조회서비스"); + + Properties callProp = new Properties(); + callProp.put("OUTBOUND", sub); + callProp.setProperty("ADAPTER_NAME", "MY_ADAPTER"); + + StandardMessage msg = buildMsg("E001", "오류", "", null); + String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.OUTBOUND.IF_ID}\",\"svcNm\":\"${callprop.OUTBOUND.SVC_NM}\"}"; + + String result = handler.render(template, msg, callProp); + + assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\",\"svcNm\":\"조회서비스\"}", result); + } + + @Test + @DisplayName("중첩 Properties 3단계 탐색") + void callProp_3단계_중첩() { + Properties level2 = new Properties(); + level2.setProperty("CODE", "L2CODE"); + + Properties level1 = new Properties(); + level1.put("LEVEL2", level2); + + Properties callProp = new Properties(); + callProp.put("LEVEL1", level1); + + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("L2CODE", handler.render("${callprop.LEVEL1.LEVEL2.CODE}", msg, callProp)); + } + + @Test + @DisplayName("중간 값이 Properties 가 아니면 전체 keyPath 로 폴백 조회") + void callProp_중간값_비Properties_폴백() { + Properties callProp = new Properties(); + callProp.setProperty("A.B", "FALLBACK_VALUE"); // 점 포함 키 + callProp.setProperty("A", "NOT_PROPS"); // String 값 + + StandardMessage msg = buildMsg("E001", "오류", "", null); + + // A 는 String 이므로 Properties 가 아님 → "A.B" 전체 키로 폴백 + assertEquals("FALLBACK_VALUE", handler.render("${callprop.A.B}", msg, callProp)); + } + + @Test + @DisplayName("중첩 Properties 에서 없는 키는 빈 문자열") + void callProp_중첩_없는키() { + Properties sub = new Properties(); + sub.setProperty("EXIST", "VALUE"); + + Properties callProp = new Properties(); + callProp.put("SUB", sub); + + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("", handler.render("${callprop.SUB.NONE_KEY}", msg, callProp)); + } } // ================================================================ @@ -341,16 +404,16 @@ class TemplateAdapterErrorMsgHandlerTest { } // ================================================================ - // generateNonStandardErrorResponseMessage – AdapterPropManager mock + // generateNonStandardErrorResponseMessage – PropManager mock // ================================================================ @Nested @DisplayName("generateNonStandardErrorResponseMessage") class GenerateMethod { - private void injectMockPropManager(AdapterPropManager mockPropManager) throws Exception { + private void injectMockPropManager(PropManager mockPropManager) throws Exception { ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class); - Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockPropManager); + Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager); Field ctxField = ApplicationContextProvider.class.getDeclaredField("context"); ctxField.setAccessible(true); ctxField.set(null, mockCtx); @@ -359,7 +422,7 @@ class TemplateAdapterErrorMsgHandlerTest { @Test @DisplayName("템플릿 미설정 시 null 반환") void 템플릿_없으면_null() throws Exception { - AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + PropManager mockProp = Mockito.mock(PropManager.class); Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); injectMockPropManager(mockProp); @@ -373,7 +436,7 @@ class TemplateAdapterErrorMsgHandlerTest { @Test @DisplayName("빈 템플릿 시 null 반환") void 빈_템플릿_null() throws Exception { - AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + PropManager mockProp = Mockito.mock(PropManager.class); Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" "); injectMockPropManager(mockProp); @@ -388,7 +451,7 @@ class TemplateAdapterErrorMsgHandlerTest { @DisplayName("템플릿 설정 시 StandardMessage 치환 결과 반환") void 템플릿_stdmsg_치환() throws Exception { String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; - AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + PropManager mockProp = Mockito.mock(PropManager.class); Mockito.when(mockProp.getProperty( eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), eq("MY_GROUP.template"))).thenReturn(template); @@ -405,7 +468,7 @@ class TemplateAdapterErrorMsgHandlerTest { @DisplayName("callProp 값이 템플릿에 치환되어 반환") void 템플릿_callProp_치환() throws Exception { String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; - AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + PropManager mockProp = Mockito.mock(PropManager.class); Mockito.when(mockProp.getProperty( eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), eq("MY_GROUP.template"))).thenReturn(template); @@ -423,7 +486,7 @@ class TemplateAdapterErrorMsgHandlerTest { @Test @DisplayName("템플릿 키는 {adapterGroupId}.template 형식으로 조회") void 템플릿_키_형식_검증() throws Exception { - AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + PropManager mockProp = Mockito.mock(PropManager.class); Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); injectMockPropManager(mockProp);