diff --git a/src/main/java/com/eactive/eai/adapter/handler/AdapterErrorMessageHandler.java b/src/main/java/com/eactive/eai/adapter/handler/AdapterErrorMessageHandler.java index 7699f95..0668ecf 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/AdapterErrorMessageHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/AdapterErrorMessageHandler.java @@ -2,9 +2,16 @@ package com.eactive.eai.adapter.handler; import java.util.Properties; -import com.eactive.eai.message.StandardMessage; +import com.eactive.eai.common.message.EAIMessage; public interface AdapterErrorMessageHandler { - public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, Properties callProp, Object outboundRequestData, StandardMessage resStandardMessage) throws Exception; + public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, + Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception; + + public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, + String inboudnAdapterName, Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) + throws Exception; + + public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName, + Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception; } - \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/adapter/handler/DefaultAdapterErrorMessageHandler.java b/src/main/java/com/eactive/eai/adapter/handler/DefaultAdapterErrorMessageHandler.java index 35275fb..5e84c59 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/DefaultAdapterErrorMessageHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/DefaultAdapterErrorMessageHandler.java @@ -2,13 +2,25 @@ package com.eactive.eai.adapter.handler; import java.util.Properties; -import com.eactive.eai.message.StandardMessage; +import com.eactive.eai.common.message.EAIMessage; public class DefaultAdapterErrorMessageHandler implements AdapterErrorMessageHandler { @Override public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, - Properties callProp, Object outboundRequestData, StandardMessage resStandardMessage) throws Exception { + Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception { + return null; + } + + @Override + public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, + Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception { + return null; + } + + @Override + public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName, + Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception { return null; } 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 3e6cd3e..346e016 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -1,5 +1,7 @@ package com.eactive.eai.adapter.handler; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -8,13 +10,22 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import com.eactive.eai.adapter.AdapterGroupVO; +import com.eactive.eai.adapter.AdapterManager; +import com.eactive.eai.adapter.AdapterPropManager; +import com.eactive.eai.adapter.AdapterVO; +import com.eactive.eai.adapter.http.HttpStatusException; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.property.PropManager; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; import com.eactive.eai.common.util.Logger; +import com.eactive.eai.common.util.MessageUtil; import com.eactive.eai.message.StandardItem; import com.eactive.eai.message.StandardMessage; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; /** * 템플릿 기반 어댑터 에러 메시지 핸들러. @@ -87,14 +98,22 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle /** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */ static final String CALLPROP_INDIRECT_PREFIX = "callprop["; - /** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */ + /** 예외 객체 필드 참조 접두어: ${exception.필드명} */ + static final String EXCEPTION_PREFIX = "exception."; + + /** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 (renderRow 내부에서 사용) */ private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); - /** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */ - private static final Pattern FOREACH_PATTERN = - Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}", - Pattern.DOTALL); + /** + * foreach 블록과 스칼라 변수를 순서대로 단일 패스로 처리하는 통합 패턴. + * group(1) != null → foreach 블록: group(1)=경로, group(2)=블록 본문 + * group(3) != null → 스칼라 변수: group(3)=표현식 + */ + private static final Pattern COMBINED_PATTERN = + Pattern.compile( + "\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}|\\$\\{([^}]+)\\}", + Pattern.DOTALL); @Override public Object generateNonStandardErrorResponseMessage( @@ -102,9 +121,10 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle String inboundAdapterName, Properties callProp, Object outboundRequestData, - StandardMessage resStandardMessage) throws Exception { + EAIMessage resEaiMsg) throws Exception { - String templateKey = inboundAdapterGroupName + ".template"; + StandardMessage resStandardMessage = resEaiMsg.getStandardMessage(); + String templateKey = inboundAdapterGroupName + ".template"; String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey); if (StringUtils.isBlank(template)) { @@ -118,55 +138,73 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle return render(template, resStandardMessage, callProp); } + /** 3-인자 위임: exception 없이 호출하는 기존 경로 유지 */ + String render(String template, StandardMessage msg, Properties callProp) { + return render(template, msg, callProp, null); + } + /** - * 템플릿에 StandardMessage 및 callProp 값을 치환해 최종 문자열을 반환한다. + * 템플릿에 StandardMessage, callProp, exception 값을 치환해 최종 문자열을 반환한다. + * foreach 블록과 스칼라 변수를 좌→우 순서로 단일 패스 처리한다. * package-private: 단위 테스트에서 직접 호출 가능 */ - String render(String template, StandardMessage msg, Properties callProp) { - // 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리 - StringBuffer sb = new StringBuffer(); - Matcher foreachMatcher = FOREACH_PATTERN.matcher(template); - while (foreachMatcher.find()) { - String arrayPath = foreachMatcher.group(1).trim(); - String blockContent = foreachMatcher.group(2); - String rendered = renderForeach(arrayPath, blockContent, msg); - foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered)); - } - foreachMatcher.appendTail(sb); - - // 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환 - String intermediate = sb.toString(); + String render(String template, StandardMessage msg, Properties callProp, Throwable exception) { + // 관리 UI 등에서 HTML 인코딩된 엔티티를 복원 (예: # → #) + template = template.replace("#", "#") + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\""); StringBuffer result = new StringBuffer(); - Matcher varMatcher = VAR_PATTERN.matcher(intermediate); - while (varMatcher.find()) { - String expr = varMatcher.group(1).trim(); - String value = resolveScalar(expr, msg, callProp); - varMatcher.appendReplacement(result, Matcher.quoteReplacement(value)); + Matcher matcher = COMBINED_PATTERN.matcher(template); + while (matcher.find()) { + String replacement; + if (matcher.group(1) != null) { + // {{#foreach path}}...{{/foreach}} 블록 + String arrayPath = matcher.group(1).trim(); + String blockContent = matcher.group(2); + replacement = (msg != null) ? renderForeach(arrayPath, blockContent, msg) : ""; + } else { + // ${path} / ${callprop.키} / ${exception.필드} 스칼라 변수 + String expr = matcher.group(3).trim(); + replacement = resolveScalar(expr, msg, callProp, exception); + } + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } - varMatcher.appendTail(result); + matcher.appendTail(result); return result.toString(); } /** * 변수 표현식을 해석해 값을 반환한다. * 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열. - * - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회 - * - "callprop.키[:기본값]" → callProp 직접/중첩 조회 - * - "경로[:기본값]" → StandardMessage.findItemValue(경로) + * - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회 + * - "callprop.키[:기본값]" → callProp 직접/중첩 조회 + * - "exception.필드[:기본값]" → 리플렉션으로 예외 객체 getter/필드 조회 + * - "경로[:기본값]" → StandardMessage.findItemValue(경로) */ - private String resolveScalar(String expr, StandardMessage msg, Properties callProp) { + private String resolveScalar(String expr, StandardMessage msg, Properties callProp, Throwable exception) { if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) { return resolveIndirectCallProp(expr, msg, callProp); } if (expr.startsWith(CALLPROP_PREFIX)) { String rest = expr.substring(CALLPROP_PREFIX.length()); int colonIdx = rest.indexOf(':'); - String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest; + String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest; String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : ""; if (callProp == null) return defaultVal; String value = resolveCallProp(callProp, keyPath); return value != null ? value : defaultVal; } + if (expr.startsWith(EXCEPTION_PREFIX)) { + String rest = expr.substring(EXCEPTION_PREFIX.length()); + int colonIdx = rest.indexOf(':'); + String fieldName = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest; + String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : ""; + if (exception == null) return defaultVal; + String value = resolveExceptionProperty(exception, fieldName); + return value != null ? value : defaultVal; + } int colonIdx = expr.indexOf(':'); String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr; String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : ""; @@ -176,13 +214,22 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle /** * StandardMessage 경로를 해석한다. - * 1. findItemValue(path) 로 직접 조회한다. - * 2. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우 + * 1. 경로에 '[n]' 구문이 있으면 배열 인덱스 접근으로 처리한다. + * 예) MSG.MSG_LIST[0].outp_msg_cd → MSG_LIST 첫 번째 행의 outp_msg_cd 값 + * 2. findItemValue(path) 로 직접 조회한다. + * 3. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우 * 나머지 경로(subPath)를 키로 JSON 필드를 추출한다. * 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo * DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city */ private String resolveMessagePath(StandardMessage msg, String path) { + if (msg == null) return null; + + int bracketStart = path.indexOf('['); + if (bracketStart >= 0) { + return resolveArrayIndex(msg, path, bracketStart); + } + String value = msg.findItemValue(path); if (value != null) return value; @@ -200,6 +247,44 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle return null; } + /** + * 배열 인덱스 접근 구문 처리: {arrayPath}[{n}].{fieldName} + * - arrayPath : GRID 타입 아이템 경로 (예: MSG.MSG_LIST) + * - n : 0-based 행 인덱스 + * - fieldName : 행 내 컬럼명 (예: outp_msg_cd) + * 인덱스가 범위를 벗어나거나 필드가 없으면 null 반환. + */ + private String resolveArrayIndex(StandardMessage msg, String path, int bracketStart) { + if (msg == null) return null; + + int bracketEnd = path.indexOf(']', bracketStart); + if (bracketEnd < 0) return null; + + String arrayPath = path.substring(0, bracketStart); + String indexStr = path.substring(bracketStart + 1, bracketEnd); + int index; + try { + index = Integer.parseInt(indexStr.trim()); + } catch (NumberFormatException e) { + return null; + } + + String fieldName = null; + if (bracketEnd + 2 <= path.length() - 1 && path.charAt(bracketEnd + 1) == '.') { + fieldName = path.substring(bracketEnd + 2); + } + if (StringUtils.isBlank(fieldName)) return null; + + StandardItem arrayItem = msg.findItem(arrayPath); + if (arrayItem == null) return null; + + List> rows = arrayItem.getList(); + if (rows == null || index < 0 || index >= rows.size()) return null; + + StandardItem item = rows.get(index).get(fieldName); + return item != null ? StringUtils.defaultString(item.getValue()) : null; + } + /** * JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다. * 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로, @@ -276,7 +361,7 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle * 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원) */ private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) { - StandardItem arrayItem = msg.findItem(arrayPath); + StandardItem arrayItem = msg.findItem(arrayPath); if (arrayItem == null) { if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath); return ""; @@ -316,4 +401,114 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle varMatcher.appendTail(result); return result.toString().trim(); } + + @Override + public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, + Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception { + String templateKey = inboudnAdapterGroupName + ".sys.template"; + String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey); + + if (StringUtils.isBlank(template)) { + if (logger.isWarn()) { + logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP + + ", key=" + templateKey); + } + return null; + } + + return render(template, resEaiMsg.getStandardMessage(), callProp); + } + + @Override + public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName, + Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception { + + AdapterGroupVO adapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName); + AdapterVO adapterVO = AdapterManager.getInstance().getAdapterVO(adapterGroupName, adapterName); + Properties httpProp = AdapterPropManager.getInstance().getProperties(adapterVO.getPropGroupName()); + String adptMsgType = adapterVO.getAdapterGroupVO().getMessageType(); + String encode = StringUtils.defaultIfBlank(adapterGroupVO.getMessageEncode(), "UTF-8"); + + if (e instanceof HttpStatusException) { + HttpStatusException e1 = (HttpStatusException) e; + int httpCode = e1.getStatus(); + + return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e1, httpCode); + } else if (e instanceof JwtAuthException) { + return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e, + HttpStatus.UNAUTHORIZED.value()); + } else { + return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e, + HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + } + + private Object genInboundErrorResponse(String adapterGroupName, Properties callProp, Properties httpProp, + String adptMsgType, String encode, Throwable e1, int httpCode) { + String templateKey = adapterGroupName + ".in." + httpCode + ".template"; + String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey); + if (StringUtils.isBlank(template)) { + templateKey = adapterGroupName + ".in.template"; + template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey); + if (StringUtils.isBlank(template)) { + String errorResponseFormat = httpProp.getProperty("ERROR_RESPONSE_FORMAT"); + return MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, + MessageUtil.ERROR_CODE_AP_ERROR, e1.getMessage(), errorResponseFormat); + } + } + return render(template, null, callProp, e1); + } + + /** + * 리플렉션으로 예외 객체의 필드 값을 동적으로 조회한다. + * 탐색 순서: getXxx() getter → isXxx() getter → 필드 직접 접근 + * 상위 클래스(Throwable 포함)까지 계층적으로 탐색한다. + */ + private String resolveExceptionProperty(Throwable e, String fieldName) { + if (StringUtils.isBlank(fieldName)) return null; + String cap = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); + for (String prefix : new String[]{"get", "is"}) { + Method m = findMethod(e.getClass(), prefix + cap); + if (m != null) { + try { + Object val = m.invoke(e); + return val != null ? val.toString() : null; + } catch (Exception ignored) {} + } + } + Field f = findField(e.getClass(), fieldName); + if (f != null) { + try { + f.setAccessible(true); + Object val = f.get(e); + return val != null ? val.toString() : null; + } catch (Exception ignored) {} + } + return null; + } + + private Method findMethod(Class clazz, String name) { + while (clazz != null) { + try { + Method m = clazz.getDeclaredMethod(name); + m.setAccessible(true); + return m; + } catch (NoSuchMethodException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + private Field findField(Class clazz, String name) { + while (clazz != null) { + try { + return clazz.getDeclaredField(name); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + } diff --git a/src/main/java/com/eactive/eai/adapter/http/client/HttpClient5AdapterServiceSupport.java b/src/main/java/com/eactive/eai/adapter/http/client/HttpClient5AdapterServiceSupport.java index 046ace8..56bb774 100644 --- a/src/main/java/com/eactive/eai/adapter/http/client/HttpClient5AdapterServiceSupport.java +++ b/src/main/java/com/eactive/eai/adapter/http/client/HttpClient5AdapterServiceSupport.java @@ -570,8 +570,8 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception; protected boolean isTas(Properties tempProp) { - String simYn = tempProp.getProperty("SIM_YN"); + String tranType = tempProp.getProperty("tranType"); return EAIServerManager.getInstance().isTASEnabledEAIServer() - && StringUtils.equals(simYn, EAIMessageKeys.TRANTYPE_TAS); + && StringUtils.equals(tranType, EAIMessageKeys.TRANTYPE_TAS); } } \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java index aded95f..7f706ca 100644 --- a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java +++ b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java @@ -267,10 +267,14 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp Map inboundPathVariables = (Map) tempProp.get(HttpAdapterServiceKey.INBOUND_PATH_VARIABLES); // interface 에 설정된게 우선한다. String uri = null; - if (dataObject == null) { - uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData, inboundPathVariables); + if(this.isTas(tempProp)) { + uri = vo.getUrl(); } else { - uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject, inboundPathVariables); + if (dataObject == null) { + uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData, inboundPathVariables); + } else { + uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject, inboundPathVariables); + } } logger.debug("HttpClientAdapterServiceRest] dataObject2 = [" + dataObject + "]"); @@ -435,11 +439,12 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp int status = -1; + Header[] responseHeaders = null; + String responseString = null; try { byte[] responseMessage = null; // byte[] responseMessageOri = null; String responseContentType = null; - Header[] responseHeaders; if (logger.isDebug() && "N".equals(vo.getTestCallYn())) { if (dataObject instanceof JSONObject) { @@ -550,7 +555,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp */ boolean needReissue = false; - String responseString = new String(responseMessage, vo.getEncode()); + responseString = new String(responseMessage, vo.getEncode()); if (isStatusInIgnore(status,errIgnore )) { // 이경우 정상으로 처리함 @@ -697,12 +702,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp } Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders); - Map map = new HashMap(); - tempProp.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map); - map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, responseHeaderProp); - map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_MESSAGE, responseString); - map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_STATUS, status); - + assignOutboundPropertyMap(tempProp, status, responseString, responseHeaderProp); return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), responseContentType, relayResponseHeaderKeys, status, responseHeaderProp); @@ -732,6 +732,10 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp } logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(), e); + + Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders); + assignOutboundPropertyMap(tempProp, status, responseString, responseHeaderProp); + throw e; } finally { if (status != HttpStatus.SC_OK) { @@ -745,6 +749,15 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp } } + private void assignOutboundPropertyMap(Properties tempProp, int status, String responseString, + Properties responseHeaderProp) { + Map map = new HashMap(); + tempProp.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map); + map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, responseHeaderProp); + map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_MESSAGE, responseString); + map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_STATUS, status); + } + private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) { Properties headerProp = new Properties(); if (responseHeaders == null || responseHeaders.length == 0) { diff --git a/src/main/java/com/eactive/eai/common/exception/ExceptionHandler.java b/src/main/java/com/eactive/eai/common/exception/ExceptionHandler.java index 6f31671..16bc2d4 100644 --- a/src/main/java/com/eactive/eai/common/exception/ExceptionHandler.java +++ b/src/main/java/com/eactive/eai/common/exception/ExceptionHandler.java @@ -7,6 +7,10 @@ import org.json.simple.JSONValue; import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterManager; +import com.eactive.eai.adapter.AdapterPropManager; +import com.eactive.eai.adapter.AdapterVO; +import com.eactive.eai.adapter.handler.AdapterErrorMessageHandler; +import com.eactive.eai.adapter.handler.AdapterErrorMessageHandlerFactory; import com.eactive.eai.common.EAIKeys; import com.eactive.eai.common.logger.EAILogSender; import com.eactive.eai.common.message.EAIMessage; @@ -749,6 +753,53 @@ public class ExceptionHandler { String serviceType, String psvItfTp, String charset) { String error = null; EAIMessage resultEAIMessage = null; + + // AdapterErrorMessageHandler(TemplateAdapterErrorMsgHandler) 설정이 있으면 우선 호출 + try { + AdapterGroupVO inboundAdapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(sngSysItfTp); + if (inboundAdapterGroupVO != null) { + AdapterVO inboundAdapterVO = inboundAdapterGroupVO.nextAdapterVO(); + if (inboundAdapterVO != null) { + String errorHandlerClass = AdapterPropManager.getInstance().getProperty( + inboundAdapterVO.getPropGroupName(), "ERR_MSG_HANDLER"); + if (StringUtils.isNotBlank(errorHandlerClass)) { + if (logger.isInfo()) + logger.info("ExceptionHandler] errorSyncSend ERR_MSG_HANDLER=" + errorHandlerClass); + AdapterErrorMessageHandler handler = AdapterErrorMessageHandlerFactory.createHandler(errorHandlerClass); + if (handler != null) { + // 표준전문에 에러코드/에러메시지 세팅 + try { + StandardMessageManager standardManager = StandardMessageManager.getInstance(); + standardManager.getMessageCoordinator().coordinateSetStandardMessageError( + eaiMessage.getStandardMessage(), eaiMessage.getMapper(), errCode, errMsg); + } catch (Exception e) { + if (logger.isWarn()) + logger.warn("ExceptionHandler] errorSyncSend coordinateSetStandardMessageError 처리 실패: " + e.getMessage()); + } + + Object responseObj = handler.generateNonStandardInternalErrorResponseMessage( + sngSysItfTp, inboundAdapterVO.getName(), null, tmpMsg, eaiMessage); + if (responseObj != null) { + resultEAIMessage = eaiMessage; + try { + resultEAIMessage.getStandardMessage().setBizData(responseObj, charset); + } catch (Exception e) { + logger.error("setBizData error", e); + } + resultEAIMessage.getMapper().setResponseType( + resultEAIMessage.getStandardMessage(), STDMessageKeys.RESPONSE_TYPE_CODE_E); + resultEAIMessage.setRspErrCd(EAIMessageKeys.EAI_SUCCESS_CODE, false); + return resultEAIMessage; + } + } + } + } + } + } catch (Exception e) { + if (logger.isWarn()) + logger.warn("ExceptionHandler] errorSyncSend AdapterErrorMessageHandler 처리 실패: " + e.getMessage()); + } + try { if (transClassify) { if (logger.isInfo()) { diff --git a/src/main/java/com/eactive/eai/outbound/DefaultProcess.java b/src/main/java/com/eactive/eai/outbound/DefaultProcess.java index df20282..6786e1d 100644 --- a/src/main/java/com/eactive/eai/outbound/DefaultProcess.java +++ b/src/main/java/com/eactive/eai/outbound/DefaultProcess.java @@ -671,6 +671,8 @@ public abstract class DefaultProcess extends Process { StandardMessage resStandardMessage = this.resEaiMsg.getStandardMessage(); InterfaceMapper mapper = this.resEaiMsg.getMapper(); String responseType = mapper.getResponseType(resStandardMessage); + + // apigw내 오류 없이, 표준헤더내 오류설정된 경우 응답 처리 if(!STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(responseType)){ String inboudnAdapterGroupName = this.callProp.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME); String inboudnAdapterName = this.callProp.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME); @@ -693,7 +695,7 @@ public abstract class DefaultProcess extends Process { AdapterErrorMessageHandler adapterErrorMessageHandler = AdapterErrorMessageHandlerFactory.createHandler(errorResponseHandlerClass); - Object responseObj = adapterErrorMessageHandler.generateNonStandardErrorResponseMessage(inboudnAdapterGroupName, inboudnAdapterName, this.callProp, this.tgtTranObject, resStandardMessage); + Object responseObj = adapterErrorMessageHandler.generateNonStandardErrorResponseMessage(inboudnAdapterGroupName, inboudnAdapterName, this.callProp, this.tgtTranObject, this.resEaiMsg); if(responseObj != null) { resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode()); } diff --git a/src/main/java/com/eactive/eai/outbound/RESTProcess.java b/src/main/java/com/eactive/eai/outbound/RESTProcess.java index 24f10ae..98b74c0 100644 --- a/src/main/java/com/eactive/eai/outbound/RESTProcess.java +++ b/src/main/java/com/eactive/eai/outbound/RESTProcess.java @@ -522,6 +522,7 @@ public class RESTProcess extends HTTPProcess { String url = StringUtils.removeEnd(simAddr, "/") + "/mockapi/" + this.reqEaiMsg.getEAISvcCd(); this.outboundProp.put(HttpClientAdapterServiceKey.URL, url); + this.tempProp.put("tranType", reqEaiMsg.getTranType()); this.tempProp.remove(HttpAdapterServiceKey.INBOUND_REWRITE_PATH); logger.debug("simUrl=" + url); } 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 b73811e..aa9d174 100644 --- a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java +++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java @@ -1,6 +1,8 @@ package com.eactive.eai.adapter.handler; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -12,9 +14,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.mockito.Mockito; import org.springframework.context.ApplicationContext; +import com.eactive.eai.adapter.AdapterGroupVO; +import com.eactive.eai.adapter.AdapterManager; +import com.eactive.eai.adapter.AdapterPropManager; +import com.eactive.eai.adapter.AdapterVO; +import com.eactive.eai.adapter.http.HttpStatusException; +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.message.StandardItem; @@ -25,7 +35,7 @@ import com.eactive.eai.message.StandardType; * TemplateAdapterErrorMsgHandler 단위테스트 * * - render() : package-private 이므로 같은 패키지에서 직접 호출 - * - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에 + * - generateNonStandard*() : ApplicationContextProvider 에 * mock ApplicationContext 를 리플렉션으로 주입하여 PropManager 격리 */ class TemplateAdapterErrorMsgHandlerTest { @@ -38,7 +48,7 @@ class TemplateAdapterErrorMsgHandlerTest { } // ================================================================ - // StandardMessage 빌더 헬퍼 + // 공통 헬퍼 // ================================================================ /** FIELD 아이템 (type=2) */ @@ -109,6 +119,22 @@ class TemplateAdapterErrorMsgHandlerTest { return p; } + /** ApplicationContextProvider 에 mock PropManager 를 주입한다. */ + private void injectMockPropManager(PropManager mockPropManager) throws Exception { + ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class); + Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager); + Field ctxField = ApplicationContextProvider.class.getDeclaredField("context"); + ctxField.setAccessible(true); + ctxField.set(null, mockCtx); + } + + /** EAIMessage 에 StandardMessage 를 주입해 반환한다. */ + private EAIMessage toEaiMessage(StandardMessage msg) { + EAIMessage eaiMessage = new EAIMessage(); + eaiMessage.setStandardMessage(msg); + return eaiMessage; + } + // ================================================================ // 스칼라 변수 치환 ${path} // ================================================================ @@ -275,12 +301,11 @@ class TemplateAdapterErrorMsgHandlerTest { @DisplayName("중간 값이 Properties 가 아니면 전체 keyPath 로 폴백 조회") void callProp_중간값_비Properties_폴백() { Properties callProp = new Properties(); - callProp.setProperty("A.B", "FALLBACK_VALUE"); // 점 포함 키 - callProp.setProperty("A", "NOT_PROPS"); // String 값 + callProp.setProperty("A.B", "FALLBACK_VALUE"); + callProp.setProperty("A", "NOT_PROPS"); StandardMessage msg = buildMsg("E001", "오류", "", null); - // A 는 String 이므로 Properties 가 아님 → "A.B" 전체 키로 폴백 assertEquals("FALLBACK_VALUE", handler.render("${callprop.A.B}", msg, callProp)); } @@ -333,10 +358,8 @@ class TemplateAdapterErrorMsgHandlerTest { @DisplayName("callProp 간접 참조 ${callprop[경로]}") class IndirectCallPropSubstitution { - /** 표준전문에 cp_key 필드가 있는 메시지 */ private StandardMessage buildMsgWithKey(String cpKey, String errCode) { StandardMessage msg = buildMsg(errCode, "오류", "", null); - // MSG 그룹 아래 cp_key 필드 추가 StandardItem msgGroup = msg.findItem("MSG"); if (msgGroup != null) { msgGroup.addItem(field("cp_key", cpKey)); @@ -414,10 +437,6 @@ class TemplateAdapterErrorMsgHandlerTest { } } - // ================================================================ - // 배열 반복 {{#foreach path}}...{{/foreach}} - // ================================================================ - // ================================================================ // DATA 영역 변수 치환 ${DATA.xxx} // ================================================================ @@ -426,17 +445,11 @@ class TemplateAdapterErrorMsgHandlerTest { @DisplayName("DATA 영역 변수 치환 ${DATA.xxx}") class DataSectionSubstitution { - /** - * DATA > BIZDATA(FIELD, JSON String value) 구조의 StandardMessage 생성. - * BIZDATA 는 파싱 없이 JSON 문자열 그대로 세팅되는 형태. - */ private StandardMessage buildMsgWithBizData(String bizDataJson) { StandardMessage msg = buildMsg("E001", "오류", "", null); - StandardItem dataGroup = group("DATA"); dataGroup.addItem(field("BIZDATA", bizDataJson)); msg.addItem(dataGroup); - return msg; } @@ -502,6 +515,10 @@ class TemplateAdapterErrorMsgHandlerTest { } } + // ================================================================ + // 배열 반복 {{#foreach path}}...{{/foreach}} + // ================================================================ + @Nested @DisplayName("배열 반복 블록 {{#foreach}}") class ForeachBlock { @@ -538,6 +555,17 @@ class TemplateAdapterErrorMsgHandlerTest { assertEquals("[]", handler.render(template, msg, null)); } + @Test + @DisplayName("# 이 HTML 인코딩된 foreach 도 정상 처리") + void HTML_인코딩된_foreach() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{{"E001", "오류A", "", ""}}); + // 관리 UI 가 # 을 # 로 저장한 경우 + String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]"; + + assertEquals("[{\"c\":\"E001\"}]", handler.render(template, msg, null)); + } + @Test @DisplayName("foreach 경로가 없으면 빈 문자열") void 없는_경로() { @@ -556,6 +584,266 @@ class TemplateAdapterErrorMsgHandlerTest { assertEquals("{\"field\":\"accountNo\"}", handler.render(template, msg, null)); } + + @Test + @DisplayName("스칼라 → foreach → 스칼라 순서 처리 – 단일 패스 검증 (사용자 템플릿 형식)") + void 스칼라_foreach_스칼라_단일패스() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{{"E001", "오류A", "설명A", "fieldA"}}); + // 스칼라 앞, foreach 중간, 스칼라 뒤 순서로 배치 + String template = + "{\"mainCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "{{#foreach MSG.MSG_LIST}}{\"code\":\"${outp_msg_cd}\"}{{/foreach}}," + + "\"mainMsg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + + String result = handler.render(template, msg, null); + + assertEquals( + "{\"mainCode\":\"E001\"," + + "{\"code\":\"E001\"}," + + "\"mainMsg\":\"대표오류\"}", + result); + } + + @Test + @DisplayName("foreach 앞뒤 스칼라 + 2건 리스트 – 단일 패스 검증") + void 스칼라_foreach_스칼라_다건() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{ + {"E001", "오류A", "", ""}, + {"E002", "오류B", "", ""} + }); + String template = + "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "\"errors\":[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]," + + "\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + + String result = handler.render(template, msg, null); + + assertEquals( + "{\"code\":\"E001\"," + + "\"errors\":[{\"c\":\"E001\"},{\"c\":\"E002\"}]," + + "\"msg\":\"대표오류\"}", + result); + } + } + + // ================================================================ + // 예외 객체 필드 치환 ${exception.필드명} + // ================================================================ + + @Nested + @DisplayName("예외 객체 필드 치환 ${exception.필드명}") + class ExceptionSubstitution { + + @Test + @DisplayName("exception.message → getMessage() 반환") + void exception_message() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + RuntimeException e = new RuntimeException("처리 중 오류 발생"); + + String result = handler.render("${exception.message}", msg, null, e); + + assertEquals("처리 중 오류 발생", result); + } + + @Test + @DisplayName("HttpStatusException.status → getStatus() 반환") + void HttpStatusException_status() { + HttpStatusException e = new HttpStatusException("Bad Request", 400); + + assertEquals("400", handler.render("${exception.status}", null, null, e)); + } + + @Test + @DisplayName("HttpStatusException.code → getCode() 반환") + void HttpStatusException_code() { + HttpStatusException e = new HttpStatusException("오류", "ERR_CODE", 400); + + assertEquals("ERR_CODE", handler.render("${exception.code}", null, null, e)); + } + + @Test + @DisplayName("JwtAuthException.code → getCode() 반환") + void JwtAuthException_code() { + JwtAuthException e = new JwtAuthException("JWT_EXPIRED", "토큰 만료"); + + assertEquals("JWT_EXPIRED", handler.render("${exception.code}", null, null, e)); + } + + @Test + @DisplayName("JwtAuthException.message → getMessage() 반환") + void JwtAuthException_message() { + JwtAuthException e = new JwtAuthException("JWT_EXPIRED", "토큰이 만료되었습니다"); + + assertEquals("토큰이 만료되었습니다", handler.render("${exception.message}", null, null, e)); + } + + @Test + @DisplayName("존재하지 않는 필드는 빈 문자열") + void 없는_필드_빈문자열() { + RuntimeException e = new RuntimeException("오류"); + + assertEquals("", handler.render("${exception.noneField}", null, null, e)); + } + + @Test + @DisplayName("존재하지 않는 필드에 기본값 지정") + void 없는_필드_기본값() { + RuntimeException e = new RuntimeException("오류"); + + assertEquals("UNKNOWN", handler.render("${exception.noneField:UNKNOWN}", null, null, e)); + } + + @Test + @DisplayName("exception 이 null 이면 기본값 반환") + void exception_null_기본값() { + assertEquals("DEFAULT", handler.render("${exception.message:DEFAULT}", null, null, null)); + } + + @Test + @DisplayName("exception + callProp + StandardMessage 혼합 치환") + void exception_callProp_stdmsg_혼합() { + StandardMessage msg = buildMsg("E001", "대표오류", "", null); + Properties callProp = props("IF_ID", "SVC001"); + HttpStatusException e = new HttpStatusException("Bad Request", 400); + String template = + "{\"ifId\":\"${callprop.IF_ID}\"," + + "\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "\"httpStatus\":\"${exception.status}\"," + + "\"errMsg\":\"${exception.message}\"}"; + + String result = handler.render(template, msg, callProp, e); + + assertEquals( + "{\"ifId\":\"SVC001\"," + + "\"code\":\"E001\"," + + "\"httpStatus\":\"400\"," + + "\"errMsg\":\"Bad Request\"}", + result); + } + + @Test + @DisplayName("부모 클래스(Throwable) 필드도 리플렉션으로 접근") + void 부모클래스_필드_접근() { + // Throwable 의 detailMessage 필드 (getMessage() 로 접근됨) + HttpStatusException e = new HttpStatusException("서비스 불가", 503); + + // getStatus 는 HttpStatusException 직접 정의 메서드 + assertEquals("503", handler.render("${exception.status}", null, null, e)); + // getMessage 는 Throwable 상속 메서드 + assertEquals("서비스 불가", handler.render("${exception.message}", null, null, e)); + } + } + + // ================================================================ + // 배열 인덱스 직접 접근 ${path[n].field} + // ================================================================ + + @Nested + @DisplayName("배열 인덱스 직접 접근 ${path[n].field}") + class ArrayIndexSubstitution { + + @Test + @DisplayName("첫 번째 행(인덱스 0) 필드 접근") + void 첫번째_행_필드_접근() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{ + {"E001", "오류A", "설명A", "fieldA"}, + {"E002", "오류B", "설명B", "fieldB"} + }); + + assertEquals("E001", handler.render("${MSG.MSG_LIST[0].outp_msg_cd}", msg, null)); + } + + @Test + @DisplayName("두 번째 행(인덱스 1) 필드 접근") + void 두번째_행_필드_접근() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{ + {"E001", "오류A", "설명A", "fieldA"}, + {"E002", "오류B", "설명B", "fieldB"} + }); + + assertEquals("E002", handler.render("${MSG.MSG_LIST[1].outp_msg_cd}", msg, null)); + } + + @Test + @DisplayName("여러 인덱스를 한 템플릿에서 동시 참조") + void 여러_인덱스_동시_참조() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{ + {"E001", "오류A", "", ""}, + {"E002", "오류B", "", ""} + }); + String template = "{\"first\":\"${MSG.MSG_LIST[0].outp_msg_cd}\",\"second\":\"${MSG.MSG_LIST[1].outp_msg_cd}\"}"; + + assertEquals("{\"first\":\"E001\",\"second\":\"E002\"}", handler.render(template, msg, null)); + } + + @Test + @DisplayName("인덱스가 범위를 벗어나면 기본값 반환") + void 인덱스_범위_초과_기본값() { + StandardMessage msg = buildMsg("E001", "오류", "", + new String[][]{{"E001", "오류A", "", ""}}); + + assertEquals("NONE", handler.render("${MSG.MSG_LIST[5].outp_msg_cd:NONE}", msg, null)); + } + + @Test + @DisplayName("인덱스가 범위를 벗어나면 빈 문자열 (기본값 미지정)") + void 인덱스_범위_초과_빈문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", + new String[][]{{"E001", "오류A", "", ""}}); + + assertEquals("", handler.render("${MSG.MSG_LIST[9].outp_msg_cd}", msg, null)); + } + + @Test + @DisplayName("MSG_LIST 가 비어있을 때 인덱스 접근은 빈 문자열") + void 빈_배열_인덱스_접근() { + StandardMessage msg = buildMsg("E001", "오류", "", new String[][]{}); + + assertEquals("", handler.render("${MSG.MSG_LIST[0].outp_msg_cd}", msg, null)); + } + + @Test + @DisplayName("배열 경로가 없으면 기본값 반환") + void 없는_배열_경로() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("DEFAULT", handler.render("${NONE.LIST[0].outp_msg_cd:DEFAULT}", msg, null)); + } + + @Test + @DisplayName("err_occu_item_nm 필드 인덱스 접근") + void err_occu_item_nm_인덱스_접근() { + StandardMessage msg = buildMsg("E001", "오류", "", + new String[][]{ + {"E001", "오류A", "", "accountNo"}, + {"E002", "오류B", "", "amount"} + }); + + assertEquals("accountNo", handler.render("${MSG.MSG_LIST[0].err_occu_item_nm}", msg, null)); + assertEquals("amount", handler.render("${MSG.MSG_LIST[1].err_occu_item_nm}", msg, null)); + } + + @Test + @DisplayName("인덱스 접근과 MAIN_MSG 스칼라 혼합") + void 인덱스_접근_스칼라_혼합() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{{"E001", "오류A", "", "fieldA"}}); + String template = + "{\"mainCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "\"firstCode\":\"${MSG.MSG_LIST[0].outp_msg_cd}\"," + + "\"firstField\":\"${MSG.MSG_LIST[0].err_occu_item_nm}\"}"; + + assertEquals( + "{\"mainCode\":\"E001\"," + + "\"firstCode\":\"E001\"," + + "\"firstField\":\"fieldA\"}", + handler.render(template, msg, null)); + } } // ================================================================ @@ -627,21 +915,14 @@ class TemplateAdapterErrorMsgHandlerTest { } // ================================================================ - // generateNonStandardErrorResponseMessage – PropManager mock + // generateNonStandardErrorResponseMessage + // 템플릿 키: {adapterGroupId}.template // ================================================================ @Nested @DisplayName("generateNonStandardErrorResponseMessage") class GenerateMethod { - private void injectMockPropManager(PropManager mockPropManager) throws Exception { - ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class); - Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager); - Field ctxField = ApplicationContextProvider.class.getDeclaredField("context"); - ctxField.setAccessible(true); - ctxField.set(null, mockCtx); - } - @Test @DisplayName("템플릿 미설정 시 null 반환") void 템플릿_없으면_null() throws Exception { @@ -649,9 +930,9 @@ class TemplateAdapterErrorMsgHandlerTest { Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); injectMockPropManager(mockProp); - StandardMessage msg = buildMsg("E001", "오류", "", null); + EAIMessage resEaiMsg = toEaiMessage(buildMsg("E001", "오류", "", null)); Object result = handler.generateNonStandardErrorResponseMessage( - "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg); + "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, resEaiMsg); assertNull(result); } @@ -663,9 +944,9 @@ class TemplateAdapterErrorMsgHandlerTest { Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" "); injectMockPropManager(mockProp); - StandardMessage msg = buildMsg("E001", "오류", "", null); + EAIMessage resEaiMsg = toEaiMessage(buildMsg("E001", "오류", "", null)); Object result = handler.generateNonStandardErrorResponseMessage( - "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg); + "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, resEaiMsg); assertNull(result); } @@ -680,9 +961,9 @@ class TemplateAdapterErrorMsgHandlerTest { eq("MY_GROUP.template"))).thenReturn(template); injectMockPropManager(mockProp); - StandardMessage msg = buildMsg("E999", "테스트오류", "", null); + EAIMessage resEaiMsg = toEaiMessage(buildMsg("E999", "테스트오류", "", null)); Object result = handler.generateNonStandardErrorResponseMessage( - "MY_GROUP", "MY_ADAPTER", new Properties(), null, msg); + "MY_GROUP", "MY_ADAPTER", new Properties(), null, resEaiMsg); assertEquals("{\"code\":\"E999\"}", result); } @@ -697,11 +978,10 @@ class TemplateAdapterErrorMsgHandlerTest { eq("MY_GROUP.template"))).thenReturn(template); injectMockPropManager(mockProp); - StandardMessage msg = buildMsg("E999", "테스트오류", "", null); + EAIMessage resEaiMsg = toEaiMessage(buildMsg("E999", "테스트오류", "", null)); Properties callProp = props("ADAPTER_NAME", "REAL_ADAPTER"); - Object result = handler.generateNonStandardErrorResponseMessage( - "MY_GROUP", "MY_ADAPTER", callProp, null, msg); + "MY_GROUP", "MY_ADAPTER", callProp, null, resEaiMsg); assertEquals("{\"adapter\":\"REAL_ADAPTER\",\"code\":\"E999\"}", result); } @@ -713,13 +993,394 @@ class TemplateAdapterErrorMsgHandlerTest { Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); injectMockPropManager(mockProp); - StandardMessage msg = buildMsg("E001", "오류", "", null); + EAIMessage resEaiMsg = toEaiMessage(buildMsg("E001", "오류", "", null)); handler.generateNonStandardErrorResponseMessage( - "SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, msg); + "SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, resEaiMsg); Mockito.verify(mockProp).getProperty( TemplateAdapterErrorMsgHandler.PROP_GROUP, "SAMPLE_GROUP.template"); } } + + // ================================================================ + // generateNonStandardInternalErrorResponseMessage (ExceptionHandler 내부 오류용) + // 템플릿 키: {adapterGroupId}.sys.template + // ================================================================ + + @Nested + @DisplayName("generateNonStandardInternalErrorResponseMessage") + class GenerateInternalMethod { + + @Test + @DisplayName("템플릿 미설정 시 null 반환") + void 템플릿_없으면_null() throws Exception { + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류", "", null)); + Object result = handler.generateNonStandardInternalErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, eaiMessage); + + assertNull(result); + } + + @Test + @DisplayName("빈 템플릿 시 null 반환") + void 빈_템플릿_null() throws Exception { + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" "); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류", "", null)); + Object result = handler.generateNonStandardInternalErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, eaiMessage); + + assertNull(result); + } + + @Test + @DisplayName("템플릿 설정 시 EAIMessage 의 StandardMessage 로 치환") + void 템플릿_stdmsg_치환() throws Exception { + String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("MY_GROUP.sys.template"))).thenReturn(template); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E999", "내부오류", "", null)); + Object result = handler.generateNonStandardInternalErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", null, null, eaiMessage); + + assertEquals("{\"code\":\"E999\",\"msg\":\"내부오류\"}", result); + } + + @Test + @DisplayName("callProp 이 null 이어도 스칼라 기본값 처리 (ExceptionHandler 에서 null 전달)") + void callProp_null_기본값_처리() throws Exception { + String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"adapter\":\"${callprop.ADAPTER_NAME:UNKNOWN}\"}"; + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("MY_GROUP.sys.template"))).thenReturn(template); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E500", "시스템오류", "", null)); + Object result = handler.generateNonStandardInternalErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", null, null, eaiMessage); + + // callProp null → 기본값 UNKNOWN 반환 + assertEquals("{\"code\":\"E500\",\"adapter\":\"UNKNOWN\"}", result); + } + + @Test + @DisplayName("MSG_LIST foreach 포함 템플릿 치환") + void foreach_MSG_LIST_포함() throws Exception { + String template = + "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "\"errors\":[{{#foreach MSG.MSG_LIST}}" + + "{\"c\":\"${outp_msg_cd}\",\"m\":\"${outp_msg_ctnt}\"}" + + "{{/foreach}}]}"; + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("GW_GROUP.sys.template"))).thenReturn(template); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "대표오류", "", + new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}})); + Object result = handler.generateNonStandardInternalErrorResponseMessage( + "GW_GROUP", "GW_ADAPTER", null, null, eaiMessage); + + assertEquals( + "{\"code\":\"E001\"," + + "\"errors\":[{\"c\":\"E001\",\"m\":\"오류A\"}," + + "{\"c\":\"E002\",\"m\":\"오류B\"}]}", + result); + } + + @Test + @DisplayName("템플릿 키는 {adapterGroupId}.sys.template 형식으로 조회") + void 템플릿_키_형식_검증() throws Exception { + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류", "", null)); + handler.generateNonStandardInternalErrorResponseMessage( + "SAMPLE_GROUP", "ANY_ADAPTER", null, null, eaiMessage); + + Mockito.verify(mockProp).getProperty( + TemplateAdapterErrorMsgHandler.PROP_GROUP, + "SAMPLE_GROUP.sys.template"); + } + + @Test + @DisplayName("스칼라 → foreach → 스칼라 순서 – 사용자 실제 템플릿 형식") + void 사용자_실제_템플릿_형식() throws Exception { + // ExceptionHandler 에서 사용되는 실제 템플릿 패턴: 스칼라, foreach, 스칼라 순서 + String template = + "{\"aeh-sys-resultCode1\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "{{#foreach MSG.MSG_LIST}}" + + "\"aeh-sys-resultCode1\":\"${outp_msg_cd}\"" + + "{{/foreach}}," + + "\"aeh-sys-resultMsg1\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("GW_GROUP.sys.template"))).thenReturn(template); + injectMockPropManager(mockProp); + + EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류메시지", "", + new String[][]{{"E001", "오류A", "", ""}})); + Object result = handler.generateNonStandardInternalErrorResponseMessage( + "GW_GROUP", "GW_ADAPTER", null, null, eaiMessage); + + assertEquals( + "{\"aeh-sys-resultCode1\":\"E001\"," + + "\"aeh-sys-resultCode1\":\"E001\"," + + "\"aeh-sys-resultMsg1\":\"오류메시지\"}", + result); + } + } + + // ================================================================ + // generateNonStandardInboundErrorResponseMessage (인바운드 오류 응답) + // 템플릿 키: {adapterGroupId}.in.{httpCode}.template → {adapterGroupId}.in.template 순서로 조회 + // ================================================================ + + @Nested + @DisplayName("generateNonStandardInboundErrorResponseMessage") + class GenerateInboundMethod { + + private AdapterManager mockAdapterManager; + private AdapterPropManager mockAdapterPropManager; + private PropManager mockPropManager; + private AdapterGroupVO mockAdapterGroupVO; + private AdapterVO mockAdapterVO; + + @BeforeEach + void setUpInbound() throws Exception { + mockAdapterManager = Mockito.mock(AdapterManager.class); + mockAdapterPropManager = Mockito.mock(AdapterPropManager.class); + mockPropManager = Mockito.mock(PropManager.class); + mockAdapterGroupVO = Mockito.mock(AdapterGroupVO.class); + mockAdapterVO = Mockito.mock(AdapterVO.class); + + Mockito.when(mockAdapterGroupVO.getMessageType()).thenReturn("JSON"); + Mockito.when(mockAdapterGroupVO.getMessageEncode()).thenReturn("UTF-8"); + Mockito.when(mockAdapterVO.getPropGroupName()).thenReturn("TEST_PROP"); + Mockito.when(mockAdapterVO.getAdapterGroupVO()).thenReturn(mockAdapterGroupVO); + Mockito.when(mockAdapterManager.getAdapterGroupVO("TEST_GROUP")).thenReturn(mockAdapterGroupVO); + Mockito.when(mockAdapterManager.getAdapterVO("TEST_GROUP", "TEST_ADAPTER")).thenReturn(mockAdapterVO); + + Properties httpProp = new Properties(); + httpProp.setProperty("ERROR_RESPONSE_FORMAT", ""); + Mockito.when(mockAdapterPropManager.getProperties("TEST_PROP")).thenReturn(httpProp); + Mockito.when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null); + // 3-인자 getProperty(group, key, defaultValue) → defaultValue 반환 + // (MessageUtil.makeJsonErrorMessage 에서 ERROR_MESSAGE_DEFAULT_FORMAT 폴백 시 사용) + Mockito.when(mockPropManager.getProperty(anyString(), anyString(), anyString())) + .thenAnswer(invocation -> invocation.getArgument(2)); + + ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class); + Mockito.when(mockCtx.getBean(AdapterManager.class)).thenReturn(mockAdapterManager); + Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockAdapterPropManager); + Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager); + Field ctxField = ApplicationContextProvider.class.getDeclaredField("context"); + ctxField.setAccessible(true); + ctxField.set(null, mockCtx); + } + + @Test + @DisplayName("HttpStatusException → httpCode 전용 템플릿 우선 사용") + void HttpStatusException_전용_템플릿() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.400.template"))) + .thenReturn("{\"status\":\"400\",\"msg\":\"Bad Request\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new HttpStatusException("Bad Request", 400)); + + assertEquals("{\"status\":\"400\",\"msg\":\"Bad Request\"}", result); + } + + @Test + @DisplayName("HttpStatusException → 전용 템플릿 없으면 공통 in.template 사용") + void HttpStatusException_공통_템플릿_폴백() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.404.template"))).thenReturn(null); + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.template"))) + .thenReturn("{\"error\":\"not found\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new HttpStatusException("Not Found", 404)); + + assertEquals("{\"error\":\"not found\"}", result); + } + + @Test + @DisplayName("JwtAuthException → 401 전용 템플릿 사용") + void JwtAuthException_401_전용_템플릿() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.401.template"))) + .thenReturn("{\"error\":\"unauthorized\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new JwtAuthException("AUTH_ERR", "토큰 인증 실패")); + + assertEquals("{\"error\":\"unauthorized\"}", result); + } + + @Test + @DisplayName("JwtAuthException → 401 전용 없으면 공통 in.template 사용") + void JwtAuthException_공통_템플릿_폴백() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.401.template"))).thenReturn(null); + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.template"))) + .thenReturn("{\"error\":\"auth failed\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new JwtAuthException("AUTH_ERR", "토큰 만료")); + + assertEquals("{\"error\":\"auth failed\"}", result); + } + + @Test + @DisplayName("기타 예외 → 500 전용 템플릿 사용") + void 기타예외_500_전용_템플릿() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.500.template"))) + .thenReturn("{\"error\":\"internal server error\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new RuntimeException("처리 중 오류")); + + assertEquals("{\"error\":\"internal server error\"}", result); + } + + @Test + @DisplayName("기타 예외 → 500 전용 없으면 공통 in.template 사용") + void 기타예외_공통_템플릿_폴백() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.500.template"))).thenReturn(null); + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.template"))) + .thenReturn("{\"error\":\"server error\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new RuntimeException("처리 실패")); + + assertEquals("{\"error\":\"server error\"}", result); + } + + @Test + @DisplayName("callProp 값이 inbound 템플릿에 치환") + void callProp_inbound_템플릿_치환() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.500.template"))) + .thenReturn("{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.IF_ID}\"}"); + + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER", "IF_ID", "SVC001"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", callProp, null, null, + new RuntimeException("오류")); + + assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\"}", result); + } + + @Test + @DisplayName("inbound 템플릿은 msg=null 이므로 StandardMessage 경로는 빈 문자열") + void inbound_템플릿_stdmsg_경로_빈문자열() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.500.template"))) + .thenReturn("{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd:NONE}\",\"fixed\":\"value\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new RuntimeException("오류")); + + // render(template, null, callProp) → StandardMessage 경로는 기본값으로 처리 + assertEquals("{\"code\":\"NONE\",\"fixed\":\"value\"}", result); + } + + @Test + @DisplayName("inbound 템플릿에서 ${exception.xxx} 로 예외 필드 참조") + void inbound_템플릿_exception_치환() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.400.template"))) + .thenReturn("{\"status\":\"${exception.status}\",\"msg\":\"${exception.message}\",\"code\":\"${exception.code}\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new HttpStatusException("Bad Request", "ERR_400", 400)); + + assertEquals("{\"status\":\"400\",\"msg\":\"Bad Request\",\"code\":\"ERR_400\"}", result); + } + + @Test + @DisplayName("JwtAuthException 템플릿에서 ${exception.code} 참조") + void inbound_jwt_exception_code_치환() throws Exception { + Mockito.when(mockPropManager.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("TEST_GROUP.in.401.template"))) + .thenReturn("{\"code\":\"${exception.code}\",\"msg\":\"${exception.message}\"}"); + + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new JwtAuthException("JWT_EXPIRED", "토큰이 만료되었습니다")); + + assertEquals("{\"code\":\"JWT_EXPIRED\",\"msg\":\"토큰이 만료되었습니다\"}", result); + } + + @Test + @DisplayName("템플릿 조회 순서: 전용(httpCode) → 공통(in) 순서로 조회") + void 템플릿_조회_순서_검증() throws Exception { + // 두 템플릿 모두 null → MessageUtil 폴백 (조회 순서만 검증) + handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new HttpStatusException("Service Unavailable", 503)); + + InOrder inOrder = Mockito.inOrder(mockPropManager); + inOrder.verify(mockPropManager).getProperty( + TemplateAdapterErrorMsgHandler.PROP_GROUP, "TEST_GROUP.in.503.template"); + inOrder.verify(mockPropManager).getProperty( + TemplateAdapterErrorMsgHandler.PROP_GROUP, "TEST_GROUP.in.template"); + } + + @Test + @DisplayName("두 템플릿 모두 없으면 MessageUtil 폴백 결과가 non-null") + void 템플릿_없으면_MessageUtil_폴백() throws Exception { + // 모든 getProperty → null (기본 설정) + Object result = handler.generateNonStandardInboundErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", null, null, null, + new RuntimeException("오류")); + + // MessageUtil.makeErrorMessageByMessageType 가 호출되어 non-null 반환 + assertNotNull(result); + } + } }