Compare commits
13 Commits
20260713_개발배포
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8701f5d459 | |||
| eaf1416e13 | |||
| c0c1544350 | |||
| d20ac6290b | |||
| 0c048a88fe | |||
| b1e7883d41 | |||
| 09ac7f4ca5 | |||
| 90ccf59b25 | |||
| 051305ac55 | |||
| 375f0e0f15 | |||
| 027ec4fcea | |||
| 229156d51d | |||
| f9e6522b8f |
+28
-4
@@ -51,13 +51,28 @@ dependencies {
|
|||||||
|
|
||||||
api 'commons-collections:commons-collections:3.2.2'
|
api 'commons-collections:commons-collections:3.2.2'
|
||||||
|
|
||||||
api 'org.apache.poi:poi-excelant:3.9'
|
// poi-excelant -> poi-ooxml -> dom4j:dom4j:1.6.1 (구 groupId) 전이 차단.
|
||||||
|
// 1.6.1 은 XXE(CVE-2020-10683) / XML 인젝션(CVE-2018-1000632) 대상이고,
|
||||||
|
// 소스의 org.dom4j 사용처(HttpClient*AdapterServiceRest 등의 SAXReader)는
|
||||||
|
// hibernate-envers 가 가져오는 org.dom4j:dom4j:2.1.3 과 패키지가 같아 그대로 동작한다.
|
||||||
|
// 두 jar 가 공존하면 WEB-INF/lib 로딩 순서에 따라 1.6.1 이 선택될 수 있으므로 제거한다.
|
||||||
|
api ('org.apache.poi:poi-excelant:3.9') {
|
||||||
|
exclude group: 'dom4j', module: 'dom4j'
|
||||||
|
}
|
||||||
|
|
||||||
api group: 'org.apache.commons', name: 'commons-pool2', version: '2.11.1'
|
api group: 'org.apache.commons', name: 'commons-pool2', version: '2.11.1'
|
||||||
api 'commons-codec:commons-codec:1.15'
|
api 'commons-codec:commons-codec:1.15'
|
||||||
|
|
||||||
//api group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
// jackson-dataformat-xml(2.13.1) 을 jackson-databind 로 치환 (2026-08-27)
|
||||||
compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
//
|
||||||
|
// XmlMapper/JacksonXml* 사용처는 전 소스에 0건이지만, 이 아티팩트가 transformer 의
|
||||||
|
// 유일한 jackson 공급원이었다(전이로 jackson-core/databind 2.13.1 을 끌고 옴).
|
||||||
|
// 그런데 compileOnly 라 WAR 에는 안 들어가고, 런타임은 Ignite 가 끌고 오는 2.12.7 이다.
|
||||||
|
// 이 컴파일(2.13.1)/런타임(2.12.7) skew 때문에 2.13 전용 API 가 컴파일을 통과했고,
|
||||||
|
// 실제로 JacksonUtil.setValue() 가 ArrayNode.set(int, String)(2.13 신규)을 써서
|
||||||
|
// 배열 경로 setText() 시 NoSuchMethodError 가 나는 상태였다.
|
||||||
|
// 런타임에 실제 존재하는 버전을 명시해 컴파일/런타임을 일치시킨다.
|
||||||
|
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.7'
|
||||||
|
|
||||||
|
|
||||||
compileOnly group: 'xalan', name: 'xalan', version: '2.6.0'
|
compileOnly group: 'xalan', name: 'xalan', version: '2.6.0'
|
||||||
@@ -78,13 +93,22 @@ dependencies {
|
|||||||
//testImplementation files("libs/json-simple-1.1.1-custom-1.2.jar")
|
//testImplementation files("libs/json-simple-1.1.1-custom-1.2.jar")
|
||||||
//testImplementation files("libs/jep-4.j18-md-1.0.jar")
|
//testImplementation files("libs/jep-4.j18-md-1.0.jar")
|
||||||
testImplementation fileTree(dir: 'libs', include: ['*.jar'])
|
testImplementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
|
||||||
testImplementation 'junit:junit:4.4'
|
testImplementation 'junit:junit:4.4'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
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 {
|
publishing {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package com.eactive.eai.transformer.engine;
|
package com.eactive.eai.transformer.engine;
|
||||||
|
|
||||||
import java.io.Serializable;
|
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.JEP;
|
||||||
import org.nfunk.jep.function.PostfixMathCommand;
|
import org.nfunk.jep.function.PostfixMathCommand;
|
||||||
@@ -26,6 +31,14 @@ public class Parser implements Serializable {
|
|||||||
|
|
||||||
private static Logger logger = Logger.getLogger("transformer");
|
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
|
private transient JEP jep = new JEP(); // Create a new parser
|
||||||
|
|
||||||
public Parser() {
|
public Parser() {
|
||||||
@@ -102,9 +115,9 @@ public class Parser implements Serializable {
|
|||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public String[] getSymbols() {
|
public String[] getSymbols() {
|
||||||
String[] symbols = new String[getJep().getSymbolTable().size()];
|
Set<String> symbols = new LinkedHashSet<>(getJep().getSymbolTable().keySet());
|
||||||
symbols = (String[])getJep().getSymbolTable().keySet().toArray(new String[symbols.length]);
|
symbols.removeAll(RESERVED_SYMBOLS);
|
||||||
return symbols;
|
return symbols.toArray(new String[symbols.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addGenericFunctions() throws Exception {
|
private void addGenericFunctions() throws Exception {
|
||||||
|
|||||||
@@ -685,7 +685,7 @@ public class Transformer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(InvalidAccessException e) {
|
} catch(InvalidAccessException e) {
|
||||||
e.printStackTrace();
|
//e.printStackTrace();
|
||||||
errors.add("Transformer] ITEM Error during conversion (source message access) - continue- " + e.getMessage());
|
errors.add("Transformer] ITEM Error during conversion (source message access) - continue- " + e.getMessage());
|
||||||
value ="";
|
value ="";
|
||||||
findSource = false;
|
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("[.]");
|
String[] data = normalized.split("[.]");
|
||||||
if (pointLength > 0) {
|
if (pointLength > 0) {
|
||||||
|
// length 는 소수점(.) 문자를 포함한 전체 길이 규약. 정수부 허용 자릿수 = length - pointLength - 1
|
||||||
if (data.length == 1) {
|
if (data.length == 1) {
|
||||||
if (data[0].length() > length - pointLength - 1) return false;
|
if (data[0].length() > length - pointLength - 1) return false;
|
||||||
} else if (data.length == 2) {
|
} else if (data.length == 2) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.io.Serializable;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.util.MaskingUtils;
|
||||||
import com.eactive.eai.transformer.message.InvalidAccessException;
|
import com.eactive.eai.transformer.message.InvalidAccessException;
|
||||||
import com.eactive.eai.transformer.message.Message;
|
import com.eactive.eai.transformer.message.Message;
|
||||||
import com.eactive.eai.transformer.message.MessageException;
|
import com.eactive.eai.transformer.message.MessageException;
|
||||||
@@ -934,6 +935,9 @@ abstract public class Item implements Serializable, Cloneable {
|
|||||||
else
|
else
|
||||||
str = val.toString();
|
str = val.toString();
|
||||||
|
|
||||||
|
if(maskLength == 0 && maskOffset > 0)
|
||||||
|
return MaskingUtils.mask(str, maskOffset);
|
||||||
|
|
||||||
int len = str.length();
|
int len = str.length();
|
||||||
if( len <= maskOffset ) return str;
|
if( len <= maskOffset ) return str;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.eactive.eai.transformer.message;
|
package com.eactive.eai.transformer.message;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
@@ -51,6 +50,12 @@ public class JSONMessage extends Message {
|
|||||||
|
|
||||||
factory = mapper.getFactory();
|
factory = mapper.getFactory();
|
||||||
factory.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
|
factory.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
|
||||||
|
// 상대가 0 패딩된 코드값을 따옴표 없이(=JSON 숫자로) 보내면 표준 파서는
|
||||||
|
// "Invalid numeric value: Leading zeroes not allowed" 로 거부한다.
|
||||||
|
// 전문 자체를 못 읽는 것보다 받아들이는 쪽이 낫다고 판단해 허용한다.
|
||||||
|
// ⚠ 이 옵션은 00001 을 숫자 1 로 만든다(선행 0 미보존).
|
||||||
|
// 표준전문 경로(common JacksonUtil.newNumberSafeMapper)와 동일한 설정이다.
|
||||||
|
factory.configure(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS.mappedFeature(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -338,14 +343,15 @@ public class JSONMessage extends Message {
|
|||||||
if( parent instanceof ObjectNode ) {
|
if( parent instanceof ObjectNode ) {
|
||||||
obj = (ObjectNode)parent;
|
obj = (ObjectNode)parent;
|
||||||
|
|
||||||
String val = getStringValue(item.getValue(),item);
|
// 출력 시점 자릿수 검증 (변환으로 생성된 값은 setData 입력검증을 거치지 않음)
|
||||||
|
validateDecimal(item.getValue(), item);
|
||||||
|
|
||||||
|
// String인 경우에만 Masking 이 가능 - 숫자는 불가능
|
||||||
int itemType = item.getDataTypeId();
|
int itemType = item.getDataTypeId();
|
||||||
if (isMask && "Y".equals(item.getMask()) && (Types.TYPE_STRING == itemType )){
|
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);
|
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"]
|
if( item.getOccMax() > 1 || item.getOccMax() == -1 ) { // FIELD ARRAY FORMAT "name":["value1", "value2"]
|
||||||
arr = (ArrayNode)obj.get(item.getName());
|
arr = (ArrayNode)obj.get(item.getName());
|
||||||
if (arr == null){
|
if (arr == null){
|
||||||
@@ -480,41 +486,22 @@ public class JSONMessage extends Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* item 값을 문자열로 변환한다. 마스킹 대상(TYPE_STRING) 필드에서만 사용된다.
|
||||||
|
*/
|
||||||
protected String getStringValue(Object value, Item item) throws Exception{
|
protected String getStringValue(Object value, Item item) throws Exception{
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
String val="";
|
String val="";
|
||||||
if (value instanceof String){
|
if (value instanceof String){
|
||||||
String data = (String)value;
|
val = (String)value;
|
||||||
|
|
||||||
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
|
|
||||||
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
|
|
||||||
}else{
|
|
||||||
val = data;
|
|
||||||
}
|
|
||||||
} else if (value instanceof byte[]){
|
} else if (value instanceof byte[]){
|
||||||
String data = new String((byte[])value);
|
val = new String((byte[])value);
|
||||||
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
|
|
||||||
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
|
|
||||||
}else{
|
|
||||||
val = data;
|
|
||||||
}
|
|
||||||
} else if ( value instanceof BigDecimal) {
|
} else if ( value instanceof BigDecimal) {
|
||||||
String data = ((BigDecimal) value).toPlainString();
|
val = ((BigDecimal) value).toPlainString();
|
||||||
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
|
|
||||||
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
|
|
||||||
}else{
|
|
||||||
val = data;
|
|
||||||
}
|
|
||||||
} else if ( value instanceof Integer) {
|
} else if ( value instanceof Integer) {
|
||||||
String data = String.valueOf(value);
|
val = String.valueOf(value);
|
||||||
if (item.getDataTypeId() == Types.TYPE_BIGDECIMAL){
|
|
||||||
val = getFormat(data,item.getLength(),item.getDecimalPointLength());
|
|
||||||
}else{
|
|
||||||
val = data;
|
|
||||||
}
|
|
||||||
} else if ( value instanceof Boolean) {
|
} else if ( value instanceof Boolean) {
|
||||||
String data = String.valueOf(value);
|
val = String.valueOf(value);
|
||||||
val = data;
|
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
@@ -591,83 +578,47 @@ public class JSONMessage extends Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFormat(String value,int length,int pointLength) throws Exception {
|
/**
|
||||||
boolean validation = SystemKeys.isValidationEnabled();
|
* BIGDECIMAL 필드의 출력값 검증.
|
||||||
return getFormat(value, length, pointLength, validation) ;
|
*
|
||||||
}
|
* 변환으로 생성된 target 메시지의 값은 setData() 입력검증을 거치지 않으므로
|
||||||
|
* (Message.setBigDecimal() 에 검증 없음) 출력 시점에 레이아웃 자릿수를 확인한다.
|
||||||
private String getFormat(String value, int length, int pointLength, boolean isValidation) throws Exception{
|
*
|
||||||
if(isValidation) {
|
* 이전에는 DecimalFormat 으로 자릿수를 맞춘 문자열을 반환하는 getFormat() 이었으나,
|
||||||
if (!Converter.isValid(value,length,pointLength)){
|
* 출력이 setJsonNodeValue() 에서 item.getValue() 를 직접 쓰는 방식으로 바뀌면서
|
||||||
throw new Exception(String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d"
|
* 반환값이 사용되지 않아 포맷 로직을 제거하고 검증만 남김.
|
||||||
, value,length,pointLength)
|
*/
|
||||||
);
|
private void validateDecimal(Object value, Item item) throws Exception {
|
||||||
}
|
if (value == null || item.getDataTypeId() != Types.TYPE_BIGDECIMAL) {
|
||||||
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
BigDecimal num = new BigDecimal(value);
|
String data;
|
||||||
return num.toPlainString();
|
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();
|
||||||
private boolean isValid(String value,int length,int pointLength){
|
int pointLength = item.getDecimalPointLength();
|
||||||
String[] data = value.replaceAll(" ", "").split("[.]");
|
if (SystemKeys.isValidationEnabled() && !Converter.isValid(data, length, pointLength)) {
|
||||||
if (pointLength > 0 ){
|
throw new InvalidAccessException(
|
||||||
if (data.length ==1){
|
String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d", data, length,
|
||||||
if (data[0].length() > length - pointLength - 1 ){ //정수부 오류
|
pointLength),
|
||||||
return false;
|
item.getItemPath());
|
||||||
}
|
|
||||||
}else if (data.length ==2){
|
|
||||||
if (data[0].length() > length - pointLength - 1 ){ //정수부 오류
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data[1].length() > pointLength ){ //실수부 오류
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
//오류
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}else{
|
|
||||||
if (data.length != 1){ //소수점이 들어와서 오류
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data[0].length() > length - pointLength - 1){ //정수부 오류
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
// 숫자형 여부 확인 - 파싱 불가 시 NumberFormatException
|
||||||
|
new BigDecimal(data.replaceAll(" ", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// private Object toJsonObjectType(int itemType, String value) {
|
// private Object toJsonObjectType(int itemType, String value) {
|
||||||
// Object jsonValueObject = null;
|
// Object jsonValueObject = null;
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class TransformValidator {
|
|||||||
try {
|
try {
|
||||||
if (!transform.getTransformItemList().iterator().hasNext()) {
|
if (!transform.getTransformItemList().iterator().hasNext()) {
|
||||||
// throw new TransformerException("source layout does not exist");
|
// throw new TransformerException("source layout does not exist");
|
||||||
throw new TransformerException("전문레이아웃 매핑에 원천 레이아웃이 없습니다.");
|
throw new TransformerException("전문레이아웃 매핑에 변환 항목(TSEAITR03)이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transform.getTargetLayout() == null) {
|
if (transform.getTargetLayout() == null) {
|
||||||
@@ -202,7 +202,7 @@ public class TransformValidator {
|
|||||||
try {
|
try {
|
||||||
if (!transform.getTransformItemList().iterator().hasNext()) {
|
if (!transform.getTransformItemList().iterator().hasNext()) {
|
||||||
// throw new TransformerException("source layout does not exist");
|
// throw new TransformerException("source layout does not exist");
|
||||||
throw new TransformerException("전문레이아웃 매핑에 원천 레이아웃이 없습니다.");
|
throw new TransformerException("전문레이아웃 매핑에 변환 항목(TSEAITR03)이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transform.getTargetLayout() == null) {
|
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
|
@Test
|
||||||
@DisplayName("실전 패턴 — true/false 결과값 (JEP 상수 1.0/0.0 반환 검증)")
|
@DisplayName("실전 패턴 — true/false 결과값 (Boolean 타입 반환 검증)")
|
||||||
void testBooleanLiteralReturnValue() {
|
void testBooleanLiteralReturnValue() {
|
||||||
// JEP addStandardConstants()가 true=1.0, false=0.0 으로 등록하는지 확인
|
// JEP addStandardConstants() 에는 true/false 가 없다.
|
||||||
// → 등록되어 있다면 변수 바인딩 없이 숫자로 반환됨
|
// Parser 가 이를 Boolean 으로 직접 등록하므로(Parser.addBooleanConstants),
|
||||||
// → 등록 안 됐다면 파싱 오류 또는 undeclared 변수(NaN)로 처리됨
|
// 결과값은 숫자 1.0/0.0 이 아니라 Boolean 이다. setUp 도 동일하게 바인딩한다.
|
||||||
jep.addVariable("intVal", 10.0);
|
jep.addVariable("intVal", 10.0);
|
||||||
Object result = eval("getTrinomial(intVal, 10, \"=\", true, false)");
|
Object result = eval("getTrinomial(intVal, 10, \"=\", true, false)");
|
||||||
assertTrue(result instanceof Number,
|
assertTrue(result instanceof Boolean,
|
||||||
"true/false 결과값이 Number 타입이어야 함 (실제: " + result.getClass().getSimpleName() + ")");
|
"true/false 결과값이 Boolean 타입이어야 함 (실제: " + result.getClass().getSimpleName() + ")");
|
||||||
assertEquals(1.0, ((Number) result).doubleValue(), 0.0,
|
assertEquals(Boolean.TRUE, result,
|
||||||
"조건 참일 때 true(=1.0) 반환 기대");
|
"조건 참일 때 true 반환 기대");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -197,9 +197,9 @@ class GetTrinomialFunctionTest {
|
|||||||
assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo());
|
assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
assertTrue(result instanceof Number,
|
assertTrue(result instanceof Boolean,
|
||||||
"결과값이 Number 타입이어야 함 (실제: " + (result == null ? "null" : result.getClass().getSimpleName()) + ")");
|
"결과값이 Boolean 타입이어야 함 (실제: " + (result == null ? "null" : result.getClass().getSimpleName()) + ")");
|
||||||
assertEquals(1.0, ((Number) result).doubleValue(), 0.0,
|
assertEquals(Boolean.TRUE, result,
|
||||||
"INT_VALUE=10, 비교값=10, 연산자=등호 → 참(true=1.0) 반환 기대");
|
"INT_VALUE=10, 비교값=10, 연산자=등호 → 참(true) 반환 기대");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.eactive.eai.transformer.message.MessageSetDataException;
|
|||||||
* - INT_VALUE : INT, length=5, decimal=0
|
* - INT_VALUE : INT, length=5, decimal=0
|
||||||
* - LONG_VALUE : LONG, length=10, decimal=0
|
* - LONG_VALUE : LONG, length=10, decimal=0
|
||||||
* - BIG_VALUE : BIGDECIMAL, length=10, decimal=5 → 정수부 최대 4자리
|
* - BIG_VALUE : BIGDECIMAL, length=10, decimal=5 → 정수부 최대 4자리
|
||||||
|
* (length 는 소수점(.) 문자를 포함한 전체 길이 규약)
|
||||||
* - BIG15_VALUE : BIGDECIMAL, length=15, decimal=0 → 정수부 최대 15자리
|
* - BIG15_VALUE : BIGDECIMAL, length=15, decimal=0 → 정수부 최대 15자리
|
||||||
* - STR_VALUE : STRING, length=20
|
* - STR_VALUE : STRING, length=20
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class LayoutRepositoryTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testFindById() {
|
void testFindById() {
|
||||||
LayoutEntity layoutEntity = layoutLoaderRepository.getReferenceById("TST_TTST00000002_RES");
|
LayoutEntity layoutEntity = layoutLoaderRepository.getReferenceById("TST_TOSSTEST1_TGT");
|
||||||
layoutEntity = layoutEntity.getLayoutItemEntities().stream().findAny().get().getLayoutEntity();
|
layoutEntity = layoutEntity.getLayoutItemEntities().stream().findAny().get().getLayoutEntity();
|
||||||
assertNotNull(layoutEntity);
|
assertNotNull(layoutEntity);
|
||||||
System.out.println(layoutEntity);
|
System.out.println(layoutEntity);
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import com.eactive.eai.transformer.message.MessageFactory;
|
|||||||
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
|
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
|
||||||
@Sql({ "/com/eactive/eai/transformer/init_tseaitr06.sql", "/com/eactive/eai/transformer/init_tseaitr07.sql",
|
@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_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)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
class TransformManagerTest {
|
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');
|
||||||
Reference in New Issue
Block a user