[framework] TxCore → common-framework (TxContext·MessageCodec) #1

Open
forge-bot wants to merge 9 commits from forge/ACM-FW-001-attempt-1-run-4a9d6591b579 into main
9 changed files with 1059 additions and 79 deletions

View file

@ -0,0 +1,3 @@
# ACM-FW-001-attempt-1-run-4a9d6591b579
Forge 이슈 작업 브랜치 `forge/ACM-FW-001-attempt-1-run-4a9d6591b579`.

View file

@ -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<String, Object>` |
| `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<TxRequest, TxResult>` 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<String,Object>` | 정형 필드는 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.<module>` + `boot/modules/<module>/`
## 2단계 의존 순서 (반드시 준수)
1. **1단계 — 공통 프레임워크 우선.** TxCore 대체(`common-framework`)가 먼저
착지해야 한다. 업무 모듈이 의존하는 `TxContext` / `MessageCodec` / 트랜잭션·로깅
규약이 여기 있다. 프레임워크 없이 업무 모듈만 이식하면 빌드가 깨진다.
2. **2단계 — 업무 모듈.** `modules/acquiring``common-framework` 에 의존한다
(`pom.xml` `<dependency>`). 매입접수 → 정산집계 → 대사 배치 순으로 유스케이스를
채운다.
## 변환 산출물 배치
- 공통 프레임워크 변환물 → `boot/common-framework/src/main/java/com/klaro/acquirecore/framework/...`
- 업무 모듈 변환물 → `boot/modules/<module>/src/main/java/com/klaro/acquirecore/<module>/...`
- 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<String, Object>`
9. Service registry via Spring DI, not manual lookup
10. Unit tests required for each component

View file

@ -1,40 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.klaro.acquirecore</groupId>
<artifactId>acquire-core-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<artifactId>acquire-core-migration</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>common-framework</artifactId>
<packaging>jar</packaging>
<name>common-framework</name>
<description>TxCore 의 Spring 대체 공통 프레임워크 스타터 (라이브러리 jar)</description>
<description>TxCore migration to Spring Boot - common framework module</description>
<dependencies>
<!-- 라이브러리이므로 web/실행 스타터 없이 코어 스타터만 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 라이브러리 모듈은 실행 가능 jar 재패키징을 하지 않는다 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<skip>true</skip>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
</plugin>
</plugins>
</build>
</project>

View file

@ -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 자리표시자.
*
* <p> 전환에서는 고정길이(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> T decode(byte[] data, Class<T> 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<String, Object> payload;
public Long txnId;
public RequestDto() {}
public RequestDto(String serviceName, Map<String, Object> payload, Long txnId) {
this.serviceName = serviceName;
this.payload = payload;
this.txnId = txnId;
}
}
private static class ResponseDto {
public String result;
public int resultCode;
public Map<String, Object> data;
public String errorMessage;
public ResponseDto() {}
public ResponseDto(String result, int resultCode, Map<String, Object> 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);
}
}

View file

@ -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<String, Object> 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> T get(String key) {
return (T) data.get(key);
}
public Map<String, Object> 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<String, Object> 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> 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<String, Object> payload;
private final TxContext context;
public TxRequest(String serviceName, Map<String, Object> 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<String, Object> payload) {
return new TxRequest(serviceName, payload, null);
}
public String getServiceName() {
return serviceName;
}
public Map<String, Object> getPayload() {
return payload;
}
public TxContext getContext() {
return context;
}
@SuppressWarnings("unchecked")
public <T> 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<String, Object> data;
private final String errorMessage;
private TxResponse(TxResult result, Map<String, Object> 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<String, Object> 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<String, Object> getData() {
return data;
}
public String getErrorMessage() {
return errorMessage;
}
public boolean isSuccess() {
return result.isSuccess();
}
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) data.get(key);
}
public TxResponse with(String key, Object value) {
Map<String, Object> newData = new HashMap<>(this.data);
newData.put(key, value);
return new TxResponse(this.result, newData, this.errorMessage);
}
}
}

View file

@ -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<String, ServiceEntry> services = new ConcurrentHashMap<>();
/**
* Register a service handler - equivalent to C tx_register().
*/
public <T extends TxService> 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<TxRequest, TxResponse> 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<String> 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<TxRequest, TxResponse> handler;
private volatile boolean enabled = true;
FunctionalTxService(String name, Function<TxRequest, TxResponse> 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;
}
}
}

View file

@ -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> T execute(TxCore.TxContext ctx, TxAction<T> 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> {
T execute(TxCore.TxContext context);
}
}

View file

@ -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<String, Object> 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<String, Object> 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"));
}
}

36
pom.xml Normal file
View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.klaro.acquirecore</groupId>
<artifactId>acquire-core-migration</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>acquire-core-migration</name>
<description>TxCore to Spring Boot migration project</description>
<modules>
<module>boot/common-framework</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.2.0</spring-boot.version>
<jackson.version>2.15.3</jackson.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>