Compare commits

..

11 Commits

Author SHA1 Message Date
curry772 f4e806fa08 test: 전체 테스트 스위트 통과하도록 격리/기대값 수정 (115/115)
전체 스위트 실행 시 115개 중 28개가 실패하던 문제 처리.

1. 클래스별 JVM 격리 (25건)
   TransformEngine/LayoutManager/MessageFactory 는 ApplicationContextProvider
   기반의 JVM 전역 싱글턴이라, 한 JVM 에서 여러 테스트 클래스를 돌리면 마지막에
   뜬 스프링 컨텍스트가 앞선 컨텍스트를 덮어쓴다. 그 결과 뒤 클래스가 앞 클래스의
   레이아웃을 보게 되어 "cannot create message" 로 실패했다.
   (각 클래스는 단독 실행 시에는 모두 통과)
   → build.gradle test 태스크에 forkEvery = 1 추가.
     느려지는 부분은 maxParallelForks 로 상쇄 (7분 → 4분 28초).

2. GetTrinomialFunctionTest 기대값 정정 (2건)
   JEP addStandardConstants() 에는 true/false 가 없고 Parser 가 이를 Boolean 으로
   등록한다(Parser.addBooleanConstants). 테스트 setUp 도 Boolean 을 바인딩해 놓고
   결과를 Number 로 단언하고 있어 자기모순이었음 → Boolean 기대로 정정.

3. LayoutRepositoryTest 시드 ID 수정 (1건)
   시드 SQL 어디에도 없는 TST_TTST00000002_RES 를 조회하고 있었음
   → init_tseaitr07/08 에 존재하는 TST_TOSSTEST1_TGT 로 변경.

운영 코드 변경 없음 (build.gradle 테스트 설정 + 테스트 코드만).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnVAVJLEqjhZnzeWbNcp5q
2026-08-27 16:25:11 +09:00
curry772 0c048a88fe fix: 변환매핑 item이 하나도 없는 변환매핑 로딩 안되는 문제 처리 2026-08-27 12:03:10 +09:00
curry772 b1e7883d41 fix: target validation 오류 발생 시 item path 정보 출력 안되는 문제 처리 2026-08-27 09:18:49 +09:00
curry772 09ac7f4ca5 printStackTrace() 주석 처리 2026-08-13 15:07:39 +09:00
curry772 90ccf59b25 변환 표현식 내에 true, false가 있을 경우, warning 출력 부분 제거 2026-08-12 17:57:42 +09:00
curry772 051305ac55 숫자형 validation 로직 원복 100.12 -> 5,2 로 설정하는 방식 2026-08-05 16:33:25 +09:00
curry772 375f0e0f15 JSONMessage 불필요한 로직 제거 2026-08-05 13:04:02 +09:00
curry772 027ec4fcea isValid 함수에서 전체 길이는 '.'를 포함하지 않는 정수부 + 소수부 길이로 함 2026-08-04 18:18:44 +09:00
curry772 229156d51d 프러퍼티그룹 이용 함수 개발 2026-07-27 09:56:24 +09:00
curry772 f9e6522b8f JJB masking 룰 적용 2026-07-23 11:32:57 +09:00
curry772 cd5cb4c0b5 JSON grid가 아닌 필드배열 처리 패치 2026-07-06 09:23:22 +09:00
14 changed files with 468 additions and 299 deletions
+10
View File
@@ -85,6 +85,16 @@ dependencies {
test {
useJUnitPlatform()
// TransformEngine/LayoutManager/MessageFactory 는 ApplicationContextProvider 기반의
// JVM 전역 싱글턴이다. 한 JVM 에서 여러 테스트 클래스를 돌리면 마지막에 뜬 스프링
// 컨텍스트가 앞선 컨텍스트를 덮어써서, 뒤 클래스가 앞 클래스의 레이아웃을 보게 된다
// ("cannot create message" 오류). 클래스마다 JVM 을 새로 띄워 격리한다.
forkEvery = 1
// 클래스별 JVM 분리로 느려지는 것을 CPU 코어 수만큼 병렬 실행해 상쇄한다.
// 포크마다 별도 JVM(=별도 H2 인메모리 DB)이라 서로 간섭하지 않는다.
maxParallelForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(2))
}
publishing {
@@ -1,6 +1,11 @@
package com.eactive.eai.transformer.engine;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.nfunk.jep.JEP;
import org.nfunk.jep.function.PostfixMathCommand;
@@ -26,6 +31,14 @@ public class Parser implements Serializable {
private static Logger logger = Logger.getLogger("transformer");
/**
* 표현식의 변수가 아니라 파서가 미리 심볼테이블에 등록해 두는 예약 심볼.
* getSymbols() 결과에서 제외하지 않으면 Transformer 가 이를 소스전문의 항목경로로 보고
* 조회를 시도해 "레이아웃항목이 존재하지 않음" 경고가 매 변환마다 발생한다.
*/
private static final Set<String> RESERVED_SYMBOLS =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("true", "false")));
private transient JEP jep = new JEP(); // Create a new parser
public Parser() {
@@ -102,9 +115,9 @@ public class Parser implements Serializable {
@SuppressWarnings("unchecked")
public String[] getSymbols() {
String[] symbols = new String[getJep().getSymbolTable().size()];
symbols = (String[])getJep().getSymbolTable().keySet().toArray(new String[symbols.length]);
return symbols;
Set<String> symbols = new LinkedHashSet<>(getJep().getSymbolTable().keySet());
symbols.removeAll(RESERVED_SYMBOLS);
return symbols.toArray(new String[symbols.size()]);
}
private void addGenericFunctions() throws Exception {
@@ -685,7 +685,7 @@ public class Transformer {
}
}
} catch(InvalidAccessException e) {
e.printStackTrace();
//e.printStackTrace();
errors.add("Transformer] ITEM Error during conversion (source message access) - continue- " + e.getMessage());
value ="";
findSource = false;
@@ -0,0 +1,107 @@
package com.eactive.eai.transformer.function.generic;
import java.util.Stack;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.PostfixMathCommand;
import com.eactive.eai.common.property.PropManager;
/**
* JEP 커스텀 함수 - PropManager 프러퍼티 그룹 기반 값 매핑
*
* <p>고정된 변환 룰이 아닌, PropManager의 프러퍼티 그룹(예: 공통코드 매핑 테이블)을
* 동적으로 조회하여 입력값을 매핑된 값으로 변환한다.
* 매핑 정보는 {@code 1000 -> AAAAA}, {@code 1001 -> BBBBB} 처럼 그룹의 key/value로 관리된다.
*
* <p>사용법:
* <ul>
* <li>{@code getmapping(group, value)} - group 프러퍼티 그룹에서 value에 매핑된 값을 반환.
* 매핑이 없으면 <b>원본 value를 그대로 반환</b>(passthrough)</li>
* <li>{@code getmapping(group, value, default)} - 매핑이 없으면 <b>default</b>를 반환</li>
* </ul>
* 파라미터:
* <ul>
* <li>{@code group} - PropManager 프러퍼티 그룹명 (문자열)</li>
* <li>{@code value} - 매핑 대상 값. 숫자여도 되며 조회 시 문자열 key로 변환된다
* (정수형 숫자는 {@code 1000.0}이 아닌 {@code "1000"}으로 조회)</li>
* <li>{@code default} - (선택) 매핑이 없을 때 반환할 기본값</li>
* </ul>
*
* <p>예: 그룹 "COMMON_CODE"에 {@code 1000=AAAAA}가 있을 때
* {@code getmapping("COMMON_CODE", "1000")} → {@code "AAAAA"}
*/
public class GetMapping extends PostfixMathCommand {
public GetMapping() {
numberOfParameters = -1;
}
/**
* 스택에서 (group, value) 또는 (group, value, default)를 꺼내 매핑값을 조회한다.
*
* @param inStack JEP 후위 연산 스택
* @throws ParseException 파라미터 개수가 2 또는 3이 아니거나 group이 문자열이 아닌 경우
*/
@SuppressWarnings("unchecked")
public void run(Stack inStack) throws ParseException {
checkStack(inStack);
int n = curNumberOfParameters;
if (n != 2 && n != 3) {
throw new ParseException(
"getmapping() requires 2 or 3 parameters: getmapping(group, value [, default])");
}
boolean hasDefault = (n == 3);
Object defaultValue = hasDefault ? inStack.pop() : null;
Object value = inStack.pop();
Object groupObj = inStack.pop();
if (!(groupObj instanceof String)) {
throw new ParseException("getmapping(): first parameter (group name) must be a string");
}
String group = (String) groupObj;
String key = toKey(value);
String mapped = (key == null) ? null : lookup(group, key);
if (mapped != null) {
inStack.push(mapped);
} else if (hasDefault) {
inStack.push(defaultValue);
} else {
inStack.push(value); // 매핑 없음 → 원본값 그대로 반환
}
}
/**
* PropManager 프러퍼티 그룹에서 key에 해당하는 값을 조회한다.
* 매핑이 없거나 그룹이 존재하지 않으면 null을 반환한다.
*
* <p>단위 테스트에서는 이 메서드를 오버라이드하여 Spring 컨텍스트 없이 매핑을 주입할 수 있다.
*/
protected String lookup(String group, String key) {
return PropManager.getInstance().getProperty(group, key);
}
/**
* 매핑 조회에 사용할 문자열 key로 변환한다.
* 정수형 숫자(JEP는 숫자를 Double로 처리)는 {@code "1000.0"}이 아닌 {@code "1000"}으로 변환한다.
*/
private String toKey(Object value) {
if (value == null) {
return null;
}
if (value instanceof String) {
return (String) value;
}
if (value instanceof Number) {
double d = ((Number) value).doubleValue();
if (!Double.isInfinite(d) && !Double.isNaN(d) && d == Math.rint(d)) {
return Long.toString((long) d);
}
}
return value.toString();
}
}
@@ -222,6 +222,7 @@ public class Converter {
}
String[] data = normalized.split("[.]");
if (pointLength > 0) {
// length 는 소수점(.) 문자를 포함한 전체 길이 규약. 정수부 허용 자릿수 = length - pointLength - 1
if (data.length == 1) {
if (data[0].length() > length - pointLength - 1) return false;
} else if (data.length == 2) {
@@ -4,6 +4,7 @@ import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import com.eactive.eai.common.util.MaskingUtils;
import com.eactive.eai.transformer.message.InvalidAccessException;
import com.eactive.eai.transformer.message.Message;
import com.eactive.eai.transformer.message.MessageException;
@@ -934,6 +935,9 @@ abstract public class Item implements Serializable, Cloneable {
else
str = val.toString();
if(maskLength == 0 && maskOffset > 0)
return MaskingUtils.mask(str, maskOffset);
int len = str.length();
if( len <= maskOffset ) return str;
@@ -1,7 +1,6 @@
package com.eactive.eai.transformer.message;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Stack;
@@ -10,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.transformer.SystemKeys;
import com.eactive.eai.transformer.layout.Converter;
import com.eactive.eai.transformer.layout.Group;
import com.eactive.eai.transformer.layout.InvalidNodeException;
import com.eactive.eai.transformer.layout.Item;
import com.eactive.eai.transformer.layout.Types;
@@ -18,6 +18,7 @@ import com.eactive.eai.transformer.util.Logger;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -37,22 +38,24 @@ public class JSONMessage extends Message {
protected JsonNode data;
protected ObjectMapper mapper = null;
protected JsonFactory factory = null;
protected boolean bStaled = false;
protected boolean bStaled = true;
public JSONMessage() {
super();
mapper = new ObjectMapper();
// if necessary, add proper options -> use system property config ?
mapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
mapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
mapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true));
factory = mapper.getFactory();
factory.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
}
@Override
public void createObject() {
data = mapper.createObjectNode();
// data = mapper.createObjectNode();
data = null;
}
@Override
@@ -98,59 +101,28 @@ public class JSONMessage extends Message {
throw new InvalidDataTypeException("Not able to generate JSON messages with data. data type = " + data.getClass().getName(), data);
}
}
if (this.data == null) {
logger.error("Parsed data is null");
return;
}
//ObjectNode node = (ObjectNode) this.data.get(getName());
/*
if(node == null) {
ObjectNode node = (ObjectNode) this.data.get(getName());
if(node == null) {
// if layoutname not appended
setValue(this.data, getName());
setValue(this.data, getName());
}
else {
setValue(node, getName());
setValue(node, getName());
}
*/
ObjectNode node = null;
// 변경된 부분: NullNode 검사 추가
if (this.data.has(getName()) && !this.data.get(getName()).isNull()) {
JsonNode nameNode = this.data.get(getName());
if (nameNode.isObject()) {
node = (ObjectNode) nameNode;
// 배열 데이터 처리 개선
processArrayNodes(this.data);
// 설정
setValue(node, getName());
} else {
logger.warn("Node with name '" + getName() + "' is not an object, skipping setValue operation");
}
} else {
// layoutname이 추가되지 않은 경우
setValue(this.data, getName());
}
} catch (Exception e) {
throw new MessageSetDataException(e.getMessage(), this, data, e);
}
}
public void setValue(JsonNode node, String itempPath) throws InvalidAccessException {
public void setValue(JsonNode node, String itempPath) throws InvalidAccessException {
try {
// this.value = Converter.convert(value, getDataTypeId());
/**
* occRef 설정을 위해
*/
HashMap validationTargets = new HashMap();
HashMap validationTargets = new HashMap();
doSetValue(node, validationTargets, itempPath);
// validation
@@ -172,22 +144,33 @@ public class JSONMessage extends Message {
} catch(Exception e) {
throw new InvalidAccessException("setValue error", getItemPath(), null, e);
}
}
}
protected void doSetValue(JsonNode node, HashMap validationTargets, String parentPath) {
protected void doSetValue(JsonNode node, HashMap validationTargets, String parentPath) {
Item item = null;
if (node.isNull()) {
// null이면 타겟 형식에서 Grid인지 확인
String itemPath = parentPath + "[-1]";
item = this.getItem(itemPath);
if (item != null && item instanceof Group && StringUtils.equalsAny(item.getOccType(), "*", "+")) {
item = getOrCreateItem(denomalizeXPath(parentPath));
if (item != null) {
item.setIndex(-1);
}
}
return;
}
if( node.isObject()) {
Iterator<String> fieldNames = node.fieldNames();
Iterator<String> fieldNames = node.fieldNames();
if(StringUtils.isNotEmpty(parentPath)) {
item = getOrCreateItem(denomalizeXPath(parentPath));
item = getOrCreateItem(denomalizeXPath(parentPath));
if(SystemKeys.isValidationEnabled() && StringUtils.isNotEmpty(item.getOccRef())) {
validationTargets.put(item.getParent().getItemPath() + Types.FIELD_SEPARATOR + item.getOccRef(), item);
}
}
while(fieldNames.hasNext()) {
String fieldName = fieldNames.next();
JsonNode fieldValue = node.get(fieldName);
try {
String path = null;
@@ -206,58 +189,42 @@ public class JSONMessage extends Message {
}
else if( node.isArray() ) {
ArrayNode arrayNode = (ArrayNode) node;
// 배열이 문자열 값만 포함하는지 확인
boolean isSimpleArray = true;
for (int i = 0; i < arrayNode.size(); i++) {
if (!arrayNode.get(i).isValueNode()) {
isSimpleArray = false;
break;
if(arrayNode.size() == 0 && StringUtils.isNotEmpty(parentPath)) {
try {
// 아직 clone 되지 않았으면 생성 안함.
item = this.getItem(denomalizeXPath(parentPath));
if (item != null) {
// clone 후에는 -1로
item = getOrCreateItem(denomalizeXPath(parentPath));
if (item != null) {
item.setIndex(-1);
}
}
} catch (InvalidNodeException ie) {
// continue silently
}
}
for(int i = 0; i < arrayNode.size(); i++) {
JsonNode arrayItem = arrayNode.get(i);
if(StringUtils.isNotEmpty(parentPath)) {
try {
item = getOrCreateItem(denomalizeXPath(parentPath + "[" + i + "]"));
} catch(InvalidNodeException ie) {
continue;
}
}
}
if (isSimpleArray) {
// 문자열 배열은 직접 설정
try {
setString(parentPath, arrayNode.toString());
} catch (Exception e) {
logger.error("Error setting simple array: " + parentPath, e);
// 개별 요소 설정
for (int i = 0; i < arrayNode.size(); i++) {
try {
String itemPath = parentPath + "[" + i + "]";
setString(itemPath, arrayNode.get(i).asText());
} catch (Exception ex) {
logger.error("Error setting array item: " + parentPath + "[" + i + "]", ex);
}
}
}
} else {
// 복잡한 배열 구조 처리
for (int i = 0; i < arrayNode.size(); i++) {
JsonNode arrayItem = arrayNode.get(i);
if (StringUtils.isNotEmpty(parentPath)) {
try {
item = getOrCreateItem(denomalizeXPath(parentPath + "[" + i + "]"));
} catch (InvalidNodeException ie) {
continue;
}
}
doSetValue(arrayItem, validationTargets, parentPath + "[" + i + "]");
}
}
doSetValue(arrayItem, validationTargets, parentPath + "[" + i + "]");
}
}
else {
else {
String itemPath = parentPath;
item = getOrCreateItem(denomalizeXPath(itemPath));
String value = node.asText();
if(node == null || node.isNull()) {
setObject(itemPath, null); // 실제 null 설정
} else if(value == null || value.length() == 0) {
if (node.isNull()) return;
if(value == null || value.length() == 0) {
setObject( itemPath, value );
} else {
}
else {
int itemType = item.getDataTypeId();
try {
switch(itemType) {
@@ -283,12 +250,12 @@ public class JSONMessage extends Message {
case Types.TYPE_FLOAT :
setFloat( itemPath, new Float(value) );
break;
case Types.TYPE_BOOLEAN :
case Types.TYPE_BOOLEAN :
setObject( itemPath, new Boolean(value) );
break;
case Types.TYPE_STRING :
setString(itemPath, value);
break;
break;
default :
setString(itemPath, value);
break;
@@ -370,23 +337,24 @@ public class JSONMessage extends Message {
if( parent instanceof ObjectNode ) {
obj = (ObjectNode)parent;
String val = getStringValue(item.getValue(),item);
int itemType = item.getDataTypeId();
// 출력 시점 자릿수 검증 (변환으로 생성된 값은 setData 입력검증을 거치지 않음)
validateDecimal(item.getValue(), item);
// String인 경우에만 Masking 가능 - 숫자는 불가능
int itemType = item.getDataTypeId();
if (isMask && "Y".equals(item.getMask()) && (Types.TYPE_STRING == itemType )){
val = maskString2(val, item.getMaskLength(), item.getMaskOffset());
String val = maskString2(getStringValue(item.getValue(),item), item.getMaskLength(), item.getMaskOffset());
item.setValue(val);
}
// Object value = toJsonObjectType(item.getDataTypeId(), val);
String value = val;
if( item.getOccMax() > 1 || item.getOccMax() == -1 ) { // FIELD ARRAY FORMAT "name":["value1", "value2"]
arr = (ArrayNode)obj.get(item.getName());
if (arr == null){
arr = mapper.createArrayNode();
setJsonArrayValue(arr, item);
obj.set(prefix+item.getName(), arr);
}else{
arr.add(value);
}else{
// arr.add(value);
setJsonArrayValue(arr, item); //IBK에서 사용.
}
} else { //FIELD
@@ -418,7 +386,7 @@ public class JSONMessage extends Message {
logger.error("not support parent type JSONArray");
throw new Exception("not support parent type JSONArray");
}
if (item.getChildCount() > 0){
if (item.getChildCount() > 0 ){
stack.push(current);
}
break;
@@ -443,38 +411,11 @@ public class JSONMessage extends Message {
private void setJsonNodeValue(ObjectNode node, String fieldName, Item item) {
int itemType = item.getDataTypeId();
Object value = item.getValue();
if(value == null) {
node.putNull(fieldName);
return;
}
if(value instanceof JsonNode) {
node.set(fieldName, (JsonNode) value);
return;
}
// New code to check if string is JSON array
if (itemType == Types.TYPE_STRING && value instanceof String) {
String strValue = (String) value;
if ((strValue.startsWith("[") && strValue.endsWith("]")) ||
(strValue.startsWith("{") && strValue.endsWith("}"))) {
try {
// Try to parse as JSON
JsonNode jsonNode = mapper.readTree(strValue);
node.set(fieldName, jsonNode);
return;
} catch (Exception e) {
// Not valid JSON, continue with normal processing
logger.debug("Failed to parse as JSON, using as string: " + e.getMessage());
}
}
}
if (!(value instanceof byte[])) {
logger.info("value : " + value.toString());
}
switch(itemType) {
case Types.TYPE_INT :
case Types.TYPE_SHORT :
@@ -484,8 +425,9 @@ public class JSONMessage extends Message {
node.put(fieldName, (long) value);
break;
case Types.TYPE_BIGDECIMAL :
BigDecimal bd = (BigDecimal)value;
node.put(fieldName, bd);
// BigDecimal bd = (BigDecimal)value;
// node.put(fieldName, bd.floatValue());
node.put(fieldName, (BigDecimal)value);
break;
case Types.TYPE_DOUBLE :
node.put(fieldName, (double) value);
@@ -497,12 +439,7 @@ public class JSONMessage extends Message {
node.put(fieldName, (boolean) value);
break;
case Types.TYPE_STRING :
node.put(fieldName, (String) value);
if (value instanceof JsonNode) {
logger.info("value : " + (String) value );
}
break;
default :
node.put(fieldName, (String) value);
@@ -524,13 +461,13 @@ public class JSONMessage extends Message {
arrayNode.add( (long) value);
break;
case Types.TYPE_BIGDECIMAL :
arrayNode.add( (float) value);
arrayNode.add( (BigDecimal)value);
break;
case Types.TYPE_DOUBLE :
arrayNode.add( (double) value);
break;
case Types.TYPE_FLOAT :
arrayNode.add( (float) value);
arrayNode.add( (float) value);
break;
case Types.TYPE_BOOLEAN :
arrayNode.add( (boolean) value);
@@ -539,52 +476,32 @@ public class JSONMessage extends Message {
arrayNode.add( (String) value);
break;
default :
arrayNode.add( (String) value);
arrayNode.add( (String) value);
}
}
/**
* item 값을 문자열로 변환한다. 마스킹 대상(TYPE_STRING) 필드에서만 사용된다.
*/
protected String getStringValue(Object value, Item item) throws Exception{
if (value == null) return null;
String val="";
if (value instanceof String){
String data = (String)value;
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
}else{
val = data;
}
val = (String)value;
} else if (value instanceof byte[]){
String data = new String((byte[])value);
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
}else{
val = data;
}
val = new String((byte[])value);
} else if ( value instanceof BigDecimal) {
String data = ((BigDecimal) value).toPlainString();
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
}else{
val = data;
}
val = ((BigDecimal) value).toPlainString();
} else if ( value instanceof Integer) {
String data = String.valueOf(value);
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
}else{
val = data;
}
val = String.valueOf(value);
} else if ( value instanceof Boolean) {
String data = String.valueOf(value);
val = data;
val = String.valueOf(value);
}
return val;
}
@Override
public Object getData() {
if (bStaled) {
if (bStaled || this.data == null) {
try {
this.data = getJSONData();
} catch(Exception e) {
@@ -604,7 +521,6 @@ public class JSONMessage extends Message {
return null;
}
if(this.data == null) return null;
if(isPretty) return data.toPrettyString();
else return data.toString();
}
@@ -656,53 +572,47 @@ public class JSONMessage extends Message {
}
}
private String getFormat(String value,int length,int pointLength) throws Exception {
boolean validation = SystemKeys.isValidationEnabled();
return getFormat(value, length, pointLength, validation) ;
}
private String getFormat(String value, int length, int pointLength, boolean isValidation) throws Exception{
if(isValidation) {
if (!Converter.isValid(value,length,pointLength)){
throw new Exception(String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d"
, value,length,pointLength)
);
}
boolean isMinus=false;
String pattern = "";
//음수/양수 check
if (value.indexOf("-")>=0){
isMinus = true;
}
String data = value.replaceAll(" ", "").replaceAll("-", "");
if (pointLength> 0 ){//소수부가 있을 경우
for(int i=isMinus?1:0;i<length-pointLength-1;i++){
pattern = pattern + "#";
}
pattern =pattern+".";
for(int i=0;i<pointLength;i++){
pattern = pattern + "0";
}
}else{//소수부가 없을경우
for(int i=isMinus?1:0;i<length-pointLength;i++){
pattern = pattern + "#";
}
}
if (isMinus) {
pattern="-"+pattern;
}
DecimalFormat format = new DecimalFormat(pattern);
// double num = Double.parseDouble(data);
BigDecimal num = new BigDecimal(data);
return format.format(num);
/**
* BIGDECIMAL 필드의 출력값 검증.
*
* 변환으로 생성된 target 메시지의 값은 setData() 입력검증을 거치지 않으므로
* (Message.setBigDecimal() 검증 없음) 출력 시점에 레이아웃 자릿수를 확인한다.
*
* 이전에는 DecimalFormat 으로 자릿수를 맞춘 문자열을 반환하는 getFormat() 이었으나,
* 출력이 setJsonNodeValue() 에서 item.getValue() 직접 쓰는 방식으로 바뀌면서
* 반환값이 사용되지 않아 포맷 로직을 제거하고 검증만 남김.
*/
private void validateDecimal(Object value, Item item) throws Exception {
if (value == null || item.getDataTypeId() != Types.TYPE_BIGDECIMAL) {
return;
}
else {
BigDecimal num = new BigDecimal(value);
return num.toPlainString();
String data;
if (value instanceof String) {
data = (String) value;
} else if (value instanceof byte[]) {
data = new String((byte[]) value);
} else if (value instanceof BigDecimal) {
data = ((BigDecimal) value).toPlainString();
} else if (value instanceof Integer) {
data = String.valueOf(value);
} else {
// 타입은 검증 대상 아님
return;
}
int length = item.getLength();
int pointLength = item.getDecimalPointLength();
if (SystemKeys.isValidationEnabled() && !Converter.isValid(data, length, pointLength)) {
throw new InvalidAccessException(
String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d", data, length,
pointLength),
item.getItemPath());
}
// 숫자형 여부 확인 - 파싱 불가 NumberFormatException
new BigDecimal(data.replaceAll(" ", ""));
}
// private Object toJsonObjectType(int itemType, String value) {
// Object jsonValueObject = null;
//
@@ -735,71 +645,12 @@ public class JSONMessage extends Message {
private String getHeader() {
return getName() + " (" + MESSAGE_NAME + ")";
}
// 배열 처리 메소드
private void processArrayNodes(JsonNode rootNode) {
Iterator<String> fieldNames = rootNode.fieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
JsonNode node = rootNode.get(fieldName);
// data_part와 같은 객체 노드 처리
if (node.isObject()) {
processArrayNodes(node);
}
// 배열 노드 처리
else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
// 배열 노드 처리
if (arrayNode.size() > 0) {
// 배열 데이터 설정
try {
if(this.data instanceof ObjectNode) {
ObjectNode objNode = (ObjectNode) this.data;
objNode.set(fieldName, arrayNode);
} else if (rootNode instanceof ObjectNode) {
((ObjectNode) rootNode).set(fieldName, arrayNode);
}
for(int i=0; i < arrayNode.size(); i++) {
JsonNode itemNode = arrayNode.get(i);
String itemPath = fieldName + "[" + i + "]";
if(itemNode.isObject()) {
processArrayNodes(itemNode);
}
}
} catch (Exception e) {
logger.error("Error setting array data for field: " + fieldName, e);
// 배열 요소를 개별적으로 처리
for (int i = 0; i < arrayNode.size(); i++) {
try {
String itemPath = fieldName + "[" + i + "]";
JsonNode itemNode = arrayNode.get(i);
if (itemNode.isTextual()) {
setString(itemPath, itemNode.asText());
} else {
setString(itemPath, itemNode.toString());
}
} catch (Exception ex) {
logger.error("Error setting array item [" + i + "] for field: " + fieldName, ex);
}
}
}
}
}
}
}
public String toString(){
// if staled -> reorganize data
String jsonString = null;
try {
jsonString = (String)getData();
jsonString = toLogString();
} catch (Exception e) {
logger.error("[JSONMessage] " + e.getMessage(), e);
}
@@ -44,7 +44,7 @@ public class TransformValidator {
try {
if (!transform.getTransformItemList().iterator().hasNext()) {
// throw new TransformerException("source layout does not exist");
throw new TransformerException("전문레이아웃 매핑에 원천 레이아웃이 없습니다.");
throw new TransformerException("전문레이아웃 매핑에 변환 항목(TSEAITR03)이 없습니다.");
}
if (transform.getTargetLayout() == null) {
@@ -202,7 +202,7 @@ public class TransformValidator {
try {
if (!transform.getTransformItemList().iterator().hasNext()) {
// throw new TransformerException("source layout does not exist");
throw new TransformerException("전문레이아웃 매핑에 원천 레이아웃이 없습니다.");
throw new TransformerException("전문레이아웃 매핑에 변환 항목(TSEAITR03)이 없습니다.");
}
if (transform.getTargetLayout() == null) {
@@ -0,0 +1,158 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.nfunk.jep.JEP;
import com.eactive.eai.transformer.function.generic.GetMapping;
@DisplayName("GetMapping 프러퍼티 그룹 매핑 함수 테스트")
class GetMappingFunctionTest {
private JEP jep;
/**
* PropManager(Spring 컨텍스트) 없이 테스트하기 위해 lookup() 로컬 맵으로 대체한 스텁.
* groups: 그룹명 (key value) 매핑 테이블
*/
static class StubGetMapping extends GetMapping {
private final Map<String, Map<String, String>> groups;
StubGetMapping(Map<String, Map<String, String>> groups) {
this.groups = groups;
}
@Override
protected String lookup(String group, String key) {
Map<String, String> g = groups.get(group);
if (g == null) {
return null;
}
return g.get(key);
}
}
@BeforeEach
void setUp() {
// 프러퍼티 그룹 "COMMON_CODE": 1000->AAAAA, 1001->BBBBB
Map<String, String> commonCode = new HashMap<>();
commonCode.put("1000", "AAAAA");
commonCode.put("1001", "BBBBB");
// 한글 그룹명/값도 지원하는지 확인용
Map<String, String> bankCode = new HashMap<>();
bankCode.put("081", "하나은행");
bankCode.put("088", "신한은행");
Map<String, Map<String, String>> groups = new HashMap<>();
groups.put("COMMON_CODE", commonCode);
groups.put("BANK_CODE", bankCode);
jep = new JEP();
jep.addStandardFunctions();
jep.addStandardConstants();
jep.setAllowUndeclared(true);
jep.addFunction("getmapping", new StubGetMapping(groups));
}
private Object eval(String expr) {
jep.parseExpression(expr);
assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo());
Object result = jep.getValueAsObject();
assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo());
return result;
}
@Test
@DisplayName("2-인자 매핑 성공 — 1000 → AAAAA")
void testMappingFound() {
assertEquals("AAAAA", eval("getmapping(\"COMMON_CODE\", \"1000\")"));
assertEquals("BBBBB", eval("getmapping(\"COMMON_CODE\", \"1001\")"));
}
@Test
@DisplayName("2-인자 매핑 없음 — 원본값 그대로 반환(passthrough)")
void testMappingNotFoundPassthrough() {
assertEquals("9999", eval("getmapping(\"COMMON_CODE\", \"9999\")"));
}
@Test
@DisplayName("2-인자 그룹 자체가 없음 — 원본값 그대로 반환")
void testGroupNotFoundPassthrough() {
assertEquals("1000", eval("getmapping(\"NO_SUCH_GROUP\", \"1000\")"));
}
@Test
@DisplayName("3-인자 매핑 성공 — default 무시하고 매핑값 반환")
void testMappingFoundWithDefault() {
assertEquals("AAAAA", eval("getmapping(\"COMMON_CODE\", \"1000\", \"UNKNOWN\")"));
}
@Test
@DisplayName("3-인자 매핑 없음 — default 반환")
void testMappingNotFoundWithDefault() {
assertEquals("UNKNOWN", eval("getmapping(\"COMMON_CODE\", \"9999\", \"UNKNOWN\")"));
}
@Test
@DisplayName("3-인자 그룹 자체가 없음 — default 반환")
void testGroupNotFoundWithDefault() {
assertEquals("UNKNOWN", eval("getmapping(\"NO_SUCH_GROUP\", \"1000\", \"UNKNOWN\")"));
}
@Test
@DisplayName("숫자 리터럴 값 — 1000.0이 아닌 \"1000\"으로 조회")
void testNumericValueLookup() {
// JEP는 1000 리터럴을 Double로 처리하므로 toKey에서 "1000"으로 변환되어야 매핑됨
assertEquals("AAAAA", eval("getmapping(\"COMMON_CODE\", 1000)"));
}
@Test
@DisplayName("숫자 리터럴 값 매핑 없음 — 원본 숫자값 그대로 반환")
void testNumericValueNotFoundPassthrough() {
Object result = eval("getmapping(\"COMMON_CODE\", 9999)");
assertEquals(9999.0, ((Number) result).doubleValue());
}
@Test
@DisplayName("변수 바인딩 값 매핑 — 필드값 변환")
void testVariableValueMapping() {
jep.addVariable("code", "1001");
assertEquals("BBBBB", eval("getmapping(\"COMMON_CODE\", code)"));
}
@Test
@DisplayName("한글 그룹/값 매핑 — 081 → 하나은행")
void testKoreanGroupAndValue() {
assertEquals("하나은행", eval("getmapping(\"BANK_CODE\", \"081\")"));
assertEquals("신한은행", eval("getmapping(\"BANK_CODE\", \"088\")"));
}
@Test
@DisplayName("첫 번째 파라미터(group)가 문자열이 아니면 오류")
void testNonStringGroupCausesError() {
jep.parseExpression("getmapping(1000, \"1000\")");
if (!jep.hasError()) {
jep.getValueAsObject();
assertTrue(jep.hasError(), "숫자 group 파라미터는 오류가 발생해야 합니다");
}
}
@Test
@DisplayName("파라미터 개수가 1개면 오류")
void testTooFewParamsCausesError() {
jep.parseExpression("getmapping(\"COMMON_CODE\")");
if (!jep.hasError()) {
jep.getValueAsObject();
assertTrue(jep.hasError(), "파라미터 1개는 오류가 발생해야 합니다");
}
}
}
@@ -146,17 +146,17 @@ class GetTrinomialFunctionTest {
//
@Test
@DisplayName("실전 패턴 — true/false 결과값 (JEP 상수 1.0/0.0 반환 검증)")
@DisplayName("실전 패턴 — true/false 결과값 (Boolean 타입 반환 검증)")
void testBooleanLiteralReturnValue() {
// JEP addStandardConstants() true=1.0, false=0.0 으로 등록하는지 확인
// 등록되어 있다면 변수 바인딩 없이 숫자로 반환됨
// 등록 됐다면 파싱 오류 또는 undeclared 변수(NaN) 처리됨
// JEP addStandardConstants() 에는 true/false 없다.
// Parser 이를 Boolean 으로 직접 등록하므로(Parser.addBooleanConstants),
// 결과값은 숫자 1.0/0.0 아니라 Boolean 이다. setUp 동일하게 바인딩한다.
jep.addVariable("intVal", 10.0);
Object result = eval("getTrinomial(intVal, 10, \"=\", true, false)");
assertTrue(result instanceof Number,
"true/false 결과값이 Number 타입이어야 함 (실제: " + result.getClass().getSimpleName() + ")");
assertEquals(1.0, ((Number) result).doubleValue(), 0.0,
"조건 참일 때 true(=1.0) 반환 기대");
assertTrue(result instanceof Boolean,
"true/false 결과값이 Boolean 타입이어야 함 (실제: " + result.getClass().getSimpleName() + ")");
assertEquals(Boolean.TRUE, result,
"조건 참일 때 true 반환 기대");
}
@Test
@@ -197,9 +197,9 @@ class GetTrinomialFunctionTest {
assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo());
}
assertTrue(result instanceof Number,
"결과값이 Number 타입이어야 함 (실제: " + (result == null ? "null" : result.getClass().getSimpleName()) + ")");
assertEquals(1.0, ((Number) result).doubleValue(), 0.0,
"INT_VALUE=10, 비교값=10, 연산자=등호 → 참(true=1.0) 반환 기대");
assertTrue(result instanceof Boolean,
"결과값이 Boolean 타입이어야 함 (실제: " + (result == null ? "null" : result.getClass().getSimpleName()) + ")");
assertEquals(Boolean.TRUE, result,
"INT_VALUE=10, 비교값=10, 연산자=등호 → 참(true) 반환 기대");
}
}
@@ -27,6 +27,7 @@ import com.eactive.eai.transformer.message.MessageSetDataException;
* - INT_VALUE : INT, length=5, decimal=0
* - LONG_VALUE : LONG, length=10, decimal=0
* - BIG_VALUE : BIGDECIMAL, length=10, decimal=5 정수부 최대 4자리
* (length 소수점(.) 문자를 포함한 전체 길이 규약)
* - BIG15_VALUE : BIGDECIMAL, length=15, decimal=0 정수부 최대 15자리
* - STR_VALUE : STRING, length=20
*
@@ -24,7 +24,7 @@ class LayoutRepositoryTest {
@Test
void testFindById() {
LayoutEntity layoutEntity = layoutLoaderRepository.getReferenceById("TST_TTST00000002_RES");
LayoutEntity layoutEntity = layoutLoaderRepository.getReferenceById("TST_TOSSTEST1_TGT");
layoutEntity = layoutEntity.getLayoutItemEntities().stream().findAny().get().getLayoutEntity();
assertNotNull(layoutEntity);
System.out.println(layoutEntity);
@@ -26,7 +26,8 @@ import com.eactive.eai.transformer.message.MessageFactory;
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
@Sql({ "/com/eactive/eai/transformer/init_tseaitr06.sql", "/com/eactive/eai/transformer/init_tseaitr07.sql",
"/com/eactive/eai/transformer/init_tseaitr08.sql", "/com/eactive/eai/transformer/init_tseaitr01.sql",
"/com/eactive/eai/transformer/init_tseaitr02.sql", "/com/eactive/eai/transformer/init_tseaitr03.sql" })
"/com/eactive/eai/transformer/init_tseaitr02.sql", "/com/eactive/eai/transformer/init_tseaitr03.sql",
"/com/eactive/eai/transformer/init_tseaitr01_noitem.sql" })
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TransformManagerTest {
@@ -240,4 +241,18 @@ class TransformManagerTest {
}
/**
* TSEAITR03(변환 항목 매핑) 건도 없더라도 TSEAITR01/02 등록된 변환은 로딩되어야 한다.
* loadAll() inner join fetch 이면 변환은 transformMap 에서 누락되고,
* 호출 "전문레이아웃매핑(변환)정보를 찾을 수 없음" 오류가 발생한다.
*/
@Test
void testTransformWithoutTransformItemIsLoaded() {
Transform transform = transformManager.getTransform("TRANSFORM_TST_NOITEM_REQ");
Assert.assertNotNull("TSEAITR03 매핑이 없는 변환도 로딩되어야 함", transform);
Assert.assertEquals(0, transform.getTransformItemList().size());
Assert.assertEquals("TST_TOSSTEST1_TGT", transform.getTargetLayout().getName());
}
}
@@ -0,0 +1,9 @@
-- 변환 마스터(TSEAITR01) / 레이아웃 지정(TSEAITR02)은 있으나
-- 변환 항목 매핑(TSEAITR03)은 한 건도 없는 변환.
-- loadAll() 이 inner join fetch 를 쓰면 이 변환은 로딩에서 누락된다.
INSERT INTO tseaitr01 (cnvsnname,cnvsndesc,lastamndhms,eaisevrdstcd,modfimgtstusdstcd,useyn,verinfo,author) VALUES
('TRANSFORM_TST_NOITEM_REQ', 'TSEAITR03 매핑 항목 없음', '20241226104652 ','E',' ','1','0',NULL);
INSERT INTO tseaitr02 (cnvsnname,loutname,sourcrsultdstcd) VALUES
('TRANSFORM_TST_NOITEM_REQ', 'TST_TOSSTEST1_TGT', 'TGT_LAYOUT'),
('TRANSFORM_TST_NOITEM_REQ', 'TST_TOSSTEST1_SRC', 'SRC_LAYOUT');