init
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.ando.dynamic_soa_mapper;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DynamicSoaMapperApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DynamicSoaMapperApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
116
src/main/java/com/ando/dynamic_soa_mapper/config/DataSeeder.java
Normal file
116
src/main/java/com/ando/dynamic_soa_mapper/config/DataSeeder.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package com.ando.dynamic_soa_mapper.config;
|
||||
|
||||
import com.ando.dynamic_soa_mapper.entity.Payment;
|
||||
import com.ando.dynamic_soa_mapper.entity.SoaMappingConfig;
|
||||
import com.ando.dynamic_soa_mapper.entity.Transaction;
|
||||
import com.ando.dynamic_soa_mapper.repository.PaymentRepository;
|
||||
import com.ando.dynamic_soa_mapper.repository.SoaMappingConfigRepository;
|
||||
import com.ando.dynamic_soa_mapper.repository.TransactionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DataSeeder implements CommandLineRunner {
|
||||
|
||||
private final PaymentRepository paymentRepo;
|
||||
private final TransactionRepository transactionRepo;
|
||||
private final SoaMappingConfigRepository configRepo;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (paymentRepo.count() == 0) {
|
||||
seedData();
|
||||
}
|
||||
}
|
||||
|
||||
private void seedData() {
|
||||
// 1. Setup Data Utama
|
||||
Payment payment = new Payment();
|
||||
payment.setId("PAY-999");
|
||||
payment.setDebitAccountNo("1234567890");
|
||||
payment.setCurrency("USD"); // Kita set USD untuk test logic
|
||||
payment.setCreatedDate(new Date());
|
||||
paymentRepo.save(payment);
|
||||
|
||||
Transaction trx = new Transaction();
|
||||
trx.setId("TRX-555");
|
||||
trx.setPaymentId("PAY-999");
|
||||
trx.setAmount(new BigDecimal("50.00"));
|
||||
trx.setEffectiveDate(new Date());
|
||||
trx.setStatus("SUCCESS");
|
||||
transactionRepo.save(trx);
|
||||
|
||||
List<SoaMappingConfig> configs = new ArrayList<>();
|
||||
|
||||
// ==========================================
|
||||
// KOTRAN 1001: Standar Mapping & Simple Math
|
||||
// ==========================================
|
||||
configs.add(createConfig("1001", "TLBF_ACC", "String", "#payment.debitAccountNo", null, 1));
|
||||
configs.add(createConfig("1001", "TLBF_AMT", "Number", "#transaction.amount * 100", null, 1)); // Kali 100
|
||||
configs.add(createConfig("1001", "TLBF_CCY", "String", "#payment.currency", "IDR", 1));
|
||||
|
||||
// ==========================================
|
||||
// KOTRAN 1002: Formatting & Conditional Logic
|
||||
// ==========================================
|
||||
// Format Date menjadi yyyyMMdd menggunakan java.text.SimpleDateFormat di dalam SpEL
|
||||
configs.add(createConfig("1002", "TLBF_DATE", "String", "new java.text.SimpleDateFormat('yyyyMMdd').format(#transaction.effectiveDate)", null, 1));
|
||||
// Logic kurs: Jika USD kali 15000, selain itu kali 1
|
||||
configs.add(createConfig("1002", "TLBF_EQV_AMT", "Number", "#payment.currency == 'USD' ? (#transaction.amount * 15000) : #transaction.amount", null, 1));
|
||||
// Concatenation string
|
||||
configs.add(createConfig("1002", "TLBF_REF", "String", "'REF-' + #transaction.id", null, 1));
|
||||
|
||||
// ==========================================
|
||||
// KOTRAN 1003: NPE Handling & Default Values
|
||||
// ==========================================
|
||||
// Misal kita coba ambil data 'promoCode' yang tidak ada di object, pakai safe navigation '?.'
|
||||
// Jika null, akan jatuh ke default value 'NO_PROMO'
|
||||
configs.add(createConfig("1003", "TLBF_PROMO", "String", "#transaction?.promoCode", "NO_PROMO", 0));
|
||||
// Mapping status dengan ternary
|
||||
configs.add(createConfig("1003", "TLBF_STS", "String", "#transaction.status == 'SUCCESS' ? '00' : '99'", null, 1));
|
||||
|
||||
// ==========================================
|
||||
// KOTRAN 1004: NESTED JSON DENGAN DOT NOTATION
|
||||
// ==========================================
|
||||
// Header level
|
||||
configs.add(createConfig("1004", "RequestHeader.MessageId", "String", "'MSG-' + #transaction.id", null, 1));
|
||||
configs.add(createConfig("1004", "RequestHeader.Kotran", "String", "'1004'", null, 1));
|
||||
configs.add(createConfig("1004", "RequestHeader.Timestamp", "String", "new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(#transaction.effectiveDate)", null, 1));
|
||||
|
||||
// Body level - Account Info
|
||||
configs.add(createConfig("1004", "RequestBody.AccountInfo.DebitAccount", "String", "#payment.debitAccountNo", null, 1));
|
||||
configs.add(createConfig("1004", "RequestBody.AccountInfo.Currency", "String", "#payment.currency", null, 1));
|
||||
|
||||
// Body level - Transaction Details
|
||||
configs.add(createConfig("1004", "RequestBody.TransactionDetail.Amount", "Number", "#transaction.amount", null, 1));
|
||||
configs.add(createConfig("1004", "RequestBody.TransactionDetail.ConvertedAmount", "Number", "#transaction.amount * 15000", null, 0)); // Opsional
|
||||
|
||||
configRepo.saveAll(configs);
|
||||
log.info("Data Seeder berhasil dieksekusi untuk Kotran 1001, 1002, 1003!");
|
||||
}
|
||||
|
||||
private SoaMappingConfig createConfig(String kotran, String targetFieldNm, String targetFieldType,
|
||||
String expressionRule, String defaultValue, Integer isMandatory) {
|
||||
SoaMappingConfig config = new SoaMappingConfig();
|
||||
config.setId(UUID.randomUUID().toString());
|
||||
config.setKotran(kotran);
|
||||
config.setTargetFieldNm(targetFieldNm);
|
||||
config.setTargetFieldType(targetFieldType);
|
||||
config.setExpressionRule(expressionRule);
|
||||
config.setDefaultValue(defaultValue);
|
||||
config.setIsMandatory(isMandatory);
|
||||
config.setStatus("ACTIVE");
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.ando.dynamic_soa_mapper.controller;
|
||||
|
||||
import com.ando.dynamic_soa_mapper.service.MappingEngineService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/soa")
|
||||
@RequiredArgsConstructor
|
||||
public class SoaMappingController {
|
||||
|
||||
private final MappingEngineService mappingEngineService;
|
||||
|
||||
/**
|
||||
* Endpoint untuk men-generate JSON payload SOA secara dinamis
|
||||
* Contoh URL: GET /api/v1/soa/generate-payload?transactionId=TRX-555&kotran=1001
|
||||
*/
|
||||
@GetMapping("/generate-payload")
|
||||
public ResponseEntity<?> generatePayload(
|
||||
@RequestParam String transactionId,
|
||||
@RequestParam String kotran) {
|
||||
|
||||
log.info("Menerima request API untuk generate payload - TRX: {}, KOTRAN: {}", transactionId, kotran);
|
||||
|
||||
try {
|
||||
// Memanggil otak utama (Engine) kita
|
||||
Map<String, Object> payload = mappingEngineService.constructPayload(transactionId, kotran);
|
||||
|
||||
// Return HTTP 200 OK dengan body berupa JSON hasil mapping
|
||||
return ResponseEntity.ok(payload);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Best practice: Bedakan error validasi (Client Error 400)
|
||||
log.warn("Validasi gagal: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"status", "FAILED",
|
||||
"error", e.getMessage()
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
// Best practice: Tangkap error sistem (Server Error 500)
|
||||
log.error("Terjadi kesalahan sistem saat generate payload: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of(
|
||||
"status", "ERROR",
|
||||
"error", "Terjadi kesalahan internal saat memproses mapping SOA."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ando.dynamic_soa_mapper.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import jakarta.persistence.*;
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
@Table(name = "payment")
|
||||
@Getter
|
||||
@Setter
|
||||
public class Payment {
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(name = "debit_account_no")
|
||||
private String debitAccountNo;
|
||||
|
||||
private String currency;
|
||||
|
||||
@Column(name = "created_date")
|
||||
private Date createdDate;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ando.dynamic_soa_mapper.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "soa_mapping_config")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SoaMappingConfig {
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String kotran;
|
||||
|
||||
@Column(name = "target_field_nm")
|
||||
private String targetFieldNm;
|
||||
|
||||
@Column(name = "target_field_type")
|
||||
private String targetFieldType;
|
||||
|
||||
@Column(name = "expression_rule")
|
||||
private String expressionRule;
|
||||
|
||||
@Column(name = "default_value")
|
||||
private String defaultValue;
|
||||
|
||||
@Column(name = "is_mandatory")
|
||||
private Integer isMandatory;
|
||||
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ando.dynamic_soa_mapper.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
@Table(name = "transaction")
|
||||
@Getter
|
||||
@Setter
|
||||
public class Transaction {
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(name = "payment_id")
|
||||
private String paymentId;
|
||||
|
||||
private BigDecimal amount; // Menggunakan BigDecimal untuk nominal uang (best practice)
|
||||
|
||||
@Column(name = "effective_date")
|
||||
private Date effectiveDate;
|
||||
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ando.dynamic_soa_mapper.repository;
|
||||
|
||||
import com.ando.dynamic_soa_mapper.entity.Payment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface PaymentRepository extends JpaRepository<Payment, String> {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ando.dynamic_soa_mapper.repository;
|
||||
|
||||
import com.ando.dynamic_soa_mapper.entity.SoaMappingConfig;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface SoaMappingConfigRepository extends JpaRepository<SoaMappingConfig, String> {
|
||||
|
||||
// Ini adalah custom method. Spring otomatis men-generate query-nya:
|
||||
// SELECT * FROM soa_mapping_config WHERE kotran = ? AND status = ? ORDER BY target_field_nm ASC
|
||||
List<SoaMappingConfig> findByKotranAndStatusOrderByTargetFieldNmAsc(String kotran, String status);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ando.dynamic_soa_mapper.repository;
|
||||
|
||||
import com.ando.dynamic_soa_mapper.entity.Transaction;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TransactionRepository extends JpaRepository<Transaction, String> {
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.ando.dynamic_soa_mapper.service;
|
||||
|
||||
import com.ando.dynamic_soa_mapper.entity.Payment;
|
||||
import com.ando.dynamic_soa_mapper.entity.SoaMappingConfig;
|
||||
import com.ando.dynamic_soa_mapper.entity.Transaction;
|
||||
import com.ando.dynamic_soa_mapper.repository.PaymentRepository;
|
||||
import com.ando.dynamic_soa_mapper.repository.SoaMappingConfigRepository;
|
||||
import com.ando.dynamic_soa_mapper.repository.TransactionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MappingEngineService {
|
||||
|
||||
private final TransactionRepository transactionRepo;
|
||||
private final PaymentRepository paymentRepo;
|
||||
private final SoaMappingConfigRepository configRepo;
|
||||
|
||||
// 1. SpEL Parser & Cache (Best Practice untuk Performance)
|
||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
|
||||
|
||||
public Map<String, Object> constructPayload(String transactionId, String kotran) {
|
||||
log.info("Memulai mapping payload untuk Transaction ID: {} dengan Kotran: {}", transactionId, kotran);
|
||||
|
||||
// 2. Fetch Data (Build The Context Foundation)
|
||||
Transaction transaction = transactionRepo.findById(transactionId)
|
||||
.orElseThrow(() -> new RuntimeException("Transaction tidak ditemukan: " + transactionId));
|
||||
|
||||
Payment payment = paymentRepo.findById(transaction.getPaymentId())
|
||||
.orElseThrow(() -> new RuntimeException("Payment tidak ditemukan untuk ID: " + transaction.getPaymentId()));
|
||||
|
||||
// 3. Setup SpEL Context
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
context.setVariable("transaction", transaction);
|
||||
context.setVariable("payment", payment);
|
||||
// Jika ke depan ada object baru (misal Customer), cukup tambahkan: context.setVariable("customer", customerData);
|
||||
|
||||
// 4. Fetch Mapping Rules
|
||||
List<SoaMappingConfig> configs = configRepo.findByKotranAndStatusOrderByTargetFieldNmAsc(kotran, "ACTIVE");
|
||||
|
||||
if (configs.isEmpty()) {
|
||||
throw new RuntimeException("Konfigurasi tidak ditemukan untuk Kotran: " + kotran);
|
||||
}
|
||||
|
||||
// Menggunakan LinkedHashMap agar urutan field JSON sesuai urutan konfigurasi di Database
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
|
||||
// 5. Evaluation Loop
|
||||
for (SoaMappingConfig config : configs) {
|
||||
Object evaluatedValue = null;
|
||||
|
||||
try {
|
||||
if (config.getExpressionRule() != null && !config.getExpressionRule().trim().isEmpty()) {
|
||||
Expression expression = expressionCache.computeIfAbsent(
|
||||
config.getExpressionRule(),
|
||||
rule -> parser.parseExpression(rule)
|
||||
);
|
||||
evaluatedValue = expression.getValue(context);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Gagal mengevaluasi rule '{}' untuk field {}. Error: {}",
|
||||
config.getExpressionRule(), config.getTargetFieldNm(), e.getMessage());
|
||||
}
|
||||
|
||||
// 6. Fallback Mechanism
|
||||
if (evaluatedValue == null && config.getDefaultValue() != null) {
|
||||
evaluatedValue = config.getDefaultValue();
|
||||
}
|
||||
|
||||
// 7. Mandatory Validation
|
||||
if (evaluatedValue == null && config.getIsMandatory() != null && config.getIsMandatory() == 1) {
|
||||
throw new RuntimeException(
|
||||
String.format("Field Mandatory gagal di-mapping! Kotran: %s, Field: %s",
|
||||
kotran, config.getTargetFieldNm())
|
||||
);
|
||||
}
|
||||
|
||||
// 8. [PERUBAHAN DISINI] Build Nested JSON based on Dot Notation
|
||||
if (evaluatedValue != null || (config.getIsMandatory() != null && config.getIsMandatory() == 0)) {
|
||||
// Tetap masukkan ke map meski null jika bukan mandatory (atau sesuai rule SOA-mu)
|
||||
putNestedValue(payload, config.getTargetFieldNm(), evaluatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Mapping selesai. Total config field yang diproses: {}", configs.size());
|
||||
return payload;
|
||||
}
|
||||
/**
|
||||
* Helper method untuk membangun nested Map berdasarkan Notasi Titik (Dot Notation).
|
||||
* Contoh: path "req.header.msgId" akan membuat map { "req": { "header": { "msgId": value } } }
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void putNestedValue(Map<String, Object> map, String path, Object value) {
|
||||
// Jika tidak ada titik, langsung put ke root map (Flat JSON)
|
||||
if (!path.contains(".")) {
|
||||
map.put(path, value);
|
||||
return;
|
||||
}
|
||||
|
||||
String[] keys = path.split("\\.");
|
||||
Map<String, Object> currentMap = map;
|
||||
|
||||
// Loop sampai elemen sebelum terakhir untuk membangun hierarki parent
|
||||
for (int i = 0; i < keys.length - 1; i++) {
|
||||
String key = keys[i];
|
||||
|
||||
// Jika parent belum ada, atau parent ada tapi bukan Map, buat LinkedHashMap baru
|
||||
currentMap.putIfAbsent(key, new LinkedHashMap<>());
|
||||
|
||||
// Pindah fokus ke map anak (child)
|
||||
currentMap = (Map<String, Object>) currentMap.get(key);
|
||||
}
|
||||
|
||||
// Put value asli di kunci paling ujung (leaf node)
|
||||
currentMap.put(keys[keys.length - 1], value);
|
||||
}
|
||||
|
||||
}
|
||||
14
src/main/resources/application.yaml
Normal file
14
src/main/resources/application.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
spring:
|
||||
application:
|
||||
name: dynamic-soa-mapper
|
||||
datasource:
|
||||
url: jdbc:oracle:thin:@//localhost:1521/FREEPDB1
|
||||
username: appuser
|
||||
password: appuser
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.OracleDialect
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ando.dynamic_soa_mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class DynamicSoaMapperApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user