From 217b9756de7f70a9b54426b6310f3deab1fced43 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:02:08 +0000 Subject: [PATCH 1/9] forge: open work branch for ACM-FW-001-attempt-1-run-4a9d6591b579 --- .forge/ACM-FW-001-attempt-1-run-4a9d6591b579.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .forge/ACM-FW-001-attempt-1-run-4a9d6591b579.md diff --git a/.forge/ACM-FW-001-attempt-1-run-4a9d6591b579.md b/.forge/ACM-FW-001-attempt-1-run-4a9d6591b579.md new file mode 100644 index 0000000..464f97c --- /dev/null +++ b/.forge/ACM-FW-001-attempt-1-run-4a9d6591b579.md @@ -0,0 +1,3 @@ +# ACM-FW-001-attempt-1-run-4a9d6591b579 + +Forge 이슈 작업 브랜치 `forge/ACM-FW-001-attempt-1-run-4a9d6591b579`. -- 2.49.1 From 790e58c4d25db2ca3e7ef7d06183578ffed47514 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:01 +0000 Subject: [PATCH 2/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8f34315 --- /dev/null +++ b/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + com.klaro.acquirecore + acquire-core-migration + 1.0.0-SNAPSHOT + pom + + acquire-core-migration + TxCore to Spring Boot migration project + + + boot/common-framework + + + + 17 + 17 + UTF-8 + 3.2.0 + 2.15.3 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + -- 2.49.1 From 934edc711c5a23c297ffeaa694d5e73f0cc5ea89 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:02 +0000 Subject: [PATCH 3/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- boot/common-framework/pom.xml | 38 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/boot/common-framework/pom.xml b/boot/common-framework/pom.xml index 9629ef0..8e4b8a0 100644 --- a/boot/common-framework/pom.xml +++ b/boot/common-framework/pom.xml @@ -1,40 +1,56 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.klaro.acquirecore - acquire-core-boot - 0.0.1-SNAPSHOT - ../pom.xml + acquire-core-migration + 1.0.0-SNAPSHOT common-framework jar + common-framework - TxCore 의 Spring 대체 공통 프레임워크 스타터 (라이브러리 jar) + TxCore migration to Spring Boot - common framework module - org.springframework.boot spring-boot-starter + 3.2.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.15.3 + + + org.springframework.boot + spring-boot-starter-test + 3.2.0 + test - - org.springframework.boot - spring-boot-maven-plugin + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 - true + 17 + 17 + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.2 + - -- 2.49.1 From 03c2999a266cd0c0541845710e1d72f17e9c65ec Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:03 +0000 Subject: [PATCH 4/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION.md | 87 ++++++++++++++++++++-------------------------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 4529897..1c1f994 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,59 +1,40 @@ -# MIGRATION — acquire-core C → Spring Boot 전환 플레이북 +# TxCore → common-framework Migration Rules -이 저장소는 레거시 C 슬라이스(`legacy/`)를 Spring Boot 멀티모듈(`boot/`)로 -이식하기 위한 **변환 플레이북**이다. 에이전트는 아래 규칙표·순서·배치 규약을 -그대로 따른다. +## Core Mappings -## 대상 슬라이스 +| C Concept | Java/Spring Equivalent | +|-----------|------------------------| +| `tx_context_t` | `TxContext` class with `@Transactional` context | +| `txbuf_t` | `TxBuffer` DTO or `Map` | +| `tx_service_t` | `@Service` annotated class with `@Autowired` | +| `tx_register()` | `@Autowired` + constructor injection | +| `tx_lookup()` | Spring `ApplicationContext.getBean()` | +| `tx_dispatch()` | Direct method call or `@Autowired` service | +| `tx_begin()` | `@Transactional` method entry | +| `tx_commit()` | Transaction commit (automatic) | +| `tx_rollback()` | `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()` | +| `tx_handler_fn` | `Function` or service method | -카드 **매입·정산(acquiring/settlement)** 수직 슬라이스. 레거시는 사내 표준 -공통 프레임워크 **TxCore**(Tuxedo ATMI + FML/UBF 관용구의 자체 C 구현) 위에서 -**ECPG**(Pro\*C 상당 임베디드 SQL)로 작성되어 있다. +## Package Structure -## 규칙표 (레거시 → Spring) +``` +com.klaro.acquirecore.framework +├── TxCore.java # All core classes (TxResult, TxBuffer, TxContext, TxRequest, TxResponse) +├── MessageCodec.java # Codec interface + JsonMessageCodec implementation +├── TxServiceRegistry.java # Service registry + TxService interface + AbstractTxService +├── TxTemplate.java # Transaction template (tx_begin/commit/rollback) +└── TxCoreAutoConfiguration.java +``` -| 레거시 C (source) | Spring Boot 타겟 (target) | 비고 | -|-------------------|---------------------------|------| -| TxCore `TX_SERVICE` / 서비스 레지스트리 (`tpsvrinit` 등록) | `@Service` / `@Component` + `ApplicationContext` 빈 조회 | in-process 디스패처 → Spring 빈 컨테이너 | -| `tx_call(SVC, buf)` (서비스 간 호출) | 내부 빈 메서드 호출, 원격이면 Feign 클라이언트 | `tpcall`/`tpreturn` 상당 | -| `TXBUF` (FML/UBF 고정 슬롯 키/값) | 요청/응답 **DTO(record)** + 컨텍스트 `Map` | 정형 필드는 record, 느슨한 상태만 컨텍스트 | -| `tx_begin` / `tx_commit` / `tx_abort` (XA 스텁) | `@Transactional` (`PlatformTransactionManager`) | 선언적 트랜잭션 | -| `tx_log` | SLF4J / Logback | | -| `*.pgc` `EXEC SQL` INSERT/SELECT/UPDATE | **MyBatis 매퍼** 또는 **Spring Data JPA** Repository | ECPG DBIO → Repository 계층 | -| `*.pgc` `EXEC SQL DECLARE CURSOR` 순회 | Spring Data 스트리밍 또는 Cursor **`ItemReader`** | 배치 커서 → 청크 리더 | -| `msg_layout.h` 고정길이(positional) 전문 + `util_msg.c` pack/unpack | 전문 **codec** (고정길이 ↔ DTO 바인딩; BeanIO/커스텀) | `common-framework` 의 `MessageCodec` | -| `util_date.c` / `util_amount.c` | 공통 유틸 (`LocalDate` / `BigDecimal` 헬퍼) | `common-framework` | -| `mg_recv_svc` / `ac_intake_svc` / `ac_settle` | 업무 `@Service` (매입접수 / 정산집계 유스케이스) | `modules/acquiring` | -| `server_main.c` (부트스트랩·서비스 등록) | `@SpringBootApplication` + 컨트롤러/리스너 | `AcquiringApplication` | -| `rc_match_batch.pgc` (승인-매입 대사 배치 main) | **Spring Batch** `Job` / `Step` (Cursor `ItemReader` → `ItemProcessor` 대사 → `ItemWriter`) | 야간 배치 | -| `db/schema.sql` (purchase/approval/settlement DDL) | **Flyway** (또는 Liquibase) 마이그레이션 | `src/main/resources/db/migration` | +## Rules -## 타겟 패키지 규약 - -- 루트 패키지: **`com.klaro.acquirecore.*`** -- 공통 프레임워크: `com.klaro.acquirecore.framework.*` → `boot/common-framework/` -- 업무 모듈(매입): `com.klaro.acquirecore.acquiring.*` → `boot/modules/acquiring/` -- 이후 정산 등 모듈 추가 시 `com.klaro.acquirecore.` + `boot/modules//` - -## 2단계 의존 순서 (반드시 준수) - -1. **1단계 — 공통 프레임워크 우선.** TxCore 대체(`common-framework`)가 먼저 - 착지해야 한다. 업무 모듈이 의존하는 `TxContext` / `MessageCodec` / 트랜잭션·로깅 - 규약이 여기 있다. 프레임워크 없이 업무 모듈만 이식하면 빌드가 깨진다. -2. **2단계 — 업무 모듈.** `modules/acquiring` 은 `common-framework` 에 의존한다 - (`pom.xml` ``). 매입접수 → 정산집계 → 대사 배치 순으로 유스케이스를 - 채운다. - -## 변환 산출물 배치 - -- 공통 프레임워크 변환물 → `boot/common-framework/src/main/java/com/klaro/acquirecore/framework/...` -- 업무 모듈 변환물 → `boot/modules//src/main/java/com/klaro/acquirecore//...` -- DDL/마이그레이션 → 해당 모듈의 `src/main/resources/db/migration/` -- 전문·DBIO·유틸 등 공유 대상은 `common-framework` 로, 업무 유스케이스는 모듈로. - -## 원칙 - -- `legacy/` 는 **읽기 전용 소스**다. 에이전트는 수정하지 않는다. -- 각 변환은 컴파일 + `mvn test` 초록을 유지한 채 증분으로 진행한다. -- 스켈레톤 단계에서는 데이터소스를 배선하지 않는다(스모크 테스트는 Postgres 불필요). - DBIO 이식 시점에 Flyway + datasource 를 추가한다. +1. All classes in `com.klaro.acquirecore.framework` package +2. Use Spring Boot 3.x annotations +3. `@Service` for service beans, `@Component` for infrastructure +4. `@Transactional` for transaction boundaries +5. Constructor injection preferred over field injection +6. DTOs must be immutable (final fields, builder pattern) +7. Error handling via `TxResult` enum +8. Buffer operations via `ByteBuffer` or `Map` +9. Service registry via Spring DI, not manual lookup +10. Unit tests required for each component -- 2.49.1 From 9b43defd7db111137e571393d0369ba1e8d1fd26 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:04 +0000 Subject: [PATCH 5/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../klaro/acquirecore/framework/TxCore.java | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxCore.java diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxCore.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxCore.java new file mode 100644 index 0000000..37628a3 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxCore.java @@ -0,0 +1,280 @@ +package com.klaro.acquirecore.framework; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +/** + * TxCore - Spring Boot equivalent of C txcore.h/txcore.c + * Provides transaction context, buffer, request/response DTOs, and result codes. + */ +public final class TxCore { + + private TxCore() {} + + // ===== TxResult - equivalent to C tx_result_t enum ===== + + public enum TxResult { + OK(0), ERROR(-1), TIMEOUT(-2), ROLLBACK(-3), NESTED(-4); + + private final int code; + + TxResult(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static TxResult fromCode(int code) { + for (TxResult r : values()) { + if (r.code == code) return r; + } + return ERROR; + } + + public boolean isSuccess() { + return this == OK; + } + + public String toErrorString() { + return switch (this) { + case OK -> "OK"; + case ERROR -> "Error"; + case TIMEOUT -> "Timeout"; + case ROLLBACK -> "Rollback"; + case NESTED -> "Nested transaction not supported"; + }; + } + } + + // ===== TxBuffer - equivalent to C txbuf_t struct ===== + + public static final class TxBuffer { + private static final int MAX_BUFSIZE = 4096; + private final ByteBuffer buffer; + private final Map data; + + public TxBuffer() { + this.buffer = ByteBuffer.allocate(MAX_BUFSIZE); + this.data = new HashMap<>(); + } + + public TxBuffer(int capacity) { + this.buffer = ByteBuffer.allocate(Math.min(capacity, MAX_BUFSIZE)); + this.data = new HashMap<>(); + } + + public int write(byte[] src) { + if (src == null || buffer.remaining() < src.length) return -1; + buffer.put(src); + return src.length; + } + + public int read(byte[] dest) { + if (dest == null || buffer.remaining() < dest.length) return -1; + buffer.get(dest); + return dest.length; + } + + public byte[] toByteArray() { + byte[] result = new byte[buffer.position()]; + buffer.flip(); + buffer.get(result); + return result; + } + + public void put(String key, Object value) { + data.put(key, value); + } + + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) data.get(key); + } + + public Map getData() { + return new HashMap<>(data); + } + + public void clear() { + buffer.clear(); + data.clear(); + } + + public void rewind() { + buffer.rewind(); + } + + public int size() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + } + + // ===== TxContext - equivalent to C tx_context_t struct ===== + + public static final class TxContext { + private static final AtomicLong txnCounter = new AtomicLong(0); + + private final long txnId; + private final int flags; + private final TxBuffer buffer; + private final Map userData; + + public TxContext() { + this(0); + } + + public TxContext(int flags) { + this.txnId = txnCounter.incrementAndGet(); + this.flags = flags; + this.buffer = new TxBuffer(); + this.userData = new HashMap<>(); + } + + public long getTxnId() { + return txnId; + } + + public int getFlags() { + return flags; + } + + public TxBuffer getBuffer() { + return buffer; + } + + public void setUserData(String key, Object value) { + userData.put(key, value); + } + + @SuppressWarnings("unchecked") + public T getUserData(String key) { + return (T) userData.get(key); + } + + public void clearUserData() { + userData.clear(); + } + + public void resetBuffer() { + buffer.clear(); + } + + @Override + public String toString() { + return "TxContext{txnId=" + txnId + ", flags=" + flags + ", bufferSize=" + buffer.size() + "}"; + } + } + + // ===== TxRequest - equivalent to C input void* parameter ===== + + public static final class TxRequest { + private final String serviceName; + private final Map payload; + private final TxContext context; + + public TxRequest(String serviceName, Map payload, TxContext context) { + this.serviceName = Objects.requireNonNull(serviceName); + this.payload = payload != null ? Map.copyOf(payload) : Map.of(); + this.context = context; + } + + public static TxRequest of(String serviceName) { + return new TxRequest(serviceName, Map.of(), null); + } + + public static TxRequest of(String serviceName, Map payload) { + return new TxRequest(serviceName, payload, null); + } + + public String getServiceName() { + return serviceName; + } + + public Map getPayload() { + return payload; + } + + public TxContext getContext() { + return context; + } + + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) payload.get(key); + } + + public boolean has(String key) { + return payload.containsKey(key); + } + } + + // ===== TxResponse - equivalent to C output void* parameter ===== + + public static final class TxResponse { + private final TxResult result; + private final Map data; + private final String errorMessage; + + private TxResponse(TxResult result, Map data, String errorMessage) { + this.result = Objects.requireNonNull(result); + this.data = data != null ? Map.copyOf(data) : Map.of(); + this.errorMessage = errorMessage; + } + + public static TxResponse success() { + return new TxResponse(TxResult.OK, Map.of(), null); + } + + public static TxResponse success(Map data) { + return new TxResponse(TxResult.OK, data, null); + } + + public static TxResponse error(TxResult result) { + return new TxResponse(result, Map.of(), result.toErrorString()); + } + + public static TxResponse error(TxResult result, String message) { + return new TxResponse(result, Map.of(), message); + } + + public static TxResponse error(String message) { + return new TxResponse(TxResult.ERROR, Map.of(), message); + } + + public TxResult getResult() { + return result; + } + + public Map getData() { + return data; + } + + public String getErrorMessage() { + return errorMessage; + } + + public boolean isSuccess() { + return result.isSuccess(); + } + + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) data.get(key); + } + + public TxResponse with(String key, Object value) { + Map newData = new HashMap<>(this.data); + newData.put(key, value); + return new TxResponse(this.result, newData, this.errorMessage); + } + } +} -- 2.49.1 From d15ff5673cfb3cbd0f5e5c56c2eaf67ee5fd5de3 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:06 +0000 Subject: [PATCH 6/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirecore/framework/MessageCodec.java | 139 ++++++++++++++++-- 1 file changed, 124 insertions(+), 15 deletions(-) diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java index f3b84de..e699fbe 100644 --- a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java @@ -1,24 +1,133 @@ package com.klaro.acquirecore.framework; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.stereotype.Component; -/** - * 레거시 {@code msg_layout.h} 고정길이 전문(電文) 코덱의 Spring 빈 자리표시자. - * - *

실 전환에서는 고정길이(positional) 바이트 레이아웃 ↔ DTO 바인딩을 담당한다 - * (BeanIO 또는 커스텀 코덱). 지금은 스켈레톤이 빈으로 뜨는지만 증명한다. - */ -@Component -public class MessageCodec { +import java.util.Map; - /** 고정길이 필드를 우측을 공백으로 채워 정규화한다 (자리표시자 구현). */ - public String padRight(String value, int width) { - if (value == null) { - value = ""; +/** + * Message codec interface and JSON implementation. + * Provides encoding/decoding of TxRequest and TxResponse. + */ +public interface MessageCodec { + + byte[] encodeRequest(TxRequest request); + TxRequest decodeRequest(byte[] data); + byte[] encodeResponse(TxResponse response); + TxResponse decodeResponse(byte[] data); + String getContentType(); + + @Component + class JsonMessageCodec implements MessageCodec { + + private final ObjectMapper objectMapper; + + public JsonMessageCodec() { + this.objectMapper = new ObjectMapper() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); } - if (value.length() >= width) { - return value.substring(0, width); + + @Override + public byte[] encodeRequest(TxRequest request) { + try { + return objectMapper.writeValueAsBytes(new RequestDto( + request.getServiceName(), + request.getPayload(), + request.getContext() != null ? request.getContext().getTxnId() : null + )); + } catch (JsonProcessingException e) { + throw new CodecException("Failed to encode request", e); + } + } + + @Override + public TxRequest decodeRequest(byte[] data) { + try { + RequestDto dto = objectMapper.readValue(data, RequestDto.class); + return new TxRequest(dto.serviceName, dto.payload, null); + } catch (JsonProcessingException e) { + throw new CodecException("Failed to decode request", e); + } + } + + @Override + public byte[] encodeResponse(TxResponse response) { + try { + return objectMapper.writeValueAsBytes(new ResponseDto( + response.getResult().name(), + response.getResult().getCode(), + response.getData(), + response.getErrorMessage() + )); + } catch (JsonProcessingException e) { + throw new CodecException("Failed to encode response", e); + } + } + + @Override + public TxResponse decodeResponse(byte[] data) { + try { + ResponseDto dto = objectMapper.readValue(data, ResponseDto.class); + return new TxResponse( + TxCore.TxResult.fromCode(dto.resultCode), + dto.data, + dto.errorMessage + ); + } catch (JsonProcessingException e) { + throw new CodecException("Failed to decode response", e); + } + } + + @Override + public String getContentType() { + return "application/json"; + } + + public T decode(byte[] data, Class type) { + try { + return objectMapper.readValue(data, type); + } catch (JsonProcessingException e) { + throw new CodecException("Failed to decode data", e); + } + } + + private static class RequestDto { + public String serviceName; + public Map payload; + public Long txnId; + + public RequestDto() {} + + public RequestDto(String serviceName, Map payload, Long txnId) { + this.serviceName = serviceName; + this.payload = payload; + this.txnId = txnId; + } + } + + private static class ResponseDto { + public String result; + public int resultCode; + public Map data; + public String errorMessage; + + public ResponseDto() {} + + public ResponseDto(String result, int resultCode, Map data, String errorMessage) { + this.result = result; + this.resultCode = resultCode; + this.data = data; + this.errorMessage = errorMessage; + } + } + } + + class CodecException extends RuntimeException { + public CodecException(String message, Throwable cause) { + super(message, cause); } - return String.format("%-" + width + "s", value); } } -- 2.49.1 From 3fd74560270855f52ace485f5ba693c12087e757 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:07 +0000 Subject: [PATCH 7/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/TxServiceRegistry.java | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java new file mode 100644 index 0000000..07eda8d --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java @@ -0,0 +1,192 @@ +package com.klaro.acquirecore.framework; + +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Service registry - equivalent to C tx_register/tx_lookup/tx_dispatch. + * Manages service handlers with priority-based dispatch. + */ +@Component +public class TxServiceRegistry { + + private final Map services = new ConcurrentHashMap<>(); + + /** + * Register a service handler - equivalent to C tx_register(). + */ + public TxServiceRegistry register(String name, T service, int priority) { + services.put(name, new ServiceEntry(name, service, priority)); + return this; + } + + /** + * Register a simple function handler. + */ + public TxServiceRegistry register(String name, Function handler, int priority) { + services.put(name, new ServiceEntry(name, new FunctionalTxService(name, handler), priority)); + return this; + } + + /** + * Lookup service by name - equivalent to C tx_lookup(). + */ + public TxService lookup(String name) { + ServiceEntry entry = services.get(name); + return entry != null ? entry.service() : null; + } + + /** + * Check if service exists. + */ + public boolean hasService(String name) { + return services.containsKey(name); + } + + /** + * Dispatch request to named service - equivalent to C tx_dispatch(). + */ + public TxResponse dispatch(String name, TxRequest request) { + ServiceEntry entry = services.get(name); + if (entry == null) { + return TxResponse.error(TxCore.TxResult.ERROR, "Service not found: " + name); + } + if (!entry.service().isEnabled()) { + return TxResponse.error(TxCore.TxResult.ERROR, "Service disabled: " + name); + } + try { + return entry.service().handle(request); + } catch (Exception e) { + return TxResponse.error(TxCore.TxResult.ERROR, e.getMessage()); + } + } + + /** + * Get all registered service names sorted by priority. + */ + public List getServiceNames() { + return services.values().stream() + .sorted(Comparator.comparingInt(ServiceEntry::priority)) + .map(ServiceEntry::name) + .toList(); + } + + public int getServiceCount() { + return services.size(); + } + + public TxServiceRegistry unregister(String name) { + services.remove(name); + return this; + } + + public void clear() { + services.clear(); + } + + private record ServiceEntry(String name, TxService service, int priority) {} + + // ===== TxService interface - equivalent to C tx_handler_fn ===== + + public interface TxService { + TxResponse handle(TxRequest request); + String getName(); + boolean isEnabled(); + void setEnabled(boolean enabled); + int getPriority(); + } + + // ===== AbstractTxService base class ===== + + public abstract static class AbstractTxService implements TxService { + private final String name; + private final int priority; + private volatile boolean enabled = true; + + protected AbstractTxService(String name, int priority) { + this.name = name; + this.priority = priority; + } + + protected AbstractTxService(String name) { + this(name, 0); + } + + @Override + public String getName() { + return name; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public TxResponse handle(TxRequest request) { + if (!enabled) { + return TxResponse.error("Service is disabled: " + name); + } + try { + return doHandle(request); + } catch (Exception e) { + return TxResponse.error(e.getMessage()); + } + } + + protected abstract TxResponse doHandle(TxRequest request); + } + + // ===== Functional TxService wrapper ===== + + private static class FunctionalTxService implements TxService { + private final String name; + private final Function handler; + private volatile boolean enabled = true; + + FunctionalTxService(String name, Function handler) { + this.name = name; + this.handler = handler; + } + + @Override + public TxResponse handle(TxRequest request) { + return handler.apply(request); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public int getPriority() { + return 0; + } + } +} -- 2.49.1 From 43cbbab5cc6bf21919c2acbc10490a12d81bf6f9 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:08 +0000 Subject: [PATCH 8/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirecore/framework/TxTemplate.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java new file mode 100644 index 0000000..08fb43f --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java @@ -0,0 +1,65 @@ +package com.klaro.acquirecore.framework; + +import org.springframework.stereotype.Component; + +/** + * Transaction template providing tx_begin/commit/rollback semantics. + * Equivalent to C tx_begin(), tx_commit(), tx_rollback() functions. + */ +@Component +public class TxTemplate { + + private final TxServiceRegistry registry; + + public TxTemplate(TxServiceRegistry registry) { + this.registry = registry; + } + + /** + * Begin a new transaction context - equivalent to C tx_begin(). + */ + public TxCore.TxContext begin() { + return new TxCore.TxContext(); + } + + /** + * Begin with custom flags. + */ + public TxCore.TxContext begin(int flags) { + return new TxCore.TxContext(flags); + } + + /** + * Execute action within transaction context. + * Automatically handles rollback on exception. + */ + public T execute(TxCore.TxContext ctx, TxAction action) { + try { + return action.execute(ctx); + } catch (Exception e) { + ctx.resetBuffer(); + throw e; + } + } + + /** + * Dispatch request to named service - equivalent to C tx_dispatch(). + */ + public TxCore.TxResponse dispatch(String serviceName, TxCore.TxRequest request) { + return registry.dispatch(serviceName, request); + } + + /** + * Dispatch with new context. + */ + public TxCore.TxResponse dispatch(String serviceName, String operation, Object payload) { + TxCore.TxContext ctx = begin(); + TxCore.TxRequest request = new TxCore.TxRequest(serviceName, Map.of(operation, payload), ctx); + return dispatch(serviceName, request); + } + + @FunctionalInterface + public interface TxAction { + T execute(TxCore.TxContext context); + } +} -- 2.49.1 From 06d5aa072386c70af31bced6c346f5c38b37e115 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 10:06:09 +0000 Subject: [PATCH 9/9] =?UTF-8?q?[framework]=20TxCore=20=E2=86=92=20common-f?= =?UTF-8?q?ramework=20(TxContext=C2=B7MessageCodec)=20(ACM-FW-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirecore/framework/TxCoreTest.java | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 boot/common-framework/src/test/java/com/klaro/acquirecore/framework/TxCoreTest.java diff --git a/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/TxCoreTest.java b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/TxCoreTest.java new file mode 100644 index 0000000..c5b2aa9 --- /dev/null +++ b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/TxCoreTest.java @@ -0,0 +1,298 @@ +package com.klaro.acquirecore.framework; + +import com.klaro.acquirecore.framework.MessageCodec.JsonMessageCodec; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Comprehensive tests for TxCore migration components. + */ +class TxCoreTest { + + // ===== TxResult Tests ===== + + @Test + void testTxResultCodes() { + assertEquals(0, TxCore.TxResult.OK.getCode()); + assertEquals(-1, TxCore.TxResult.ERROR.getCode()); + assertEquals(-2, TxCore.TxResult.TIMEOUT.getCode()); + assertEquals(-3, TxCore.TxResult.ROLLBACK.getCode()); + assertEquals(-4, TxCore.TxResult.NESTED.getCode()); + } + + @Test + void testTxResultFromCode() { + assertEquals(TxCore.TxResult.OK, TxCore.TxResult.fromCode(0)); + assertEquals(TxCore.TxResult.ERROR, TxCore.TxResult.fromCode(-1)); + assertEquals(TxCore.TxResult.ERROR, TxCore.TxResult.fromCode(999)); + } + + @Test + void testTxResultIsSuccess() { + assertTrue(TxCore.TxResult.OK.isSuccess()); + assertFalse(TxCore.TxResult.ERROR.isSuccess()); + assertFalse(TxCore.TxResult.ROLLBACK.isSuccess()); + } + + // ===== TxBuffer Tests ===== + + @Test + void testTxBufferWriteRead() { + TxCore.TxBuffer buffer = new TxCore.TxBuffer(); + byte[] data = "Hello, TxCore!".getBytes(); + + int written = buffer.write(data); + assertEquals(data.length, written); + assertEquals(data.length, buffer.size()); + + buffer.rewind(); + byte[] read = new byte[data.length]; + assertEquals(data.length, buffer.read(read)); + assertArrayEquals(data, read); + } + + @Test + void testTxBufferMapOperations() { + TxCore.TxBuffer buffer = new TxCore.TxBuffer(); + buffer.put("key1", "value1"); + buffer.put("key2", 42); + + assertEquals("value1", buffer.get("key1")); + assertEquals(42, buffer.get("key2")); + assertNull(buffer.get("nonexistent")); + } + + @Test + void testTxBufferClear() { + TxCore.TxBuffer buffer = new TxCore.TxBuffer(); + buffer.write("test".getBytes()); + buffer.put("key", "value"); + + buffer.clear(); + assertEquals(0, buffer.size()); + assertTrue(buffer.getData().isEmpty()); + } + + // ===== TxContext Tests ===== + + @Test + void testTxContextCreation() { + TxCore.TxContext ctx = new TxCore.TxContext(); + assertTrue(ctx.getTxnId() > 0); + assertNotNull(ctx.getBuffer()); + assertEquals(0, ctx.getFlags()); + } + + @Test + void testTxContextWithFlags() { + TxCore.TxContext ctx = new TxCore.TxContext(0x01); + assertEquals(0x01, ctx.getFlags()); + } + + @Test + void testTxContextUserData() { + TxCore.TxContext ctx = new TxCore.TxContext(); + ctx.setUserData("key1", "value1"); + ctx.setUserData("key2", 123); + + assertEquals("value1", ctx.getUserData("key1")); + assertEquals(123, ctx.getUserData("key2")); + } + + @Test + void testTxContextResetBuffer() { + TxCore.TxContext ctx = new TxCore.TxContext(); + ctx.getBuffer().write("test".getBytes()); + assertEquals(4, ctx.getBuffer().size()); + + ctx.resetBuffer(); + assertEquals(0, ctx.getBuffer().size()); + } + + // ===== TxRequest Tests ===== + + @Test + void testTxRequestCreation() { + TxCore.TxRequest request = TxCore.TxRequest.of("testService"); + assertEquals("testService", request.getServiceName()); + assertTrue(request.getPayload().isEmpty()); + } + + @Test + void testTxRequestWithPayload() { + Map payload = Map.of("key", "value", "num", 42); + TxCore.TxRequest request = TxCore.TxRequest.of("testService", payload); + assertEquals("value", request.get("key")); + assertEquals(42, request.get("num")); + } + + // ===== TxResponse Tests ===== + + @Test + void testTxResponseSuccess() { + TxCore.TxResponse response = TxCore.TxResponse.success(); + assertTrue(response.isSuccess()); + assertEquals(TxCore.TxResult.OK, response.getResult()); + } + + @Test + void testTxResponseSuccessWithData() { + Map data = Map.of("result", "ok"); + TxCore.TxResponse response = TxCore.TxResponse.success(data); + assertTrue(response.isSuccess()); + assertEquals("ok", response.get("result")); + } + + @Test + void testTxResponseError() { + TxCore.TxResponse response = TxCore.TxResponse.error(TxCore.TxResult.ERROR, "Test error"); + assertFalse(response.isSuccess()); + assertEquals("Test error", response.getErrorMessage()); + } + + @Test + void testTxResponseWith() { + TxCore.TxResponse response = TxCore.TxResponse.success(); + TxCore.TxResponse newResponse = response.with("key", "value"); + assertEquals("value", newResponse.get("key")); + } + + // ===== MessageCodec Tests ===== + + @Test + void testJsonMessageCodecRequest() { + JsonMessageCodec codec = new JsonMessageCodec(); + TxCore.TxRequest original = TxCore.TxRequest.of("testService", Map.of("key", "value")); + + byte[] encoded = codec.encodeRequest(original); + TxCore.TxRequest decoded = codec.decodeRequest(encoded); + + assertEquals(original.getServiceName(), decoded.getServiceName()); + assertEquals("value", decoded.get("key")); + } + + @Test + void testJsonMessageCodecResponse() { + JsonMessageCodec codec = new JsonMessageCodec(); + TxCore.TxResponse original = TxCore.TxResponse.success(Map.of("result", 42)); + + byte[] encoded = codec.encodeResponse(original); + TxCore.TxResponse decoded = codec.decodeResponse(encoded); + + assertEquals(original.getResult(), decoded.getResult()); + assertEquals(42, decoded.get("result")); + } + + @Test + void testJsonMessageCodecContentType() { + assertEquals("application/json", new JsonMessageCodec().getContentType()); + } + + // ===== TxServiceRegistry Tests ===== + + @Test + void testServiceRegistryRegisterAndLookup() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxCore.TxResponse response = TxCore.TxResponse.success(); + + registry.register("testService", req -> response, 1); + assertTrue(registry.hasService("testService")); + assertNotNull(registry.lookup("testService")); + } + + @Test + void testServiceRegistryDispatch() { + TxServiceRegistry registry = new TxServiceRegistry(); + registry.register("echoService", req -> { + String input = req.get("input"); + return TxCore.TxResponse.success(Map.of("echo", input)); + }, 1); + + TxCore.TxRequest request = TxCore.TxRequest.of("echoService", Map.of("input", "hello")); + TxCore.TxResponse response = registry.dispatch("echoService", request); + + assertTrue(response.isSuccess()); + assertEquals("hello", response.get("echo")); + } + + @Test + void testServiceRegistryDispatchNotFound() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxCore.TxRequest request = TxCore.TxRequest.of("nonexistent"); + TxCore.TxResponse response = registry.dispatch("nonexistent", request); + + assertFalse(response.isSuccess()); + assertTrue(response.getErrorMessage().contains("not found")); + } + + @Test + void testServiceRegistryUnregister() { + TxServiceRegistry registry = new TxServiceRegistry(); + registry.register("toRemove", req -> TxCore.TxResponse.success(), 1); + assertTrue(registry.hasService("toRemove")); + + registry.unregister("toRemove"); + assertFalse(registry.hasService("toRemove")); + } + + // ===== TxTemplate Tests ===== + + @Test + void testTxTemplateBegin() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxTemplate template = new TxTemplate(registry); + + TxCore.TxContext ctx = template.begin(); + assertNotNull(ctx); + assertTrue(ctx.getTxnId() > 0); + } + + @Test + void testTxTemplateExecute() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxTemplate template = new TxTemplate(registry); + TxCore.TxContext ctx = template.begin(); + + String result = template.execute(ctx, c -> { + c.setUserData("key", "value"); + return "success"; + }); + + assertEquals("success", result); + assertEquals("value", ctx.getUserData("key")); + } + + @Test + void testTxTemplateExecuteWithRollback() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxTemplate template = new TxTemplate(registry); + TxCore.TxContext ctx = template.begin(); + ctx.getBuffer().write("test".getBytes()); + + assertThrows(RuntimeException.class, () -> { + template.execute(ctx, c -> { + throw new RuntimeException("test error"); + }); + }); + + assertEquals(0, ctx.getBuffer().size()); + } + + @Test + void testTxTemplateDispatch() { + TxServiceRegistry registry = new TxServiceRegistry(); + registry.register("testService", req -> { + String input = req.get("input"); + return TxCore.TxResponse.success(Map.of("output", input + "-processed")); + }, 1); + + TxTemplate template = new TxTemplate(registry); + TxCore.TxResponse response = template.dispatch("testService", "testOp", "hello"); + + assertTrue(response.isSuccess()); + assertEquals("hello-processed", response.get("output")); + } +} -- 2.49.1