diff --git a/java-migration/.gitignore b/java-migration/.gitignore new file mode 100644 index 0000000..2469533 --- /dev/null +++ b/java-migration/.gitignore @@ -0,0 +1,13 @@ +# Gradle +.gradle/ +build/ + +# IDE +.classpath +.project +.settings/ +.idea/ +*.iml + +# OS +.DS_Store diff --git a/java-migration/MIGRATION_NOTES.md b/java-migration/MIGRATION_NOTES.md new file mode 100644 index 0000000..9010025 --- /dev/null +++ b/java-migration/MIGRATION_NOTES.md @@ -0,0 +1,162 @@ +# Migration Notes: COBOL to Java Spring Boot + +This document records the key translation decisions made during the migration from GnuCOBOL to Java Spring Boot. + +## Data Type Translations + +### COBOL PIC Clauses to Java Types + +| COBOL Type | Example | Java Type | Notes | +|---|---|---|---| +| `PIC X(n)` | `PIC X(8)` | `String` | `.trim()` applied at data boundaries to handle COBOL space-padding | +| `PIC 9(n)` | `PIC 9(5)` | `int` or `long` | Width mapped to appropriate integer type | +| `PIC 9(n) COMP` | `PIC 999 COMP` | `BigDecimal` (via `NumericUtils.fromComp`) | Binary integer storage → BigDecimal for precision | +| `PIC COMP-2` | `ws-total COMP-2` | `BigDecimal` (via `NumericUtils.fromComp2`) | Double-precision float → BigDecimal to avoid FP errors | +| `PIC COMP-3` | Packed decimal | `BigDecimal` (via `NumericUtils.fromComp3`) | BCD packed → BigDecimal with explicit scale | +| `PIC COMP-5` | `PIC S9(4) COMP-5` | `int` | Native binary → primitive int | +| `PIC X` (single char) | `ws-sql-account-is-enabled` | `Character` | Single character fields map to Character wrapper | +| `PIC X(20)` (timestamp) | `ws-sql-account-create-dt` | `LocalDateTime` | COBOL string timestamps → Java temporal types | +| `PIC ZZ9` | Dynamic display | `String` via `NumericUtils.toDynamicDisplay` | Leading zero suppression | +| 88-level conditions | `88 ws-account-enabled VALUE 'Y'` | Boolean methods or Character comparison | Condition names → explicit comparisons | + +### REDEFINES → Inheritance / Conversion Methods + +COBOL `REDEFINES` allows multiple interpretations of the same memory area. In Java: +- **Simple redefines**: Use type conversion methods (e.g., `Integer.parseInt(stringField)`) +- **Complex redefines**: Use separate classes with conversion constructors +- **Union-type redefines**: Use Java records or sealed classes with pattern matching + +Example from `redifines/redefines.cbl`: A COMP-2 field redefining a PIC X field becomes a conversion method that parses the string representation into a BigDecimal. + +## Database Layer Translations + +### Cursor Fetch → JPA Queries + +| COBOL Pattern | Java Equivalent | +|---|---| +| `DECLARE cursor CURSOR FOR SELECT...` | Spring Data JPA method or `@Query` annotation | +| `OPEN cursor` | Implicit (JPA manages connection lifecycle) | +| `FETCH cursor INTO :host-vars` | Return value from repository method | +| `CLOSE cursor` | Implicit (JPA manages connection lifecycle) | +| `CONNECT TO :connection-string` | `application.properties` + Spring auto-configuration | +| `CONNECT RESET` | Implicit (connection pool management) | +| `SQLSTATE` / `SQLCODE` checking | Exception handling (try/catch or `@Transactional`) | + +### Specific Cursor Mappings + +| COBOL Cursor | SQL | Java Method | +|---|---|---| +| `ACCOUNT-ALL-CUR` | `SELECT ... FROM ACCOUNTS ORDER BY ID` | `findAllByOrderByIdAsc()` | +| `ACCOUNT-DISABLED-CUR` | `SELECT ... WHERE IS_ENABLED = 'N' ORDER BY ID` | `findByIsEnabledOrderByIdAsc('N')` | +| `ACCOUNT-QUERY-CUR` | `SELECT ... WHERE FIRST_NAME LIKE :val OR ...` | `searchAccounts(String)` with `@Query` | + +### Variable-Length Strings in SQL + +COBOL uses `PIC S9(4) COMP-5` + `PIC X(50)` pair for variable-length strings in WHERE clauses (sql_example.cbl lines 65-67). In Java, JPA/JDBC handles string length automatically — no special handling needed. + +## Subprogram Call Translations + +### BY CONTENT vs BY REFERENCE + +| COBOL Pattern | Java Equivalent | Semantics | +|---|---|---| +| `CALL "sub" USING BY CONTENT ws-var` | `callByContent(String value)` | Pass immutable copy — caller's variable unchanged | +| `CALL "sub" USING ws-var` (BY REFERENCE) | `callByReference(MutableString ref)` | Pass mutable wrapper — caller's variable can be modified | +| `CANCEL "sub-app"` | `cancel()` | Reset service instance state | + +### Storage Section Mapping + +| COBOL Section | Java Equivalent | Lifetime | +|---|---|---| +| WORKING-STORAGE SECTION | Instance fields | Persists between method calls until `cancel()` | +| LOCAL-STORAGE SECTION | Method-local variables | Fresh on each invocation | +| LINKAGE SECTION | Method parameters | Passed by caller | + +## File Processing Translations + +### SORT/MERGE → Java Collections + +| COBOL Statement | Java Equivalent | +|---|---| +| `MERGE fd ON ASCENDING KEY f-id USING f1 f2 GIVING f-out` | Read both files → `ArrayList.addAll()` → `Collections.sort()` → write output | +| `SORT fd ON DESCENDING KEY f-contract-id USING f-in GIVING f-out` | Read file → `sort(Comparator.reversed())` → write output | +| `SD fd-sorting-file` (sort description) | Not needed — sorting is in-memory | +| File status checking (`ws-fs-status`) | IOException handling | + +### Fixed-Width Records + +COBOL FD records use fixed-width fields defined by PIC clauses. In Java: +- Parsing: `String.substring()` with calculated offsets +- Writing: `String.format()` with width specifiers +- The `CustomerRecord.fromFixedWidth()` and `toString()` methods handle this conversion + +## Serialization Translations + +### JSON GENERATE → Jackson + +| COBOL Feature | Jackson Equivalent | +|---|---| +| `JSON GENERATE ws-output FROM ws-record` | `objectMapper.writeValueAsString(dto)` | +| `NAME OF ws-field IS "jsonName"` | `@JsonProperty("jsonName")` | +| `COUNT IN ws-count` | `json.length()` | +| Suppress blank fields | `@JsonInclude(JsonInclude.Include.NON_EMPTY)` | + +### XML GENERATE → JAXB + +| COBOL Feature | JAXB Equivalent | +|---|---| +| `XML GENERATE ws-output FROM ws-record` | `marshaller.marshal(dto, writer)` | +| `WITH XML-DECLARATION` | `Marshaller.JAXB_FRAGMENT = false` | +| `NAME OF ws-field IS "xmlName"` | `@XmlElement(name = "xmlName")` | +| `TYPE OF ws-field IS ATTRIBUTE` | `@XmlAttribute(name = "attrName")` | +| `SUPPRESS WHEN SPACES` | Set field to `null` in constructor when blank | +| `COUNT IN ws-count` | `xml.length()` | + +## Report Writer Translation + +### RD (Report Description) → ReportService + +| COBOL Feature | Java Equivalent | +|---|---| +| `RD r-report PAGE LIMIT IS 66` | `PAGE_LIMIT = 66` constant | +| `HEADING IS 1` | Header generated at start of each page | +| `FIRST DETAIL 6` | Detail lines start after 5 header lines | +| `LAST DETAIL 42` | `maxDetailsPerPage = 42 - 6 + 1 = 37` | +| `TYPE REPORT HEADING` | `generatePageHeader()` method | +| `TYPE DETAIL LINE PLUS 1` | `formatDetailLine()` method | +| `SOURCE page-counter` | `pageNumber` local variable | +| `INITIATE r-report` | Constructor / method start | +| `GENERATE report-line` | Add formatted line to list | +| `TERMINATE r-report` | Return the complete list | + +## String Operation Translations + +| COBOL Operation | Java Equivalent | Source File | +|---|---|---| +| `FUNCTION TRIM(val)` | `String.trim()` / `StringUtils.trim()` | `trim/trim.cbl` | +| `FUNCTION TRIM(val LEADING)` | `String.stripLeading()` | `trim/trim.cbl` | +| `FUNCTION TRIM(val TRAILING)` | `String.stripTrailing()` | `trim/trim.cbl` | +| `UNSTRING src DELIMITED BY delim INTO dest1 dest2` | `StringUtils.unstring()` / `String.split()` | `unstring/unstring.cbl` | +| `val IS NUMERIC` | `StringUtils.isNumeric()` (regex) | `is_numeric/is_numeric.cbl` | +| `FUNCTION NUMVAL(val)` | `StringUtils.numval()` → `BigDecimal` | `numval_test/numval_test.cbl` | +| `ACCEPT val FROM COMMAND-LINE` | `ApplicationArguments.getSourceArgs()` | `read_command_args/` | +| `INSPECT ... TALLYING ... FOR ALL` | `String.contains()` / stream count | `read_command_args/` | +| `FUNCTION UPPER-CASE(val)` | `String.toUpperCase()` | `sql_example.cbl` | +| `FUNCTION LOWER-CASE(val)` | `String.toLowerCase()` | `read_cmd_line_args.cbl` | +| `FUNCTION STORED-CHAR-LENGTH(val)` | `String.length()` (after trim) | `sql_example.cbl` | + +## Design Decisions + +1. **BigDecimal over primitives**: All financial/numeric-precision fields use `BigDecimal` to prevent floating-point errors that COBOL's packed decimal types naturally avoid. + +2. **`.trim()` at boundaries**: COBOL PIC X fields are right-padded with spaces. We apply `.trim()` when reading from database, files, or user input to normalize data. + +3. **Spring profiles for CLI vs REST**: The `cli` profile activates the interactive Scanner-based menu. The default profile runs only the REST API, providing a modern HTTP interface. + +4. **H2 for testing**: Tests use an in-memory H2 database in PostgreSQL compatibility mode, avoiding the need for a running PostgreSQL instance during CI/CD. + +5. **No ORM for file processing**: The `FileMergeService` uses plain file I/O (`java.nio.file`) rather than JPA, since the COBOL SORT/MERGE operates on sequential files, not database tables. + +6. **Mutable wrapper for by-reference**: Java strings are immutable, so we introduced `SubProgramService.MutableString` to faithfully model COBOL's CALL BY REFERENCE semantics where the called program can modify the caller's variables. + +7. **Null for SUPPRESS WHEN SPACES**: COBOL's `SUPPRESS WHEN SPACES` in XML GENERATE is implemented by setting blank fields to `null` in the DTO constructor, which causes JAXB to omit them from output. diff --git a/java-migration/README.md b/java-migration/README.md new file mode 100644 index 0000000..5d2fb6c --- /dev/null +++ b/java-migration/README.md @@ -0,0 +1,142 @@ +# COBOL-to-Java Spring Boot Migration + +This directory contains the Java Spring Boot equivalent of the COBOL programs in the parent repository. The migration preserves the original business logic while modernizing the technology stack. + +## Technology Stack + +- **Java 17+** +- **Spring Boot 3.2.x** +- **Spring Data JPA** (replaces embedded SQL / ODBC) +- **PostgreSQL** (same database as COBOL version) +- **Jackson** (replaces COBOL JSON GENERATE) +- **JAXB** (replaces COBOL XML GENERATE) +- **JUnit 5 + AssertJ** (testing) +- **H2** (in-memory database for tests) +- **Gradle** (build system) + +## Project Structure + +``` +java-migration/ +├── build.gradle +├── settings.gradle +├── README.md +├── MIGRATION_NOTES.md +├── src/ +│ ├── main/ +│ │ ├── java/com/cobolmigration/ +│ │ │ ├── CobolMigrationApplication.java # Spring Boot entry point +│ │ │ ├── model/ +│ │ │ │ ├── Account.java # JPA entity (sql_example.cbl) +│ │ │ │ ├── CustomerRecord.java # File record (merge_sort_test.cbl) +│ │ │ │ └── StudentRecord.java # Report record (report_test.cbl) +│ │ │ ├── repository/ +│ │ │ │ └── AccountRepository.java # JPA repository (3 COBOL cursors) +│ │ │ ├── service/ +│ │ │ │ ├── AccountService.java # Account CRUD (sql_example.cbl) +│ │ │ │ ├── FileMergeService.java # SORT/MERGE (merge_sort_test.cbl) +│ │ │ │ ├── SubProgramService.java # CALL patterns (sub_program/) +│ │ │ │ ├── JsonService.java # JSON gen (json_generate.cbl) +│ │ │ │ ├── XmlService.java # XML gen (xml_generate.cbl) +│ │ │ │ └── ReportService.java # Report Writer (report_test.cbl) +│ │ │ ├── dto/ +│ │ │ │ ├── RecordDto.java # JSON DTO with @JsonProperty +│ │ │ │ └── RecordXmlDto.java # XML DTO with @XmlAttribute +│ │ │ ├── util/ +│ │ │ │ ├── StringUtils.java # TRIM, UNSTRING, IS NUMERIC, NUMVAL +│ │ │ │ └── NumericUtils.java # COMP/COMP-3 handling +│ │ │ ├── cli/ +│ │ │ │ ├── AccountCli.java # Interactive menu (sql_example.cbl) +│ │ │ │ └── CommandLineArgsRunner.java # CLI args (read_cmd_line_args.cbl) +│ │ │ └── controller/ +│ │ │ └── AccountController.java # REST API alternative +│ │ └── resources/ +│ │ ├── application.properties # DB config +│ │ └── db/migration/ +│ │ └── V1__create_accounts_table.sql # Schema + seed data +│ └── test/ +│ ├── java/com/cobolmigration/ +│ │ ├── repository/AccountRepositoryTest.java +│ │ ├── service/ +│ │ │ ├── AccountServiceTest.java +│ │ │ ├── FileMergeServiceTest.java +│ │ │ ├── SubProgramServiceTest.java +│ │ │ ├── JsonServiceTest.java +│ │ │ ├── XmlServiceTest.java +│ │ │ └── ReportServiceTest.java +│ │ ├── util/ +│ │ │ ├── StringUtilsTest.java +│ │ │ └── NumericUtilsTest.java +│ │ └── controller/AccountControllerTest.java +│ └── resources/ +│ └── application-test.properties +``` + +## COBOL Module to Java Mapping + +| COBOL Module | COBOL File(s) | Java Equivalent | Description | +|---|---|---|---| +| SQL Database Access | `sql/sql_example.cbl` | `Account.java`, `AccountRepository.java`, `AccountService.java` | PostgreSQL CRUD with cursors → JPA + Spring Data | +| SQL Schema | `sql/create_test_db.sql` | `V1__create_accounts_table.sql` | Database schema and seed data | +| JSON Generation | `json_generate/json_generate.cbl` | `RecordDto.java`, `JsonService.java` | JSON GENERATE → Jackson ObjectMapper | +| XML Generation | `xml_generate/xml_generate.cbl` | `RecordXmlDto.java`, `XmlService.java` | XML GENERATE → JAXB Marshaller | +| Sort/Merge | `merge_sort/merge_sort_test.cbl` | `CustomerRecord.java`, `FileMergeService.java` | SORT/MERGE → Collections.sort + Streams | +| Report Writer | `report_writer/report_test.cbl` | `StudentRecord.java`, `ReportService.java` | RD report → String.format() | +| Subprogram Calls | `sub_program/main_app.cbl`, `sub_program/sub.cbl` | `SubProgramService.java` | CALL BY CONTENT/REFERENCE → method params | +| String Trim | `trim/trim.cbl` | `StringUtils.trim/trimLeading/trimTrailing` | FUNCTION TRIM → String.strip* | +| Unstring | `unstring/unstring.cbl` | `StringUtils.unstring()` | UNSTRING → String.split with regex | +| Is Numeric | `is_numeric/is_numeric.cbl` | `StringUtils.isNumeric()` | IS NUMERIC → regex pattern | +| Numval | `numval_test/numval_test.cbl` | `StringUtils.numval()` | FUNCTION NUMVAL → BigDecimal parsing | +| COMP Types | `comp_test/comp_test.cbl` | `NumericUtils` | COMP/COMP-2/COMP-3 → BigDecimal | +| Command Args | `read_command_args/read_cmd_line_args.cbl` | `CommandLineArgsRunner.java` | ACCEPT FROM COMMAND-LINE → ApplicationArguments | +| Terminal Menu | `sql/sql_example.cbl` (menu loop) | `AccountCli.java` | ACCEPT/DISPLAY menu → Scanner-based CLI | +| REST API | N/A (new) | `AccountController.java` | Modern HTTP alternative to terminal UI | +| Accept Input | `accept/accept.cbl`, `accept/accept_from.cbl` | Covered by CLI + Spring Boot args | ACCEPT → Scanner / ApplicationArguments | +| Secure Accept | `accept/accept-secure.cbl` | Covered by CLI (Console.readPassword) | ACCEPT SECURE → Console.readPassword | +| Display | `display_test/display_test.cbl` | Standard System.out.println | DISPLAY → println | +| Display Timing | `display_timing/display_timing.cbl` | System.nanoTime() | ACCEPT FROM TIME → Instant.now() | +| Screen Size | `screen_size/get_screen_size.cbl` | N/A (terminal-specific) | CBL_GET_SCR_SIZE → not applicable in web context | +| Mouse Input | `mouse/mouse.cbl` | N/A (terminal-specific) | Mouse drawing → not applicable | +| Search/Search All | `search/search.cbl` | Java Collections / binary search | SEARCH/SEARCH ALL → List operations | +| Redefines | `redifines/redefines.cbl` | Java inheritance / conversion methods | REDEFINES → type conversion | + +## Running the Application + +### Prerequisites +- Java 17+ +- PostgreSQL with `cobol_db_example` database (see `sql/create_test_db.sql`) + +### Build +```bash +cd java-migration +./gradlew build +``` + +### Run (REST API mode) +```bash +./gradlew bootRun +``` +The REST API will be available at `http://localhost:8080/accounts`. + +### Run (CLI mode) +```bash +./gradlew bootRun --args='--spring.profiles.active=cli' +``` + +### Run Tests +```bash +./gradlew test +``` + +## REST API Endpoints + +| Method | Endpoint | Description | COBOL Equivalent | +|---|---|---|---| +| GET | `/accounts` | List all accounts | display-all-accounts | +| GET | `/accounts/disabled` | List disabled accounts | display-disabled-accounts | +| GET | `/accounts/search?q={term}` | Search accounts | query-accounts | +| GET | `/accounts/{id}` | Get account by ID | N/A | +| POST | `/accounts` | Create new account | N/A (extended) | +| PUT | `/accounts/{id}` | Update account | N/A (extended) | +| DELETE | `/accounts/{id}` | Delete account | N/A (extended) | +| PUT | `/accounts/{id}/toggle` | Toggle enabled status | N/A (extended) | diff --git a/java-migration/build.gradle b/java-migration/build.gradle new file mode 100644 index 0000000..d6271f9 --- /dev/null +++ b/java-migration/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.4' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'com.cobolmigration' +version = '1.0.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + // Spring Boot starters + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + + // PostgreSQL driver + runtimeOnly 'org.postgresql:postgresql' + + // Jackson (JSON) - included via spring-boot-starter-web + // JAXB (XML) + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.1' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.4' + + // Testing + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.h2database:h2' + runtimeOnly 'com.h2database:h2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/java-migration/gradle/wrapper/gradle-wrapper.jar b/java-migration/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/java-migration/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java-migration/gradle/wrapper/gradle-wrapper.properties b/java-migration/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /dev/null +++ b/java-migration/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-migration/gradlew b/java-migration/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/java-migration/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/java-migration/gradlew.bat b/java-migration/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/java-migration/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-migration/settings.gradle b/java-migration/settings.gradle new file mode 100644 index 0000000..1180a84 --- /dev/null +++ b/java-migration/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cobol-migration' diff --git a/java-migration/src/main/java/com/cobolmigration/CobolMigrationApplication.java b/java-migration/src/main/java/com/cobolmigration/CobolMigrationApplication.java new file mode 100644 index 0000000..35e793d --- /dev/null +++ b/java-migration/src/main/java/com/cobolmigration/CobolMigrationApplication.java @@ -0,0 +1,18 @@ +package com.cobolmigration; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Main Spring Boot application entry point. + * Replaces the COBOL program-id entry points across all .cbl files. + * + * @see sql/sql_example.cbl - main-procedure + */ +@SpringBootApplication +public class CobolMigrationApplication { + + public static void main(String[] args) { + SpringApplication.run(CobolMigrationApplication.class, args); + } +} diff --git a/java-migration/src/main/java/com/cobolmigration/cli/AccountCli.java b/java-migration/src/main/java/com/cobolmigration/cli/AccountCli.java new file mode 100644 index 0000000..af0c9f7 --- /dev/null +++ b/java-migration/src/main/java/com/cobolmigration/cli/AccountCli.java @@ -0,0 +1,179 @@ +package com.cobolmigration.cli; + +import com.cobolmigration.model.Account; +import com.cobolmigration.service.AccountService; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Interactive command-line interface replacing the terminal menu from sql_example.cbl. + * + *
COBOL menu mapping (sql_example.cbl lines 158-185): + *
COBOL equivalent (read_cmd_line_args.cbl): + *
+ * ACCEPT ws-cmd-args FROM COMMAND-LINE + * INSPECT FUNCTION LOWER-CASE(ws-cmd-args) + * TALLYING ws-test-arg-count FOR ALL "--test" + *+ * + *
Uses Spring Boot's {@link ApplicationArguments} instead of COBOL's + * ACCEPT FROM COMMAND-LINE. + * + * @see read_command_args/read_cmd_line_args.cbl + */ +@Component +@Profile("cli") +public class CommandLineArgsRunner { + + private final ApplicationArguments applicationArguments; + + public CommandLineArgsRunner(ApplicationArguments applicationArguments) { + this.applicationArguments = applicationArguments; + } + + /** + * Checks if the "--test" argument was provided on the command line. + * Replaces the COBOL INSPECT TALLYING pattern. + * + * @return true if --test was passed + */ + public boolean hasTestArg() { + return applicationArguments.containsOption("test") + || applicationArguments.getNonOptionArgs().stream() + .anyMatch(arg -> arg.equalsIgnoreCase("--test")); + } + + /** + * Returns the full command line arguments as a single string, + * equivalent to COBOL's ACCEPT ws-cmd-args FROM COMMAND-LINE. + * + * @return concatenated command line arguments + */ + public String getFullCommandLine() { + return String.join(" ", applicationArguments.getSourceArgs()); + } + + /** + * Returns all non-option arguments. + * + * @return array of non-option arguments + */ + public String[] getNonOptionArgs() { + return applicationArguments.getNonOptionArgs().toArray(new String[0]); + } +} diff --git a/java-migration/src/main/java/com/cobolmigration/controller/AccountController.java b/java-migration/src/main/java/com/cobolmigration/controller/AccountController.java new file mode 100644 index 0000000..a928753 --- /dev/null +++ b/java-migration/src/main/java/com/cobolmigration/controller/AccountController.java @@ -0,0 +1,128 @@ +package com.cobolmigration.controller; + +import com.cobolmigration.model.Account; +import com.cobolmigration.service.AccountService; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST API layer providing a modern alternative to the COBOL terminal UI. + * Maps the account operations from sql/sql_example.cbl to HTTP endpoints. + * + *
Endpoint mapping: + *
COBOL field mapping (json_generate.cbl lines 45-48): + *
COBOL XML mapping (xml_generate.cbl lines 45-50): + *
COBOL field mapping: + *
COBOL field mapping: + *
COBOL field mapping: + *
Cursor mapping: + *
Operations mapped from COBOL paragraphs: + *
COBOL operation mapping: + *
COBOL equivalent (json_generate.cbl lines 42-54): + *
+ * JSON GENERATE ws-json-output FROM ws-record + * COUNT IN ws-json-char-count + * NAME OF ws-record-name IS "name", ... + *+ * + * @see json_generate/json_generate.cbl + */ +@Service +public class JsonService { + + private final ObjectMapper objectMapper; + + public JsonService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Generates JSON from a RecordDto. + * Returns a result containing the JSON string and its character count, + * matching the COBOL COUNT IN behavior. + * + * @param record the record to serialize + * @return the generated JSON string + * @throws JsonProcessingException if serialization fails + */ + public String generateJson(RecordDto record) throws JsonProcessingException { + return objectMapper.writeValueAsString(record); + } + + /** + * Generates JSON and returns the character count alongside the output, + * matching the COBOL COUNT IN ws-json-char-count behavior. + * + * @param record the record to serialize + * @return a JsonResult containing the JSON string and character count + * @throws JsonProcessingException if serialization fails + */ + public JsonResult generateJsonWithCount(RecordDto record) throws JsonProcessingException { + String json = objectMapper.writeValueAsString(record); + return new JsonResult(json, json.length()); + } + + /** + * Result holder matching COBOL's dual output of JSON content and character count. + */ + public static class JsonResult { + private final String json; + private final int charCount; + + public JsonResult(String json, int charCount) { + this.json = json; + this.charCount = charCount; + } + + public String getJson() { + return json; + } + + public int getCharCount() { + return charCount; + } + } +} diff --git a/java-migration/src/main/java/com/cobolmigration/service/ReportService.java b/java-migration/src/main/java/com/cobolmigration/service/ReportService.java new file mode 100644 index 0000000..a2d8604 --- /dev/null +++ b/java-migration/src/main/java/com/cobolmigration/service/ReportService.java @@ -0,0 +1,109 @@ +package com.cobolmigration.service; + +import com.cobolmigration.model.StudentRecord; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Service; + +/** + * Service replacing the COBOL Report Writer (RD) from report_writer/report_test.cbl. + * + *
COBOL Report Writer mapping: + *
COBOL subprogram semantics mapping: + *
COBOL storage section mapping: + *
COBOL equivalent (xml_generate.cbl lines 41-56): + *
+ * XML GENERATE ws-xml-output FROM ws-record + * COUNT IN ws-xml-char-count + * WITH XML-DECLARATION + * NAME OF ws-record-name IS "name", ... + * TYPE OF ws-record-flag IS ATTRIBUTE + * SUPPRESS WHEN SPACES + *+ * + * @see xml_generate/xml_generate.cbl + */ +@Service +public class XmlService { + + /** + * Generates XML from a RecordXmlDto with XML declaration. + * The XML declaration header matches the COBOL WITH XML-DECLARATION option. + * The enabled field is rendered as an attribute (TYPE IS ATTRIBUTE). + * Fields with only spaces are suppressed (SUPPRESS WHEN SPACES). + * + * @param record the record to serialize + * @return the generated XML string + * @throws JAXBException if marshalling fails + */ + public String generateXml(RecordXmlDto record) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(RecordXmlDto.class); + Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.FALSE); + // WITH XML-DECLARATION: include the header + marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.FALSE); + + StringWriter writer = new StringWriter(); + marshaller.marshal(record, writer); + return writer.toString(); + } + + /** + * Generates XML and returns the character count alongside the output, + * matching the COBOL COUNT IN ws-xml-char-count behavior. + * + * @param record the record to serialize + * @return an XmlResult containing the XML string and character count + * @throws JAXBException if marshalling fails + */ + public XmlResult generateXmlWithCount(RecordXmlDto record) throws JAXBException { + String xml = generateXml(record); + return new XmlResult(xml, xml.length()); + } + + /** + * Result holder matching COBOL's dual output of XML content and character count. + */ + public static class XmlResult { + private final String xml; + private final int charCount; + + public XmlResult(String xml, int charCount) { + this.xml = xml; + this.charCount = charCount; + } + + public String getXml() { + return xml; + } + + public int getCharCount() { + return charCount; + } + } +} diff --git a/java-migration/src/main/java/com/cobolmigration/util/NumericUtils.java b/java-migration/src/main/java/com/cobolmigration/util/NumericUtils.java new file mode 100644 index 0000000..4c74ce9 --- /dev/null +++ b/java-migration/src/main/java/com/cobolmigration/util/NumericUtils.java @@ -0,0 +1,119 @@ +package com.cobolmigration.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Utility class replacing COBOL COMP/COMP-3 numeric handling from comp_test/comp_test.cbl. + * + *
In COBOL, numeric storage types include: + *
In Java, we use BigDecimal for precision-sensitive calculations, replacing all + * COMP variants to avoid floating-point precision loss. + * + * @see comp_test/comp_test.cbl + * @see numval_test/numval_test.cbl + */ +public final class NumericUtils { + + private NumericUtils() { + // Utility class - prevent instantiation + } + + /** + * Converts a COBOL COMP (binary) value representation to BigDecimal. + * In COBOL, COMP stores values as binary integers (comp_test.cbl line 13: PIC 999 COMP). + * + * @param value the integer value + * @return BigDecimal representation + */ + public static BigDecimal fromComp(int value) { + return BigDecimal.valueOf(value); + } + + /** + * Converts a COBOL COMP-2 (double-precision float) value to BigDecimal. + * Used in numval_test.cbl (line 17: ws-total COMP-2). + * + * @param value the double value + * @return BigDecimal representation with controlled precision + */ + public static BigDecimal fromComp2(double value) { + return BigDecimal.valueOf(value); + } + + /** + * Converts a COBOL COMP-3 (packed decimal) conceptual value to BigDecimal. + * COMP-3 stores two digits per byte in BCD format. + * In Java, BigDecimal natively handles decimal precision. + * + * @param value the string representation of the packed decimal value + * @param scale the number of decimal places implied by the COBOL PIC clause + * @return BigDecimal with the correct scale + */ + public static BigDecimal fromComp3(String value, int scale) { + BigDecimal result = new BigDecimal(value.trim()); + return result.setScale(scale, RoundingMode.HALF_UP); + } + + /** + * Formats a BigDecimal to a COBOL DISPLAY format string. + * Replaces the COBOL MOVE of COMP to DISPLAY (comp_test.cbl lines 27-28). + * + * @param value the BigDecimal to format + * @param width the total character width (PIC 999 = width 3) + * @return zero-padded string representation + */ + public static String toDisplay(BigDecimal value, int width) { + if (value == null) { + return "0".repeat(width); + } + long longVal = value.longValue(); + return String.format("%0" + width + "d", longVal); + } + + /** + * Formats a BigDecimal to a COBOL dynamic display format (suppressed leading zeros). + * Replaces the PIC ZZ9 format (comp_test.cbl line 17). + * + * @param value the BigDecimal to format + * @param width the total character width + * @return right-justified string with leading spaces instead of zeros + */ + public static String toDynamicDisplay(BigDecimal value, int width) { + if (value == null) { + return " ".repeat(width - 1) + "0"; + } + return String.format("%" + width + "d", value.longValue()); + } + + /** + * Multiplies two values, equivalent to COBOL MULTIPLY ... GIVING. + * Uses BigDecimal for precision (comp_test.cbl line 24). + * + * @param a first operand + * @param b second operand + * @return product as BigDecimal + */ + public static BigDecimal multiply(BigDecimal a, BigDecimal b) { + return a.multiply(b); + } + + /** + * Adds two values, equivalent to COBOL ADD or COMPUTE. + * + * @param a first operand + * @param b second operand + * @return sum as BigDecimal + */ + public static BigDecimal add(BigDecimal a, BigDecimal b) { + return a.add(b); + } +} diff --git a/java-migration/src/main/java/com/cobolmigration/util/StringUtils.java b/java-migration/src/main/java/com/cobolmigration/util/StringUtils.java new file mode 100644 index 0000000..d3faefc --- /dev/null +++ b/java-migration/src/main/java/com/cobolmigration/util/StringUtils.java @@ -0,0 +1,159 @@ +package com.cobolmigration.util; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Utility class replacing COBOL string/data operations: + *
In COBOL, UNSTRING splits a source string into destination fields
+ * based on specified delimiters (unstring/unstring.cbl lines 58-61).
+ *
+ * @param source the source string to split
+ * @param delimiters one or more delimiter strings
+ * @return list of parts after splitting
+ * @see unstring/unstring.cbl
+ */
+ public static List