commit e0aabf80fd9086e1fa9b176d08beab80fdd7e9a4 Author: mjkhan21 Date: Tue Oct 31 15:35:30 2023 +0900 최초 커밋 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..106d481 --- /dev/null +++ b/pom.xml @@ -0,0 +1,158 @@ + + 4.0.0 + + cokr.xit.interfaces + xit-lntris + 23.04.01-SNAPSHOT + jar + + xit-lntris + http://maven.apache.org + + + UTF-8 + + 17 + ${java.version} + ${java.version} + + + + + mvn2s + https://repo1.maven.org/maven2/ + + true + + + true + + + + egovframe + https://maven.egovframe.go.kr/maven/ + + true + + + false + + + + maven-public + https://nas.xit.co.kr:8888/repository/maven-public/ + + + + + + + cokr.xit.interfaces + xit-filejob + 23.04.01-SNAPSHOT + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + org.springdoc + springdoc-openapi-javadoc + 1.7.0 + + + + + + install + ${basedir}/target + ${artifactId}-${version} + + + ${basedir}/src/main/resources + + + ${basedir}/src/test/resources + ${basedir}/src/main/resources + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + **/*.class + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + true + xml + + **/Abstract*.java + **/*Suite.java + + + **/*Test.java + + + + + org.codehaus.mojo + emma-maven-plugin + true + + + org.apache.maven.plugins + maven-source-plugin + 2.2 + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + + + + + + + maven-snapshot + https://nas.xit.co.kr:8888/repository/maven-snapshots/ + + + + maven-release + https://nas.xit.co.kr:8888/repository/maven-releases/ + + + + + diff --git a/src/main/java/cokr/xit/interfaces/lntris/DataFileSupport.java b/src/main/java/cokr/xit/interfaces/lntris/DataFileSupport.java new file mode 100644 index 0000000..75551a1 --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/DataFileSupport.java @@ -0,0 +1,160 @@ +package cokr.xit.interfaces.lntris; + +import java.io.File; +import java.io.FileWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import cokr.xit.foundation.Assert; + +public class DataFileSupport> { + private static final String FIELD = "||"; + private static final String CR = "^||"; + + private Charset charset; + private Supplier supplier; + private ArrayList messages; + private DecimalFormat seqFormat = new DecimalFormat("000"); + + public DataFileSupport setCharset(Charset charset) { + this.charset = charset; + return this; + } + + public DataFileSupport setSupplier(Supplier supplier) { + this.supplier = supplier; + return this; + } + + public List getMessages() { + return Assert.ifEmpty(messages, Collections::emptyList); + } + + public T getMessage() { + List msgs = getMessages(); + return !msgs.isEmpty() ? msgs.get(0) : null; + } + + public DataFileSupport read(Path path) { + InterfaceHeader header = new InterfaceHeader(); + messages = new ArrayList<>(); + try { + Files.lines(path, charset).forEach(line -> { + String[] fields = line + .replace("\uFEFF", "") // UTF-8(BOM) -> UTF-8 + .replace(CR, "") + .split("\\|\\|", -1); + + if (header.empty()) + read(header, fields); + else { + T info = read(fields); + info.setHeader(header); + messages.add(info); + } + }); + return this; + } catch (Exception e) { + throw Assert.runtimeException(e); + } + } + + private void read(InterfaceHeader header, String[] fields) { + header.setIfId(supplier.get().interfaceID()); + + List> setters = header.setters(); + if (setters.size() > fields.length) + throw new RuntimeException("header setters.size() > fields.length"); + + for (int i = 0; i < setters.size(); ++i) { + Consumer setter = setters.get(i); + String field = fields[i]; + if (field != null) + field = field.trim(); + setter.accept(field); + } + } + + private T read(String[] fields) { + T t = supplier.get(); + List> setters = t.setters(); + if (setters.size() != fields.length) + throw new RuntimeException("info setters.size() > fields.length"); + + for (int i = 0; i < setters.size(); ++i) { + Consumer setter = setters.get(i); + String field = fields[i]; + if (field != null) + field = field.trim(); + setter.accept(field); + } + return t; + } + + public List write(String dir, List intfList) { + if (intfList == null || intfList.isEmpty()) + return Collections.emptyList(); + + ArrayList paths = new ArrayList<>(); + Map> bySource = intfList.stream().collect( + Collectors.groupingBy(info -> info.getSourceMessage().getHeader().getSource()) + ); + + File d = new File(dir); + if (!d.exists()) + d.mkdirs(); + + for (List intfs: bySource.values()) { + InterfaceInfo info = intfs.get(0); + InterfaceHeader header = info.getSourceMessage().getHeader().setIfFormat("D"); + String filename = String.format( + "%s_%s_%s@%s", //인터페이스 아이디(30)_파일생성일시(14)_순번(3)@수신 자치단체 코드(7)수신 시스템 코드(3) + header.getIfId(), header.getIfDate(), seqFormat.format(paths.size() + 1), header.getSource() //header.getTarget() + ); + Path path = Paths.get(dir, filename); + + + try (FileWriter writer = new FileWriter(path.toFile());) { + write(writer, header); + for (T t: intfs) { + write(writer, t); + } + writer.flush(); + paths.add(path); + } catch (Exception e) { + throw Assert.runtimeException(e); + } + } + return paths; + } + + private void write(FileWriter writer, InterfaceHeader header) throws Exception { + String line = List.of( + header.getIfDate(), + header.getIfMsgKey(), + header.getIfId(), + header.getSource(), + header.getTarget(), + header.getIfType(), + header.getIfFormat() + ).stream().collect(Collectors.joining(FIELD)); + writer.append(line + CR + "\n"); + } + + private void write(FileWriter writer, T t) throws Exception { + String line = t.getters().stream() + .map(getter -> Assert.ifEmpty(getter.get(), "")) + .collect(Collectors.joining(FIELD)); + writer.append(line + CR + "\n"); + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/InterfaceConfig.java b/src/main/java/cokr/xit/interfaces/lntris/InterfaceConfig.java new file mode 100644 index 0000000..582fb75 --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/InterfaceConfig.java @@ -0,0 +1,179 @@ +package cokr.xit.interfaces.lntris; + +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.core.io.ClassPathResource; + +import cokr.xit.foundation.AbstractComponent; +import cokr.xit.foundation.data.JSON; +import lombok.Getter; +import lombok.Setter; + +/**차세대 세외수입 연계 설정을 로드하고 제공한다. + * 설정은 클래스패스 상의 intf-conf/lntris.conf 파일에 지정하며 + * 지정하는 내용은 다음과 같다. + *
 {
+ *   "local": [
+ *     {"organization": "지역 자치단체 코드(7자리)", "systems": ["지역 시스템 코드(3자리)"}, ...],
+ *     ...
+ *   ],
+ *   "remote": {
+ *     "organization": "지방세외수입 기관 코드(7자리)",
+ *     "systems": ["지방세외수입 시스템 코드(3자리)"],
+ *     "url": "지방세외수입 시스템 url"
+ *   }
+ * }
+ * @author mjkhan + */ +public class InterfaceConfig extends AbstractComponent { + private static final InterfaceConfig conf; + + static { + try { + conf = new JSON().parse(new ClassPathResource("intf-conf/lntris.conf").getInputStream(), InterfaceConfig.class); + } catch (Exception e) { + throw runtimeException(e); + } + } + + private boolean useDatabase = true; + + /**데이터베이스 사용여부를 설정한다. + * @param useDatabase 데이터베이스 사용여부 + */ + public void setUseDatabase(String useDatabase) { + this.useDatabase = !"false".equals(useDatabase); + } + + /** 지자체 시스템 설정 */ + private List locals; + /** 지방세외수입 시스템 설정 */ + private EndPoint remote; + + /**지자체 설정을 반환한다. + * @return 지자체 설정 + */ + public List getLocals(String... orgs) { + return locals; + } + + /**지자체 설정을 설정한다. + * @param locals 지자체 설정 + */ + public void setLocals(List locals) { + this.locals = locals; + } + + /**지방세외수입 시스템 설정을 반환한다. + * @return 지방세외수입 시스템 설정 + */ + public EndPoint getRemote() { + return remote; + } + + /**지방세외수입 시스템 설정을 설정한다. + * @param remote 지방세외수입 시스템 설정 + */ + public void setRemote(EndPoint remote) { + this.remote = remote; + } + + public static boolean useDatabase() { + return conf.useDatabase; + } + + public static void databaseActive(Runnable task) { + if (task == null) return; + if (conf.useDatabase) + task.run(); + } + + public static T databaseActive(Supplier on, Supplier off) { + if (conf.useDatabase) { + return on != null ? on.get() : null; + } else { + return off != null ? off.get() : null; + } + } + + /**지정하는 지자체 설정 목록을 반환한다. + * @param orgs 단체/기관 코드 + * @return 지자체 설정 목록 + */ + public static List locals(String... orgs) { + if (isEmpty(orgs)) + return conf.locals; + + List keys = List.of(orgs); + return conf.locals.stream() + .filter(local -> keys.contains(local.organization)) + .toList(); + } + + /**지정하는 지자체 설정을 반환한다. + * @param org 단체/기관 코드 + * @return 지자체 설정 + */ + public static EndPoint local(String org) { + List locals = locals(org); + if (locals.isEmpty()) + throw new IllegalArgumentException("local not found for " + org); + return locals.get(0); + } + + public static EndPoint remote() { + return conf.remote; + } + + public static String organizationSystem(String org, String sys) { + return org + sys; + } + + /**단체/기관의 접속정보 + *
  • organization - 단체/기관 코드(7자리)
  • + *
  • systems - 시스템 코드(3자리) 목록
  • + *
  • url - 시스템 url(선택)
  • + *
+ * @author mjkhan + */ + @Getter + @Setter + public static class EndPoint { + private String organization; + private List systems; + private String url; + + /**단체/기관의 첫번째 시스템 코드를 반환한다. + * @return 단체/기관의 첫번째 시스템 코드 + */ + public String getSystem() { + return !isEmpty(systems) ? systems.get(0) : ""; + } + + /**단체/기관코드 + 시스템 코드 목록을 반환한다. + * 시스템 코드를 지정하지 않으면 설정된 모든 시스템에 대해 목록을 반환한다. + * @param sysCodes 시스템 코드 + * @return 단체/기관코드 + 시스템 코드 목록 + */ + public List getOrganizationSystems(String... sysCodes) { + List keys = isEmpty(sysCodes) ? systems : List.of(sysCodes); + return keys.stream().map(key -> organizationSystem(organization, key)).toList(); + } + + public String getOrganizationSystem() { + List orgSys = getOrganizationSystems(); + return !orgSys.isEmpty() ? orgSys.get(0) : ""; + } + + /**단체/기관의 시스템 url이 로컬 시스템에 대한 것인지 반환한다. + * @return 단체/기관의 시스템 url의 로컬 시스템 여부 + *
  • 단체/기관의 시스템 url이 로컬 시스템에 대한 것이면 true
  • + *
  • 그렇지 않으면 false
  • + *
+ */ + public boolean isLocal() { + return ifEmpty(url, "").contains("localhost"); + } + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/InterfaceHeader.java b/src/main/java/cokr/xit/interfaces/lntris/InterfaceHeader.java new file mode 100644 index 0000000..e332e7f --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/InterfaceHeader.java @@ -0,0 +1,259 @@ +package cokr.xit.interfaces.lntris; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import cokr.xit.foundation.Assert; + +/**연계 표준 헤더 + * @author mjkhan + */ +public class InterfaceHeader { + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS"); + + /** 전송일자 */ + private String ifDate; + /** 연계 메시지키 */ + private String ifMsgKey; + /** 인터페이스 ID */ + private String ifId; + /** 출발지 시스템 코드 */ + private String source; + /** 도착지 시스템 코드 */ + private String target; + /** 데이터 구분 */ + private String ifType; + /** 데이터 형식 */ + private String ifFormat; + /** 연계처리 결과명 */ + private String retName; + /** 연계처리 결과코드 */ + private String retCode; + + /**헤더가 비어있는지 반환한다. + * @return + *
  • 헤더가 비어있으면 true
  • + *
  • 그렇지 않으면 false
  • + *
+ */ + public boolean empty() { + return Assert.isEmpty(ifDate) && Assert.isEmpty(ifMsgKey) && Assert.isEmpty(ifId); + } + + /**전송일자(yyyyMMddHHmmss)를 반환한다. + * @return 전송일자(yyyyMMddHHmmss) + */ + public String getIfDate() { + return Assert.ifEmpty(ifDate, () -> ifDate = dateFormat.format(new Date()).substring(0, 14)); + } + /**전송일자(yyyyMMddHHmmss)를 설정한다. + * @param ifDate 전송일자(yyyyMMddHHmmss) + */ + public void setIfDate(String ifDate) { + this.ifDate = ifDate; + } + + /**연계 메시지키를 반환한다. + * @return 연계 메시지키 + */ + public String getIfMsgKey() { + if (Assert.isEmpty(ifMsgKey)) + setMsgKey("Z"); + return ifMsgKey; + } + /**연계 메시지키를 설정한다. + * @param ifMsgKey 연계 메시지키 + * @return 현재 InterfaceHeader + */ + public InterfaceHeader setIfMsgKey(String ifMsgKey) { + this.ifMsgKey = ifMsgKey; + return this; + } + /**지정한 시스템 코드로 연계 메시지키를 생성한다. + * @param systemCode 시스템 코드 + *
  • T - 세무행정
  • + *
  • W - 대국민
  • + *
  • N - 세외수입
  • + *
  • A - 행정지원
  • + *
  • L - 연계
  • + *
  • E - 유관기관
  • + *
  • Z - 개별시스템
  • + *
+ * @return 현재 InterfaceHeader + */ + public InterfaceHeader setMsgKey(String systemCode) { + String now = dateFormat.format(new Date()).substring(2); + String guid = UUID.randomUUID().toString().replace("-", "").toUpperCase(); + String msgKey = String.format("%s%s-%s", systemCode, now, guid); + return setIfMsgKey(msgKey); + } + + /**인터페이스 ID를 반환한다. + * @return 인터페이스 ID + */ + public String getIfId() { + return ifId; + } + /**인터페이스 ID를 설정한다. + * @param ifId 인터페이스 ID + * @return 현재 InterfaceHeader + */ + public InterfaceHeader setIfId(String ifId) { + this.ifId = ifId; + return this; + } + + /**출발지 시스템 코드를 반환한다. + * 디폴트는 lntris.conf의 local 설정 + * @return 출발지 시스템 코드 + */ + public String getSource() { + return source; + } + + /**출발지 시스템 코드를 설정한다. + * @param source 출발지 시스템 코드 + * @return 현재 InterfaceHeader + */ + public InterfaceHeader setSource(String source) { + this.source = source; + return this; + } + + /**도착지 시스템 코드를 반환한다. + * 디폴트는 lntris.conf의 lntris 설정 + * @return 도착지 시스템 코드 + */ + public String getTarget() { + return Assert.ifEmpty(target, () -> target = InterfaceConfig.remote().getOrganizationSystem()); + } + /**도착지 시스템 코드를 설정한다. + * @param target 도착지 시스템 코드 + * @return 현재 InterfaceHeader + */ + public InterfaceHeader setTarget(String target) { + this.target = target; + return this; + } + + /**데이터 구분을 반환한다. + * @return 데이터 구분 + *
  • S - 송신(디폴트)
  • + *
  • R - 수신
  • + *
+ */ + public String getIfType() { + return Assert.ifEmpty(ifType, () -> ifType = "S"); + } + /**데이터 구분을 설정한다. + * @param ifType 데이터 구분 + *
  • S - 요청, 송신(디폴트)
  • + *
  • R - 응답, 수신
  • + *
+ * @return 현재 InterfaceHeader + */ + public InterfaceHeader setIfType(String ifType) { + this.ifType = ifType; + return this; + } + + /**데이터 형식을 반환한다. + * @return 데이터 형식 + *
  • D - 구분자
  • + *
  • F - 고정길이
  • + *
  • X - XML
  • + *
  • J - JSON(디폴트)
  • + *
+ */ + public String getIfFormat() { + return Assert.ifEmpty(ifFormat, () -> ifFormat = "J"); + } + /**데이터 형식을 설정한다. + * @param ifFormat 데이터 형식 + *
  • D - 구분자
  • + *
  • F - 고정길이
  • + *
  • X - XML
  • + *
  • J - JSON(디폴트)
  • + *
+ * @return 현재 InterfaceHeader + */ + public InterfaceHeader setIfFormat(String ifFormat) { + this.ifFormat = ifFormat; + return this; + } + + /**연계처리 결과명을 반환한다. + * @return 연계처리 결과명 + */ + public String getRetName() { + return retName; + } + /**연계처리 결과명을 설정한다. + * @param retName 연계처리 결과명 + * @return 현재 InterfaceHeader + */ + public InterfaceHeader setRetName(String retName) { + this.retName = retName; + return this; + } + + /**연계처리 결과코드를 반환한다. + * @return 연계처리 결과코드 + *
  • 200 - 성공(디폴트)
  • + *
  • 500 - 실패
  • + *
+ */ + public String getRetCode() { + if (Assert.isEmpty(retCode)) { + if ("R".equals(getIfType())) + retCode = "200"; + } + return retCode; + } + /**연계처리 결과코드를 설정한다. + * @param retCode 연계처리 결과코드 + *
  • 200 - 성공(디폴트)
  • + *
  • 500 - 실패
  • + *
+ * @return 현재 InterfaceHeader + */ + public InterfaceHeader setRetCode(String retCode) { + this.retCode = retCode; + return this; + } + + /**헤더 필드값의 setter 메소드 목록을 반환한다. + * @return 헤더 필드값의 setter 메소드 목록 + */ + public List> setters() { + return List.of( + this::setIfDate, + this::setIfMsgKey, + this::setIfId, + this::setSource, + this::setTarget, + this::setIfType, + this::setIfFormat + ); + } + + /**현재 InterfaceHeader를 복사하여 반환한다. + * @return 현재 InterfaceHeader의 복사본 + */ + public InterfaceHeader copy() { + InterfaceHeader copy = new InterfaceHeader(); + copy.ifDate = ifDate; + copy.ifMsgKey = ifMsgKey; + copy.ifId = ifId; + copy.source = source; + copy.target = target; + copy.ifType = ifType; + copy.ifFormat = ifFormat; + copy.retName = retName; + copy.retCode = retCode; + return copy; + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/InterfaceInfo.java b/src/main/java/cokr/xit/interfaces/lntris/InterfaceInfo.java new file mode 100644 index 0000000..46baa7b --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/InterfaceInfo.java @@ -0,0 +1,181 @@ +package cokr.xit.interfaces.lntris; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.type.TypeReference; + +import cokr.xit.foundation.AbstractEntity; +import cokr.xit.foundation.Assert; +import cokr.xit.foundation.data.JSON; + +/**세외수입 연계정보.
+ * 연계 표준헤더, 출발지 메시지, 도착지 메시지로 구성되며 데이터베이스의 해당 연계 테이블에 저장된다. + * @param 출발지 메시지의 요청정보 유형 + * @param 도착지 메시지의 응답정보 유형 + * @author mjkhan + */ +public abstract class InterfaceInfo extends AbstractEntity { + /** 업무(시스템) 코드 */ + private String taskSeCd; + /** 연계 표준 헤더 */ + private InterfaceHeader header; + /** 출발지가 보낸 메시지 */ + protected SourceMessage sourceMessage; + /** 도착지가 보낸 메시지 */ + protected TargetMessage targetMessage; + private JSON json; + + /**업무(시스템) 코드를 반환한다. + * @return 업무(시스템) 코드 + */ + public String getTaskSeCd() { + return taskSeCd; + } + /**업무(시스템) 코드를 설정한다. + * @param taskSeCd 업무(시스템) 코드 + */ + public void setTaskSeCd(String taskSeCd) { + this.taskSeCd = taskSeCd; + } + + /**인터페이스 아이디를 반환한다. + * @return 인터페이스 아이디 + */ + public abstract String interfaceID(); + + /**연계 표준 헤더를 반환한다. + * @return 연계 표준 헤더 + */ + public InterfaceHeader getHeader() { + return header; + } + /**연계 표준 헤더를 설정한다. + * @param header 연계 표준 헤더 + */ + public InterfaceInfo setHeader(InterfaceHeader header) { + this.header = header; + return this; + } + + /**출발지가 보낸 메시지를 반환한다. + * @return 출발지가 보낸 메시지 + */ + public SourceMessage getSourceMessage() { + if (sourceMessage == null) { + sourceMessage = new SourceMessage(); + sourceMessage.setHeader( + new InterfaceHeader().setIfId(interfaceID()) + ); + sourceMessage.setBody(newRequest()); + } + return sourceMessage; + } + + protected Q newRequest() { + throw new UnsupportedOperationException(String.format("Implement the %s.newRequest() method", getClass().getName())); + } + + /**출발지가 보낸 메시지를 설정한다. + * @param sourceMessage 출발지가 보낸 메시지메시지 + * @return 현재 InterfaceInfo + */ + public InterfaceInfo setSourceMessage(SourceMessage request) { + this.sourceMessage = request; + return this; + } + /**출발지가 보낸 요청정보를 반환한다. + * @return 요청정보 + */ + public T getRequest() { + return (T)getSourceMessage().getBody().getReqVo(); + } + + /**출발지 메시지 헤더의 source를 설정한다. + * @return 현재 InterfaceInfo + */ + public InterfaceInfo setSourceHeaderCodes() { + throw new UnsupportedOperationException(String.format("Implement the %s.setSourceHeaderCodes() method", getClass().getName())); + } + + /**도착지가 보낸 메시지를 반환한다. + * @return 도착지가 보낸 메시지 + */ + public TargetMessage getTargetMessage() { + if (targetMessage == null) { + targetMessage = new TargetMessage<>(); + targetMessage.setHeader( + getSourceMessage() + .getHeader().copy() + .setIfType("R") + ); + targetMessage.setBody(newResponse()); + } + return targetMessage; + } + + protected R newResponse() { + throw new UnsupportedOperationException(String.format("Implement the %s.newResponse() method", getClass().getName())); + } + + /**도착지가 보낸 메시지를 설정한다. + * @param targetMessage 도착지가 보낸 메시지 + * @return 현재 InterfaceInfo + */ + public InterfaceInfo setTargetMessage(TargetMessage response) { + this.targetMessage = response; + return this; + } + /**도착지가 보낸 응답정보를 반환한다. + * @return 요청정보 + */ + public R getResponse() { + return getTargetMessage().getBody(); + } + + @JsonIgnore + public List> getters() { + throw new UnsupportedOperationException(String.format("Implement the %s.getters() method", getClass().getName())); + } + + @JsonIgnore + public List> setters() { + throw new UnsupportedOperationException(String.format("Implement the %s.setters() method", getClass().getName())); + } + + /**json 문자열을 파싱하여 도착지 메시지를 설정한다. + * @param json json 문자열 + */ + public void parseTargetMessage(String json) { + setTargetMessage(json().parse(json, targetMessageType())); + } + + /**도착지 메시지 타입을 반환한다. + * @return 도착지 메시지 타입 + */ + protected abstract TypeReference> targetMessageType(); + + protected TypeReference> defaultTargetMessageType() { + return new TypeReference>() {}; + } + + /**데이터 파싱에 사용할 JSON을 반환한다. + * @return 데이터 파싱에 사용할 JSON + */ + protected JSON json() { + return Assert.ifEmpty(json, () -> json = new JSON()); + } + + /**데이터 파싱에 사용할 JSON을 설정한다. + * @param json 데이터 파싱에 사용할 JSON + * @return 현재 InterfaceInfo + */ + public InterfaceInfo json(JSON json) { + this.json = json; + return this; + } + + public static class Detail implements Assert.Support {} +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/InterfaceInfoReader.java b/src/main/java/cokr/xit/interfaces/lntris/InterfaceInfoReader.java new file mode 100644 index 0000000..feee1aa --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/InterfaceInfoReader.java @@ -0,0 +1,83 @@ +package cokr.xit.interfaces.lntris; + +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import cokr.xit.interfaces.filejob.service.bean.FileJobBean; + +public abstract class InterfaceInfoReader> extends FileJobBean { + protected abstract Supplier interfaceInfoSupplier(); + protected abstract Consumer insertInterfaceInfo(); + + /**수신 연계정보 파일을 읽어들여 저장한다. + * 시스템 코드를 지정하지 않으면 해당 단체의 모든 시스템의 수신전문을 처리한다. + * @param orgs 자치단체 코드 목록 + * @param sysCodes 시스템 코드 목록 + */ + public List read(String[] orgs, String... sysCodes) { + String interfaceID = interfaceInfoSupplier().get().interfaceID(); + List tails = InterfaceConfig.locals(orgs).stream() + .flatMap(org -> org.getOrganizationSystems(sysCodes).stream()) + .map(orgSys -> "@" + orgSys) + .toList(); + + List paths = getReceivedFilePaths(path -> { + String str = path.toString(); + + for (String tail: tails) + if (str.contains(interfaceID) && str.contains(tail)) + return true; + + return false; + }); + if (paths.isEmpty()) return Collections.emptyList(); + + List fileStatus = processReceived(paths); + + Map> successFail = fileStatus.stream() //처리 결과를 성공 / 실패로 분류 + .collect(Collectors.groupingBy(file -> file.isSuccess())); + + List success = successFail.get(true); + move(FileStatus.getPaths(success), successDir()); // 성공 디렉토리로 이동 + move(FileStatus.getPaths(successFail.get(false)), failDir()); // 실패 디렉토리로 이동 + + return isEmpty(success) ? + Collections.emptyList() : + success.stream().flatMap(status -> ((List)status.get("messages")).stream()).toList(); + } + + protected List processReceived(List paths) { + DataFileSupport reader = new DataFileSupport() + .setSupplier(interfaceInfoSupplier()) + .setCharset(Charset.forName("UTF-8")); + return isEmpty(paths) ? + Collections.emptyList() : + paths.stream() + .map(path -> { + FileStatus status = new FileStatus().setPath(path); + try { + reader + .read(path) + .getMessages() + .forEach(info -> { + insertInterfaceInfo().accept(info); + List msgs = (List)status.get("messages"); + if (msgs == null) + status.set("messages", msgs = new ArrayList<>()); + msgs.add(info); + }); + } catch (Exception e) { + status.setCause(e); + } + return status; + }) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/JsonParamSupport.java b/src/main/java/cokr/xit/interfaces/lntris/JsonParamSupport.java new file mode 100644 index 0000000..bc6250c --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/JsonParamSupport.java @@ -0,0 +1,19 @@ +package cokr.xit.interfaces.lntris; + +public class JsonParamSupport { + private String taskSeCd; + + /**업무(시스템) 코드를 반환한다. + * @return 업무(시스템) 코드 + */ + public String getTaskSeCd() { + return taskSeCd; + } + + /**업무(시스템) 코드를 설정한다. + * @param taskSeCd 업무(시스템) 코드 + */ + public void setTaskSeCd(String taskSeCd) { + this.taskSeCd = taskSeCd; + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/Requestor.java b/src/main/java/cokr/xit/interfaces/lntris/Requestor.java new file mode 100644 index 0000000..693fc66 --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/Requestor.java @@ -0,0 +1,104 @@ +package cokr.xit.interfaces.lntris; + +import java.net.Socket; +import java.net.http.HttpResponse; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedTrustManager; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import cokr.xit.foundation.Assert; +import cokr.xit.foundation.Log; +import cokr.xit.foundation.data.JSON; +import cokr.xit.foundation.web.WebClient; + +/**연계 메시지를 출발지에서 도착지로 전송하고, 응답을 수신한다. + * @author mjkhan + */ +public class Requestor { + public static final String NAME = "requestor"; + private static final SSLContext sslContext; + static { + try { + TrustManager[] trustAll = { + new X509ExtendedTrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1, SSLEngine arg2) throws CertificateException {} + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1, Socket arg2) throws CertificateException {} + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1, SSLEngine arg2) throws CertificateException {} + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1, Socket arg2) throws CertificateException {} + } + }; + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAll, new SecureRandom()); + } catch (Exception e) { + throw Assert.runtimeException(e); + } + } + private JSON json = new JSON(); + private WebClient webClient; + + /**세외수입 연계정보의 출발지 메시지를 전송하고 도착지 메시지와 응답을 설정한다. + * @param intfInfo 세외수입 연계정보 + */ + public void request(InterfaceInfo intfInfo) { + try { + init(); + + String src = intfInfo.getSourceMessage().getHeader().getSource(); + HttpResponse hresp = webClient.post(req -> req + .uri(InterfaceConfig.remote().getUrl()) + + .json(json) + .header("Accept-Charset", "UTF-8") + + .header("User-Agent", "Mozila/5.0") + .header("Accept-Language", "en-US,en;q=0.5") + + .header("IF_ID", intfInfo.interfaceID()) + .header("SRC_ORG_CD", src.substring(0, 7)) + .header("SRC_SYS_CD", src.substring(8)) + + .bodyData(intfInfo.getSourceMessage()) + ); + + if (WebClient.Request.SUCCESS != hresp.statusCode()) + throw new RuntimeException(intfInfo.interfaceID() + ": " + hresp.statusCode()); + + Log.get(Requestor.class).debug("targetMessage:\n{}", hresp.body()); + + intfInfo + .json(json) + .parseTargetMessage(hresp.body()); + } catch (Exception e) { + throw Assert.runtimeException(e); + } + } + + private void init() throws Exception { + if (webClient != null) return; + + webClient = new WebClient() + .timeout(3) + .sslContext(sslContext); + + json.getObjectMapper().setSerializationInclusion(Include.NON_NULL); + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/SourceMessage.java b/src/main/java/cokr/xit/interfaces/lntris/SourceMessage.java new file mode 100644 index 0000000..86bff30 --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/SourceMessage.java @@ -0,0 +1,51 @@ +package cokr.xit.interfaces.lntris; + +/**출발지가 보내는 메시지 + * @param 요청정보 유형 + * @author mjkhan + */ +public class SourceMessage { + private InterfaceHeader header; + private Q body; + + /**연계 표준헤더를 반환한다. + * @return 연계 표준헤더 + */ + public InterfaceHeader getHeader() { + return header; + } + /**연계 표준헤더를 설정한다. + * @param header 연계 표준헤더 + * @return 현재 SourceMessage + */ + public SourceMessage setHeader(InterfaceHeader header) { + this.header = header; + return this; + } + + /**메시지의 본문을 반환한다. + * @return 메시지 본문 + */ + public Q getBody() { + return body; + } + + /**메시지의 본문을 설정한다. + * @param body 메시지 본문 + * @return 현재 SourceMessage + */ + public SourceMessage setBody(Q body) { + this.body = body; + return this; + } + + /**요청정보 + * @author mjkhan + */ + public static abstract class Request { + /**reqVo을(를) 반환한다. + * @return reqVo + */ + public abstract Object getReqVo(); + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/interfaces/lntris/TargetMessage.java b/src/main/java/cokr/xit/interfaces/lntris/TargetMessage.java new file mode 100644 index 0000000..ff49db0 --- /dev/null +++ b/src/main/java/cokr/xit/interfaces/lntris/TargetMessage.java @@ -0,0 +1,94 @@ +package cokr.xit.interfaces.lntris; + +/**도착지가 보낸 메시지 + * @param 요청정보 유형 + * @author mjkhan + */ +public class TargetMessage { + private InterfaceHeader header; + private R body; + + /**연계 표준헤더를 반환한다. + * @return 연계 표준헤더 + */ + public InterfaceHeader getHeader() { + return header; + } + /**연계 표준헤더를 설정한다. + * @param header 연계 표준헤더 + */ + public TargetMessage setHeader(InterfaceHeader header) { + this.header = header; + return this; + } + + /**본문을 반환한다. + * @return 본문 + */ + public R getBody() { + return body; + } + /**본문을 설정한다. + * @param body 본문 + */ + public TargetMessage setBody(R body) { + this.body = body; + return this; + } + + /**도착지의 처리가 성공했는지 반환한다. + * @return 도착지 처리의 성공여부 + *
  • 도착지의 처리가 성공했으면 true
  • + *
  • 그렇지 않으면 false
  • + *
+ */ + public boolean success() { + return body != null && body.success(); + } + + /**응답정보(도착지 메시지의 본문) + * @author mjkhan + */ + public static class Response { + /** 연계결과 코드 */ + private String linkRstCd; + /** 연계결과 메시지 */ + private String linkRstMsg; + + /**연계결과 코드를 반환한다. + * @return 연계결과 코드 + */ + public String getLinkRstCd() { + return linkRstCd; + } + /**연계결과 코드를 설정한다. + * @param linkRstCd 연계결과 코드 + */ + public void setLinkRstCd(String linkRstCd) { + this.linkRstCd = linkRstCd; + } + + /**연계결과 메시지를 반환한다. + * @return 연계결과 메시지 + */ + public String getLinkRstMsg() { + return linkRstMsg; + } + /**연계결과 메시지를 설정한다. + * @param linkRstMsg 연계결과 메시지 + */ + public void setLinkRstMsg(String linkRstMsg) { + this.linkRstMsg = linkRstMsg; + } + + /**도착지의 처리가 성공했는지 반환한다. + * @return 도착지 처리의 성공여부 + *
  • 도착지의 처리가 성공했으면 true
  • + *
  • 그렇지 않으면 false
  • + *
+ */ + public boolean success() { + return "000".equals(getLinkRstCd()); + } + } +} \ No newline at end of file diff --git a/src/main/resources/intf-conf/lntris.conf b/src/main/resources/intf-conf/lntris.conf new file mode 100644 index 0000000..b24c8ce --- /dev/null +++ b/src/main/resources/intf-conf/lntris.conf @@ -0,0 +1,15 @@ +{ + "locals": [ + {"organization": "4060000",/* 지역 자치단체 코드(7자리) */ + /* 지역 시스템 코드(3자리) */ + "systems": ["DPV"] + } + ], + + "remote": { + "organization": "1741000", /* 지방세외수입 기관 코드(7자리) */ + "systems": ["NIS"], /* 지방세외수입 시스템 코드(3자리) */ + /*"url": "https://10.60.75.57:22411/mediate/ltis" /* 연계 운영 url */ + "url": "https://10.60.75.138:22411/mediate/ltis" /* 연계 검증 url */ + } +} \ No newline at end of file diff --git a/src/test/java/cokr/xit/interfaces/lntris/InterfaceConfigTest.java b/src/test/java/cokr/xit/interfaces/lntris/InterfaceConfigTest.java new file mode 100644 index 0000000..10fa718 --- /dev/null +++ b/src/test/java/cokr/xit/interfaces/lntris/InterfaceConfigTest.java @@ -0,0 +1,41 @@ +package cokr.xit.interfaces.lntris; + +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import cokr.xit.foundation.test.TestSupport; + +public class InterfaceConfigTest extends TestSupport { + @Test + void config() { + System.out.println("useDatabase: " + InterfaceConfig.useDatabase()); + InterfaceConfig.EndPoint remote = InterfaceConfig.remote(); + System.out.println("========== remote =========="); + System.out.println("organization: " + remote.getOrganization()); + System.out.println("systems: " + remote.getSystems().stream().collect(Collectors.joining(", "))); + System.out.println("organizationSystem: " + remote.getOrganizationSystem()); + System.out.println("url: " + remote.getUrl()); + + System.out.println("========== locals =========="); + InterfaceConfig.locals().forEach(local -> { + System.out.println("organization: " + local.getOrganization()); + System.out.println("systems: " + local.getSystems().stream().collect(Collectors.joining(", "))); + System.out.println("organizationSystems: " + local.getOrganizationSystems().stream().collect(Collectors.joining(", "))); + }); + } + + @Test + void header() { + InterfaceHeader header = new InterfaceHeader(); + System.out.println("ifDate: " + header.getIfDate()); + System.out.println("ifMsgKey: " + header.getIfMsgKey()); + System.out.println("ifId: " + header.getIfId()); + System.out.println("source: " + header.getSource()); + System.out.println("target: " + header.getTarget()); + System.out.println("ifType: " + header.getIfType()); + System.out.println("ifFormat: " + header.getIfFormat()); + System.out.println("retName: " + header.getRetName()); + System.out.println("retCode: " + header.getRetCode()); + } +} \ No newline at end of file diff --git a/src/test/resources/log4jdbc.log4j2.properties b/src/test/resources/log4jdbc.log4j2.properties new file mode 100644 index 0000000..3b8ff2b --- /dev/null +++ b/src/test/resources/log4jdbc.log4j2.properties @@ -0,0 +1,4 @@ +log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator + +log4jdbc.dump.sql.maxlinelength=0 +log4jdbc.drivers=org.mariadb.jdbc.Driver diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..91fd719 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + ${LOG_PATTERN} + + + + + ${LOG_PATH}/${LOG_FILE_NAME}.log + + ${LOG_PATTERN} + + + + + ${LOG_PATH}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.log + + 10MB + + + 30 + + + + + + + error + ACCEPT + DENY + + + ${LOG_PATH}/${ERR_LOG_FILE_NAME}.log + + ${LOG_PATTERN} + + + + + ${LOG_PATH}/${ERR_LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.log + + + 10MB + + + 60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/lombok.config b/src/test/resources/lombok.config new file mode 100644 index 0000000..0a8874c --- /dev/null +++ b/src/test/resources/lombok.config @@ -0,0 +1,2 @@ +# see https://projectlombok.org/features/constructor lombok.copyableAnnotations +lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier diff --git a/src/test/resources/spring/context-common.xml b/src/test/resources/spring/context-common.xml new file mode 100644 index 0000000..b8c88b0 --- /dev/null +++ b/src/test/resources/spring/context-common.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + classpath:message/message-common + classpath:message/authentication-message + classpath:org/egovframe/rte/fdl/property/messages/properties + + + + + 60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/context-datasource.xml b/src/test/resources/spring/context-datasource.xml new file mode 100644 index 0000000..971a5b9 --- /dev/null +++ b/src/test/resources/spring/context-datasource.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/sql/mapper/base/test-mapper.xml b/src/test/resources/sql/mapper/base/test-mapper.xml new file mode 100644 index 0000000..d674130 --- /dev/null +++ b/src/test/resources/sql/mapper/base/test-mapper.xml @@ -0,0 +1,13 @@ + + + + +${sql} + +${sql} + +${sql} + +COMMIT + + \ No newline at end of file diff --git a/src/test/resources/sql/mapper/base/utility.xml b/src/test/resources/sql/mapper/base/utility.xml new file mode 100644 index 0000000..5187fae --- /dev/null +++ b/src/test/resources/sql/mapper/base/utility.xml @@ -0,0 +1,35 @@ + + + + + + +SELECT QROWS.* FROM ( + SELECT ROW_NUMBER() OVER() ROW_NUM + , COUNT(*) OVER() TOT_CNT, QBODY.* + FROM ( + + ) QBODY + ) QROWS +WHERE ROW_NUM BETWEEN ((#{pageNum} - 1) * #{fetchSize}) + 1 AND (#{pageNum} * #{fetchSize}) + + + +ORDER BY ${orderBy} + + + +DATE_FORMAT(CURRENT_TIMESTAMP(), '%Y%m%d%H%i%s') + +SELECTNOW + +DATE_FORMAT(CURRENT_DATE, '%Y%m%d') + +SELECTTODAY + +IFNULL(#{thisDay},) + +SELECTTHIS_DAY + + \ No newline at end of file diff --git a/src/test/resources/sql/mybatis-config.xml b/src/test/resources/sql/mybatis-config.xml new file mode 100644 index 0000000..03ad4e8 --- /dev/null +++ b/src/test/resources/sql/mybatis-config.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file