-
Notifications
You must be signed in to change notification settings - Fork 6
Java migration foundation + HISTLD00 reference vertical slice #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e543728
5c2baf4
7fbadb3
1a917a4
52291b0
7dda782
ed7214a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| target/ |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # COBOL → Java Migration Guide | ||
|
|
||
| This directory contains the Java migration of the Enterprise COBOL Investment | ||
| Portfolio Management System. The original COBOL source remains under `src/` | ||
| for reference. The first vertical slice migrated end-to-end is **HISTLD00** | ||
| (Position History DB2 Load), which establishes the conventions below for all | ||
| subsequent programs. | ||
|
|
||
| ## Project layout | ||
|
|
||
| ``` | ||
| java/ | ||
| ├── pom.xml Maven, Java 17, Spring Boot 3 (Batch + Data JPA + H2) | ||
| └── src/main/java/com/portfolio/ | ||
| ├── model/copybook/ Step 1: copybook record layouts → POJOs | ||
| ├── domain/ Step 2: DB2 DDL + VSAM KSDS → JPA entities | ||
| ├── repository/ Spring Data JPA repositories | ||
| ├── common/ Step 3: DB2CONN/DB2CMT/ERRPROC migrations | ||
| └── batch/ Step 4: HISTLD00 Spring Batch job | ||
| ``` | ||
|
|
||
| ## Mapping conventions | ||
|
|
||
| ### Copybook → class (`model/copybook/`) | ||
|
|
||
| Each `01` record layout in `src/copybook/` becomes one Java class with the | ||
| same field names (COBOL-CASE → camelCase) and the original PIC clause | ||
| preserved in Javadoc. Level-88 condition names and constant copybooks | ||
| (BCHCON, COMMON) become `public static final` constants. `OCCURS n TIMES` | ||
| tables become `List<NestedType>`. | ||
|
|
||
| | COBOL | Java | | ||
| |---|---| | ||
| | `HISTREC` | `HistoryRecord` | | ||
| | `BCHCTL` | `BatchControlRecord` | | ||
| | `BCHCON` | `BatchControlConstants` | | ||
| | `CKPRST` | `CheckpointControl` | | ||
| | `PRCSEQ` | `ProcessSequenceRecord` | | ||
| | `TRNREC` | `TransactionRecord` | | ||
| | `POSREC` | `PositionRecord` | | ||
| | `PORTFLIO` | `PortfolioRecord` | | ||
| | `AUDITLOG` | `AuditRecord` | | ||
| | `RTNCODE`/`RETHND` | `ReturnCodeArea` | | ||
| | `ERRHAND` | `ErrorMessage` | | ||
| | `PORTVAL` | `PortfolioValidation` | | ||
| | `SQLCA` | `SqlStatusCodes` (constants only — see below) | | ||
| | `DBTBLS` (host variables) | JPA entities in `domain/` | | ||
|
|
||
| ### PIC → Java type | ||
|
|
||
| | COBOL PIC | Java type | | ||
| |---|---| | ||
| | `PIC X(n)` | `String` (length documented / `@Column(length = n)`) | | ||
| | `PIC 9(n)` / `PIC S9(n)` display | `int` / `long` (`String` if it encodes a date/time) | | ||
| | `PIC S9(4) COMP` | `int` | | ||
| | `PIC S9(9) COMP` | `long` (counters) or `int` (codes) | | ||
| | `PIC S9(n)V9(m) COMP-3` (packed decimal, financial) | `BigDecimal` — always, never `double` | | ||
| | Dates `PIC X(8)`/`PIC X(10)` (YYYYMMDD / ISO) | `LocalDate` in entities, `String` in raw copybook models | | ||
| | Times `PIC X(6)`/`PIC X(8)` | `LocalTime` in entities | | ||
| | Timestamps `PIC X(26)` | `LocalDateTime` in entities | | ||
| | Level-88 values | `static final` constants | | ||
|
|
||
| ### VSAM KSDS → relational table (`domain/`) | ||
|
|
||
| Each VSAM indexed file becomes a table named `VSAM_<cluster>` whose composite | ||
| primary key (`@EmbeddedId`) is exactly the COBOL `RECORD KEY`: | ||
|
|
||
| | VSAM file | Entity | Key (COBOL RECORD KEY) | | ||
| |---|---|---| | ||
| | TRANHIST | `TransactionHistoryFileRecord` | trans date + time + portfolio + sequence (`TH-KEY`) | | ||
| | BCHCTL | `BatchControl` | job name + process date + sequence (`BCT-KEY`) | | ||
|
|
||
| DB2 tables from `src/database/db2/` map 1:1 to entities: `POSHIST` → | ||
| `PositionHistory`, `ERRLOG` → `ErrorLog`, `RTNCODES` → `ReturnCodeLog`, | ||
| `PORTFOLIO_MASTER` → `PortfolioMaster`, `INVESTMENT_POSITIONS` → | ||
| `InvestmentPosition`, `TRANSACTION_HISTORY` → `TransactionHistory`. | ||
| `DECIMAL(p,s)` → `BigDecimal` with matching `precision`/`scale`. | ||
|
|
||
| ### EXEC SQL → JPA / Spring Data | ||
|
|
||
| - Singleton `SELECT`/`INSERT`/`UPDATE` → Spring Data repository methods. | ||
| - Cursors → paged reads (`RepositoryItemReader`) or streaming queries. | ||
| - `SQLCODE` checks → exceptions: non-zero SQLCODE branches become | ||
| `SqlProcessingException` (or Spring's `DataAccessException`); code-specific | ||
| behavior is preserved explicitly (e.g. `-803` duplicate → existence check). | ||
| - Two-character `FILE STATUS` checks → `FileProcessingException`, preserving | ||
| the original status value (e.g. `'23'` record not found). | ||
|
|
||
| ### Common subprograms → shared infrastructure (`common/`) | ||
|
|
||
| | COBOL | Java | | ||
| |---|---| | ||
| | `DB2CONN` (connect/disconnect/status, retry) | `DataSourceConfig` — Spring Boot pooled `DataSource` (HikariCP) | | ||
| | `DB2CMT` (commit/rollback/savepoint, frequency) | `TransactionHelper` + Spring transactions; in batch jobs, chunk boundaries | | ||
| | `ERRPROC` (format + write ERRLOG, return severity) | `ErrorHandlingService.logError(...)` → `ERRLOG` table, REQUIRES_NEW | | ||
|
|
||
| ### JCL → Spring Batch | ||
|
|
||
| | JCL / COBOL batch concept | Spring Batch | | ||
| |---|---| | ||
| | Job step running a program | `Job` + `Step` bean (`HistoryLoadJobConfig`) | | ||
| | Sequential file read loop | `ItemReader` | | ||
| | Record validation / field moves | `ItemProcessor` | | ||
| | DB2 INSERT + commit threshold | `ItemWriter` + chunk size (HISTLD00: 1000) | | ||
| | Checkpoint REWRITE of BCHCTL | `ChunkListener.afterChunk` → `BatchControlService` | | ||
| | `MOVE ... TO RETURN-CODE` | `ExitCodeGenerator` (JVM exit code) | | ||
| | Abort condition (`WS-ERROR-COUNT > 100`) | exception from processor fails the job | | ||
| | JCL parameters (dates etc.) | `JobParameters` | | ||
|
|
||
| ### CICS → REST (deferred) | ||
|
|
||
| The online layer (`src/programs/online/`, `src/maps/`) is **not** migrated in | ||
| this pass. BMS screens and CICS transactions do not translate mechanically — | ||
| they require a UI/API redesign (REST controllers + a separate frontend, | ||
| pseudo-conversational state → stateless requests, EIBAID keys → HTTP verbs). | ||
| Flagged as a separate redesign effort. The online copybooks are already | ||
| modeled in `model/copybook/` so the future REST layer can reuse them. | ||
|
|
||
| ## Reference slice: HISTLD00 | ||
|
|
||
| `src/programs/batch/HISTLD00.cbl` → `com.portfolio.batch`: | ||
|
|
||
| - Reads `VSAM_TRANHIST` in RECORD KEY order (`RepositoryItemReader`). | ||
| - Validates and maps TH-* → PH-* fields (`HistoryItemProcessor`). | ||
| - Inserts into `POSHIST`, skipping duplicates like SQLCODE `-803` | ||
| (`HistoryItemWriter`). | ||
| - Commits every 1000 records via the chunk size (WS-COMMIT-THRESHOLD). | ||
| - Updates the batch control record after each commit and at job end | ||
| (`BatchControlService`). | ||
| - Counts errors (`HistoryLoadStats`); aborts when the count exceeds 100 and | ||
| returns the error count as the process exit code (`HistoryLoadJobRunner`). | ||
|
|
||
| ## Building and running | ||
|
|
||
| ```bash | ||
| cd java | ||
| mvn test # runs the HISTLD00 job tests against embedded H2 | ||
| mvn spring-boot:run -Dspring-boot.run.profiles=histld00 \ | ||
| -Dspring-boot.run.arguments=20240320 # sample run with seeded data | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <?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"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-parent</artifactId> | ||
| <version>3.2.5</version> | ||
| <relativePath/> | ||
| </parent> | ||
|
|
||
| <groupId>com.portfolio</groupId> | ||
| <artifactId>portfolio-mgmt</artifactId> | ||
| <version>0.1.0-SNAPSHOT</version> | ||
| <name>portfolio-mgmt</name> | ||
| <description>Java migration of the Enterprise COBOL Investment Portfolio Management System</description> | ||
|
|
||
| <properties> | ||
| <java.version>17</java.version> | ||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-batch</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-data-jpa</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.h2database</groupId> | ||
| <artifactId>h2</artifactId> | ||
| <scope>runtime</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-test</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.batch</groupId> | ||
| <artifactId>spring-batch-test</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-maven-plugin</artifactId> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.portfolio; | ||
|
|
||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
|
|
||
| /** | ||
| * Java migration of the Enterprise COBOL Investment Portfolio Management System. | ||
| * | ||
| * <p>The CICS online layer (src/programs/online, src/maps) is intentionally NOT | ||
| * migrated here — it is flagged as a separate redesign effort (CICS → REST). | ||
| */ | ||
| @SpringBootApplication | ||
| public class PortfolioApplication { | ||
|
|
||
| public static void main(String[] args) { | ||
| System.exit(SpringApplication.exit(SpringApplication.run(PortfolioApplication.class, args))); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package com.portfolio.batch; | ||
|
|
||
| import com.portfolio.common.FileProcessingException; | ||
| import com.portfolio.model.copybook.BatchControlConstants; | ||
| import com.portfolio.domain.BatchControl; | ||
| import com.portfolio.repository.BatchControlRepository; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.time.LocalTime; | ||
| import java.time.format.DateTimeFormatter; | ||
|
|
||
| /** | ||
| * Batch control file handling from HISTLD00 (1300-INIT-CHECKPOINTS, | ||
| * 2310-UPDATE-CHECKPOINT) against the BCHCTL migration table. | ||
| * | ||
| * <p>Status values are from {@code src/copybook/batch/BCHCON.cpy}: | ||
| * 'R' = ready, 'A' = active, 'W' = waiting, 'D' = done, 'E' = error. | ||
| */ | ||
| @Service | ||
| public class BatchControlService { | ||
|
|
||
| private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:ss"); | ||
|
|
||
| /** DB2-style 26-character timestamp, matching BCT-ATTEMPT-TS / BCT-COMPLETE-TS PIC X(26). */ | ||
| private static final DateTimeFormatter TS_FMT = | ||
| DateTimeFormatter.ofPattern("yyyy-MM-dd-HH.mm.ss.SSSSSS"); | ||
|
|
||
| private final BatchControlRepository repository; | ||
|
|
||
| public BatchControlService(BatchControlRepository repository) { | ||
| this.repository = repository; | ||
| } | ||
|
|
||
| /** | ||
| * HISTLD00 1300-INIT-CHECKPOINTS: read the control record for the job and | ||
| * mark it active. INVALID KEY (record not found) becomes a | ||
| * {@link FileProcessingException} with FILE STATUS '23'. | ||
| */ | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public BatchControl.Key markActive(String jobName, String processDate) { | ||
| BatchControl control = find(jobName, processDate); | ||
| // Restart = previous attempt did not finish cleanly (still ACTIVE or ERROR) | ||
| if (BatchControlConstants.STAT_ACTIVE.equals(control.getStatus()) | ||
| || BatchControlConstants.STAT_ERROR.equals(control.getStatus())) { | ||
| control.setRestartCount(control.getRestartCount() + 1); | ||
| } | ||
| control.setStatus(BatchControlConstants.STAT_ACTIVE); | ||
| control.setStartTime(LocalTime.now().format(TIME_FMT)); | ||
| control.setAttemptTimestamp(LocalDateTime.now().format(TS_FMT)); | ||
| repository.save(control); | ||
| return control.getKey(); | ||
| } | ||
|
|
||
| /** HISTLD00 2310-UPDATE-CHECKPOINT: persist read/written counters. */ | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void updateCheckpoint(String jobName, String processDate, | ||
| long recordsRead, long recordsWritten) { | ||
| BatchControl control = find(jobName, processDate); | ||
| control.setRecordsRead(recordsRead); | ||
| control.setRecordsWritten(recordsWritten); | ||
| repository.save(control); | ||
| } | ||
|
|
||
| /** Final control update at job end: status, counters, and return code. */ | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void markComplete(String jobName, String processDate, | ||
| long recordsRead, long recordsWritten, int returnCode, | ||
| boolean jobFailed) { | ||
| BatchControl control = find(jobName, processDate); | ||
| control.setStatus(jobFailed || returnCode > HistoryLoadStats.MAX_ERRORS | ||
| ? BatchControlConstants.STAT_ERROR | ||
| : BatchControlConstants.STAT_DONE); | ||
|
Comment on lines
+73
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Terminal DONE/ERROR status is new behavior vs COBOL
Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional deviation: COBOL HISTLD00 indeed leaves the control record ACTIVE, but that makes success indistinguishable from a crash mid-run. Since BCHCON already defines D/E, writing a terminal status gives operators (and the restart-count logic) a reliable signal. It's documented as a conscious improvement; can revert to strict fidelity if preferred. |
||
| control.setRecordsRead(recordsRead); | ||
| control.setRecordsWritten(recordsWritten); | ||
| control.setReturnCode(returnCode); | ||
| control.setEndTime(LocalTime.now().format(TIME_FMT)); | ||
| control.setCompleteTimestamp(LocalDateTime.now().format(TS_FMT)); | ||
| repository.save(control); | ||
| } | ||
|
|
||
| private BatchControl find(String jobName, String processDate) { | ||
| return repository.findById(new BatchControl.Key(jobName, processDate, 1)) | ||
| .orElseThrow(() -> new FileProcessingException( | ||
| "Control record not found for job " + jobName | ||
| + " date " + processDate, "23")); | ||
| } | ||
|
Comment on lines
+84
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Control-record lookup key differs from COBOL COBOL Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional convention: BCHCTL's RECORD KEY is (job name, process date, sequence), and the COBOL read-by-job-name-only works because the benchmark's VSAM file holds a single record per job. Keying the lookup on (jobName, processDate, 1) makes the control table hold one row per job per business date, which the checkpoint/restart design needs once multiple dates coexist in a table. Sequence is fixed at 1 because HISTLD00 is a single-step job; multi-step slices would pass their step sequence. Documented convention — happy to adjust if maintainers prefer strict job-name-only lookup. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.portfolio.batch; | ||
|
|
||
| /** | ||
| * Thrown to abort the HISTLD00 job when the error count exceeds 100, | ||
| * matching the COBOL loop condition | ||
| * {@code PERFORM 2000-PROCESS UNTIL END-OF-FILE OR WS-ERROR-COUNT > 100}. | ||
| */ | ||
| public class ErrorLimitExceededException extends RuntimeException { | ||
|
|
||
| public ErrorLimitExceededException(long errorCount) { | ||
| super("HISTLD00 aborted: error count " + errorCount + " exceeded limit of " | ||
| + HistoryLoadStats.MAX_ERRORS); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Performance impact remains undocumented
Each accepted row performs an existence query and save. The required performance analysis does not measure query count, batching, or representative throughput.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
Playground