Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb8a91f5f2 | |||
| 0f02f4d261 | |||
| bc9a4caf4a | |||
| 82b7c779f1 | |||
| 65bd1ff005 | |||
| e9fbffa87e | |||
| 585a9e4520 | |||
| e3419e4d43 | |||
| f11d27e05e | |||
| 2865c6bd03 | |||
| cc016d9d81 | |||
| 23bdca4777 | |||
| 398c26537f | |||
| 3f75bd9b49 | |||
| 51d95d8824 | |||
| 5c3de18cc9 | |||
| a5a072fef4 | |||
| 078331b3b8 | |||
| 428ab1103a | |||
| 4813a2ef0e | |||
| 752156b86f | |||
| 5732ddf962 | |||
| 5ffc48c039 | |||
| cbb6f33ce5 | |||
| 44093681b7 | |||
| a24b437f5c | |||
| 67dba6ca4a | |||
| f4d7ac2fad | |||
| 6ca653aa1e | |||
| 90941bff54 |
+12
-3
@@ -25,7 +25,8 @@ java {
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
sourceSets.main.java { srcDir generatedJavaDir }
|
||||
// generatedJavaDir 는 아래 generatedSourceOutputDirectory 로 APT 가 이미 컴파일함.
|
||||
// srcDir 로도 등록하면 낡은 생성물이 입력소스가 돼 APT 재생성 시 duplicate class 발생 → 등록 금지.
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
|
||||
aptOptions {
|
||||
@@ -50,7 +51,15 @@ dependencies {
|
||||
//implementation project(':elink-online-transformer')
|
||||
api project(':elink-online-transformer')
|
||||
|
||||
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
|
||||
// damo-manager.jar 를 제외한 나머지 libs 는 기존대로 컴파일 시점에만 사용한다.
|
||||
// (WAS lib 또는 다른 경로에서 런타임에 제공됨)
|
||||
compileOnly fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
|
||||
// damo-manager.jar: 기존에는 tomcat/lib 에 직접 넣어 런타임에 제공했으나,
|
||||
// 배포 WAR(eapim-online.war)의 WEB-INF/lib 에 포함시키기 위해 런타임 의존성으로 전환한다.
|
||||
// implementation 이므로 이 모듈의 compile/test classpath 와, 이 모듈을 project 의존성으로
|
||||
// 참조하는 eapim-online 루트의 runtimeClasspath(= war 패키징 대상)에 함께 포함된다.
|
||||
implementation files('libs/damo-manager.jar')
|
||||
|
||||
api (group: 'org.apache.activemq', name: 'activemq-console', version: '5.14.5'){
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
@@ -142,7 +151,7 @@ dependencies {
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
|
||||
testImplementation 'junit:junit:4.4'
|
||||
testImplementation files('libs/damo-manager.jar', 'libs/kjb-safedb.jar')
|
||||
testImplementation files('libs/kjb-safedb.jar')
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
Binary file not shown.
@@ -128,6 +128,9 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
|
||||
String templateKey = inboundAdapterGroupName + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
|
||||
if (StringUtils.isBlank(template))
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
|
||||
@@ -320,6 +323,8 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
|
||||
? expr.substring(closeIdx + 2)
|
||||
: "";
|
||||
|
||||
if (msg == null) return defaultVal;
|
||||
|
||||
String key = msg.findItemValue(msgPath);
|
||||
if (StringUtils.isBlank(key)) return defaultVal;
|
||||
if (callProp == null) return defaultVal;
|
||||
@@ -406,8 +411,18 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
|
||||
@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);
|
||||
String template = "";
|
||||
String templateKey = "";
|
||||
if (resEaiMsg != null && StringUtils.isNotEmpty(resEaiMsg.getRspErrCd())) {
|
||||
templateKey = inboudnAdapterGroupName + ".sys." + resEaiMsg.getRspErrCd() + ".template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
if (StringUtils.isBlank(template)) {
|
||||
templateKey = inboudnAdapterGroupName + ".sys.template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
if (StringUtils.isBlank(template))
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.sys.template");
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
if (logger.isWarn()) {
|
||||
@@ -456,6 +471,10 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
|
||||
throws Exception {
|
||||
String templateKey = outboundadapterGroupName + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
|
||||
if (StringUtils.isBlank(template))
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
return null;
|
||||
}
|
||||
@@ -466,15 +485,27 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
|
||||
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 = "default.in." + httpCode + ".template";
|
||||
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)) {
|
||||
templateKey = "default.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);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -7,6 +7,7 @@ import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.TxFileLogger;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
@@ -60,7 +61,9 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
|
||||
if(MessageType.JSON.equals(prop.getProperty("messageType"))) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
// 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
ObjectNode jsonNode = (ObjectNode) mapper.readTree(sendData);
|
||||
ObjectNode headerPart = (ObjectNode) jsonNode.get("header_part");
|
||||
|
||||
|
||||
+61
-12
@@ -71,6 +71,8 @@ import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.RestSendBodyLogUtils;
|
||||
import com.eactive.eai.common.util.TxFileLogger;
|
||||
import com.eactive.eai.common.util.XMLUtils;
|
||||
import com.eactive.eai.message.StandardItem;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
import com.eactive.eai.util.JsonPathUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
@@ -110,6 +112,9 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
|
||||
static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : ";
|
||||
|
||||
// URL Path Variable 이 표준전문(요청) 항목을 참조할 때 사용하는 접두어. 접두어 뒤는 표준전문 루트 기준 전체 경로.
|
||||
static final String STD_MSG_VARIABLE_PREFIX = "stdHeaderGroup.";
|
||||
|
||||
private boolean useAdapterToken;
|
||||
|
||||
/**
|
||||
@@ -242,9 +247,9 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
uri = vo.getUrl();
|
||||
} else {
|
||||
if (dataObject == null) {
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData, inboundPathVariables);
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData, inboundPathVariables, tempProp);
|
||||
} else {
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject, inboundPathVariables);
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject, inboundPathVariables, tempProp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,8 +532,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
// 이경우 정상으로 처리함
|
||||
} else if (status >= 200 && status <= 207) {
|
||||
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
needReissue = checkTokenRetry(status, responseMessage, tokenErrorHttpStatusCode, tokenErrorCodeKey,
|
||||
tokenErrorCodeValues, vo.getEncode());
|
||||
}
|
||||
} else if (status == 302) {
|
||||
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
|
||||
@@ -542,15 +547,14 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
|
||||
if (status >= 400 && status < 500) {
|
||||
if (useAdapterToken) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
needReissue = checkTokenRetry(status, responseMessage, tokenErrorHttpStatusCode,
|
||||
tokenErrorCodeKey, tokenErrorCodeValues, vo.getEncode());
|
||||
}
|
||||
|
||||
if (!needReissue) {
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
} else {
|
||||
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
}
|
||||
@@ -882,8 +886,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
|
||||
String encode) {
|
||||
protected boolean checkTokenRetry(int status, byte[] responseMessage, String tokenErrorHttpStatusCode,
|
||||
String tokenErrorCodeKey, String tokenErrorCodeValues, String encode) {
|
||||
if (responseMessage == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -1156,7 +1160,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
return document;
|
||||
}
|
||||
|
||||
private String changeUrl(String messageType, String url, String restOption, Object sendData, Map<String, String> inboundPathVariables) {
|
||||
private String changeUrl(String messageType, String url, String restOption, Object sendData,
|
||||
Map<String, String> inboundPathVariables, Properties tempProp) {
|
||||
if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) {
|
||||
try {
|
||||
if (StringUtils.isBlank(restOption)) {
|
||||
@@ -1189,17 +1194,28 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
// jsonObject = (JSONObject) JSONValue.parse((String) sendData);
|
||||
// }
|
||||
for (String urlVaribleId : uriVariables) {
|
||||
String uriVariable = JsonPathUtil.getAt(jsonNode, urlVaribleId, "\\.");
|
||||
String uriVariable;
|
||||
if (urlVaribleId.startsWith(STD_MSG_VARIABLE_PREFIX)) {
|
||||
uriVariable = getStandardMessageValue(tempProp, urlVaribleId);
|
||||
} else {
|
||||
uriVariable = JsonPathUtil.getAt(jsonNode, urlVaribleId, "\\.");
|
||||
}
|
||||
// String uriVariable = null;
|
||||
// if( jsonObject != null ) {
|
||||
// uriVariable = (String) jsonObject.get(urlVaribleId);
|
||||
// jsonObject.remove(urlVaribleId);
|
||||
// }
|
||||
|
||||
if (StringUtils.isBlank(uriVariable) && inboundPathVariables.containsKey(urlVaribleId)) {
|
||||
if (StringUtils.isBlank(uriVariable) && inboundPathVariables != null
|
||||
&& inboundPathVariables.containsKey(urlVaribleId)) {
|
||||
uriVariable = inboundPathVariables.get(urlVaribleId);
|
||||
}
|
||||
|
||||
// expand()는 이름이 아닌 순서로 매핑하므로 값이 없어도 자리를 유지해야 한다.
|
||||
if (uriVariable == null) {
|
||||
logger.warn("HttpClientAdapterServiceRest] uri variable not found=[" + urlVaribleId + "] ");
|
||||
uriVariable = "";
|
||||
}
|
||||
urlVariableList.add(uriVariable);
|
||||
|
||||
}
|
||||
@@ -1212,7 +1228,17 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
for (Object tempObject : uriVariables) {
|
||||
String urlVaribleId = (String) tempObject;
|
||||
if (urlVaribleId.startsWith(STD_MSG_VARIABLE_PREFIX)) {
|
||||
String stdValue = getStandardMessageValue(tempProp, urlVaribleId);
|
||||
urlVariableList.add(stdValue == null ? "" : stdValue);
|
||||
continue;
|
||||
}
|
||||
Element element = (Element) doc.selectSingleNode("//" + urlVaribleId);
|
||||
if (element == null) {
|
||||
logger.warn("HttpClientAdapterServiceRest] uri variable not found=[" + urlVaribleId + "] ");
|
||||
urlVariableList.add("");
|
||||
continue;
|
||||
}
|
||||
urlVariableList.add(element.getText());
|
||||
if (doc.getRootElement() == element) {
|
||||
doc = null;
|
||||
@@ -1235,6 +1261,29 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL Path Variable 이 {@code stdHeaderGroup.} 접두어를 가지면 요청 표준전문에서 값을 찾는다.
|
||||
*
|
||||
* @param tempProp 어댑터 임시 속성 (요청 표준전문 보관)
|
||||
* @param urlVaribleId 접두어를 포함한 Path Variable 이름
|
||||
* @return 표준전문 항목 값. 표준전문이 없거나 항목을 찾지 못하면 null
|
||||
*/
|
||||
private String getStandardMessageValue(Properties tempProp, String urlVaribleId) {
|
||||
Object stdMessageObject = tempProp.get(HttpAdapterServiceKey.OUT_REQ_STD_MSG);
|
||||
if (!(stdMessageObject instanceof StandardMessage)) {
|
||||
logger.warn("HttpClientAdapterServiceRest] request standard message not found=[" + urlVaribleId + "] ");
|
||||
return null;
|
||||
}
|
||||
// 접두어는 표기용 이름이므로 제거한 나머지가 표준전문 루트 기준 전체 경로가 된다.
|
||||
String itemPath = urlVaribleId.substring(STD_MSG_VARIABLE_PREFIX.length());
|
||||
StandardItem item = ((StandardMessage) stdMessageObject).findItem(itemPath);
|
||||
if (item == null) {
|
||||
logger.warn("HttpClientAdapterServiceRest] standard message item not found=[" + itemPath + "] ");
|
||||
return null;
|
||||
}
|
||||
return item.getValue();
|
||||
}
|
||||
|
||||
private String getUrl(String baseUrl, String extraPath) {
|
||||
if (StringUtils.isBlank(extraPath)) {
|
||||
return baseUrl;
|
||||
|
||||
@@ -86,6 +86,9 @@ public interface HttpAdapterServiceKey {
|
||||
//응답 처리 용 표준 전문 오브젝트
|
||||
static final String STANDARD_MESSAGE_OBJECT = "STANDARD_MESSAGE_OBJECT";
|
||||
|
||||
//송신(Outbound) 요청 표준 전문 오브젝트
|
||||
static final String OUT_REQ_STD_MSG = "OUT_REQ_STD_MSG";
|
||||
|
||||
// 어댑터별 인증 키 헤더 이름
|
||||
static final String ADAPTER_TOKEN_HEADER_NAME = "ADAPTER_TOKEN_HEADER_NAME";
|
||||
static final String ADAPTER_APIKEY_HEADER_NAME = "ADAPTER_APIKEY_HEADER_NAME";
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactory;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.TxSiftContext;
|
||||
import com.eactive.eai.common.util.UUIDGenerator;
|
||||
@@ -31,13 +32,20 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
public static final String PROPERTIES_NAME_CLIENT_ID = "clientId";
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
EAIServerManager eaiServerManager;
|
||||
String instid = null;
|
||||
|
||||
public Object service(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
boolean bMDCput = false;
|
||||
|
||||
if(uuid == null) {
|
||||
uuid = UUIDGenerator.getUUID().toString().replaceAll("-", "");
|
||||
if(instid == null) {
|
||||
eaiServerManager = EAIServerManager.getInstance();
|
||||
instid = eaiServerManager.getGroupInstId();
|
||||
}
|
||||
uuid = instid + UUIDGenerator.getUUID().toString().replaceAll("-", "");
|
||||
prop.setProperty(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
|
||||
public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
|
||||
public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";
|
||||
public static final String PASS_SCOPE_LIST = "pass.scope.list";
|
||||
|
||||
public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL";
|
||||
public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL";
|
||||
@@ -59,7 +60,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";
|
||||
|
||||
private JWSVerifier jwsVerifier;
|
||||
|
||||
private String[] passScopeArr = {};
|
||||
// // CA 토큰 저장소
|
||||
// private final Map<String, BearerTokenInfo> CATokenStore = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -89,6 +90,10 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
} catch (IOException | NoSuchAlgorithmException | RuntimeException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String passScope = vo.getProperty(PASS_SCOPE_LIST, "oob,public");
|
||||
passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -316,9 +321,6 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
String passScope = "oob,public";
|
||||
String[] passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
|
||||
|
||||
for (String scope : scopeArr) {
|
||||
for (String pScope : passScopeArr) {
|
||||
if ( StringUtils.equalsAny(scope, pScope)) {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.JsonPathUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class JsonToSetStatusFilter implements HttpAdapterFilter {
|
||||
private static final String PROPGROUP = "JsonToSetStatusFilter";
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
/** HTTP 상태코드 유효 범위 (RFC 7231) */
|
||||
private static final int MIN_HTTP_STATUS = 100;
|
||||
private static final int MAX_HTTP_STATUS = 599;
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
// do nothing
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String fieldName = "";
|
||||
String statusParam = "";
|
||||
try {
|
||||
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
|
||||
fieldName = getFieldName(adptGrpName);
|
||||
if (rootNode != null && StringUtils.isNotBlank(fieldName) && rootNode.has(fieldName)) {
|
||||
statusParam = rootNode.get(fieldName).asText();
|
||||
int httpStatus = Integer.parseInt(statusParam);
|
||||
if (isValidHttpStatus(httpStatus)) {
|
||||
response.setStatus(httpStatus);
|
||||
} else {
|
||||
logger.warn("유효하지 않은 HTTP 상태코드. 상태코드를 설정하지 않음. fieldName={}, value={}", fieldName, statusParam);
|
||||
}
|
||||
} else {
|
||||
logger.warn("설정과 맞지 않는 메시지. 상태코드를 설정하지 않음. fieldName={}", fieldName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("상태코드 추출 실패. fieldName={}, value={}", fieldName, statusParam, e);
|
||||
}
|
||||
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
/** HTTP 상태코드로 사용 가능한 값인지 확인한다. (100 ~ 599) */
|
||||
private boolean isValidHttpStatus(int httpStatus) {
|
||||
return httpStatus >= MIN_HTTP_STATUS && httpStatus <= MAX_HTTP_STATUS;
|
||||
}
|
||||
|
||||
private String getFieldName(String adptGrpName) {
|
||||
return PropManager.getInstance().getProperty(PROPGROUP, adptGrpName);
|
||||
}
|
||||
|
||||
private JsonNode parseJson(String adptGrpName, Object message) throws Exception {
|
||||
return JsonPathUtil.toTree(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* 수신 Http Header 중 <b>허용 목록(white list)에 등록된 헤더만</b> 응답 헤더로 복사하는 Inbound Adapter Filter.
|
||||
*
|
||||
* <p>모든 헤더를 복사하고 제외 목록으로 걸러내는 {@link ReflectAllHeaderFilter} 와 달리,
|
||||
* 복사 대상을 어댑터 그룹별로 명시하는 방식이다.
|
||||
*
|
||||
* <p>설정은 <code>HttpHeaderFilter</code> property 그룹에 등록하며, 아래 순서로 가장 먼저 찾은 키 하나만 사용한다.
|
||||
* (합집합이 아니라 override 이므로 어댑터 그룹 키를 지정하면 전역 키는 무시된다.)
|
||||
*
|
||||
* <pre>
|
||||
* 1. ReflectHeaderFilter.whiteList.{어댑터그룹명} 어댑터 그룹 단위
|
||||
* 2. ReflectHeaderFilter.whiteList 전역 기본값
|
||||
* </pre>
|
||||
*
|
||||
* <p>값은 콤마로 구분한 헤더명 목록이며 대소문자를 구분하지 않는다.
|
||||
* 헤더명 끝에 <code>*</code> 를 붙이면 접두사 일치로 처리한다.
|
||||
*
|
||||
* <pre>
|
||||
* HttpHeaderFilter.ReflectHeaderFilter.whiteList = x-elink-client-id
|
||||
* HttpHeaderFilter.ReflectHeaderFilter.whiteList.djbTrans = x-obp-txid, x-obp-partnercode, X-KKB-*
|
||||
* </pre>
|
||||
*
|
||||
* <p>설정이 없거나 비어 있으면 어떤 헤더도 복사하지 않는다.
|
||||
*/
|
||||
public class ReflectHeaderFilter implements HttpAdapterFilter {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
|
||||
public static final String HEADER_KEY_NAMES = "ReflectHeaderFilter.whiteList";
|
||||
|
||||
/**
|
||||
* 허용 목록에 등록되어 있어도 복사하지 않는 헤더.
|
||||
* 요청측 값이 응답 본문/커넥션과 불일치하면 응답 자체가 깨지므로 설정으로 열 수 없게 한다.
|
||||
*/
|
||||
private static final Set<String> NEVER_REFLECT = createHeaderSet(
|
||||
"Content-Length",
|
||||
"Transfer-Encoding",
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Upgrade",
|
||||
"TE",
|
||||
"Trailer");
|
||||
|
||||
/** 어댑터 그룹별 허용 목록 캐시. 필터 인스턴스는 HttpAdapterFilterFactory 에서 싱글톤으로 공유된다. */
|
||||
private final ConcurrentHashMap<String, CachedWhiteList> whiteListCache = new ConcurrentHashMap<String, CachedWhiteList>();
|
||||
|
||||
public ReflectHeaderFilter() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
logger.debug("doPreFilter ReflectHeaderFilter Start.");
|
||||
|
||||
reflectHeaders(adptGrpName, request, response);
|
||||
|
||||
logger.debug("doPreFilter ReflectHeaderFilter End.");
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
logger.debug("doPostFilter ReflectHeaderFilter Start.");
|
||||
|
||||
reflectHeaders(adptGrpName, request, response);
|
||||
|
||||
logger.debug("doPostFilter ReflectHeaderFilter End.");
|
||||
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
private void reflectHeaders(String adptGrpName, HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
WhiteList whiteList = getWhiteList(adptGrpName);
|
||||
|
||||
if (whiteList.isEmpty()) {
|
||||
logger.debug("No reflect header configured for adapter group [" + adptGrpName + "]. Skip all.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (String headerName : whiteList.getNames()) {
|
||||
|
||||
String headerValue = request.getHeader(headerName);
|
||||
if (headerValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setHeader(headerName, headerValue, response);
|
||||
}
|
||||
|
||||
// 접두사(*) 설정이 있을 때만 수신 헤더를 순회한다.
|
||||
if (whiteList.hasPrefix()) {
|
||||
java.util.Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames != null && headerNames.hasMoreElements()) {
|
||||
|
||||
String headerName = headerNames.nextElement();
|
||||
if (!whiteList.matchesPrefix(headerName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setHeader(headerName, request.getHeader(headerName), response);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeader(String headerName, String headerValue, HttpServletResponse response) {
|
||||
|
||||
if (NEVER_REFLECT.contains(headerName)) {
|
||||
logger.debug("Skip Processing Key [" + headerName + "] in NEVER_REFLECT list");
|
||||
return;
|
||||
}
|
||||
|
||||
// 헤더명/값에 CR, LF 가 있으면 응답 분할(response splitting) 위험이 있다.
|
||||
if (containsCrLf(headerName) || containsCrLf(headerValue)) {
|
||||
logger.error("Skip Processing Key [" + headerName + "] - CR/LF detected in header name or value");
|
||||
return;
|
||||
}
|
||||
|
||||
response.setHeader(headerName, headerValue);
|
||||
logger.debug("Processing Key [" + headerName + "], value [" + headerValue + "]");
|
||||
}
|
||||
|
||||
private boolean containsCrLf(String value) {
|
||||
return value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 어댑터 그룹 → 전역 순으로 property 를 찾아 허용 목록을 반환한다.
|
||||
* property 값이 바뀌지 않는 동안은 파싱 결과를 재사용한다.
|
||||
*/
|
||||
private WhiteList getWhiteList(String adptGrpName) {
|
||||
|
||||
String cacheKey = StringUtils.defaultString(adptGrpName);
|
||||
String propValue = findPropValue(adptGrpName);
|
||||
|
||||
CachedWhiteList cached = whiteListCache.get(cacheKey);
|
||||
if (cached != null && StringUtils.equals(cached.propValue, propValue)) {
|
||||
return cached.whiteList;
|
||||
}
|
||||
|
||||
WhiteList whiteList = parse(propValue);
|
||||
whiteListCache.put(cacheKey, new CachedWhiteList(propValue, whiteList));
|
||||
return whiteList;
|
||||
}
|
||||
|
||||
private String findPropValue(String adptGrpName) {
|
||||
|
||||
PropManager propManager = PropManager.getInstance();
|
||||
|
||||
if (StringUtils.isNotBlank(adptGrpName)) {
|
||||
String value = propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES + "." + adptGrpName, "");
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
return StringUtils.trimToEmpty(value);
|
||||
}
|
||||
}
|
||||
|
||||
return StringUtils.trimToEmpty(propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, ""));
|
||||
}
|
||||
|
||||
private static WhiteList parse(String propValue) {
|
||||
|
||||
Set<String> names = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
List<String> prefixes = new ArrayList<String>();
|
||||
|
||||
if (StringUtils.isNotBlank(propValue)) {
|
||||
for (String token : propValue.split(",")) {
|
||||
String name = StringUtils.trimToEmpty(token);
|
||||
if (name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.endsWith("*")) {
|
||||
String prefix = name.substring(0, name.length() - 1);
|
||||
if (!prefix.isEmpty()) {
|
||||
prefixes.add(prefix);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return new WhiteList(names, prefixes);
|
||||
}
|
||||
|
||||
private static Set<String> createHeaderSet(String... names) {
|
||||
Set<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
Collections.addAll(set, names);
|
||||
return Collections.unmodifiableSet(set);
|
||||
}
|
||||
|
||||
/** 파싱된 허용 목록. 헤더명 완전일치 목록과 접두사(*) 목록으로 구성된다. */
|
||||
private static class WhiteList {
|
||||
|
||||
private final Set<String> names;
|
||||
private final List<String> prefixes;
|
||||
|
||||
WhiteList(Set<String> names, List<String> prefixes) {
|
||||
this.names = Collections.unmodifiableSet(names);
|
||||
this.prefixes = Collections.unmodifiableList(prefixes);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return names.isEmpty() && prefixes.isEmpty();
|
||||
}
|
||||
|
||||
Set<String> getNames() {
|
||||
return names;
|
||||
}
|
||||
|
||||
boolean hasPrefix() {
|
||||
return !prefixes.isEmpty();
|
||||
}
|
||||
|
||||
boolean matchesPrefix(String headerName) {
|
||||
for (String prefix : prefixes) {
|
||||
if (StringUtils.startsWithIgnoreCase(headerName, prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CachedWhiteList {
|
||||
|
||||
private final String propValue;
|
||||
private final WhiteList whiteList;
|
||||
|
||||
CachedWhiteList(String propValue, WhiteList whiteList) {
|
||||
this.propValue = propValue;
|
||||
this.whiteList = whiteList;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -13,12 +13,15 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class JsonToStdConverterFilter implements HttpAdapterFilter {
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
// 파싱 후 rootNode.toString() 으로 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
|
||||
+4
-1
@@ -11,6 +11,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -19,7 +20,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class KbankEaiJsonParseFilter implements HttpAdapterFilter {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
// 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
|
||||
+766
@@ -0,0 +1,766 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.impl;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.fileupload.FileItem;
|
||||
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.mina.common.ByteBuffer;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.adapter.Keys;
|
||||
import com.eactive.eai.adapter.http.HttpMemoryLogger;
|
||||
import com.eactive.eai.adapter.http.HttpMethodType;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.env.ElinkConfig;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
import com.eactive.eai.message.StandardMessageUtil;
|
||||
|
||||
/*
|
||||
* Kbank 가상계좌 INBOUND
|
||||
* @see com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
|
||||
* @Deprecated
|
||||
*/
|
||||
// FIXME : kbank - kbank에서는 Controller 방식을 사용하므로, 이 어댑터는 사용되지 않음 (VirtualAccountCryptoFilter 로 대체)
|
||||
public class HttpAdapterServiceVirtualAccount extends HttpAdapterServiceSupport {
|
||||
public static final String HEADER_GROUP = "HEADER_GROUP";
|
||||
// HEADER_GROUP JSON에 추가할 항목 정의, 없으면 전체 header 추가
|
||||
public static final String HEADER_KEYS = "HEADER_KEYS";
|
||||
public static final String HTTP_STATUS = "HTTP_STATUS";
|
||||
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
private String logPrefix = "HttpAdapterServiceRest] ";
|
||||
|
||||
private static final String JSON_CONTENT_TYPE = "application/json";
|
||||
private static final String JSON_FIELD_NAME = "json-body";
|
||||
private static final String FILE_GROUP_NAME = "image-file";
|
||||
private static final String UPLOAD_ROOT_PATH = "UPLOAD_ROOT_PATH";
|
||||
|
||||
private Properties addCryptoFilter(Properties prop) {
|
||||
String cryptoFilterName = "com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter";
|
||||
String addedPreFilter = prop.getProperty(PRE_FILTERS);
|
||||
String addedPostFilter = prop.getProperty(POST_FILTERS);
|
||||
|
||||
if(StringUtils.isBlank(addedPreFilter)) {
|
||||
addedPreFilter = cryptoFilterName;
|
||||
}
|
||||
else {
|
||||
addedPreFilter = addedPreFilter + "," +cryptoFilterName;
|
||||
}
|
||||
|
||||
if(StringUtils.isBlank(addedPostFilter)) {
|
||||
addedPostFilter = cryptoFilterName;
|
||||
}
|
||||
else {
|
||||
addedPostFilter = cryptoFilterName + "," +addedPostFilter;
|
||||
}
|
||||
|
||||
prop.setProperty(PRE_FILTERS, addedPreFilter);
|
||||
prop.setProperty(POST_FILTERS, addedPostFilter);
|
||||
return prop;
|
||||
}
|
||||
|
||||
private String readMultipartBody(HttpServletRequest request) throws Exception {
|
||||
String jsonString = null;
|
||||
// Create a factory for disk-based file items
|
||||
DiskFileItemFactory factory = new DiskFileItemFactory();
|
||||
|
||||
// Set the maximum size of the files to be uploaded
|
||||
factory.setSizeThreshold(1024 * 1024);
|
||||
|
||||
// Set the temporary directory to store uploaded files
|
||||
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
|
||||
factory.setRepository(tempDir);
|
||||
|
||||
ServletFileUpload upload = new ServletFileUpload(factory);
|
||||
Map<String, String> fileMap = new HashMap<>();
|
||||
|
||||
InputStream fin = null;
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
int read = 0;
|
||||
|
||||
List<FileItem> items = upload.parseRequest(request);
|
||||
for (FileItem item : items) {
|
||||
if (!item.isFormField()) {
|
||||
// file
|
||||
String fieldName = item.getFieldName();
|
||||
String fileName = item.getName();
|
||||
fin = item.getInputStream();
|
||||
ByteArrayOutputStream fo = new ByteArrayOutputStream();
|
||||
while ((read = fin.read(buffer)) > 0) {
|
||||
fo.write(buffer, 0, read);
|
||||
}
|
||||
int fileSize = fo.size();
|
||||
byte[] fileBytes = fo.toByteArray();
|
||||
String fileContents = new String(fileBytes);
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FILE]-------------------------------------------------->");
|
||||
logger.info("Field name = " + fieldName);
|
||||
logger.info("File name = " + fileName + " contents length = " + fileSize);
|
||||
logger.info("File Contents [" + fileContents + "]");
|
||||
logger.info("[FILE]<--------------------------------------------------");
|
||||
}
|
||||
fileMap.put(fileName, fileContents);
|
||||
fin.close();
|
||||
} else {
|
||||
// regular form field
|
||||
String fieldName = item.getFieldName();
|
||||
String fieldValue = item.getString();
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
|
||||
}
|
||||
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|
||||
|| JSON_FIELD_NAME.equals(fieldName)) {
|
||||
jsonString = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("Json body [" + jsonString + "]");
|
||||
}
|
||||
|
||||
if (jsonString == null) {
|
||||
jsonString = "{}";
|
||||
} else {
|
||||
// parsing json & add file contents
|
||||
JSONObject jsonObject = (JSONObject) JSONValue.parse(jsonString);
|
||||
if (jsonObject == null) {
|
||||
jsonString = "{}";
|
||||
} else {
|
||||
JSONObject fileGroup = new JSONObject();
|
||||
|
||||
for (Map.Entry<String, String> entry : fileMap.entrySet()) {
|
||||
fileGroup.put("fileName", entry.getKey());
|
||||
fileGroup.put("fileContents", entry.getValue());
|
||||
}
|
||||
jsonObject.put(FILE_GROUP_NAME, fileGroup);
|
||||
jsonString = jsonObject.toJSONString();
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("Json with file [" + jsonString + "]");
|
||||
}
|
||||
return jsonString;
|
||||
} catch (Exception e) {
|
||||
logger.error("Read multipart body error.", e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (fin != null) {
|
||||
try {
|
||||
fin.close();
|
||||
} catch (Exception ex) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String checkRootPath(String path) {
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
logger.info("upload dir not set(UPLOAD_ROOT_PATH), use system temp " + path);
|
||||
return System.getProperty("java.io.tmpdir");
|
||||
}
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
file.mkdirs();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private String uploadMultipartBody(HttpServletRequest request, String uploadRootPath) throws Exception {
|
||||
String jsonString = null;
|
||||
// Create a factory for disk-based file items
|
||||
DiskFileItemFactory factory = new DiskFileItemFactory();
|
||||
|
||||
// Set the maximum size of the files to be uploaded
|
||||
factory.setSizeThreshold(1024 * 1024);
|
||||
|
||||
// Set the temporary directory to store uploaded files
|
||||
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
|
||||
factory.setRepository(tempDir);
|
||||
|
||||
ServletFileUpload upload = new ServletFileUpload(factory);
|
||||
try {
|
||||
// if not exist, create folders
|
||||
String uploadDir = checkRootPath(uploadRootPath);
|
||||
List<FileItem> items = upload.parseRequest(request);
|
||||
for (FileItem item : items) {
|
||||
if (!item.isFormField()) {
|
||||
// file
|
||||
String fieldName = item.getFieldName();
|
||||
String fileName = item.getName();
|
||||
String uploadFilePath = uploadDir + File.separator + fileName;
|
||||
File uploadFile = new File(uploadFilePath);
|
||||
item.write(uploadFile);
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FILE]-------------------------------------------------->");
|
||||
logger.info("Field name = " + fieldName);
|
||||
logger.info("File name = " + fileName + " path = " + uploadFile.getAbsolutePath());
|
||||
logger.info("[FILE]<--------------------------------------------------");
|
||||
}
|
||||
} else {
|
||||
// regular form field
|
||||
String fieldName = item.getFieldName();
|
||||
String fieldValue = item.getString();
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
|
||||
}
|
||||
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|
||||
|| JSON_FIELD_NAME.equals(fieldName)) {
|
||||
jsonString = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("Json body [" + jsonString + "]");
|
||||
}
|
||||
return jsonString;
|
||||
} catch (Exception e) {
|
||||
logger.error("Read multipart body error.", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "deprecation" })
|
||||
public void service(String adptGrpName, String adptName, HttpServletRequest request, HttpServletResponse response) {
|
||||
int traceLevel = 0;
|
||||
|
||||
AdapterVO adptVO = null;
|
||||
AdapterPropManager manager = null;
|
||||
|
||||
Properties httpProp = null;
|
||||
String responseType = null;
|
||||
String urlDecodeYn = null;
|
||||
String encode = null;
|
||||
|
||||
String traceLevelTemp = null;
|
||||
String relayRequestHeaderKeys = null;
|
||||
String headerGroupName = null;
|
||||
|
||||
boolean isParameterType = false;
|
||||
String message = null;
|
||||
|
||||
StopWatch stopWatch = null;
|
||||
Properties prop = null;
|
||||
String paramValue = null;
|
||||
String adptMsgType = null;
|
||||
String errorResponseFormat = null;
|
||||
String uploadRootPath = null;
|
||||
try {
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
adptVO = adapterManager.getAdapterVO(adptGrpName, adptName);
|
||||
if (adptVO == null) {
|
||||
throw new Exception("Adapter not found error");
|
||||
}
|
||||
|
||||
manager = AdapterPropManager.getInstance();
|
||||
|
||||
httpProp = manager.getProperties(adptVO.getPropGroupName());
|
||||
responseType = httpProp.getProperty(RESPONSE_TYPE, "SYNC");
|
||||
urlDecodeYn = httpProp.getProperty(URL_DECODE_YN, "N");
|
||||
// encode = httpProp.getProperty(ENCODE, "UTF-8");
|
||||
encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(),
|
||||
"UTF-8");
|
||||
traceLevelTemp = httpProp.getProperty(TRACE_LEVEL, "0");
|
||||
relayRequestHeaderKeys = httpProp.getProperty(HEADER_KEYS);
|
||||
headerGroupName = httpProp.getProperty(HEADER_GROUP);
|
||||
errorResponseFormat = httpProp.getProperty(ERROR_RESPONSE_FORMAT);
|
||||
uploadRootPath = httpProp.getProperty(UPLOAD_ROOT_PATH);
|
||||
prop = new Properties();
|
||||
prop.put(INBOUND_METHOD, request.getMethod());
|
||||
prop.put(INBOUND_URI, request.getRequestURI());
|
||||
prop.put(INBOUND_HEADER, getHeaders(request));
|
||||
prop.put(INBOUND_EXTPARAMS, StringUtils.defaultString(request.getQueryString()));
|
||||
if (StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_REST)
|
||||
|| StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_HTTP_CUSTOM)) {
|
||||
// /api/v1/public/getUserInfo.svc
|
||||
String extUrl = StringUtils.removeStart(request.getRequestURI(), request.getContextPath());
|
||||
prop.put(INBOUND_EXTURI, extUrl);
|
||||
} else {
|
||||
prop.put(INBOUND_EXTURI, getExtUri(request));
|
||||
}
|
||||
prop.put(Processor.REQUEST_ACTION, adptVO.getAdapterGroupVO().getRefClass());
|
||||
prop.put(API_PATH, httpProp.getProperty(API_PATH, ""));
|
||||
prop.put(PRE_FILTERS, httpProp.getProperty(PRE_FILTERS, ""));
|
||||
prop.put(POST_FILTERS, httpProp.getProperty(POST_FILTERS, ""));
|
||||
prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod());
|
||||
prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, ""));
|
||||
|
||||
isParameterType = false;
|
||||
try {
|
||||
traceLevel = Integer.parseInt(traceLevelTemp);
|
||||
} catch (Exception e) {
|
||||
traceLevel = 0;
|
||||
}
|
||||
|
||||
stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
logger.debug("시작 >> encode = [" + encode + "]");
|
||||
|
||||
switch (HttpMethodType.getValue(request.getMethod())) {
|
||||
case GET:
|
||||
case DELETE:
|
||||
isParameterType = true;
|
||||
break;
|
||||
case POST:
|
||||
case PUT:
|
||||
if (StringUtils.contains(request.getContentType(), "application/x-www-form-urlencoded")) {
|
||||
isParameterType = true;
|
||||
} else {
|
||||
isParameterType = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isParameterType) {
|
||||
paramValue = request.getQueryString();
|
||||
if (paramValue == null)
|
||||
paramValue = "";
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
|
||||
}
|
||||
|
||||
// json으로 변환
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
Map<String, String[]> paramMap = assignParameterMap(request, adptGrpName, adptName, null, prop);
|
||||
int i = 0;
|
||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||
if (i > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\"").append(entry.getKey()).append("\":");
|
||||
String[] values = entry.getValue();
|
||||
if (values.length > 1) {
|
||||
// ["111", "222"]
|
||||
sb.append("[");
|
||||
for (int j = 0; j < values.length; j++) {
|
||||
if (j > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\"").append(JSONValue.escape(values[j])).append("\"");
|
||||
}
|
||||
sb.append("]");
|
||||
} else {
|
||||
sb.append("\"").append(JSONValue.escape(values[0])).append("\"");
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
sb.append("}");
|
||||
|
||||
paramValue = sb.toString();
|
||||
} else {
|
||||
if (ServletFileUpload.isMultipartContent(request)) {
|
||||
// TODO : 아래의 로직은 업무에 맞게 수정이 필요함.
|
||||
// 불필요할 경우 제거
|
||||
// if(StringUtils.isEmpty(uploadRootPath)) {
|
||||
// uploadRootPath = System.getProperty("java.io.tmpdir");
|
||||
// }
|
||||
|
||||
// 임시로직 : UPLOAD_ROOT_PATH 가 없는 경우에는 JSON에 추가
|
||||
if (StringUtils.isEmpty(uploadRootPath)) {
|
||||
paramValue = readMultipartBody(request);
|
||||
} else {
|
||||
paramValue = uploadMultipartBody(request, uploadRootPath);
|
||||
}
|
||||
// TEST : 테스트용 임시코드
|
||||
// response.setCharacterEncoding(encode);
|
||||
// response.getWriter().print(paramValue);
|
||||
// return;
|
||||
} else {
|
||||
ServletInputStream sis = request.getInputStream();
|
||||
ByteBuffer bb = ByteBuffer.allocate(1024).setAutoExpand(true);
|
||||
int i = 0;
|
||||
byte[] cbuf = new byte[1024];
|
||||
while ((i = sis.read(cbuf, 0, 1024)) != -1) {
|
||||
if (i == 1024) {
|
||||
bb.put(cbuf);
|
||||
} else {
|
||||
byte[] tail = new byte[i];
|
||||
System.arraycopy(cbuf, 0, tail, 0, i);
|
||||
bb.put(tail);
|
||||
}
|
||||
}
|
||||
byte[] data = new byte[bb.position()];
|
||||
bb.position(0);
|
||||
bb.get(data);
|
||||
paramValue = new String(data, encode);
|
||||
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (paramValue == null) { // parameter가 없는 경우때문에 처리
|
||||
paramValue = "";
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] RECV (" + adptGrpName + ") = [" + paramValue + "]\n"
|
||||
+ CommonLib.getDumpMessage(paramValue));
|
||||
}
|
||||
|
||||
if ("Y".equals(urlDecodeYn) && isParameterType) {
|
||||
message = URLDecoder.decode(paramValue);
|
||||
} else {
|
||||
message = paramValue;
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
String[] msgArgs = new String[2];
|
||||
msgArgs[0] = adptGrpName;
|
||||
msgArgs[1] = message;
|
||||
String resMsg = ExceptionUtil.make("RICEAIAHA005", msgArgs);
|
||||
logger.debug(logPrefix + resMsg);
|
||||
}
|
||||
|
||||
adptMsgType = adptVO.getAdapterGroupVO().getMessageType();
|
||||
if (StringUtils.equals(adptMsgType, MessageType.JSON)) {
|
||||
response.setContentType(JSON_CONTENT_TYPE+"; charset="+encode);
|
||||
}
|
||||
|
||||
// HEADER_GROUP 셋팅
|
||||
if (MessageType.JSON.equals(adptMsgType) && StringUtils.isNotBlank(headerGroupName)
|
||||
&& StringUtils.isNotBlank(relayRequestHeaderKeys)) {
|
||||
JSONObject jsonMessage = (JSONObject) JSONValue.parse(message);
|
||||
JSONObject headerJson = new JSONObject();
|
||||
if (StringUtils.equalsIgnoreCase(relayRequestHeaderKeys, "ALL")) {
|
||||
for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements();) {
|
||||
String key = e.nextElement();
|
||||
headerJson.put(key, request.getHeader(key));
|
||||
}
|
||||
} else {
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils
|
||||
.tokenizeToStringArray(relayRequestHeaderKeys, ",");
|
||||
|
||||
for (String key : relayKeyArr) {
|
||||
String headerValue = request.getHeader(key);
|
||||
if (StringUtils.isNotBlank(headerValue)) {
|
||||
headerJson.put(key, headerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (headerJson.size() > 0) {
|
||||
jsonMessage.put(headerGroupName, headerJson);
|
||||
message = jsonMessage.toJSONString();
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
message = "";
|
||||
}
|
||||
|
||||
// com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
|
||||
prop = addCryptoFilter(prop);
|
||||
|
||||
// 로컬 서비스 호출 ,encoding 처리 추가
|
||||
String result = (String) service(adptGrpName, adptName, message, prop, request, response);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] result " + encode + " (" + adptGrpName + ") = [" + result + "]");
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
String responseData = "";
|
||||
if (RESPONSE_TYPE_ASYNC.equals(responseType)) {
|
||||
if (stopWatch.getTime() > slowTranTime && (logger.isInfo())) {
|
||||
logger.info("HttpAdapterServiceRest] dummy response time = " + stopWatch.toString() + ", message = "
|
||||
+ message);
|
||||
|
||||
}
|
||||
if (result == null) {
|
||||
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
|
||||
}
|
||||
|
||||
response.setCharacterEncoding(encode);
|
||||
response.getWriter().print(responseData);
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
|
||||
}
|
||||
} else {
|
||||
responseData = result;
|
||||
logger.info("종료 >> encode = [" + encode + "]");
|
||||
|
||||
JSONObject dataObject = null;
|
||||
if (MessageType.JSON.equals(adptMsgType)) {
|
||||
dataObject = (JSONObject) JSONValue.parse(responseData);
|
||||
}
|
||||
|
||||
// HEADER_GROUP 하위 필드를 response Header에 세팅한다.
|
||||
HashMap<String, String> header = new HashMap<>();
|
||||
boolean redirect = assignHttpHeaders(header, dataObject, headerGroupName);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] response header field (" + adptGrpName + ") = ["
|
||||
+ header.toString() + "]");
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
response.setStatus(302);
|
||||
logger.debug("HttpAdapterServiceRest] set response status_code: 302");
|
||||
} else {
|
||||
String httpStatus = header.get(HTTP_STATUS);
|
||||
if (httpStatus != null && !"".equals(httpStatus))
|
||||
response.setStatus(Integer.parseInt(httpStatus));
|
||||
header.remove(HTTP_STATUS);
|
||||
}
|
||||
|
||||
// response header 셋팅
|
||||
for (Map.Entry<String, String> entry : header.entrySet()) {
|
||||
response.setHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
if (dataObject != null) {
|
||||
responseData = dataObject.toJSONString();
|
||||
}
|
||||
|
||||
// UI와 통신시(UTF-8) 변환오류로 ENCODE 제거
|
||||
response.setCharacterEncoding(encode);
|
||||
response.getWriter().print(responseData);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = [" + responseData + "]");
|
||||
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = "
|
||||
+ CommonLib.getDumpMessage(responseData));
|
||||
}
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
|
||||
}
|
||||
}
|
||||
} catch (HttpStatusException e) {
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
|
||||
}
|
||||
logger.warn("HttpAdapter] " + adptGrpName + "-" + adptName + ">>" + e.getMessage());
|
||||
response.setStatus(e.getStatus());
|
||||
try {
|
||||
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
|
||||
e.getMessage(), errorResponseFormat);
|
||||
response.getWriter().println(errorMsg);
|
||||
} catch (Exception ex) {
|
||||
// IGNORE
|
||||
}
|
||||
} catch (JwtAuthException e) {
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
|
||||
}
|
||||
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
try {
|
||||
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
|
||||
e.getMessage(), errorResponseFormat);
|
||||
response.getWriter().println(errorMsg);
|
||||
} catch (Exception ex) {
|
||||
// IGNORE
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
|
||||
}
|
||||
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
try {
|
||||
response.getWriter().println(e.getMessage());
|
||||
String errCode = ExceptionUtil.getErrorCode(e, "RECEAIAHA003");
|
||||
throw new Exception(errCode);
|
||||
} catch (Exception ex) {
|
||||
// IGNORE
|
||||
logger.warn(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
}
|
||||
} finally {
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
String url = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_EXTURI);
|
||||
String method = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_METHOD);
|
||||
String adapterGroupName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME);
|
||||
String adapterName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME);
|
||||
int httpStatusCode = response.getStatus();
|
||||
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, 400, adapterGroupName, adapterName, new HashMap<>(), url, method, httpStatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String[]> assignParameterMap(HttpServletRequest request, String adptGrpName, String adptName,
|
||||
Object requestBytes, Properties prop) {
|
||||
// PathVariable 체크
|
||||
if (StringUtils.equalsAnyIgnoreCase(request.getMethod(), HttpMethod.GET.name(), HttpMethod.DELETE.name())
|
||||
&& StringUtils.isBlank(request.getQueryString())) {
|
||||
try {
|
||||
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
|
||||
RequestAction action = ActionFactory.createAction(actionName);
|
||||
action.setAdapterInfo(adptGrpName, adptName, prop);
|
||||
String[] keys = action.perform(requestBytes);
|
||||
String requestPath = keys[0];
|
||||
|
||||
// PathVariable 지원 추가
|
||||
String ruledPath = StandardMessageUtil.getMatchedKey(requestPath, actionName);
|
||||
if (!StringUtils.equals(requestPath, ruledPath) && StringUtils.contains(ruledPath, "{")) {
|
||||
Map<String, String> paramMap = new AntPathMatcher().extractUriTemplateVariables(ruledPath,
|
||||
requestPath);
|
||||
if (paramMap != null && paramMap.size() > 0) {
|
||||
Map<String, String[]> returnMap = new HashMap<>();
|
||||
for (String key : paramMap.keySet()) {
|
||||
if (StringUtils.equalsIgnoreCase(key, "method")) {
|
||||
continue;
|
||||
}
|
||||
returnMap.put(key, new String[] { paramMap.get(key) });
|
||||
}
|
||||
|
||||
return returnMap;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return request.getParameterMap();
|
||||
}
|
||||
|
||||
private void validateServiceAndAdapter(String adptGrpName, String adptName, byte[] requestBytes, Properties prop)
|
||||
throws JwtAuthException {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* request header 를 hashmap으로 조립
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
private Properties getHeaders(HttpServletRequest request) {
|
||||
Properties prop = new Properties();
|
||||
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String key = headerNames.nextElement();
|
||||
String value = request.getHeader(key);
|
||||
prop.setProperty(key, value);
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* adapter property의 HEADER_GROUP으로 정의된 (MFE_HEADER) 그룹의 하위 필드를 http Header에
|
||||
* 세팅한다.
|
||||
*
|
||||
* @param header
|
||||
* @param object
|
||||
*/
|
||||
private boolean assignHttpHeaders(HashMap<String, String> header, Object msg, String headerGroupName) {
|
||||
boolean redirect = false;
|
||||
|
||||
if (msg == null || StringUtils.isBlank(headerGroupName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg instanceof JSONObject) {
|
||||
JSONObject headerObject = (JSONObject) ((JSONObject) msg).get(headerGroupName);
|
||||
|
||||
if (headerObject == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// List<String> headerKeys = new ArrayList<>();
|
||||
for (Object key : headerObject.keySet()) {
|
||||
|
||||
Object obj = headerObject.get(key);
|
||||
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
header.put((String) key, (String) obj);
|
||||
|
||||
if (StringUtils.equalsIgnoreCase((String) key, "Location")) {
|
||||
redirect = true;
|
||||
}
|
||||
}
|
||||
|
||||
((JSONObject) msg).remove(headerGroupName);
|
||||
}
|
||||
|
||||
return redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 어댑터 명 이후의 URI값을 가져온다.
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
private String getExtUri(HttpServletRequest request) {
|
||||
String orgUri = request.getRequestURI().replaceAll(request.getContextPath(), "");
|
||||
String uri = getExtUri(orgUri, 3);
|
||||
if (uri != null && uri.trim().length() > 0) {
|
||||
return "/" + uri;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExtUri(String url, int length) {
|
||||
String[] urls = url.split("/");
|
||||
List<String> newUrls = new ArrayList<>();
|
||||
Collections.addAll(newUrls, urls);
|
||||
return StringUtils.join(newUrls.subList(length, urls.length).toArray(), "/");
|
||||
}
|
||||
|
||||
// public static void main(String[] args) throws Exception {
|
||||
// String orgUri = "/HTT/CbsInNetSys/abcd/123456";
|
||||
// String result = "";
|
||||
// result = getExtUri(orgUri, 3);
|
||||
// System.out.println(result);
|
||||
// }
|
||||
}
|
||||
@@ -163,7 +163,14 @@ public class EncryptionManager implements Lifecycle {
|
||||
if (this.isEncrypted(dbData)) {
|
||||
if ("DAMO".equals(dbEncryptSolutionName)) {
|
||||
com.eactive.ext.djb.DamoManager damoManager = new com.eactive.ext.djb.DamoManager();
|
||||
try {
|
||||
return damoManager.decrypt(dbData);
|
||||
} catch (Exception e) {
|
||||
// 복호화 실패시 원본 반환
|
||||
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
|
||||
logger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
|
||||
return dbData;
|
||||
}
|
||||
} else if ("SAFEDB".equals(dbEncryptSolutionName)) {
|
||||
KjbSafedbWrapper safeDBWrapper = KjbSafedbWrapper.getInstance();
|
||||
try {
|
||||
|
||||
@@ -791,7 +791,9 @@ public class ExceptionHandler {
|
||||
}
|
||||
resultEAIMessage.getMapper().setResponseType(
|
||||
resultEAIMessage.getStandardMessage(), STDMessageKeys.RESPONSE_TYPE_CODE_E);
|
||||
resultEAIMessage.setOrgRspErrCd(resultEAIMessage.getRspErrCd());
|
||||
resultEAIMessage.setRspErrCd(EAIMessageKeys.EAI_SUCCESS_CODE, false);
|
||||
|
||||
return resultEAIMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.eactive.eai.common.logger;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.eactive.eai.adapter.ElinkAdapter;
|
||||
@@ -150,7 +151,7 @@ public class DBLogTransactionLogger implements TransactionLogger {
|
||||
// 템플릿만 남겨두고, 나머지는 사이트에 맞게 수정 필요함.
|
||||
//---------------------------------------------------->
|
||||
boolean itsmEnabled = false;
|
||||
if((itsmEnabled) && !MessageUtil.checkRspErrCd(message.getRspErrCd())) {
|
||||
if((itsmEnabled) && !MessageUtil.checkRspErrCd(message.getLogRspErrCd())) {
|
||||
if(logger.isDebug()) {
|
||||
logger.debug(guidLogPrefix + " ITSM Error Message Notify! ");
|
||||
}
|
||||
@@ -176,6 +177,83 @@ public class DBLogTransactionLogger implements TransactionLogger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 여러 건의 로깅 정보를 한 트랜잭션으로 적재 (COMMIT 1회)
|
||||
* 2. 처리 개요 :
|
||||
* - log()의 건별 처리와 달리 COMMIT 횟수를 배치 크기만큼 줄인다.
|
||||
* - LOG_TYPE 판정 규칙은 log()과 동일하게 유지한다.
|
||||
* 3. 주의사항
|
||||
* - 실시간 모니터링(EAIServiceMonitor) 전달은 발행측 EAILogSender.send()에서
|
||||
* 이미 처리하므로 여기서는 다루지 않는다.
|
||||
*
|
||||
* @param items {EAIMessage, Properties} 쌍의 목록
|
||||
**/
|
||||
public void logBatch(List<Object[]> items) {
|
||||
if (items == null || items.isEmpty()) return;
|
||||
logCount += items.size();
|
||||
try {
|
||||
insertLogBatch(items);
|
||||
} catch (Exception e) {
|
||||
errCount++;
|
||||
if (logger.isError()) logger.error("DBLogTransactionLogger] logBatch ERROR. - " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void insertLogBatch(List<Object[]> items) {
|
||||
if (items == null || items.isEmpty()) return;
|
||||
|
||||
// Direct DB Logging - 판정 규칙은 log()과 동일
|
||||
String logType = "DB";
|
||||
String setLogType = PropManager.getInstance().getProperty("LOG_TYPE");
|
||||
if (setLogType != null) {
|
||||
logType = setLogType;
|
||||
}
|
||||
|
||||
if (!"DB".equals(logType) || !EAIDBLogControl.isEnable()) {
|
||||
for (Object[] item : items) {
|
||||
writeFileLog((EAIMessage) item[0], (Properties) item[1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
EAILogBatchWriter writer = ApplicationContextProvider.getContext().getBean(EAILogBatchWriter.class);
|
||||
try {
|
||||
writer.writeBatch(items);
|
||||
} catch (Exception be) {
|
||||
// 배치는 한 건만 실패해도 트랜잭션 전체가 롤백된다.
|
||||
// 정상 건까지 유실되지 않도록 건별 독립 트랜잭션으로 재시도한다.
|
||||
if (logger.isError()) {
|
||||
logger.error("DBLogTransactionLogger] insertLogBatch failed, retry one by one. size=" + items.size(), be);
|
||||
}
|
||||
for (Object[] item : items) {
|
||||
EAIMessage eaiMessage = (EAIMessage) item[0];
|
||||
Properties prop = (Properties) item[1];
|
||||
try {
|
||||
writer.writeOne(eaiMessage, prop);
|
||||
} catch (Exception e) {
|
||||
String message = e.getMessage();
|
||||
// DB Connection Error일 경우에만 DB 로깅을 중단한다.
|
||||
if ("ConnectionError".equals(message) || StringUtils.contains(message, "JDBCConnectionException")
|
||||
|| StringUtils.contains(message, "Unable to acquire JDBC Connection")) {
|
||||
EAIDBLogControl.setEnable(false);
|
||||
}
|
||||
if (logger.isError()) {
|
||||
logger.error("DBLogTransactionLogger] insertLogBatch single retry failed. - " + message, e);
|
||||
}
|
||||
writeFileLog(eaiMessage, prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeFileLog(EAIMessage eaiMessage, Properties prop) {
|
||||
try {
|
||||
EAIFileLogger.getInstance().setLog(eaiMessage, prop);
|
||||
} catch (Exception fe) {
|
||||
if (logger.isError()) logger.error("DBLogTransactionLogger] file log failed. - " + fe.getMessage(), fe);
|
||||
}
|
||||
}
|
||||
|
||||
public static void insertLog(EAIMessage eaiMessage, Properties prop) throws EAILogException {
|
||||
String guidLogPrefix = "DBLogTransactionLogger] GUID["+ eaiMessage.getMapper().getGuid(eaiMessage.getStandardMessage())
|
||||
+"] UUID["+eaiMessage.getSvcOgNo()+"] ";
|
||||
|
||||
@@ -276,6 +276,8 @@ public class EAIFileLogger
|
||||
String psvItfTp = svcMsg.getPsvItfTp(); // 수동 Syunc/Async
|
||||
int logPssSno = message.getLogPssSno();
|
||||
String rspErrCd = message.getRspErrCd();
|
||||
// 내부오류 대체응답 시 원본 에러코드 기준으로 로깅한다.
|
||||
String logRspErrCd = message.getLogRspErrCd();
|
||||
|
||||
// Duplication Error 방지를 위해
|
||||
// 원 로그처리일련번호를 저장 : 2009.07.13
|
||||
@@ -629,7 +631,7 @@ public class EAIFileLogger
|
||||
|
||||
sb.appendAndDelimeter( NullControl.addSpace(message.getSngSysItfTp())); //기동시스템어댑터업무그룹명
|
||||
sb.appendAndDelimeter( NullControl.addSpace(message.getLydMsgID())); //현재메시지ID명
|
||||
sb.appendAndDelimeter( NullControl.addSpace(message.getRspErrCd())); //응답에러코드명
|
||||
sb.appendAndDelimeter( NullControl.addSpace(logRspErrCd)); //응답에러코드명
|
||||
sb.appendAndDelimeter( msgPssTm); //메시지처리시각
|
||||
sb.appendAndDelimeter( String.valueOf(message.getSvrLogLvl())); //서버로그레벨번호
|
||||
//index 20
|
||||
@@ -715,8 +717,8 @@ public class EAIFileLogger
|
||||
sb.appendAndDelimeter( NullControl.addSpace(message.getRspnsChngMsgType())); // 응답변환유형
|
||||
|
||||
// log level 'E' or 'F'
|
||||
if ("E".equals(message.getRspErrCd().substring(1, 2)) || "F".equals(message.getRspErrCd().substring(1, 2))) {
|
||||
sb.appendAndDelimeter( NullControl.addSpace(message.getRspErrCd())); //EAI에러코드
|
||||
if ("E".equals(logRspErrCd.substring(1, 2)) || "F".equals(logRspErrCd.substring(1, 2))) {
|
||||
sb.appendAndDelimeter( NullControl.addSpace(logRspErrCd)); //EAI에러코드
|
||||
sb.appendAndDelimeter( StringUtil.chunkString(message.getRspErrMsg(),1000)); //EAI에러내용
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.eactive.eai.common.logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
|
||||
/**
|
||||
* 1. 기능 : 비동기 거래로그를 여러 건 묶어 한 트랜잭션으로 적재
|
||||
* 2. 처리 개요 :
|
||||
* - EAILogDAO는 클래스 레벨 @Transactional(기본 propagation REQUIRED)이므로
|
||||
* writeBatch 안에서 호출하면 별도 트랜잭션을 열지 않고 바깥 트랜잭션에 합류한다.
|
||||
* - 결과적으로 N건이 COMMIT 1회로 처리되어 Oracle log file sync 대기가 1/N로 줄어든다.
|
||||
* 3. 주의사항
|
||||
* - 배치 중 한 건이라도 예외가 나면 트랜잭션 전체가 롤백된다.
|
||||
* 호출측(DBLogTransactionLogger.insertLogBatch)에서 건별 재시도로 폴백해야 한다.
|
||||
*/
|
||||
@Service
|
||||
public class EAILogBatchWriter {
|
||||
|
||||
@Autowired
|
||||
private EAILogDAO dao;
|
||||
|
||||
/**
|
||||
* N건을 하나의 트랜잭션으로 적재한다. (COMMIT 1회)
|
||||
*
|
||||
* @param items {EAIMessage, Properties} 쌍의 목록
|
||||
*/
|
||||
@Transactional
|
||||
public void writeBatch(List<Object[]> items) throws Exception {
|
||||
for (Object[] item : items) {
|
||||
dao.addEAISvcLog((EAIMessage) item[0], (Properties) item[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 실패 시 건별 재시도용. 각 건이 독립 트랜잭션이므로
|
||||
* 특정 건의 실패가 나머지 건에 영향을 주지 않는다.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void writeOne(EAIMessage message, Properties prop) throws Exception {
|
||||
dao.addEAISvcLog(message, prop);
|
||||
}
|
||||
}
|
||||
@@ -220,6 +220,8 @@ public class EAILogDAO {
|
||||
String psvItfTp = svcMsg.getPsvItfTp(); // 수동 Syunc/Async
|
||||
int logPssSno = message.getLogPssSno();
|
||||
String rspErrCd = message.getRspErrCd();
|
||||
// 내부오류 대체응답 시 원본 에러코드 기준으로 로깅한다.
|
||||
String logRspErrCd = message.getLogRspErrCd();
|
||||
|
||||
// Duplication Error 방지를 위해
|
||||
// 원 로그처리일련번호를 저장 : 2009.07.13
|
||||
@@ -565,7 +567,7 @@ public class EAILogDAO {
|
||||
// 현재메시지ID명
|
||||
eaiLog.setPrsntmsgidname(message.getLydMsgID());
|
||||
// 응답에러코드명
|
||||
eaiLog.setRspnserrcdname(message.getRspErrCd());
|
||||
eaiLog.setRspnserrcdname(logRspErrCd);
|
||||
// 메시지처리시각
|
||||
eaiLog.setMsgprcssyms(msgPssTm);
|
||||
// 서버로그레벨번호
|
||||
@@ -659,10 +661,10 @@ public class EAILogDAO {
|
||||
eaiLog.setRspnschngmsgtype(message.getRspnsChngMsgType());
|
||||
|
||||
// log level 'E' or 'F'
|
||||
if ("E".equals(message.getRspErrCd().substring(1, 2))
|
||||
|| "F".equals(message.getRspErrCd().substring(1, 2))) {
|
||||
if ("E".equals(logRspErrCd.substring(1, 2))
|
||||
|| "F".equals(logRspErrCd.substring(1, 2))) {
|
||||
// EAI에러코드
|
||||
eaiLog.setEaierrcd(message.getRspErrCd());
|
||||
eaiLog.setEaierrcd(logRspErrCd);
|
||||
// EAI에러내용
|
||||
eaiLog.setEaierrctnt(StringUtil.chunkString(message.getRspErrMsg(), 1000));
|
||||
}
|
||||
@@ -724,11 +726,11 @@ public class EAILogDAO {
|
||||
}
|
||||
|
||||
// 에러로그를 별도의 테이블에 저장하도록 한다.
|
||||
if (!MessageUtil.checkRspErrCd(message.getRspErrCd())) {
|
||||
if (!MessageUtil.checkRspErrCd(logRspErrCd)) {
|
||||
try {
|
||||
// 거래통제, 유량제어에 의한 에러는 저장하지 않도록 한다.
|
||||
if (!(EAIMessageKeys.EAI_BLOCKED_CODE.equals(message.getRspErrCd())
|
||||
|| EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(message.getRspErrCd()))) {
|
||||
if (!(EAIMessageKeys.EAI_BLOCKED_CODE.equals(logRspErrCd)
|
||||
|| EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(logRspErrCd))) {
|
||||
addErrorLog(message); // 에러로그
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@@ -741,6 +743,8 @@ public class EAILogDAO {
|
||||
public void addErrorLog(EAIMessage message) throws DAOException {
|
||||
try {
|
||||
String serverName = EAIServerManager.getInstance().getLocalServerName();
|
||||
// 내부오류 대체응답 시 원본 에러코드 기준으로 로깅한다.
|
||||
String logRspErrCd = message.getLogRspErrCd();
|
||||
|
||||
EAIErrorLog eaiErrorLog = (EAIErrorLog) applicationContext.getBean(RollingTable.class, EAIErrorLog.class,
|
||||
message.getMsgRcvTm());
|
||||
@@ -760,16 +764,16 @@ public class EAILogDAO {
|
||||
// EAI서비스명
|
||||
eaiErrorLog.setEaisvcname(message.getEAISvcCd());
|
||||
// 응답에러코드명
|
||||
eaiErrorLog.setRspnserrcdname(message.getRspErrCd());
|
||||
eaiErrorLog.setRspnserrcdname(logRspErrCd);
|
||||
// 기동시스템어댑터업무그룹명
|
||||
eaiErrorLog.setGstatsysadptrbzwkgroupname(message.getSngSysItfTp());
|
||||
// 수동시스템어댑터업무그룹명
|
||||
eaiErrorLog.setPsvsysadptrbzwkgroupname(message.getCurrentSvcMsg().getPsvSysItfTp());
|
||||
|
||||
if ("E".equals(message.getRspErrCd().substring(1, 2))
|
||||
|| "F".equals(message.getRspErrCd().substring(1, 2))) {
|
||||
if ("E".equals(logRspErrCd.substring(1, 2))
|
||||
|| "F".equals(logRspErrCd.substring(1, 2))) {
|
||||
// EAI에러코드
|
||||
eaiErrorLog.setEaierrcd(message.getRspErrCd());
|
||||
eaiErrorLog.setEaierrcd(logRspErrCd);
|
||||
// EAI에러내용
|
||||
eaiErrorLog.setEaierrctnt(StringUtil.chunkString(message.getRspErrMsg(), 500));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.eactive.eai.common.logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.SerializationUtils;
|
||||
@@ -71,7 +72,7 @@ public class EAILogSender {
|
||||
}
|
||||
}
|
||||
else if(svcLogLvl == 1) {
|
||||
isLogging = ! MessageUtil.checkRspErrCd(message.getRspErrCd());
|
||||
isLogging = ! MessageUtil.checkRspErrCd(message.getLogRspErrCd());
|
||||
}
|
||||
else {
|
||||
isLogging = false;
|
||||
@@ -105,23 +106,48 @@ public class EAILogSender {
|
||||
|
||||
// 실시간 모니터링 로그
|
||||
EAIServiceMonitor servicemonitor = EAIServiceMonitor.getInstance();
|
||||
if(EAIMessageKeys.EAI_BLOCKED_CODE.equals(message.getRspErrCd())) {
|
||||
if(logger.isWarn()) {
|
||||
logger.warn(guidLogPrefix + " 거래통제 실시간 모니터링 SKIP - " + message.getEAISvcCd()
|
||||
+ ", "+message.getMapper().getGuid(message.getStandardMessage()) );
|
||||
}
|
||||
}else if (EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(message.getRspErrCd())) {
|
||||
if(logger.isWarn()) {
|
||||
logger.warn(guidLogPrefix + " 유량제어 실시간 모니터링 SKIP - " + message.getEAISvcCd()
|
||||
+ ", "+ message.getMapper().getGuid(message.getStandardMessage()) );
|
||||
}
|
||||
}else {
|
||||
servicemonitor.receiveLogMessage(message);
|
||||
}
|
||||
// if(EAIMessageKeys.EAI_BLOCKED_CODE.equals(message.getRspErrCd())) {
|
||||
// if(logger.isWarn()) {
|
||||
// logger.warn(guidLogPrefix + " 거래통제 실시간 모니터링 SKIP - " + message.getEAISvcCd()
|
||||
// + ", "+message.getMapper().getGuid(message.getStandardMessage()) );
|
||||
// }
|
||||
// }else if (EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(message.getRspErrCd())) {
|
||||
// if(logger.isWarn()) {
|
||||
// logger.warn(guidLogPrefix + " 유량제어 실시간 모니터링 SKIP - " + message.getEAISvcCd()
|
||||
// + ", "+ message.getMapper().getGuid(message.getStandardMessage()) );
|
||||
// }
|
||||
// }else {
|
||||
// servicemonitor.receiveLogMessage(message);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
public static void logDirect(EAIMessage message, Properties prop) throws EAILogException {
|
||||
txLogger.log(message, prop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 건을 한 트랜잭션(COMMIT 1회)으로 적재한다.
|
||||
* 비동기 로깅 컨슈머(CustomEventHandler)가 모아둔 배치를 넘길 때 사용한다.
|
||||
*
|
||||
* @param items {EAIMessage, Properties} 쌍의 목록
|
||||
*/
|
||||
public static void logDirectBatch(List<Object[]> items) {
|
||||
if (items == null || items.isEmpty()) return;
|
||||
|
||||
if (txLogger instanceof DBLogTransactionLogger) {
|
||||
((DBLogTransactionLogger) txLogger).logBatch(items);
|
||||
return;
|
||||
}
|
||||
|
||||
// 배치를 지원하지 않는 TransactionLogger 구현이면 건별 처리로 폴백한다.
|
||||
for (Object[] item : items) {
|
||||
try {
|
||||
txLogger.log((EAIMessage) item[0], (Properties) item[1]);
|
||||
} catch (Exception e) {
|
||||
logger.error("logDirectBatch fallback failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.eactive.eai.common.logger;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.eactive.eai.common.logger.mapper.HttpAdapterExtraLogMapper;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
||||
@Service
|
||||
@@ -22,6 +25,20 @@ public class HttpLoggingService {
|
||||
@Autowired
|
||||
private HttpAdapterExtraLogFileLogger fileLogger;
|
||||
|
||||
/**
|
||||
* 여러 건을 한 트랜잭션(COMMIT 1회)으로 적재한다.
|
||||
* HttpAdapterExtraLogLogger가 @Transactional(REQUIRED)이므로 이 트랜잭션에 합류한다.
|
||||
*
|
||||
* 배치 중 한 건이라도 실패하면 전체가 롤백되므로,
|
||||
* 호출측에서 insertHttpAdapterExtraLog()로 건별 재시도해야 한다.
|
||||
*/
|
||||
@Transactional
|
||||
public void insertHttpAdapterExtraLogBatch(List<HttpAdapterExtraLogVo> voList) throws Throwable {
|
||||
for (HttpAdapterExtraLogVo vo : voList) {
|
||||
dbLogger.save(mapper.toEntity(vo));
|
||||
}
|
||||
}
|
||||
|
||||
public void insertHttpAdapterExtraLog(HttpAdapterExtraLogVo httpAdapterExtraLogVo) throws Throwable{
|
||||
HttpAdapterExtraLog httpAdapterExtraLog = mapper.toEntity(httpAdapterExtraLogVo);
|
||||
if (EAIDBLogControl.isEnable()) {
|
||||
|
||||
@@ -3,59 +3,124 @@ package com.eactive.eai.common.logger.async;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.eactive.eai.common.logger.EAILogException;
|
||||
import com.eactive.eai.common.logger.EAILogSender;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.lmax.disruptor.EventHandler;
|
||||
import com.lmax.disruptor.LifecycleAware;
|
||||
import com.lmax.disruptor.TimeoutHandler;
|
||||
|
||||
public class CustomEventHandler implements EventHandler<LoggingEvent> {
|
||||
/**
|
||||
* 1. 기능 : 거래로그 이벤트를 모아 한 트랜잭션(COMMIT 1회)으로 적재
|
||||
* 2. 처리 개요 :
|
||||
* - Disruptor의 endOfBatch는 "지금 링버퍼에 더 처리할 이벤트가 없다"는 신호다.
|
||||
* 이것을 flush 조건으로 쓰면 한산할 때는 건당 즉시 적재되어 지연이 늘지 않고,
|
||||
* 부하가 몰릴 때만 배치가 커진다. 별도 타임아웃 flush 스레드가 필요 없다.
|
||||
* - batchSize는 하한이 아니라 상한이다. "100건 모일 때까지 대기"가 아니라
|
||||
* "한 트랜잭션이 100건을 넘지 않게 끊는다"는 의미다.
|
||||
* - 안전망으로 TimeoutHandler를 구현한다. 유휴 상태가 지속되면 Disruptor가
|
||||
* onTimeout()을 호출하므로, 버퍼에 남은 로그가 방치되지 않는다.
|
||||
* (WaitStrategy가 TimeoutBlockingWaitStrategy = 설정값 "TIME"일 때 동작. 기본값)
|
||||
* 3. 주의사항
|
||||
* - LoggingEvent.clear()는 EAIMessage 내용까지 비우므로 버퍼에 담은 뒤 호출하면 안 된다.
|
||||
* 슬롯 참조만 끊고, EAIMessage 해제는 적재 완료 후 releaseMessages()에서 처리한다.
|
||||
*/
|
||||
public class CustomEventHandler implements EventHandler<LoggingEvent>, LifecycleAware, TimeoutHandler {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
String name;
|
||||
int sleepMs;
|
||||
private int batchSize = 1;
|
||||
|
||||
private int count = 0;
|
||||
private final List<LoggingEvent> eventList = new ArrayList<>();
|
||||
private final String name;
|
||||
private final int sleepMs;
|
||||
private final int batchSize;
|
||||
|
||||
public CustomEventHandler() {
|
||||
}
|
||||
private final List<Object[]> buffer;
|
||||
|
||||
public CustomEventHandler(String name, int sleepMs, int batchSize) {
|
||||
this.name = name;
|
||||
this.sleepMs = sleepMs;
|
||||
this.batchSize = batchSize;
|
||||
this.batchSize = (batchSize < 1) ? 1 : batchSize;
|
||||
this.buffer = new ArrayList<Object[]>(this.batchSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(LoggingEvent event, long sequence, boolean endOfBatch) throws Exception {
|
||||
if (sleepMs > 0) Thread.sleep(sleepMs);
|
||||
if(batchSize > 1) {
|
||||
eventList.add(event);
|
||||
if (++count >= batchSize) {
|
||||
processBatch();
|
||||
eventList.clear();
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
EAIMessage message = event.getMessage();
|
||||
if(logger.isInfo()) {
|
||||
logger.info(String.format("CustomWorkHandler: %s LoggingEvent: %s %s\n"
|
||||
,name, message.getSvcOgNo() ,message.getLogPssSno())
|
||||
);
|
||||
if (message != null) {
|
||||
buffer.add(new Object[] { message, event.getProperty() });
|
||||
}
|
||||
EAILogSender.logDirect(event.getMessage(), event.getProperty());
|
||||
if(event != null) {
|
||||
event.clear();
|
||||
event = null;
|
||||
|
||||
// 링버퍼 슬롯은 재사용되므로 참조만 끊는다. (event.clear() 사용 금지 - 상단 주석 참고)
|
||||
event.setMessage(null);
|
||||
event.setProperty(null);
|
||||
|
||||
if (endOfBatch || buffer.size() >= batchSize) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
if (buffer.isEmpty()) return;
|
||||
int size = buffer.size();
|
||||
// info 레벨이 꺼져 있으면 시각 측정 자체를 하지 않는다.
|
||||
final boolean measure = logger.isInfo();
|
||||
final long t0 = measure ? System.currentTimeMillis() : 0L;
|
||||
try {
|
||||
EAILogSender.logDirectBatch(buffer);
|
||||
} catch (Throwable th) {
|
||||
logger.error(String.format("%s] batch log failed. size=%d", name, size), th);
|
||||
} finally {
|
||||
// DB 적재 시간만 측정한다. releaseMessages()는 EAIMessage 100건의
|
||||
// setBizData(null)/svcMsgs.clear()를 도는 비용이라 측정에 섞이면 안 된다.
|
||||
final long elapsed = measure ? (System.currentTimeMillis() - t0) : 0L;
|
||||
releaseMessages();
|
||||
buffer.clear();
|
||||
if (measure) {
|
||||
logger.info("{} batch logging. size={}, elapsed={}ms", name, size, elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processBatch() throws EAILogException {
|
||||
for(LoggingEvent event:eventList) {
|
||||
EAILogSender.logDirect(event.getMessage(), event.getProperty());
|
||||
/** 적재가 끝난 EAIMessage의 내부 버퍼를 해제한다. (기존 LoggingEvent.clear()가 하던 역할) */
|
||||
private void releaseMessages() {
|
||||
for (Object[] item : buffer) {
|
||||
EAIMessage message = (EAIMessage) item[0];
|
||||
if (message == null) continue;
|
||||
try {
|
||||
message.clear();
|
||||
} catch (Exception e) {
|
||||
// 해제 실패는 적재 결과에 영향이 없으므로 무시한다.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 유휴 시 Disruptor가 호출하는 안전망.
|
||||
* 정상 흐름에서는 endOfBatch로 이미 flush되지만, onEvent가 예외로 중단되어
|
||||
* 버퍼가 남은 뒤 거래가 끊기는 경우를 대비한다.
|
||||
*/
|
||||
@Override
|
||||
public void onTimeout(long sequence) throws Exception {
|
||||
if (buffer.isEmpty()) return;
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(String.format("%s] flush by timeout. remain=%d", name, buffer.size()));
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn(String.format(">> %s start. batchSize = %d", name, batchSize));
|
||||
}
|
||||
}
|
||||
|
||||
/** 컨슈머 스레드 종료 시 버퍼에 남은 로그를 반드시 적재한다. */
|
||||
@Override
|
||||
public void onShutdown() {
|
||||
flush();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn(String.format("<< %s shutdown.", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.eactive.eai.common.logger.async;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.eactive.eai.common.logger.EAIDBLogControl;
|
||||
import com.eactive.eai.common.logger.HttpAdapterExtraLogVo;
|
||||
import com.eactive.eai.common.logger.HttpLoggingService;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.lmax.disruptor.EventHandler;
|
||||
import com.lmax.disruptor.LifecycleAware;
|
||||
import com.lmax.disruptor.TimeoutHandler;
|
||||
|
||||
/**
|
||||
* 1. 기능 : HTTP 헤더 로그 이벤트를 모아 한 트랜잭션(COMMIT 1회)으로 적재
|
||||
* 2. 처리 개요 :
|
||||
* - endOfBatch를 flush 조건으로 사용한다. 한산할 때는 건당 즉시 적재되고
|
||||
* 부하가 몰릴 때만 배치가 커지므로 별도 타임아웃 스레드가 필요 없다.
|
||||
* - batchSize는 하한이 아니라 상한이다. "100건 모일 때까지 대기"가 아니라
|
||||
* "한 트랜잭션이 100건을 넘지 않게 끊는다"는 의미다.
|
||||
* - 안전망으로 TimeoutHandler를 구현한다. 유휴 시 Disruptor가 onTimeout()을
|
||||
* 호출하므로 버퍼에 남은 로그가 방치되지 않는다.
|
||||
* (WaitStrategy가 TimeoutBlockingWaitStrategy = 설정값 "TIME"일 때 동작. 기본값)
|
||||
* - 배치 실패 시 기존 건별 경로(insertHttpAdapterExtraLog)로 재시도한다.
|
||||
* 그 경로가 DB 장애 판정과 파일로그 폴백을 이미 담고 있다.
|
||||
* 3. 주의사항
|
||||
* - HttpLoggingService 빈은 필드에 캐싱한다. 이벤트마다 타입 기반 getBean을
|
||||
* 호출하면 컨슈머 처리량이 떨어진다.
|
||||
*/
|
||||
public class HttpLoggingBatchEventHandler implements EventHandler<HttpLoggingEvent>, LifecycleAware, TimeoutHandler {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private final String name;
|
||||
private final int batchSize;
|
||||
|
||||
private final List<HttpAdapterExtraLogVo> buffer;
|
||||
|
||||
private HttpLoggingService service;
|
||||
|
||||
public HttpLoggingBatchEventHandler(String name, int batchSize) {
|
||||
this.name = name;
|
||||
this.batchSize = (batchSize < 1) ? 1 : batchSize;
|
||||
this.buffer = new ArrayList<HttpAdapterExtraLogVo>(this.batchSize);
|
||||
}
|
||||
|
||||
private HttpLoggingService service() {
|
||||
if (service == null) {
|
||||
service = ApplicationContextProvider.getContext().getBean(HttpLoggingService.class);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(HttpLoggingEvent event, long sequence, boolean endOfBatch) throws Exception {
|
||||
HttpAdapterExtraLogVo vo = event.getHttpAdapterExtraLogVo();
|
||||
if (vo != null) {
|
||||
buffer.add(vo);
|
||||
}
|
||||
// 링버퍼 슬롯은 재사용되므로 참조를 끊는다. VO 내용은 유지된다.
|
||||
event.clear();
|
||||
|
||||
if (endOfBatch || buffer.size() >= batchSize) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
if (buffer.isEmpty()) return;
|
||||
int size = buffer.size();
|
||||
// info 레벨이 꺼져 있으면 시각 측정 자체를 하지 않는다.
|
||||
final boolean measure = logger.isInfo();
|
||||
final long t0 = measure ? System.currentTimeMillis() : 0L;
|
||||
try {
|
||||
if (EAIDBLogControl.isEnable()) {
|
||||
service().insertHttpAdapterExtraLogBatch(buffer);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(String.format("%s] flushed %d http log(s) in one transaction", name, size));
|
||||
}
|
||||
} else {
|
||||
writeEach();
|
||||
}
|
||||
} catch (Throwable th) {
|
||||
// 배치는 한 건만 실패해도 전체가 롤백된다. 건별로 재시도해 정상 건을 살린다.
|
||||
logger.error(String.format("%s] batch http log failed, retry one by one. size=%d", name, size), th);
|
||||
writeEach();
|
||||
} finally {
|
||||
buffer.clear();
|
||||
}
|
||||
if (measure) {
|
||||
logger.info("{} batch logging. size={}, elapsed={}ms", name, size, System.currentTimeMillis() - t0);
|
||||
}
|
||||
}
|
||||
|
||||
/** 기존 건별 경로. DB 장애 판정과 파일로그 폴백이 이 안에 있다. */
|
||||
private void writeEach() {
|
||||
for (HttpAdapterExtraLogVo vo : buffer) {
|
||||
try {
|
||||
service().insertHttpAdapterExtraLog(vo);
|
||||
} catch (Throwable th) {
|
||||
logger.error("failed to insert async http log ", th);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 유휴 시 Disruptor가 호출하는 안전망.
|
||||
* 정상 흐름에서는 endOfBatch로 이미 flush되지만, onEvent가 예외로 중단되어
|
||||
* 버퍼가 남은 뒤 거래가 끊기는 경우를 대비한다.
|
||||
*/
|
||||
@Override
|
||||
public void onTimeout(long sequence) throws Exception {
|
||||
if (buffer.isEmpty()) return;
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(String.format("%s] flush by timeout. remain=%d", name, buffer.size()));
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn(String.format(">> %s start. batchSize = %d", name, batchSize));
|
||||
}
|
||||
}
|
||||
|
||||
/** 컨슈머 스레드 종료 시 버퍼에 남은 로그를 반드시 적재한다. */
|
||||
@Override
|
||||
public void onShutdown() {
|
||||
flush();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn(String.format("<< %s shutdown.", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,14 @@ import lombok.Data;
|
||||
public class HttpLoggingEvent {
|
||||
private HttpAdapterExtraLogVo httpAdapterExtraLogVo;
|
||||
|
||||
/**
|
||||
* 링버퍼 슬롯의 참조만 끊는다.
|
||||
* VO 자체는 컨슈머가 배치 버퍼에 담아 사용하므로 내용을 비우면 안 된다.
|
||||
*/
|
||||
public void clear() {
|
||||
this.httpAdapterExtraLogVo = null;
|
||||
}
|
||||
|
||||
public final static EventFactory<HttpLoggingEvent> EVENT_FACTORY = new EventFactory<HttpLoggingEvent>() {
|
||||
public HttpLoggingEvent newInstance() {
|
||||
return new HttpLoggingEvent();
|
||||
|
||||
@@ -17,6 +17,8 @@ public class HttpLoggingPoolObject {
|
||||
Disruptor<HttpLoggingEvent> disruptor = null;
|
||||
RingBuffer<HttpLoggingEvent> ringBuffer = null;
|
||||
int id = 0;
|
||||
// 실제 링버퍼 크기로 생성자에서 설정한다. shutdown()이 이 값과 remainingCapacity를
|
||||
// 비교하므로 하드코딩하면 queue.size 변경 시 종료되지 않는다.
|
||||
int queueMax = (int)Math.pow(2, 10);
|
||||
int workerSize = 0;
|
||||
|
||||
@@ -25,7 +27,6 @@ public class HttpLoggingPoolObject {
|
||||
}
|
||||
|
||||
private WaitStrategy getWaitStrategy(String waitStrategy) {
|
||||
WaitStrategy ws = null;
|
||||
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_BLOCK.equals(waitStrategy)) {
|
||||
// throughput and low-latency are not as important as CPU resource
|
||||
return new BlockingWaitStrategy();
|
||||
@@ -47,14 +48,23 @@ public class HttpLoggingPoolObject {
|
||||
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_YIELD.equals(waitStrategy)) {
|
||||
return new YieldingWaitStrategy();
|
||||
}
|
||||
return ws;
|
||||
// 알 수 없는 값이면 null이 아니라 기본값(TIME)을 돌려준다.
|
||||
// null을 넘기면 컨슈머 스레드가 NPE로 죽어 로깅이 통째로 멈춘다.
|
||||
// TimeoutBlockingWaitStrategy여야 배치 핸들러의 onTimeout 안전망도 동작한다.
|
||||
if(waitStrategy != null && logger.isWarn()) {
|
||||
logger.warn(String.format(">> unknown waitStrategy [%s], fallback to TIME(100ms)", waitStrategy));
|
||||
}
|
||||
return new TimeoutBlockingWaitStrategy(100 * 1000, TimeUnit.MICROSECONDS);
|
||||
}
|
||||
|
||||
public HttpLoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy) {
|
||||
public HttpLoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy, int batchSize) {
|
||||
this.id = id;
|
||||
this.queueMax = queueSize;
|
||||
this.workerSize = workerSize;
|
||||
if(logger.isWarn()) {
|
||||
logger.warn(String.format(">> Disruptor-%d queueSize = %d", id, queueSize));
|
||||
logger.warn(String.format(">> Disruptor-%d workerSize = %d", id, workerSize));
|
||||
logger.warn(String.format(">> Disruptor-%d batchSize = %d", id, batchSize));
|
||||
logger.warn(String.format(">> Disruptor-%d waitStrategy = %s", id, waitStrategy));
|
||||
}
|
||||
CustomThreadFactory tFactory = new CustomThreadFactory();
|
||||
@@ -63,12 +73,21 @@ public class HttpLoggingPoolObject {
|
||||
ProducerType.SINGLE,
|
||||
getWaitStrategy(waitStrategy));
|
||||
// BlockingWaitStrategy | SleepingWaitStrategy | YieldingWaitStrategy | BusySpinWaitStrategy
|
||||
if(workerSize > 1) {
|
||||
// 건별 COMMIT 경로. WorkHandler에는 endOfBatch가 없어 배치 적재가 불가능하다.
|
||||
WorkHandler<HttpLoggingEvent>[] handlers = new WorkHandler[workerSize];
|
||||
for(int i=0; i< handlers.length; i++) {
|
||||
WorkHandler handler = new HttpLoggingWorkHandler();
|
||||
handlers[i] = handler;
|
||||
}
|
||||
disruptor.handleEventsWithWorkerPool(handlers);
|
||||
}
|
||||
else {
|
||||
// 배치 COMMIT 경로. 컨슈머 병렬도는 디스크럽터 풀 개수(pool.maxsize)로 확보한다.
|
||||
HttpLoggingBatchEventHandler handler =
|
||||
new HttpLoggingBatchEventHandler(String.format("HttpLoggingBatchEventHandler%d-%d", id, 0), batchSize);
|
||||
disruptor.handleEventsWith(handler);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ public class HttpLoggingPoolObjectFactory extends BasePooledObjectFactory<HttpLo
|
||||
int queueSize = ElinkConfig.getAsyncQueueSize();
|
||||
int workers = ElinkConfig.getAsyncWorkers();
|
||||
String waitStrategy = ElinkConfig.getWaitStrategy();
|
||||
return new HttpLoggingPoolObject(i++, queueSize, workers, waitStrategy);
|
||||
int batchSize = ElinkConfig.getAsyncBatchSize();
|
||||
return new HttpLoggingPoolObject(i++, queueSize, workers, waitStrategy, batchSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,6 +22,8 @@ public class LoggingPoolObject {
|
||||
Disruptor<LoggingEvent> disruptor = null;
|
||||
RingBuffer<LoggingEvent> ringBuffer = null;
|
||||
int id = 0;
|
||||
// 실제 링버퍼 크기로 생성자에서 설정한다. shutdown()이 이 값과 remainingCapacity를
|
||||
// 비교하므로 하드코딩하면 queue.size 변경 시 종료되지 않는다.
|
||||
int queueMax = (int)Math.pow(2, 10);
|
||||
int workerSize = 0;
|
||||
|
||||
@@ -30,7 +32,6 @@ public class LoggingPoolObject {
|
||||
}
|
||||
|
||||
private WaitStrategy getWaitStrategy(String waitStrategy) {
|
||||
WaitStrategy ws = null;
|
||||
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_BLOCK.equals(waitStrategy)) {
|
||||
// throughput and low-latency are not as important as CPU resource
|
||||
return new BlockingWaitStrategy();
|
||||
@@ -52,14 +53,23 @@ public class LoggingPoolObject {
|
||||
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_YIELD.equals(waitStrategy)) {
|
||||
return new YieldingWaitStrategy();
|
||||
}
|
||||
return ws;
|
||||
// 알 수 없는 값이면 null이 아니라 기본값(TIME)을 돌려준다.
|
||||
// null을 넘기면 컨슈머 스레드가 NPE로 죽어 로깅이 통째로 멈춘다.
|
||||
// TimeoutBlockingWaitStrategy여야 배치 핸들러의 onTimeout 안전망도 동작한다.
|
||||
if(waitStrategy != null && logger.isWarn()) {
|
||||
logger.warn(String.format(">> unknown waitStrategy [%s], fallback to TIME(100ms)", waitStrategy));
|
||||
}
|
||||
return new TimeoutBlockingWaitStrategy(100 * 1000, TimeUnit.MICROSECONDS);
|
||||
}
|
||||
|
||||
public LoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy) {
|
||||
public LoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy, int batchSize) {
|
||||
this.id = id;
|
||||
this.queueMax = queueSize;
|
||||
this.workerSize = workerSize;
|
||||
if(logger.isWarn()) {
|
||||
logger.warn(String.format(">> Disruptor-%d queueSize = %d", id, queueSize));
|
||||
logger.warn(String.format(">> Disruptor-%d workerSize = %d", id, workerSize));
|
||||
logger.warn(String.format(">> Disruptor-%d batchSize = %d", id, batchSize));
|
||||
logger.warn(String.format(">> Disruptor-%d waitStrategy = %s", id, waitStrategy));
|
||||
}
|
||||
CustomThreadFactory tFactory = new CustomThreadFactory();
|
||||
@@ -69,6 +79,8 @@ public class LoggingPoolObject {
|
||||
getWaitStrategy(waitStrategy));
|
||||
// BlockingWaitStrategy | SleepingWaitStrategy | YieldingWaitStrategy | BusySpinWaitStrategy
|
||||
if(workerSize > 1) {
|
||||
// 건별 COMMIT 경로. WorkHandler에는 endOfBatch가 없어 배치 적재가 불가능하다.
|
||||
// 커밋 횟수를 줄이려면 worker.size=1로 두고 아래 배치 EventHandler를 사용한다.
|
||||
WorkHandler<LoggingEvent>[] handlers = new WorkHandler[workerSize];
|
||||
for(int i=0; i< handlers.length; i++) {
|
||||
// TODO : 현재는 delay 없이 처리하도록 하고, 추후 DB부하를 줄이려면 sleep을 정의.
|
||||
@@ -78,7 +90,8 @@ public class LoggingPoolObject {
|
||||
disruptor.handleEventsWithWorkerPool(handlers);
|
||||
}
|
||||
else {
|
||||
CustomEventHandler handler = new CustomEventHandler(String.format("CustomEventHandler%d-%d",id, 0), 0, 1);
|
||||
// 배치 COMMIT 경로. 컨슈머 병렬도는 디스크럽터 풀 개수(pool.maxsize)로 확보한다.
|
||||
CustomEventHandler handler = new CustomEventHandler(String.format("CustomEventHandler%d-%d",id, 0), 0, batchSize);
|
||||
disruptor.handleEventsWith(handler);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ public class LoggingPoolObjectFactory extends BasePooledObjectFactory<LoggingPoo
|
||||
int queueSize = ElinkConfig.getAsyncQueueSize();
|
||||
int workers = ElinkConfig.getAsyncWorkers();
|
||||
String waitStrategy = ElinkConfig.getWaitStrategy();
|
||||
return new LoggingPoolObject(i++, queueSize, workers, waitStrategy);
|
||||
int batchSize = ElinkConfig.getAsyncBatchSize();
|
||||
return new LoggingPoolObject(i++, queueSize, workers, waitStrategy, batchSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -112,6 +112,8 @@ public class EAIMessage implements EAIMessageKeys, Serializable, Comparable<EAIM
|
||||
|
||||
private Properties callProp;
|
||||
|
||||
private String orgRspErrCd;
|
||||
|
||||
public EAIMessage() {
|
||||
this.svcMsgs = new ArrayList<>();
|
||||
this.svcPssSeq = 1;
|
||||
@@ -674,6 +676,23 @@ public class EAIMessage implements EAIMessageKeys, Serializable, Comparable<EAIM
|
||||
this.callProp = callProp;
|
||||
}
|
||||
|
||||
public String getOrgRspErrCd() {
|
||||
return orgRspErrCd;
|
||||
}
|
||||
|
||||
public void setOrgRspErrCd(String orgRspErrCd) {
|
||||
this.orgRspErrCd = orgRspErrCd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로깅/모니터링용 응답에러코드를 반환한다.
|
||||
* 내부오류를 어댑터 에러메시지 핸들러로 대체 응답한 경우
|
||||
* rspErrCd 에는 정상코드가 설정되므로 원본 에러코드(orgRspErrCd)를 우선한다.
|
||||
*/
|
||||
public String getLogRspErrCd() {
|
||||
return (orgRspErrCd != null && orgRspErrCd.length() > 0) ? orgRspErrCd : rspErrCd;
|
||||
}
|
||||
|
||||
/**
|
||||
* [비동기 전달 전용] 컨텍스트 전달용 Map 설정
|
||||
* @see #transactionContextTransfer
|
||||
|
||||
@@ -292,7 +292,7 @@ public class EAIServiceMonitor implements Lifecycle {
|
||||
msgPssTm = msg.getMsgPssTm();
|
||||
msgRcvTm = msg.getMsgRcvTm();
|
||||
logPssSno = msg.getLogPssSno();
|
||||
rspErrCd = msg.getRspErrCd();
|
||||
rspErrCd = msg.getLogRspErrCd();
|
||||
eaiSvcCd = msg.getEAISvcCd();
|
||||
svcOgNo = msg.getSvcOgNo();
|
||||
bwkCls = msg.getBwkCls();
|
||||
@@ -317,6 +317,10 @@ public class EAIServiceMonitor implements Lifecycle {
|
||||
error = rspErrCd.substring(1, 2);
|
||||
}
|
||||
|
||||
if (rspErrCd.equals("RECEAIINA001")) {
|
||||
error = "S";
|
||||
}
|
||||
|
||||
// 에러와 타임아웃을 분리 : 이동훈
|
||||
if (errorCode[0].equals(error.toUpperCase()) || errorCode[1].equals(error.toUpperCase())) {
|
||||
iErrorCode = 1;
|
||||
|
||||
@@ -2,10 +2,8 @@ package com.eactive.eai.common.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -63,13 +61,17 @@ public class HttpAdapterExtraLogUtil {
|
||||
httpAdapterExtraLogVo.setHttpMethod(httpMethod);
|
||||
|
||||
for (HttpAdapterExtraHeaderVo httpAdapterExtraHeaderVo : headerVoList) {
|
||||
// String name = httpAdapterExtraHeaderVo.getName();
|
||||
String name = httpAdapterExtraHeaderVo.getName();
|
||||
// if(StringUtils.isNotBlank(name) && "authorization".equals(name.toLowerCase())) {
|
||||
// httpAdapterExtraHeaderVo.setValue("{hidden}");
|
||||
// }
|
||||
|
||||
if(StringUtils.isNotBlank(name) && BODY_FIELD_NAME.equals(name))
|
||||
continue;
|
||||
|
||||
String value = httpAdapterExtraHeaderVo.getValue();
|
||||
if(StringUtils.isNotBlank(value) && value.length() > MAX_HEADER_VALUE_SIZE) {
|
||||
value = value.substring(0, 400) + "...";
|
||||
value = value.substring(0, MAX_HEADER_VALUE_SIZE) + "...";
|
||||
httpAdapterExtraHeaderVo.setValue(value);
|
||||
}else if(value == null){
|
||||
httpAdapterExtraHeaderVo.setValue(" ");
|
||||
@@ -116,7 +118,7 @@ public class HttpAdapterExtraLogUtil {
|
||||
}
|
||||
|
||||
public static List<HttpAdapterExtraHeaderVo> convertHeaderToListOfHttpAdapterExtraHeaderVo(Header[] headers) {
|
||||
Set<String> seenNames = new HashSet<>();
|
||||
// Set<String> seenNames = new HashSet<>();
|
||||
return Arrays.stream(headers)
|
||||
// .filter(header -> seenNames.add(header.getName())) // 중복된 이름을 스킵
|
||||
.map(header -> new HttpAdapterExtraHeaderVo(header.getName(), header.getValue()))
|
||||
|
||||
@@ -6,10 +6,12 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
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.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
@@ -35,9 +37,23 @@ public final class JacksonUtil {
|
||||
private static final Pattern TOKEN_PATTERN = Pattern.compile("([^\\[\\]]*)((?:\\[\\d+\\])*)");
|
||||
private static final Pattern INDEX_PATTERN = Pattern.compile("\\[(\\d+)\\]");
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = newNumberSafeMapper();
|
||||
|
||||
/**
|
||||
* JSON 숫자를 double 로 좁히지 않고 BigDecimal 로, 수신한 자릿수 그대로 유지하는 ObjectMapper.
|
||||
*
|
||||
* readTree() 로 파싱한 뒤 writeValueAsString() 으로 다시 문자열을 만드는 왕복에서,
|
||||
* 기본 설정이면 100000000.00 이 1.0E8 로 변형된다.
|
||||
* USE_BIG_DECIMAL_FOR_FLOATS 만 켜고 withExactBigDecimals(true) 를 빼면 기본
|
||||
* JsonNodeFactory 가 stripTrailingZeros() 를 적용해 scale 이 음수가 되어 1E+8 이 된다.
|
||||
* 두 옵션을 함께 켜야 수신한 값이 그대로 보존된다.
|
||||
*/
|
||||
public static ObjectMapper newNumberSafeMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
|
||||
objectMapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true));
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
private JacksonUtil() {
|
||||
// 인스턴스화 방지
|
||||
|
||||
@@ -13,6 +13,8 @@ public interface ConfigKeys {
|
||||
public static final String LOGGER_ASYNC_INITPOOLS = "logger.async.pool.initsize";
|
||||
public static final String LOGGER_ASYNC_QUEUES = "logger.async.queue.size";
|
||||
public static final String LOGGER_ASYNC_WORKERS = "logger.async.worker.size";
|
||||
// 한 트랜잭션(COMMIT 1회)에 묶어 적재할 최대 로그 건수
|
||||
public static final String LOGGER_ASYNC_BATCHSIZE = "logger.async.batch.size";
|
||||
|
||||
public static final String LOGGER_ASYNC_WAITSTRATEGY = "logger.async.worker.waitstrategy";
|
||||
public static final String LOGGER_ASYNC_WAITSTRATEGY_BLOCK = "BLOCK";
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ElinkConfig implements ConfigKeys {
|
||||
private static int asyncPoolInitSize = 8;
|
||||
private static int asyncQueueSize = 1024;
|
||||
private static int asyncWorkers = 16;
|
||||
private static int asyncBatchSize = 100;
|
||||
|
||||
private static String waitStrategy = LOGGER_ASYNC_WAITSTRATEGY;
|
||||
|
||||
@@ -71,6 +72,7 @@ public class ElinkConfig implements ConfigKeys {
|
||||
sb.append( String.format("%s = %s\n", LOGGER_ASYNC_INITPOOLS, asyncPoolInitSize) );
|
||||
sb.append( String.format("%s = %s\n", LOGGER_ASYNC_QUEUES, asyncQueueSize) );
|
||||
sb.append( String.format("%s = %s\n", LOGGER_ASYNC_WORKERS, asyncWorkers) );
|
||||
sb.append( String.format("%s = %s\n", LOGGER_ASYNC_BATCHSIZE, asyncBatchSize) );
|
||||
sb.append( String.format("%s = %s\n", LOGGER_ASYNC_WAITSTRATEGY, waitStrategy) );
|
||||
sb.append(">> Async Dummy Configuration\n");
|
||||
sb.append(String.format("%s = %s\n", HTTP_ASYNC_DEFAULT_DUMMY_DATA, asyncDefaultDummyData));
|
||||
@@ -169,6 +171,16 @@ public class ElinkConfig implements ConfigKeys {
|
||||
asyncWorkers = 16;
|
||||
}
|
||||
|
||||
try {
|
||||
sCount = env.getProperty(LOGGER_ASYNC_BATCHSIZE, "100");
|
||||
asyncBatchSize = Integer.parseInt(sCount);
|
||||
if (asyncBatchSize < 1) {
|
||||
asyncBatchSize = 1;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
asyncBatchSize = 100;
|
||||
}
|
||||
|
||||
try {
|
||||
waitStrategy = env.getProperty(LOGGER_ASYNC_WAITSTRATEGY, LOGGER_ASYNC_WAITSTRATEGY_TIME);
|
||||
} catch (Exception ex) {
|
||||
@@ -271,6 +283,14 @@ public class ElinkConfig implements ConfigKeys {
|
||||
ElinkConfig.asyncWorkers = asyncWorkers;
|
||||
}
|
||||
|
||||
public static int getAsyncBatchSize() {
|
||||
return asyncBatchSize;
|
||||
}
|
||||
|
||||
public static void setAsyncBatchSize(int asyncBatchSize) {
|
||||
ElinkConfig.asyncBatchSize = asyncBatchSize;
|
||||
}
|
||||
|
||||
public static String getWaitStrategy() {
|
||||
return waitStrategy;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public class RequestProcessor extends RequestProcessorSupport {
|
||||
|
||||
// UUID 생성 : UUID에서 - 없는 32자리
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
uuid = uuid == null ? UUIDGenerator.getUUID().toString().replaceAll("-", "") : uuid;
|
||||
uuid = uuid == null ? instid+UUIDGenerator.getUUID().toString().replaceAll("-", "") : uuid;
|
||||
// UUID 생성 : UUID = server구분4자리 + UUID
|
||||
/*
|
||||
String uuid = "";
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
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.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeType;
|
||||
|
||||
public class JsonReader implements StandardReader {
|
||||
@@ -43,17 +44,21 @@ public class JsonReader implements StandardReader {
|
||||
JsonParser parser = null;
|
||||
mapper = new ObjectMapper();
|
||||
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
|
||||
// USE_BIG_DECIMAL_FOR_FLOATS 만 켜면 기본 JsonNodeFactory 가 DecimalNode 생성 시
|
||||
// stripTrailingZeros() 를 적용해 scale 이 음수가 되고, 그 결과
|
||||
// 100000000.00 이 1E+8 로 변형된다. exact 모드로 수신한 자릿수를 보존한다.
|
||||
mapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true));
|
||||
factory = mapper.getFactory();
|
||||
try {
|
||||
parser = factory.createParser(jsonString);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
//e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
jsonNode = mapper.readTree(parser);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
//e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -702,13 +702,23 @@ public abstract class DefaultProcess extends Process {
|
||||
AdapterErrorMessageHandler adapterErrorMessageHandler = AdapterErrorMessageHandlerFactory.createHandler(errorResponseHandlerClass);
|
||||
|
||||
Object responseObj = adapterErrorMessageHandler.generateNonStandardErrorResponseMessage(inboudnAdapterGroupName, inboudnAdapterName, this.callProp, this.tgtTranObject, this.resEaiMsg);
|
||||
if(responseObj != null) {
|
||||
resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode());
|
||||
if ((com.eactive.eai.adapter.Keys.IF_STANDARD.equals(this.adptrMsgPtrnCd)
|
||||
||com.eactive.eai.adapter.Keys.IF_SUBSTANDARD.equals(this.adptrMsgPtrnCd))) {
|
||||
String errorCode = mapper.getErrorCode(resStandardMessage);
|
||||
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
|
||||
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc"));
|
||||
this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
|
||||
} else {
|
||||
this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신");
|
||||
}
|
||||
|
||||
if(responseObj != null) {
|
||||
resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode());
|
||||
this.resEaiMsg.setOrgRspErrCd(this.resEaiMsg.getRspErrCd());
|
||||
this.resEaiMsg.setRspErrCd(EAIMessageKeys.BWK_FAILMSG_CODE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTgtTranCall() {
|
||||
return this.isTgtTran;
|
||||
|
||||
@@ -273,7 +273,10 @@ public class HTTPProcess extends DefaultProcess {
|
||||
try {
|
||||
// 응답전문으로 응답 구조체(resEaiMsg) SET
|
||||
resEaiMsg = setRcvLogInfo(resEaiMsg, this.adptrMsgType, this.resObject, this.outboundCharset);
|
||||
this.resObject = resEaiMsg.getStandardMessage().getBizDataBytes();
|
||||
// JSON/XML 은 String 으로 넘긴다. byte[] 로 넘기면 charset 정보가 유실되어 변환 시 깨진다.
|
||||
this.resObject = MessageUtil.isBytesMessage(this.adptrMsgType)
|
||||
? resEaiMsg.getStandardMessage().getBizDataBytes()
|
||||
: resEaiMsg.getStandardMessage().getBizData();
|
||||
} catch (Exception e) {
|
||||
String[] msgArgs = new String[1];
|
||||
msgArgs[0] = this.reqEaiMsg.getEAISvcCd();
|
||||
@@ -289,7 +292,10 @@ public class HTTPProcess extends DefaultProcess {
|
||||
logger.debug(guidLogPrefix + "SUB표준 업무데이터 추출");
|
||||
try {
|
||||
this.resEaiMsg = convertToStandardMessage(resEaiMsg, adptrMsgType, this.resObject, this.outboundCharset);
|
||||
this.resObject = resEaiMsg.getStandardMessage().getBizDataBytes();
|
||||
// JSON/XML 은 String 으로 넘긴다. byte[] 로 넘기면 charset 정보가 유실되어 변환 시 깨진다.
|
||||
this.resObject = MessageUtil.isBytesMessage(this.adptrMsgType)
|
||||
? resEaiMsg.getStandardMessage().getBizDataBytes()
|
||||
: resEaiMsg.getStandardMessage().getBizData();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
String[] msgArgs = new String[1];
|
||||
@@ -883,5 +889,20 @@ public class HTTPProcess extends DefaultProcess {
|
||||
standardMessage.setData(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 시스템환경구분코드 설정 (D/T/P)
|
||||
EAIServerManager eaiServer = EAIServerManager.getInstance();
|
||||
String sysEnvDvcd = getSysEnvDvcd(eaiServer);
|
||||
if(StringUtils.equals("P", sysEnvDvcd)) {
|
||||
StandardMessage standardMessage = this.reqEaiMsg.getStandardMessage();
|
||||
this.reqEaiMsg.getMapper().setOperationEnv(standardMessage, "P");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getSysEnvDvcd(EAIServerManager server) {
|
||||
if (server.isPEAIServer()) return "P"; // 운영
|
||||
if (server.isSEAIServer()) return "T"; // 검증/테스트
|
||||
return "D"; // 개발
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eactive.eai.util;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
@@ -15,7 +16,10 @@ public class JsonPathUtil {
|
||||
|
||||
}
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
// readTree() 로 파싱한 뒤 writeValueAsString() 으로 되돌리는 왕복이 잦으므로,
|
||||
// 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private static final ObjectMapper objectMapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// JsonNode 기반 단일 파싱 API — 연속 get/set 시 파싱 횟수 절감
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.eactive.eai.util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
public class QueryStringUtils {
|
||||
|
||||
private QueryStringUtils() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
public static Map<String, String[]> parseQueryString(String queryString, String charset) throws UnsupportedEncodingException {
|
||||
return parseQueryString(queryString, charset, true);
|
||||
}
|
||||
|
||||
public static Map<String, String[]> parseQueryString(String queryString) throws UnsupportedEncodingException {
|
||||
return parseQueryString(queryString, null, false);
|
||||
}
|
||||
|
||||
public static Map<String, String[]> parseQueryString(String queryString, String charset, boolean urlDecode) throws UnsupportedEncodingException {
|
||||
Map<String, String[]> paramMap = new HashMap<>();
|
||||
Map<String, List<String>> tempMap = new HashMap<>();
|
||||
|
||||
if(StringUtils.isEmpty(queryString))
|
||||
return paramMap;
|
||||
|
||||
String[] pairs = queryString.split("&");
|
||||
for(String pair : pairs) {
|
||||
|
||||
if(StringUtils.isEmpty(pair))
|
||||
continue;
|
||||
|
||||
int idx = pair.indexOf("=");
|
||||
String key = null;
|
||||
String value = null;
|
||||
if(idx > 0) {
|
||||
if(urlDecode)
|
||||
key = URLDecoder.decode(pair.substring(0, idx), charset);
|
||||
else
|
||||
if(charset != null)
|
||||
key = new String((pair.substring(0, idx)).getBytes(charset));
|
||||
else
|
||||
key = pair.substring(0, idx);
|
||||
|
||||
|
||||
if(urlDecode)
|
||||
value = URLDecoder.decode(pair.substring(idx + 1), charset);
|
||||
else
|
||||
if(charset != null)
|
||||
value = new String((pair.substring(idx + 1)).getBytes(charset));
|
||||
else
|
||||
value = pair.substring(idx + 1);
|
||||
|
||||
} else {
|
||||
key = urlDecode ? URLDecoder.decode(pair, charset) : new String(pair.getBytes(charset));
|
||||
value = "";
|
||||
}
|
||||
tempMap.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
|
||||
}
|
||||
|
||||
for(Map.Entry<String, List<String>> entry : tempMap.entrySet()) {
|
||||
paramMap.put(entry.getKey(), entry.getValue().toArray(new String[0]));
|
||||
}
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
public static String makeJson(Map<String, String[]> paramMap, boolean bUrlDecode) throws UnsupportedEncodingException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
int i = 0;
|
||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||
if (i > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\"").append(entry.getKey()).append("\":");
|
||||
String[] values = entry.getValue();
|
||||
if (values.length > 1) {
|
||||
// ["111", "222"]
|
||||
sb.append("[");
|
||||
for (int j = 0; j < values.length; j++) {
|
||||
if (j > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
String value = bUrlDecode ? URLDecoder.decode(values[j], StandardCharsets.UTF_8.name()) : values[j];
|
||||
sb.append("\"").append(JSONValue.escape(value)).append("\"");
|
||||
}
|
||||
sb.append("]");
|
||||
} else {
|
||||
String value = bUrlDecode ? URLDecoder.decode(values[0], StandardCharsets.UTF_8.name()) : values[0];
|
||||
sb.append("\"").append(JSONValue.escape(value)).append("\"");
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.eactive.eai.util.json;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.json.transformer.ValueTransformer;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -14,7 +15,9 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
|
||||
|
||||
public class JsonPathsTransform {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private static final ObjectMapper objectMapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
public static <T, R> String modifyValuesAtPaths(String jsonString,
|
||||
Map<String, ValueTransformer<T, R>> pathTransformerMap, boolean isPretty) {
|
||||
@@ -23,9 +26,11 @@ public class JsonPathsTransform {
|
||||
// DocumentContext documentContext = JsonPath.parse(jsonString);
|
||||
|
||||
// 변경헤도 별차이가 없음.
|
||||
// provider 에 mapper 를 넘기지 않으면 json-path 가 자체 기본 ObjectMapper 를 쓰게 되어
|
||||
// documentContext.jsonString() 단계에서 이미 숫자 자릿수가 유실된다.
|
||||
Configuration conf = Configuration.builder()
|
||||
.jsonProvider(new JacksonJsonNodeJsonProvider())
|
||||
.mappingProvider(new JacksonMappingProvider())
|
||||
.jsonProvider(new JacksonJsonNodeJsonProvider(objectMapper))
|
||||
.mappingProvider(new JacksonMappingProvider(objectMapper))
|
||||
.build();
|
||||
DocumentContext documentContext = JsonPath.using(conf).parse(jsonString);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eactive.eai.util.json;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.json.transformer.ValueTransformer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -11,7 +12,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class JsonSimplePathsTransform {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private static final ObjectMapper objectMapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
public static <T, R> String modifyValuesAtPaths(String jsonString,
|
||||
Map<String, ValueTransformer<T, R>> pathTransformerMap, boolean isPretty) {
|
||||
|
||||
+424
@@ -1383,4 +1383,428 @@ class TemplateAdapterErrorMsgHandlerTest {
|
||||
assertNotNull(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// default.* 템플릿 폴백
|
||||
//
|
||||
// 그룹 전용 키가 미설정/공백이면 "default." 로 시작하는 공통 키를 재조회한다.
|
||||
// generateNonStandardErrorResponseMessage : {group}.template → default.template
|
||||
// generateOutboundErrorResponseMessage : {group}.template → default.template
|
||||
// generateNonStandardInternalErrorResponseMessage : {group}.sys.template → default.sys.template
|
||||
// generateNonStandardInboundErrorResponseMessage : {group}.in.{code}.template
|
||||
// → default.in.{code}.template
|
||||
// → {group}.in.template
|
||||
// → default.in.template
|
||||
// → MessageUtil 폴백
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("default.* 템플릿 폴백")
|
||||
class DefaultTemplateFallback {
|
||||
|
||||
private static final String G = TemplateAdapterErrorMsgHandler.PROP_GROUP;
|
||||
|
||||
private PropManager mockPropManager;
|
||||
private AdapterManager mockAdapterManager;
|
||||
private AdapterPropManager mockAdapterPropManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUpDefaults() throws Exception {
|
||||
mockPropManager = Mockito.mock(PropManager.class);
|
||||
mockAdapterManager = Mockito.mock(AdapterManager.class);
|
||||
mockAdapterPropManager = Mockito.mock(AdapterPropManager.class);
|
||||
|
||||
// 명시적으로 stub 하지 않은 모든 키는 미설정(null) 로 간주
|
||||
Mockito.when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null);
|
||||
Mockito.when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(2));
|
||||
|
||||
AdapterGroupVO adapterGroupVO = Mockito.mock(AdapterGroupVO.class);
|
||||
AdapterVO adapterVO = Mockito.mock(AdapterVO.class);
|
||||
Mockito.when(adapterGroupVO.getMessageType()).thenReturn("JSON");
|
||||
Mockito.when(adapterGroupVO.getMessageEncode()).thenReturn("UTF-8");
|
||||
Mockito.when(adapterVO.getPropGroupName()).thenReturn("TEST_PROP");
|
||||
Mockito.when(adapterVO.getAdapterGroupVO()).thenReturn(adapterGroupVO);
|
||||
Mockito.when(mockAdapterManager.getAdapterGroupVO("MY_GROUP")).thenReturn(adapterGroupVO);
|
||||
Mockito.when(mockAdapterManager.getAdapterVO("MY_GROUP", "MY_ADAPTER")).thenReturn(adapterVO);
|
||||
|
||||
Properties httpProp = new Properties();
|
||||
httpProp.setProperty("ERROR_RESPONSE_FORMAT", "");
|
||||
Mockito.when(mockAdapterPropManager.getProperties("TEST_PROP")).thenReturn(httpProp);
|
||||
|
||||
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
|
||||
Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager);
|
||||
Mockito.when(mockCtx.getBean(AdapterManager.class)).thenReturn(mockAdapterManager);
|
||||
Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockAdapterPropManager);
|
||||
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
|
||||
ctxField.setAccessible(true);
|
||||
ctxField.set(null, mockCtx);
|
||||
}
|
||||
|
||||
private void prop(String key, String value) {
|
||||
Mockito.when(mockPropManager.getProperty(eq(G), eq(key))).thenReturn(value);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// generateNonStandardErrorResponseMessage : default.template
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@Nested
|
||||
@DisplayName("generateNonStandardErrorResponseMessage – default.template")
|
||||
class NonStandardError {
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 템플릿이 있으면 default.template 을 조회하지 않는다")
|
||||
void 그룹_템플릿_우선() throws Exception {
|
||||
prop("MY_GROUP.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
|
||||
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
assertEquals("{\"code\":\"E001\"}", result);
|
||||
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 템플릿 미설정 시 default.template 으로 폴백")
|
||||
void default_폴백() throws Exception {
|
||||
prop("default.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"src\":\"default\"}");
|
||||
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E777", "오류", "", null)));
|
||||
|
||||
assertEquals("{\"code\":\"E777\",\"src\":\"default\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 템플릿이 공백 문자열이어도 default.template 으로 폴백")
|
||||
void 공백_템플릿도_폴백() throws Exception {
|
||||
prop("MY_GROUP.template", " ");
|
||||
prop("default.template", "{\"src\":\"default\"}");
|
||||
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
assertEquals("{\"src\":\"default\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹/default 둘 다 없으면 기존대로 null 반환")
|
||||
void 둘다_없으면_null() throws Exception {
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
assertNull(result);
|
||||
// 폴백 조회가 실제로 시도되었는지 확인
|
||||
Mockito.verify(mockPropManager).getProperty(G, "MY_GROUP.template");
|
||||
Mockito.verify(mockPropManager).getProperty(G, "default.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회 순서: {group}.template → default.template")
|
||||
void 조회_순서() throws Exception {
|
||||
handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(mockPropManager);
|
||||
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.template");
|
||||
inOrder.verify(mockPropManager).getProperty(G, "default.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("default.template 에서도 callProp/foreach 치환이 동일하게 동작")
|
||||
void default_템플릿_전체_치환() throws Exception {
|
||||
prop("default.template",
|
||||
"{\"adapter\":\"${callprop.ADAPTER_NAME}\"," +
|
||||
"\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
|
||||
"\"errors\":[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]}");
|
||||
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", props("ADAPTER_NAME", "REAL"), null,
|
||||
toEaiMessage(buildMsg("E001", "대표오류", "",
|
||||
new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}})));
|
||||
|
||||
assertEquals(
|
||||
"{\"adapter\":\"REAL\",\"code\":\"E001\"," +
|
||||
"\"errors\":[{\"c\":\"E001\"},{\"c\":\"E002\"}]}",
|
||||
result);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// generateNonStandardInternalErrorResponseMessage : default.sys.template
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@Nested
|
||||
@DisplayName("generateNonStandardInternalErrorResponseMessage – default.sys.template")
|
||||
class InternalError {
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 sys 템플릿이 있으면 default.sys.template 을 조회하지 않는다")
|
||||
void 그룹_템플릿_우선() throws Exception {
|
||||
prop("MY_GROUP.sys.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
|
||||
|
||||
Object result = handler.generateNonStandardInternalErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E500", "내부오류", "", null)));
|
||||
|
||||
assertEquals("{\"code\":\"E500\"}", result);
|
||||
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.sys.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 sys 템플릿 미설정 시 default.sys.template 으로 폴백")
|
||||
void default_폴백() throws Exception {
|
||||
prop("default.sys.template",
|
||||
"{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}");
|
||||
|
||||
Object result = handler.generateNonStandardInternalErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E500", "시스템오류", "", null)));
|
||||
|
||||
assertEquals("{\"code\":\"E500\",\"msg\":\"시스템오류\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("내부오류는 default.template 이 아니라 default.sys.template 을 조회한다")
|
||||
void sys_전용_키_사용() throws Exception {
|
||||
// default.template 만 설정된 상태 → 내부오류에는 적용되지 않아야 한다
|
||||
prop("default.template", "{\"src\":\"non-sys\"}");
|
||||
|
||||
Object result = handler.generateNonStandardInternalErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E500", "시스템오류", "", null)));
|
||||
|
||||
assertNull(result);
|
||||
Mockito.verify(mockPropManager).getProperty(G, "default.sys.template");
|
||||
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹/default 둘 다 없으면 기존대로 null 반환")
|
||||
void 둘다_없으면_null() throws Exception {
|
||||
Object result = handler.generateNonStandardInternalErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E500", "오류", "", null)));
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회 순서: {group}.sys.template → default.sys.template")
|
||||
void 조회_순서() throws Exception {
|
||||
handler.generateNonStandardInternalErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E500", "오류", "", null)));
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(mockPropManager);
|
||||
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.sys.template");
|
||||
inOrder.verify(mockPropManager).getProperty(G, "default.sys.template");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// generateOutboundErrorResponseMessage : default.template
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@Nested
|
||||
@DisplayName("generateOutboundErrorResponseMessage – default.template")
|
||||
class OutboundError {
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 템플릿이 있으면 default.template 을 조회하지 않는다")
|
||||
void 그룹_템플릿_우선() throws Exception {
|
||||
prop("OUT_GROUP.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
|
||||
|
||||
Object result = handler.generateOutboundErrorResponseMessage(
|
||||
"OUT_GROUP", "OUT_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
assertEquals("{\"code\":\"E001\"}", result);
|
||||
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 템플릿 미설정 시 default.template 으로 폴백")
|
||||
void default_폴백() throws Exception {
|
||||
prop("default.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"src\":\"default\"}");
|
||||
|
||||
Object result = handler.generateOutboundErrorResponseMessage(
|
||||
"OUT_GROUP", "OUT_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E888", "오류", "", null)));
|
||||
|
||||
assertEquals("{\"code\":\"E888\",\"src\":\"default\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("아웃바운드/인바운드가 동일한 default.template 을 공유한다")
|
||||
void inbound_outbound_default_공유() throws Exception {
|
||||
prop("default.template", "{\"shared\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
|
||||
|
||||
Object outbound = handler.generateOutboundErrorResponseMessage(
|
||||
"OUT_GROUP", "OUT_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
Object inbound = handler.generateNonStandardErrorResponseMessage(
|
||||
"IN_GROUP", "IN_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
// 두 경로 모두 같은 키를 쓰므로 결과가 동일하다.
|
||||
// 방향별로 다른 기본 응답이 필요하면 그룹 전용 키를 설정해야 한다.
|
||||
assertEquals("{\"shared\":\"E001\"}", outbound);
|
||||
assertEquals(outbound, inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹/default 둘 다 없으면 기존대로 null 반환")
|
||||
void 둘다_없으면_null() throws Exception {
|
||||
Object result = handler.generateOutboundErrorResponseMessage(
|
||||
"OUT_GROUP", "OUT_ADAPTER", null, null,
|
||||
toEaiMessage(buildMsg("E001", "오류", "", null)));
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// generateNonStandardInboundErrorResponseMessage : default.in.*
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@Nested
|
||||
@DisplayName("generateNonStandardInboundErrorResponseMessage – default.in.*")
|
||||
class InboundError {
|
||||
|
||||
/** 500 으로 매핑되는 일반 예외로 호출한다. */
|
||||
private Object call500(Properties callProp) throws Exception {
|
||||
return handler.generateNonStandardInboundErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", callProp, null, null,
|
||||
new RuntimeException("처리 중 오류"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1순위: {group}.in.{code}.template 이 있으면 그것을 사용")
|
||||
void 그룹_코드별_템플릿_우선() throws Exception {
|
||||
prop("MY_GROUP.in.500.template", "{\"src\":\"group-code\"}");
|
||||
prop("default.in.500.template", "{\"src\":\"default-code\"}");
|
||||
prop("MY_GROUP.in.template", "{\"src\":\"group-common\"}");
|
||||
prop("default.in.template", "{\"src\":\"default-common\"}");
|
||||
|
||||
assertEquals("{\"src\":\"group-code\"}", call500(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2순위: {group}.in.{code} 가 없으면 default.in.{code}.template 사용")
|
||||
void default_코드별_템플릿() throws Exception {
|
||||
prop("default.in.500.template", "{\"src\":\"default-code\"}");
|
||||
prop("MY_GROUP.in.template", "{\"src\":\"group-common\"}");
|
||||
prop("default.in.template", "{\"src\":\"default-common\"}");
|
||||
|
||||
// 주의: 현재 구현은 코드별 default 가 그룹 공통보다 우선한다.
|
||||
assertEquals("{\"src\":\"default-code\"}", call500(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3순위: 코드별 키가 모두 없으면 {group}.in.template 사용")
|
||||
void 그룹_공통_템플릿() throws Exception {
|
||||
prop("MY_GROUP.in.template", "{\"src\":\"group-common\"}");
|
||||
prop("default.in.template", "{\"src\":\"default-common\"}");
|
||||
|
||||
assertEquals("{\"src\":\"group-common\"}", call500(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4순위: 앞의 3개가 모두 없으면 default.in.template 사용")
|
||||
void default_공통_템플릿() throws Exception {
|
||||
prop("default.in.template", "{\"src\":\"default-common\"}");
|
||||
|
||||
assertEquals("{\"src\":\"default-common\"}", call500(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4개 키가 모두 없으면 MessageUtil 폴백")
|
||||
void 전부_없으면_MessageUtil_폴백() throws Exception {
|
||||
Object result = call500(null);
|
||||
|
||||
assertNotNull(result);
|
||||
Mockito.verify(mockPropManager).getProperty(G, "MY_GROUP.in.500.template");
|
||||
Mockito.verify(mockPropManager).getProperty(G, "default.in.500.template");
|
||||
Mockito.verify(mockPropManager).getProperty(G, "MY_GROUP.in.template");
|
||||
Mockito.verify(mockPropManager).getProperty(G, "default.in.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회 순서: group.code → default.code → group.common → default.common")
|
||||
void 조회_순서_4단계() throws Exception {
|
||||
handler.generateNonStandardInboundErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null, null,
|
||||
new HttpStatusException("Service Unavailable", 503));
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(mockPropManager);
|
||||
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.in.503.template");
|
||||
inOrder.verify(mockPropManager).getProperty(G, "default.in.503.template");
|
||||
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.in.template");
|
||||
inOrder.verify(mockPropManager).getProperty(G, "default.in.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("코드별 default 키 형식은 'default.in.{code}.template' (점 누락 회귀 방지)")
|
||||
void default_코드별_키_형식_검증() throws Exception {
|
||||
handler.generateNonStandardInboundErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null, null,
|
||||
new HttpStatusException("Bad Request", 400));
|
||||
|
||||
Mockito.verify(mockPropManager).getProperty(G, "default.in.400.template");
|
||||
// 점이 빠진 잘못된 키로 조회하지 않아야 한다
|
||||
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.in400.template");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JwtAuthException 은 default.in.401.template 로 폴백")
|
||||
void jwt_401_default_폴백() throws Exception {
|
||||
prop("default.in.401.template", "{\"code\":\"${exception.code}\",\"msg\":\"${exception.message}\"}");
|
||||
|
||||
Object result = handler.generateNonStandardInboundErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", null, null, null,
|
||||
new JwtAuthException("JWT_EXPIRED", "토큰이 만료되었습니다"));
|
||||
|
||||
assertEquals("{\"code\":\"JWT_EXPIRED\",\"msg\":\"토큰이 만료되었습니다\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("default.in.template 에서 exception/callProp 치환이 동일하게 동작")
|
||||
void default_공통_템플릿_치환() throws Exception {
|
||||
prop("default.in.template",
|
||||
"{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"msg\":\"${exception.message}\"}");
|
||||
|
||||
Object result = call500(props("ADAPTER_NAME", "REAL"));
|
||||
|
||||
assertEquals("{\"adapter\":\"REAL\",\"msg\":\"처리 중 오류\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("default.in.template 에 ${callprop[경로]} 를 써도 msg=null 로 NPE 없이 기본값 반환")
|
||||
void default_공통_템플릿_간접참조_NPE_회귀방지() throws Exception {
|
||||
// 인바운드 경로는 render(template, null, ...) 로 호출되므로
|
||||
// 간접 참조의 표준전문 경로 조회 대상이 없다 → 기본값으로 처리되어야 한다.
|
||||
prop("default.in.template", "{\"key\":\"${callprop[MSG.cp_key]:NO_MSG}\"}");
|
||||
|
||||
Object result = call500(props("ADAPTER_NAME", "REAL"));
|
||||
|
||||
assertEquals("{\"key\":\"NO_MSG\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("그룹 코드별 템플릿이 공백이면 default 로 폴백")
|
||||
void 공백_템플릿도_폴백() throws Exception {
|
||||
prop("MY_GROUP.in.500.template", " ");
|
||||
prop("default.in.500.template", "{\"src\":\"default-code\"}");
|
||||
|
||||
assertEquals("{\"src\":\"default-code\"}", call500(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* JsonToSetStatusFilter 단위 테스트.
|
||||
*
|
||||
* 상태코드 필드명은 PropManager(그룹: JsonToSetStatusFilter, 키: 어댑터그룹명)에서 조회하므로
|
||||
* PropManager 를 Mock 으로 등록하고 필터 로직만 검증한다.
|
||||
*
|
||||
* 4번 그룹은 "현재 구현의 동작을 그대로 고정(characterization)"한 테스트로,
|
||||
* 개선 여부 판단용이다. 구현을 보완하면 해당 테스트도 함께 수정해야 한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class JsonToSetStatusFilterTest {
|
||||
|
||||
private static final String GRP = "TEST_GRP";
|
||||
private static final String ADPT = "TEST_ADPT";
|
||||
private static final String FIELD = "apiRsltCd";
|
||||
private static final String PROP_GROUP = "JsonToSetStatusFilter";
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static PropManager mockPropManager;
|
||||
private static JsonToSetStatusFilter filter;
|
||||
|
||||
private HttpServletRequest mockRequest;
|
||||
private HttpServletResponse mockResponse;
|
||||
private Properties prop;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() {
|
||||
mockPropManager = mock(PropManager.class);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("propManager", mockPropManager);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
filter = new JsonToSetStatusFilter();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
reset(mockPropManager);
|
||||
mockRequest = mock(HttpServletRequest.class);
|
||||
mockResponse = mock(HttpServletResponse.class);
|
||||
prop = new Properties();
|
||||
|
||||
when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn(FIELD);
|
||||
}
|
||||
|
||||
private String body(String rsltCd) {
|
||||
return "{\"" + FIELD + "\":\"" + rsltCd + "\",\"msg\":\"OK\"}";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. 상태코드 정상 반영
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. 문자열 body의 상태코드 필드값을 HTTP 상태코드로 설정한다")
|
||||
void testPostFilter_string_setsStatus() throws Exception {
|
||||
Object result = filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setStatus(404);
|
||||
assertEquals(body("404"), result, "원 메시지를 그대로 반환해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. 숫자 타입 필드값도 상태코드로 설정한다")
|
||||
void testPostFilter_numericNode_setsStatus() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, "{\"" + FIELD + "\":503}", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setStatus(503);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 업무팀 주 사용 케이스 - 2xx 상태코드를 설정한다")
|
||||
void testPostFilter_successStatusCodes() throws Exception {
|
||||
for (int status : new int[] { 200, 201, 202, 204 }) {
|
||||
reset(mockResponse);
|
||||
|
||||
filter.doPostFilter(GRP, ADPT, body(String.valueOf(status)), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setStatus(status);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. 어댑터 그룹마다 다른 필드명을 사용할 수 있다")
|
||||
void testPostFilter_perGroupFieldName() throws Exception {
|
||||
when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn("rspCd");
|
||||
|
||||
filter.doPostFilter(GRP, ADPT, "{\"rspCd\":\"403\",\"" + FIELD + "\":\"500\"}", prop,
|
||||
mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setStatus(403);
|
||||
verify(mockResponse, never()).setStatus(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. JSONObject 타입 응답도 처리한다")
|
||||
void testPostFilter_jsonObjectMessage_setsStatus() throws Exception {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(FIELD, "404");
|
||||
|
||||
Object result = filter.doPostFilter(GRP, ADPT, json, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setStatus(404);
|
||||
assertSame(json, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-6. JsonNode 타입 응답도 처리한다")
|
||||
void testPostFilter_jsonNodeMessage_setsStatus() throws Exception {
|
||||
JsonNode node = new ObjectMapper().readTree(body("401"));
|
||||
|
||||
Object result = filter.doPostFilter(GRP, ADPT, node, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setStatus(401);
|
||||
assertSame(node, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-7. 경계값 100 / 599는 설정한다")
|
||||
void testPostFilter_boundaryValues() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, body("100"), prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setStatus(100);
|
||||
|
||||
reset(mockResponse);
|
||||
filter.doPostFilter(GRP, ADPT, body("599"), prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setStatus(599);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. 상태코드를 변경하지 않는 경우
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. 상태코드 필드가 없으면 setStatus를 호출하지 않는다")
|
||||
void testPostFilter_fieldAbsent_noStatusChange() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, "{\"msg\":\"OK\"}", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. 상태코드 필드값이 빈 문자열이면 setStatus를 호출하지 않는다")
|
||||
void testPostFilter_emptyValue_noStatusChange() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, body(""), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. 프로퍼티에 필드명 설정이 없으면(null) 예외 없이 통과한다")
|
||||
void testPostFilter_noConfiguredField_noStatusChange() throws Exception {
|
||||
when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn(null);
|
||||
|
||||
Object result = filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
assertEquals(body("404"), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4. 상태코드 필드값이 숫자가 아니면 로그만 남기고 상태코드를 변경하지 않는다")
|
||||
void testPostFilter_nonNumericValue_noStatusChange() throws Exception {
|
||||
Object result = filter.doPostFilter(GRP, ADPT, body("E0001"), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
assertEquals(body("E0001"), result, "파싱 실패해도 원 메시지는 그대로 반환되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-5. 업무결과코드 0000은 유효한 상태코드가 아니므로 설정하지 않는다")
|
||||
void testPostFilter_businessCode0000_noStatusChange() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, body("0000"), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-6. HTTP 상태코드 범위(100~599)를 벗어난 값은 설정하지 않는다")
|
||||
void testPostFilter_outOfRangeStatus_noStatusChange() throws Exception {
|
||||
for (String value : new String[] { "0", "99", "600", "9999", "-200" }) {
|
||||
reset(mockResponse);
|
||||
|
||||
filter.doPostFilter(GRP, ADPT, body(value), prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-7. JSON 배열 응답이면 상태코드를 변경하지 않는다")
|
||||
void testPostFilter_jsonArray_noStatusChange() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, "[{\"" + FIELD + "\":\"404\"}]", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-8. 필드값이 JSON null이면 상태코드를 변경하지 않는다")
|
||||
void testPostFilter_jsonNullValue_noStatusChange() throws Exception {
|
||||
filter.doPostFilter(GRP, ADPT, "{\"" + FIELD + "\":null}", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. 비정상 입력에서도 예외를 던지지 않는다 (거래 실패 방지)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. JSON 형식이 아닌 응답이어도 예외 없이 통과한다")
|
||||
void testPostFilter_nonJsonBody_noException() throws Exception {
|
||||
String xml = "<xml><rslt>0000</rslt></xml>";
|
||||
|
||||
Object result = assertDoesNotThrow(
|
||||
() -> filter.doPostFilter(GRP, ADPT, xml, prop, mockRequest, mockResponse));
|
||||
|
||||
assertSame(xml, result);
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. 깨진 JSON 응답이어도 예외 없이 통과한다")
|
||||
void testPostFilter_malformedJson_noException() {
|
||||
String broken = "{\"" + FIELD + "\":\"404\"";
|
||||
|
||||
assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, broken, prop, mockRequest, mockResponse));
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. null 응답이어도 예외 없이 null을 그대로 반환한다")
|
||||
void testPostFilter_nullMessage_noException() {
|
||||
Object result = assertDoesNotThrow(
|
||||
() -> filter.doPostFilter(GRP, ADPT, null, prop, mockRequest, mockResponse));
|
||||
|
||||
assertNull(result);
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-4. 빈 문자열 응답이어도 예외 없이 통과한다")
|
||||
void testPostFilter_emptyBody_noException() {
|
||||
assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, "", prop, mockRequest, mockResponse));
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-5. 프로퍼티 그룹 미등록(PropManager가 null 반환)이어도 예외 없이 통과한다")
|
||||
void testPostFilter_propGroupMissing_noException() {
|
||||
when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null);
|
||||
|
||||
assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse));
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. doPreFilter / 현재 동작 고정 (개선 검토 대상)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. doPreFilter는 아무 것도 하지 않고 요청 메시지를 그대로 반환한다")
|
||||
void testPreFilter_doesNothing() throws Exception {
|
||||
String message = body("404");
|
||||
|
||||
Object result = filter.doPreFilter(GRP, ADPT, message, prop, mockRequest, mockResponse);
|
||||
|
||||
assertSame(message, result);
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
verifyNoInteractions(mockPropManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. [확인필요] byte[] 응답은 JSON으로 파싱되지 않아 상태코드가 설정되지 않는다")
|
||||
void testPostFilter_byteArray_notSupported() throws Exception {
|
||||
byte[] msg = body("404").getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
Object result = filter.doPostFilter(GRP, ADPT, msg, prop, mockRequest, mockResponse);
|
||||
|
||||
// JsonPathUtil.toTree 가 byte[] 를 toString() 처리하므로 "[B@..." 가 되어 파싱에 실패한다.
|
||||
verify(mockResponse, never()).setStatus(anyInt());
|
||||
assertSame(msg, result);
|
||||
}
|
||||
}
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* ReflectHeaderFilter 단위 테스트.
|
||||
*
|
||||
* 복사 대상 헤더 목록은 PropManager(그룹: HttpHeaderFilter,
|
||||
* 키: ReflectHeaderFilter.whiteList[.어댑터그룹명])에서 조회하므로
|
||||
* PropManager 를 Mock 으로 등록하고 필터 로직만 검증한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class ReflectHeaderFilterTest {
|
||||
|
||||
private static final String GRP = "TEST_GRP";
|
||||
private static final String ADPT = "TEST_ADPT";
|
||||
private static final String PROP_GROUP = ReflectHeaderFilter.PROPERTIES_GROUP_NAME;
|
||||
private static final String KEY = ReflectHeaderFilter.HEADER_KEY_NAMES;
|
||||
private static final String KEY_GRP = KEY + "." + GRP;
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static PropManager mockPropManager;
|
||||
|
||||
private ReflectHeaderFilter filter;
|
||||
private HttpServletRequest mockRequest;
|
||||
private HttpServletResponse mockResponse;
|
||||
private Properties prop;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() {
|
||||
mockPropManager = mock(PropManager.class);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("propManager", mockPropManager);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
reset(mockPropManager);
|
||||
mockRequest = mock(HttpServletRequest.class);
|
||||
mockResponse = mock(HttpServletResponse.class);
|
||||
prop = new Properties();
|
||||
|
||||
// 미설정 키는 실제 PropManager 와 동일하게 기본값을 반환한다.
|
||||
when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
|
||||
.thenAnswer(inv -> inv.getArgument(2));
|
||||
|
||||
// 필터별 캐시가 테스트간에 섞이지 않도록 매번 새 인스턴스를 사용한다.
|
||||
filter = new ReflectHeaderFilter();
|
||||
}
|
||||
|
||||
/** 프로퍼티 설정 - 어댑터 그룹 단위 키. */
|
||||
private void givenGroupWhiteList(String value) {
|
||||
when(mockPropManager.getProperty(PROP_GROUP, KEY_GRP, "")).thenReturn(value);
|
||||
}
|
||||
|
||||
/** 프로퍼티 설정 - 전역 키. */
|
||||
private void givenGlobalWhiteList(String value) {
|
||||
when(mockPropManager.getProperty(PROP_GROUP, KEY, "")).thenReturn(value);
|
||||
}
|
||||
|
||||
/** 수신 요청 헤더 설정. getHeader 는 서블릿 스펙대로 대소문자를 구분하지 않는다. */
|
||||
private void givenRequestHeaders(String... nameValuePairs) {
|
||||
Map<String, String> headers = new LinkedHashMap<>();
|
||||
for (int i = 0; i < nameValuePairs.length; i += 2) {
|
||||
headers.put(nameValuePairs[i], nameValuePairs[i + 1]);
|
||||
}
|
||||
|
||||
when(mockRequest.getHeader(anyString())).thenAnswer(inv -> {
|
||||
String wanted = inv.getArgument(0);
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey().equalsIgnoreCase(wanted)) return e.getValue();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// doPreFilter/doPostFilter 가 각각 순회할 수 있도록 호출마다 새 Enumeration 을 반환한다.
|
||||
when(mockRequest.getHeaderNames())
|
||||
.thenAnswer(inv -> Collections.enumeration(new ArrayList<>(headers.keySet())));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. 허용 목록에 등록된 헤더만 복사
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. 허용 목록에 등록된 헤더만 응답으로 복사한다")
|
||||
void testWhiteListedHeadersOnly() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid, x-elink-client-id");
|
||||
givenRequestHeaders(
|
||||
"x-obp-txid", "TX-001",
|
||||
"x-elink-client-id", "CLIENT-A",
|
||||
"Authorization", "Bearer secret",
|
||||
"Cookie", "JSESSIONID=abc");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-001");
|
||||
verify(mockResponse).setHeader("x-elink-client-id", "CLIENT-A");
|
||||
verify(mockResponse, never()).setHeader(eq("Authorization"), anyString());
|
||||
verify(mockResponse, never()).setHeader(eq("Cookie"), anyString());
|
||||
verifyNoMoreInteractions(mockResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. 헤더명 대소문자를 구분하지 않고 매칭한다")
|
||||
void testCaseInsensitiveMatch() throws Exception {
|
||||
givenGroupWhiteList("X-OBP-TXID");
|
||||
givenRequestHeaders("x-obp-txid", "TX-002");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("X-OBP-TXID", "TX-002");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 허용 목록에 있어도 수신 요청에 없는 헤더는 복사하지 않는다")
|
||||
void testAbsentHeaderNotReflected() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid, x-not-sent");
|
||||
givenRequestHeaders("x-obp-txid", "TX-003");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-003");
|
||||
verify(mockResponse, never()).setHeader(eq("x-not-sent"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. 값이 빈 문자열인 헤더도 복사한다")
|
||||
void testEmptyValueReflected() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. 목록의 공백/빈 항목은 무시한다")
|
||||
void testBlankTokensIgnored() throws Exception {
|
||||
givenGroupWhiteList(" x-obp-txid , , , ");
|
||||
givenRequestHeaders("x-obp-txid", "TX-005");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-005");
|
||||
verifyNoMoreInteractions(mockResponse);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. 프로퍼티 조회 우선순위 (어댑터 그룹 → 전역)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. 어댑터 그룹 키가 있으면 전역 키를 무시한다(override)")
|
||||
void testGroupKeyOverridesGlobal() throws Exception {
|
||||
givenGlobalWhiteList("x-global-only");
|
||||
givenGroupWhiteList("x-group-only");
|
||||
givenRequestHeaders(
|
||||
"x-global-only", "G",
|
||||
"x-group-only", "S");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-group-only", "S");
|
||||
verify(mockResponse, never()).setHeader(eq("x-global-only"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. 어댑터 그룹 키가 없으면 전역 키를 사용한다")
|
||||
void testFallbackToGlobalKey() throws Exception {
|
||||
givenGlobalWhiteList("x-global-only");
|
||||
givenRequestHeaders("x-global-only", "G");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-global-only", "G");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. 어댑터 그룹 키가 공백이면 전역 키로 폴백한다")
|
||||
void testBlankGroupKeyFallsBackToGlobal() throws Exception {
|
||||
givenGroupWhiteList(" ");
|
||||
givenGlobalWhiteList("x-global-only");
|
||||
givenRequestHeaders("x-global-only", "G");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-global-only", "G");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4. 어댑터 그룹마다 다른 목록을 적용한다")
|
||||
void testPerGroupWhiteList() throws Exception {
|
||||
when(mockPropManager.getProperty(PROP_GROUP, KEY + ".GRP_A", "")).thenReturn("x-a");
|
||||
when(mockPropManager.getProperty(PROP_GROUP, KEY + ".GRP_B", "")).thenReturn("x-b");
|
||||
givenRequestHeaders("x-a", "A", "x-b", "B");
|
||||
|
||||
filter.doPreFilter("GRP_A", ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setHeader("x-a", "A");
|
||||
verify(mockResponse, never()).setHeader(eq("x-b"), anyString());
|
||||
|
||||
reset(mockResponse);
|
||||
|
||||
filter.doPreFilter("GRP_B", ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setHeader("x-b", "B");
|
||||
verify(mockResponse, never()).setHeader(eq("x-a"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-5. 어댑터 그룹명이 null 이어도 전역 키로 동작한다")
|
||||
void testNullGroupName() throws Exception {
|
||||
givenGlobalWhiteList("x-global-only");
|
||||
givenRequestHeaders("x-global-only", "G");
|
||||
|
||||
filter.doPreFilter(null, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-global-only", "G");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-6. 어댑터명은 프로퍼티 키 조회에 사용하지 않는다")
|
||||
void testAdapterNameNotUsedInKeyLookup() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-006");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockPropManager, never()).getProperty(eq(PROP_GROUP), contains(ADPT), anyString());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. 미설정 시 아무 헤더도 복사하지 않음
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. 설정이 없으면 어떤 헤더도 복사하지 않는다")
|
||||
void testNoConfigReflectsNothing() throws Exception {
|
||||
givenRequestHeaders("x-obp-txid", "TX-007", "Authorization", "Bearer secret");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. 설정값이 콤마뿐이어도 예외 없이 아무것도 복사하지 않는다")
|
||||
void testCommaOnlyConfigReflectsNothing() throws Exception {
|
||||
givenGroupWhiteList(",,,");
|
||||
givenRequestHeaders("x-obp-txid", "TX-008");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. 접두사(*) 매칭
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. 접두사(*) 설정은 해당 접두사로 시작하는 수신 헤더를 모두 복사한다")
|
||||
void testPrefixMatch() throws Exception {
|
||||
givenGroupWhiteList("X-KKB-*");
|
||||
givenRequestHeaders(
|
||||
"X-KKB-API-NAME", "transfer",
|
||||
"X-KKB-API-TX-ID", "TX-009",
|
||||
"X-OBP-TXID", "OTHER");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("X-KKB-API-NAME", "transfer");
|
||||
verify(mockResponse).setHeader("X-KKB-API-TX-ID", "TX-009");
|
||||
verify(mockResponse, never()).setHeader(eq("X-OBP-TXID"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. 접두사 매칭도 대소문자를 구분하지 않는다")
|
||||
void testPrefixMatchCaseInsensitive() throws Exception {
|
||||
givenGroupWhiteList("x-kkb-*");
|
||||
givenRequestHeaders("X-KKB-API-NAME", "transfer");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("X-KKB-API-NAME", "transfer");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3. 완전일치와 접두사를 함께 설정할 수 있다")
|
||||
void testExactAndPrefixTogether() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid, X-KKB-*");
|
||||
givenRequestHeaders(
|
||||
"x-obp-txid", "TX-010",
|
||||
"X-KKB-API-NAME", "transfer",
|
||||
"Authorization", "Bearer secret");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-010");
|
||||
verify(mockResponse).setHeader("X-KKB-API-NAME", "transfer");
|
||||
verify(mockResponse, never()).setHeader(eq("Authorization"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4. 접두사 설정이 없으면 수신 헤더 전체를 순회하지 않는다")
|
||||
void testNoPrefixSkipsHeaderNamesScan() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-011");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockRequest, never()).getHeaderNames();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-5. '*' 단독 설정은 접두사가 비어 무시된다")
|
||||
void testBareAsteriskIgnored() throws Exception {
|
||||
givenGroupWhiteList("*");
|
||||
givenRequestHeaders("x-obp-txid", "TX-012");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-6. getHeaderNames() 가 null 이어도 예외가 발생하지 않는다")
|
||||
void testNullHeaderNamesEnumeration() throws Exception {
|
||||
givenGroupWhiteList("X-KKB-*");
|
||||
when(mockRequest.getHeaderNames()).thenReturn(null);
|
||||
|
||||
assertDoesNotThrow(() -> filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse));
|
||||
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. 복사 금지 헤더 / CRLF 방어
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. 응답 프레이밍 헤더는 허용 목록에 있어도 복사하지 않는다")
|
||||
void testNeverReflectHeaders() throws Exception {
|
||||
givenGroupWhiteList("Content-Length, Transfer-Encoding, Connection, Keep-Alive, Upgrade, TE, Trailer, x-obp-txid");
|
||||
givenRequestHeaders(
|
||||
"Content-Length", "100",
|
||||
"Transfer-Encoding", "chunked",
|
||||
"Connection", "keep-alive",
|
||||
"Keep-Alive", "timeout=5",
|
||||
"Upgrade", "websocket",
|
||||
"TE", "trailers",
|
||||
"Trailer", "Expires",
|
||||
"x-obp-txid", "TX-013");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-013");
|
||||
verifyNoMoreInteractions(mockResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. 복사 금지 헤더 판정도 대소문자를 구분하지 않는다")
|
||||
void testNeverReflectCaseInsensitive() throws Exception {
|
||||
givenGroupWhiteList("content-length");
|
||||
givenRequestHeaders("content-length", "100");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3. 접두사 매칭으로 걸린 헤더도 복사 금지 목록이 우선한다")
|
||||
void testNeverReflectAppliesToPrefixMatch() throws Exception {
|
||||
givenGroupWhiteList("Content-*");
|
||||
givenRequestHeaders("Content-Length", "100", "Content-MD5", "abc");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setHeader(eq("Content-Length"), anyString());
|
||||
verify(mockResponse).setHeader("Content-MD5", "abc");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-4. 값에 CR/LF 가 있으면 응답 분할 방지를 위해 복사하지 않는다")
|
||||
void testCrLfInValueBlocked() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid, x-safe");
|
||||
givenRequestHeaders(
|
||||
"x-obp-txid", "TX\r\nX-Injected: 1",
|
||||
"x-safe", "OK");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, never()).setHeader(eq("x-obp-txid"), anyString());
|
||||
verify(mockResponse).setHeader("x-safe", "OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-5. LF 단독, CR 단독 값도 차단한다")
|
||||
void testLoneCrOrLfBlocked() throws Exception {
|
||||
givenGroupWhiteList("x-lf, x-cr");
|
||||
givenRequestHeaders("x-lf", "a\nb", "x-cr", "a\rb");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 6. doPreFilter / doPostFilter 반환값
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("6-1. doPreFilter 는 원 메시지를 그대로 반환한다")
|
||||
void testDoPreFilterReturnsMessage() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-014");
|
||||
|
||||
Object message = "request-body";
|
||||
Object result = filter.doPreFilter(GRP, ADPT, message, prop, mockRequest, mockResponse);
|
||||
|
||||
assertSame(message, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-2. doPostFilter 도 헤더를 복사하고 원 메시지를 그대로 반환한다")
|
||||
void testDoPostFilterReflectsAndReturnsMessage() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-015");
|
||||
|
||||
Object message = "response-body";
|
||||
Object result = filter.doPostFilter(GRP, ADPT, message, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-015");
|
||||
assertSame(message, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-3. setHeader 사용으로 pre/post 연속 호출 시 헤더가 중복되지 않는다")
|
||||
void testPreAndPostUseSetHeader() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-016");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
filter.doPostFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse, times(2)).setHeader("x-obp-txid", "TX-016");
|
||||
verify(mockResponse, never()).addHeader(anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-4. 응답 헤더 설정 중 예외가 발생해도 필터는 메시지를 반환한다")
|
||||
void testExceptionSwallowed() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-017");
|
||||
doThrow(new IllegalStateException("response already committed"))
|
||||
.when(mockResponse).setHeader(anyString(), anyString());
|
||||
|
||||
Object result = filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals("msg", result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 7. 프로퍼티 캐시
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("7-1. 같은 설정으로 반복 호출해도 동작이 동일하다(캐시 재사용)")
|
||||
void testCacheReuse() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-018");
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
reset(mockResponse);
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-018");
|
||||
verifyNoMoreInteractions(mockResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-2. 프로퍼티가 변경되면 다음 호출에 즉시 반영된다")
|
||||
void testCacheInvalidatedOnPropertyChange() throws Exception {
|
||||
givenGroupWhiteList("x-first");
|
||||
givenRequestHeaders("x-first", "F", "x-second", "S");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setHeader("x-first", "F");
|
||||
|
||||
reset(mockResponse);
|
||||
givenGroupWhiteList("x-second");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setHeader("x-second", "S");
|
||||
verify(mockResponse, never()).setHeader(eq("x-first"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-3. 설정이 제거되면 다음 호출부터 아무것도 복사하지 않는다")
|
||||
void testCacheInvalidatedOnPropertyRemoval() throws Exception {
|
||||
givenGroupWhiteList("x-obp-txid");
|
||||
givenRequestHeaders("x-obp-txid", "TX-019");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
verify(mockResponse).setHeader("x-obp-txid", "TX-019");
|
||||
|
||||
reset(mockResponse);
|
||||
givenGroupWhiteList("");
|
||||
|
||||
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
|
||||
verifyNoInteractions(mockResponse);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user