Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
114 changes: 114 additions & 0 deletions java-migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# COBOL-Examples Java Spring Boot Migration

This project is a migration of [COG-GTM/COBOL-Examples](https://github.com/COG-GTM/COBOL-Examples) from GnuCOBOL to Java Spring Boot. Each COBOL program has been translated into idiomatic Java, preserving the original business logic while leveraging modern frameworks and patterns.

## Prerequisites

- **Java 17+** (JDK)
- **Maven 3.8+**
- **PostgreSQL** (for running the application) or **Docker** (for Testcontainers integration tests)

## Project Structure

```
java-migration/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/com/example/cobolmigration/
│ │ │ ├── Application.java # Spring Boot entry point
│ │ │ ├── model/ # Domain model classes
│ │ │ │ ├── Account.java # JPA entity (sql/sql_example.cbl)
│ │ │ │ ├── Customer.java # Abstract base (redefines/redefines.cbl)
│ │ │ │ ├── PersonCustomer.java # Person variant (type=1)
│ │ │ │ ├── CorpCustomer.java # Corporate variant (type=2)
│ │ │ │ ├── CustomerRecord.java # File records (merge_sort/)
│ │ │ │ ├── StudentRecord.java # Report records (report_writer/)
│ │ │ │ └── SerializableRecord.java # JSON/XML (json_generate/, xml_generate/)
│ │ │ ├── repository/
│ │ │ │ └── AccountRepository.java # JPA repository (replaces SQL cursors)
│ │ │ ├── service/
│ │ │ │ ├── StringUtilService.java # trim, unstring, isNumeric
│ │ │ │ ├── SearchService.java # linear & binary search
│ │ │ │ ├── NumericConversionService.java # COMP/DISPLAY conversions
│ │ │ │ ├── AccountService.java # Account business logic
│ │ │ │ ├── FileMergeService.java # File merge & sort
│ │ │ │ ├── ReportService.java # Report generation
│ │ │ │ └── SerializationService.java # JSON & XML generation
│ │ │ └── cli/
│ │ │ ├── AccountCommands.java # Account shell commands
│ │ │ ├── UtilityCommands.java # Utility demo commands
│ │ │ └── SubProgramCommands.java # Sub-program demo commands
│ │ └── resources/
│ │ ├── application.yml # Spring Boot configuration
│ │ └── db/migration/
│ │ └── V1__create_accounts.sql # Flyway migration
│ └── test/
│ └── java/com/example/cobolmigration/
│ ├── service/
│ │ ├── StringUtilServiceTest.java
│ │ ├── SearchServiceTest.java
│ │ ├── SerializationServiceTest.java
│ │ └── FileMergeServiceTest.java
│ └── repository/
│ └── AccountRepositoryIT.java # Testcontainers integration test
```

## How to Build

```bash
cd java-migration
mvn clean install
```

To skip integration tests (which require Docker):

```bash
mvn clean install -DskipTests
```

To run only unit tests:

```bash
mvn test -Dtest='!*IT'
```

## How to Run

The application starts as a Spring Shell CLI:

```bash
mvn spring-boot:run
```

> **Note:** Requires a running PostgreSQL instance at `localhost:5432` with a database named `cobol_db_example`. See `application.yml` for connection settings.

## Available Shell Commands

| Command | COBOL Equivalent | Description |
|---|---|---|
| `display-all` | sql_example.cbl menu option 1 | Display all accounts |
| `display-disabled` | sql_example.cbl menu option 2 | Display disabled accounts |
| `search --term <text>` | sql_example.cbl menu option 3 | Search accounts by name/phone/address |
| `trim-demo --input <text>` | trim/trim.cbl | Demonstrate trim operations |
| `unstring-demo --input <text> --delimiter <delim>` | unstring/unstring.cbl | Split string by delimiter |
| `is-numeric --input <text>` | is_numeric/is_numeric.cbl | Check if string is numeric |
| `generate-json --name <n> --value <v> --enabled <e>` | json_generate/json_generate.cbl | Generate JSON from record |
| `generate-xml --name <n> --value <v> --enabled <e>` | xml_generate/xml_generate.cbl | Generate XML from record |
| `merge-sort-demo` | merge_sort/merge_sort_test.cbl | Run file merge and sort demo |
| `report-demo` | report_writer/report_test.cbl | Generate student report |
| `call-by-content --item1 <v> --item2 <v>` | sub_program/main_app.cbl (BY CONTENT) | Demo pass-by-value |
| `call-by-reference --item1 <v> --item2 <v>` | sub_program/main_app.cbl (BY REFERENCE) | Demo pass-by-reference |
| `cancel-sub-program` | sub_program/main_app.cbl (CANCEL) | Reset working-storage |
| `show-state` | — | Show current working-storage state |

## Migration Approach

- **COBOL REDEFINES** replaced by Java class inheritance (Customer hierarchy)
- **Embedded SQL / Cursors** replaced by Spring Data JPA repository methods
- **COBOL file I/O (FD)** replaced by `java.nio.file` operations
- **COBOL SORT/MERGE** replaced by `java.util.Collections.sort()`
- **COBOL SEARCH / SEARCH ALL** replaced by Stream filter and `Collections.binarySearch()`
- **COBOL CALL BY CONTENT/REFERENCE** demonstrated via Spring Shell commands
- **COBOL JSON/XML GENERATE** replaced by Jackson ObjectMapper and XmlMapper
- **COBOL Report Writer** replaced by String formatting
94 changes: 94 additions & 0 deletions java-migration/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?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.example.cobolmigration</groupId>
<artifactId>cobol-migration</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cobol-migration</name>
<description>Migration of COG-GTM/COBOL-Examples from GnuCOBOL to Java Spring Boot</description>

<properties>
<java.version>17</java.version>
<spring-shell.version>3.2.4</spring-shell.version>
<testcontainers.version>1.19.7</testcontainers.version>
</properties>

<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Spring Shell -->
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>${spring-shell.version}</version>
</dependency>

<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<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,17 @@
package com.example.cobolmigration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Main Spring Boot application class.
* Migrated from the collection of GnuCOBOL example programs in COG-GTM/COBOL-Examples.
* Starts a Spring Shell CLI that exposes commands equivalent to the original COBOL programs.
*/
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.cobolmigration.cli;

import com.example.cobolmigration.service.AccountService;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;

/**
* Spring Shell component mapping the main menu from sql/sql_example.cbl (lines 157-185).
*
* COBOL menu:
* 1) Display all accounts
* 2) Display disabled accounts
* 3) Query accounts
* 4) Exit
*/
@ShellComponent
public class AccountCommands {

private final AccountService accountService;

public AccountCommands(AccountService accountService) {
this.accountService = accountService;
}

@ShellMethod("Display all accounts")
public String displayAll() {
return accountService.formatAccountTable(accountService.getAllAccounts());
}

@ShellMethod("Display disabled accounts")
public String displayDisabled() {
return accountService.formatAccountTable(accountService.getDisabledAccounts());
}

@ShellMethod("Search accounts")
public String search(@ShellOption String term) {
return accountService.formatAccountTable(accountService.searchAccounts(term));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.example.cobolmigration.cli;

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;

/**
* Spring Shell component mapping sub_program/main_app.cbl and sub_program/sub.cbl.
*
* Demonstrates:
* - By-content calling: pass copies of values, originals unchanged
* - By-reference calling: pass mutable objects, callee can modify
* - Cancel command: resets the service's internal state (mapping COBOL CANCEL
* which resets working-storage)
*
* In COBOL, working-storage persists between CALL invocations and is only reset
* on CANCEL. Local-storage is re-initialized on each call. We simulate this with
* instance fields (working-storage) that persist between commands and can be
* explicitly reset via the cancel command.
*/
@ShellComponent
public class SubProgramCommands {

// Simulates COBOL working-storage variables that persist between calls
private String wsItem1 = "";
private String wsItem2 = "";

@ShellMethod("Call sub-program by content (copies values, originals unchanged)")
public String callByContent(@ShellOption String item1, @ShellOption String item2) {
// By-content: work with copies, don't modify the passed values
String localItem1 = item1;
String localItem2 = item2;

StringBuilder sb = new StringBuilder();
sb.append("CALL BY CONTENT:\n");
sb.append(" Input item-1: ").append(localItem1).append("\n");
sb.append(" Input item-2: ").append(localItem2).append("\n");
sb.append(" Working-storage before: ws-item-1=\"").append(wsItem1)
.append("\", ws-item-2=\"").append(wsItem2).append("\"\n");

// Sub-program copies linkage values to working-storage
wsItem1 = localItem1;
wsItem2 = localItem2;

// Sub-program modifies linkage values, but by-content means originals are unchanged
String modifiedItem1 = "replace1";
String modifiedItem2 = "replace2";

sb.append(" Working-storage after: ws-item-1=\"").append(wsItem1)
.append("\", ws-item-2=\"").append(wsItem2).append("\"\n");
sb.append(" Modified copies: item-1=\"").append(modifiedItem1)
.append("\", item-2=\"").append(modifiedItem2).append("\"\n");
sb.append(" Originals unchanged: item-1=\"").append(item1)
.append("\", item-2=\"").append(item2).append("\"");
return sb.toString();
}

@ShellMethod("Call sub-program by reference (callee can modify values)")
public String callByReference(@ShellOption String item1, @ShellOption String item2) {
StringBuilder sb = new StringBuilder();
sb.append("CALL BY REFERENCE:\n");
sb.append(" Input item-1: ").append(item1).append("\n");
sb.append(" Input item-2: ").append(item2).append("\n");
sb.append(" Working-storage before: ws-item-1=\"").append(wsItem1)
.append("\", ws-item-2=\"").append(wsItem2).append("\"\n");

// Sub-program copies linkage values to working-storage
wsItem1 = item1;
wsItem2 = item2;

// By-reference: sub-program modifies the passed values
String modifiedItem1 = "replace1";
String modifiedItem2 = "replace2";

sb.append(" Working-storage after: ws-item-1=\"").append(wsItem1)
.append("\", ws-item-2=\"").append(wsItem2).append("\"\n");
sb.append(" Modified (by reference): item-1=\"").append(modifiedItem1)
.append("\", item-2=\"").append(modifiedItem2).append("\"");
return sb.toString();
}

@ShellMethod("Cancel sub-program (resets working-storage)")
public String cancelSubProgram() {
wsItem1 = "";
wsItem2 = "";
return "Sub-program cancelled. Working-storage reset to initial values.\n"
+ "ws-item-1=\"" + wsItem1 + "\", ws-item-2=\"" + wsItem2 + "\"";
}

@ShellMethod("Show current working-storage state")
public String showState() {
return "Current working-storage state:\n"
+ " ws-item-1=\"" + wsItem1 + "\"\n"
+ " ws-item-2=\"" + wsItem2 + "\"";
}
}
Loading