Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions java/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
140 changes: 140 additions & 0 deletions java/MIGRATION.md

Copy link
Copy Markdown

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.

Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Required verification is undocumented

The contribution rules require performance analysis and sample translation-tool testing. The PR records neither result, leaving mandatory validation incomplete.

Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

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
```
58 changes: 58 additions & 0 deletions java/pom.xml
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>
18 changes: 18 additions & 0 deletions java/src/main/java/com/portfolio/PortfolioApplication.java
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)));
}
}
82 changes: 82 additions & 0 deletions java/src/main/java/com/portfolio/batch/BatchControlService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.portfolio.batch;

import com.portfolio.common.FileProcessingException;
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}:
* 'A' = active, 'C'/'D' = complete/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);
control.setStatus("A");
control.setStartTime(LocalTime.now().format(TIME_FMT));
control.setAttemptTimestamp(LocalDateTime.now().format(TS_FMT));
control.setRestartCount(control.getRestartCount() + 1);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
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) {
BatchControl control = find(jobName, processDate);
control.setStatus(returnCode > HistoryLoadStats.MAX_ERRORS ? "E" : "C");
Comment thread
staging-devin-ai-integration[bot] marked this conversation as resolved.
Outdated
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Control-record lookup key differs from COBOL

COBOL 1300-INIT-CHECKPOINTS reads the control record by job name alone (date and sequence left as spaces). find instead looks up (jobName, processDate, 1) with a hardcoded sequence of 1. Sample data and tests seed exactly this key, but a control record keyed differently is not found and the job fails. Worth confirming the intended BCHCTL key convention.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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);
}
}
Loading